diff --git a/.appveyor.yml b/.appveyor.yml index 0e8e0a553fd4..63801100307d 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -11,6 +11,8 @@ branches: clone_depth: 50 +image: Visual Studio 2017 + environment: global: @@ -18,20 +20,14 @@ environment: PYTHONIOENCODING: UTF-8 PYTEST_ARGS: -raR --numprocesses=auto --timeout=300 --durations=25 --cov-report= --cov=lib --log-level=DEBUG - PINNEDVERS: "pyzmq!=21.0.0 pyzmq!=22.0.0" matrix: - # In theory we could use a single CONDA_INSTALL_LOCN because we construct - # the envs anyway. But using one for the right python version hopefully - # making things faster due to package caching. - - PYTHON_VERSION: "3.7" - CONDA_INSTALL_LOCN: "C:\\Miniconda37-x64" - TEST_ALL: "no" - EXTRAREQS: "-r requirements/testing/extra.txt" - PYTHON_VERSION: "3.8" - CONDA_INSTALL_LOCN: "C:\\Miniconda37-x64" + CONDA_INSTALL_LOCN: "C:\\Miniconda3-x64" + TEST_ALL: "no" + - PYTHON_VERSION: "3.9" + CONDA_INSTALL_LOCN: "C:\\Miniconda3-x64" TEST_ALL: "no" - EXTRAREQS: "-r requirements/testing/extra.txt" # We always use a 64-bit machine, but can build x86 distributions # with the PYTHON_ARCH variable @@ -55,17 +51,13 @@ install: - conda config --prepend channels conda-forge # For building, use a new environment - - conda create -q -n test-environment python=%PYTHON_VERSION% tk - - activate test-environment - # pull pywin32 from conda because on py38 there is something wrong with finding - # the dlls when insalled from pip + # Add python version to environment + # `^ ` escapes spaces for indentation + - echo ^ ^ - python=%PYTHON_VERSION% >> environment.yml + - conda env create -f environment.yml + - activate mpl-dev - conda install -c conda-forge pywin32 - echo %PYTHON_VERSION% %TARGET_ARCH% - # Install dependencies from PyPI. - - python -m pip install --upgrade -r requirements/testing/all.txt %EXTRAREQS% %PINNEDVERS% - # Install optional dependencies from PyPI. - # Sphinx is needed to run sphinxext tests - - python -m pip install --upgrade sphinx # Show the installed packages + versions - conda list @@ -89,23 +81,13 @@ test_script: - echo The following args are passed to pytest %PYTEST_ARGS% - pytest %PYTEST_ARGS% -after_test: - # After the tests were a success, build wheels. - # Hide the output, the copied files really clutter the build log... - - 'python setup.py bdist_wheel > NUL:' - - dir dist\ - - echo finished... - artifacts: - - path: dist\* - name: packages - - path: result_images\* name: result_images type: zip on_finish: - - pip install codecov + - conda install codecov - codecov -e PYTHON_VERSION PLATFORM on_failure: diff --git a/.circleci/config.yml b/.circleci/config.yml index e70b1befe053..9c1f09172060 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -9,6 +9,34 @@ version: 2.1 # commands: + check-skip: + steps: + - run: + name: Check-skip + command: | + export git_log=$(git log --max-count=1 --pretty=format:"%B" | tr "\n" " ") + echo "Got commit message:" + echo "${git_log}" + if [[ -v CIRCLE_PULL_REQUEST ]] && ([[ "$git_log" == *"[skip circle]"* ]] || [[ "$git_log" == *"[circle skip]"* ]]); then + echo "Skip detected, exiting job ${CIRCLE_JOB} for PR ${CIRCLE_PULL_REQUEST}." + circleci-agent step halt; + fi + + merge: + steps: + - run: + name: Merge with upstream + command: | + if ! git remote -v | grep upstream; then + git remote add upstream https://github.com/matplotlib/matplotlib.git + fi + git fetch upstream + if [[ "$CIRCLE_BRANCH" != "main" ]] && \ + [[ "$CIRCLE_PR_NUMBER" != "" ]]; then + echo "Merging ${CIRCLE_PR_NUMBER}" + git pull --ff-only upstream "refs/pull/${CIRCLE_PR_NUMBER}/merge" + fi + apt-install: steps: - run: @@ -27,10 +55,12 @@ commands: texlive-latex-recommended \ texlive-pictures \ texlive-xetex \ + ttf-wqy-zenhei \ graphviz \ fonts-crosextra-carlito \ fonts-freefont-otf \ fonts-humor-sans \ + fonts-noto-cjk \ optipng fonts-install: @@ -56,9 +86,9 @@ commands: command: | python -m pip install --upgrade --user pip python -m pip install --upgrade --user wheel - python -m pip install --upgrade --user setuptools + python -m pip install --upgrade --user 'setuptools!=60.6.0' - deps-install: + doc-deps-install: parameters: numpy_version: type: string @@ -67,6 +97,8 @@ commands: - run: name: Install Python dependencies command: | + python -m pip install --no-deps --user \ + git+https://github.com/matplotlib/mpl-sphinx-theme.git python -m pip install --user \ numpy<< parameters.numpy_version >> codecov coverage \ -r requirements/doc/doc-requirements.txt @@ -75,7 +107,22 @@ commands: steps: - run: name: Install Matplotlib - command: python -m pip install --user -ve . + command: | + if [[ "$CIRCLE_BRANCH" == v*-doc ]]; then + # The v*-doc branches must build against the specified release. + version=${CIRCLE_BRANCH%-doc} + version=${version#v} + python -m pip install matplotlib==${version} + else + python -m pip install --user -ve . + fi + - save_cache: + key: build-deps-1 + paths: + # FreeType 2.6.1 tarball. + - ~/.cache/matplotlib/0a3c7dfbda6da1e8fce29232e8e96d987ababbbf71ebc8c75659e4132c367014 + # Qhull 2020.2 tarball. + - ~/.cache/matplotlib/b5c2d7eb833278881b952c8a52d20179eab87766b00b865000469a45c1838b7e doc-build: steps: @@ -88,15 +135,14 @@ commands: command: | # Set epoch to date of latest tag. export SOURCE_DATE_EPOCH="$(git log -1 --format=%at $(git describe --abbrev=0))" - # Include analytics only when deploying to devdocs. - if [ "$CIRCLE_PROJECT_USERNAME" != "matplotlib" ] || \ - [ "$CIRCLE_BRANCH" != "master" ] || \ - [[ "$CIRCLE_PULL_REQUEST" == https://github.com/matplotlib/matplotlib/pull/* ]]; then - export ANALYTICS=False - else - export ANALYTICS=True + # Set release mode only when deploying to devdocs. + if [ "$CIRCLE_PROJECT_USERNAME" = "matplotlib" ] && \ + [ "$CIRCLE_BRANCH" = "main" ] && \ + [ "$CIRCLE_PR_NUMBER" = "" ]; then + export RELEASE_TAG='-t release' fi - make html O="-T -Ainclude_analytics=$ANALYTICS" + mkdir -p logs + make html O="-T $RELEASE_TAG -j4 -w /tmp/sphinxerrorswarnings.log" rm -r build/html/_sources working_directory: doc - save_cache: @@ -104,11 +150,51 @@ commands: paths: - doc/build/doctrees + doc-show-errors-warnings: + steps: + - run: + name: Extract possible build errors and warnings + command: | + (grep "WARNING\|ERROR" /tmp/sphinxerrorswarnings.log || + echo "No errors or warnings") + # Save logs as an artifact, and convert from absolute paths to + # repository-relative paths. + sed "s~$PWD/~~" /tmp/sphinxerrorswarnings.log > \ + doc/logs/sphinx-errors-warnings.log + when: always + - store_artifacts: + path: doc/logs/sphinx-errors-warnings.log + + doc-show-deprecations: + steps: + - run: + name: Extract possible deprecation warnings in examples and tutorials + command: | + (grep -rl DeprecationWarning doc/build/html/gallery || + echo "No deprecation warnings in gallery") + (grep -rl DeprecationWarning doc/build/html/plot_types || + echo "No deprecation warnings in plot_types") + (grep -rl DeprecationWarning doc/build/html/tutorials || + echo "No deprecation warnings in tutorials") + # Save deprecations that are from this absolute directory, and + # convert to repository-relative paths. + (grep -Ero --no-filename "$PWD/.+DeprecationWarning.+$" \ + doc/build/html/{gallery,plot_types,tutorials} || echo) | \ + sed "s~$PWD/~~" > doc/logs/sphinx-deprecations.log + when: always + - store_artifacts: + path: doc/logs/sphinx-deprecations.log + doc-bundle: steps: - run: name: Bundle sphinx-gallery documentation artifacts - command: tar cf doc/build/sphinx-gallery-files.tar.gz doc/api/_as_gen doc/gallery doc/tutorials + command: > + tar cf doc/build/sphinx-gallery-files.tar.gz + doc/api/_as_gen + doc/gallery + doc/plot_types + doc/tutorials when: always - store_artifacts: path: doc/build/sphinx-gallery-files.tar.gz @@ -119,70 +205,37 @@ commands: # jobs: - docs-python37: - docker: - - image: circleci/python:3.7 - steps: - - checkout - - - apt-install - - fonts-install - - pip-install - - - deps-install - - mpl-install - - - doc-build - - - doc-bundle - - - store_artifacts: - path: doc/build/html - - docs-python38-min: - docker: - - image: circleci/python:3.8 - steps: - - checkout - - - apt-install - - fonts-install - - pip-install - - - deps-install: - numpy_version: "==1.16.0" - - mpl-install - - - doc-build - - - doc-bundle - - - store_artifacts: - path: doc/build/html - docs-python38: docker: - - image: circleci/python:3.8 + - image: cimg/python:3.8 + resource_class: large steps: - checkout + - check-skip + - merge - apt-install - fonts-install - pip-install - - deps-install - mpl-install + - doc-deps-install - doc-build + - doc-show-errors-warnings + - doc-show-deprecations - doc-bundle - store_artifacts: path: doc/build/html + - store_test_results: + path: doc/build/test-results - add_ssh_keys: fingerprints: - - "78:13:59:08:61:a9:e5:09:af:df:3a:d8:89:c2:84:c0" + - "be:c3:c1:d8:fb:a1:0e:37:71:72:d7:a3:40:13:8f:14" + - deploy: name: "Deploy new docs" command: ./.circleci/deploy-docs.sh @@ -195,6 +248,6 @@ workflows: version: 2 build: jobs: - - docs-python37 + # NOTE: If you rename this job, then you must update the `if` condition + # and `circleci-jobs` option in `.github/workflows/circleci.yml`. - docs-python38 - - docs-python38-min diff --git a/.circleci/deploy-docs.sh b/.circleci/deploy-docs.sh index 83037d2561a4..8801d5fd073e 100755 --- a/.circleci/deploy-docs.sh +++ b/.circleci/deploy-docs.sh @@ -3,10 +3,10 @@ set -e if [ "$CIRCLE_PROJECT_USERNAME" != "matplotlib" ] || \ - [ "$CIRCLE_BRANCH" != "master" ] || \ + [ "$CIRCLE_BRANCH" != "main" ] || \ [[ "$CIRCLE_PULL_REQUEST" == https://github.com/matplotlib/matplotlib/pull/* ]]; then echo "Not uploading docs for ${CIRCLE_SHA1}"\ - "from non-master branch (${CIRCLE_BRANCH})"\ + "from non-main branch (${CIRCLE_BRANCH})"\ "or pull request (${CIRCLE_PULL_REQUEST})"\ "or non-Matplotlib org (${CIRCLE_PROJECT_USERNAME})." exit diff --git a/.circleci/fetch_doc_logs.py b/.circleci/fetch_doc_logs.py new file mode 100644 index 000000000000..40452cea7792 --- /dev/null +++ b/.circleci/fetch_doc_logs.py @@ -0,0 +1,63 @@ +""" +Download artifacts from CircleCI for a documentation build. + +This is run by the :file:`.github/workflows/circleci.yml` workflow in order to +get the warning/deprecation logs that will be posted on commits as checks. Logs +are downloaded from the :file:`docs/logs` artifact path and placed in the +:file:`logs` directory. + +Additionally, the artifact count for a build is produced as a workflow output, +by appending to the file specified by :env:`GITHUB_OUTPUT`. + +If there are no logs, an "ERROR" message is printed, but this is not fatal, as +the initial 'status' workflow runs when the build has first started, and there +are naturally no artifacts at that point. + +This script should be run by passing the CircleCI build URL as its first +argument. In the GitHub Actions workflow, this URL comes from +``github.event.target_url``. +""" +import json +import os +from pathlib import Path +import sys +from urllib.parse import urlparse +from urllib.request import urlopen + + +if len(sys.argv) != 2: + print('USAGE: fetch_doc_results.py CircleCI-build-url') + sys.exit(1) + +target_url = urlparse(sys.argv[1]) +*_, organization, repository, build_id = target_url.path.split('/') +print(f'Fetching artifacts from {organization}/{repository} for {build_id}') + +artifact_url = ( + f'https://circleci.com/api/v2/project/gh/' + f'{organization}/{repository}/{build_id}/artifacts' +) +print(artifact_url) +with urlopen(artifact_url) as response: + artifacts = json.load(response) +artifact_count = len(artifacts['items']) +print(f'Found {artifact_count} artifacts') + +with open(os.environ['GITHUB_OUTPUT'], 'w+') as fd: + fd.write(f'count={artifact_count}\n') + +logs = Path('logs') +logs.mkdir(exist_ok=True) + +found = False +for item in artifacts['items']: + path = item['path'] + if path.startswith('doc/logs/'): + path = Path(path).name + print(f'Downloading {path} from {item["url"]}') + with urlopen(item['url']) as response: + (logs / path).write_bytes(response.read()) + found = True + +if not found: + print('ERROR: Did not find any artifact logs!') diff --git a/.flake8 b/.flake8 index ec89adb38b2e..490ea57d9891 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,5 @@ [flake8] -max-line-length = 79 +max-line-length = 88 select = # flake8 default C90, E, F, W, @@ -16,8 +16,8 @@ ignore = # flake8 default E121,E123,E126,E226,E24,E704,W503,W504, # Additional ignores: - E122, E127, E131, - E265, E266, + E127, E131, + E266, E305, E306, E722, E741, F841, @@ -35,7 +35,6 @@ exclude = doc/gallery doc/tutorials # External files. - versioneer.py tools/gh_api.py tools/github_stats.py .tox @@ -43,52 +42,31 @@ exclude = per-file-ignores = setup.py: E402 - setupext.py: E501 tests.py: F401 - tools/subset.py: E221, E251, E261, E302, E501 - - lib/matplotlib/__init__.py: F401 + lib/matplotlib/__init__.py: E402, F401 lib/matplotlib/_api/__init__.py: F401 - lib/matplotlib/_cm.py: E202, E203, E302 + lib/matplotlib/_cm.py: E122, E202, E203, E302 lib/matplotlib/_mathtext.py: E221, E251 - lib/matplotlib/_mathtext_data.py: E203, E261 - lib/matplotlib/animation.py: F401 + lib/matplotlib/_mathtext_data.py: E122, E203, E261 lib/matplotlib/_animation_data.py: E501 lib/matplotlib/axes/__init__.py: F401, F403 - lib/matplotlib/axes/_axes.py: F401 - lib/matplotlib/backends/backend_*.py: F401 + lib/matplotlib/backends/backend_template.py: F401 lib/matplotlib/backends/qt_editor/formlayout.py: F401, F403 - lib/matplotlib/cbook/__init__.py: F401 - lib/matplotlib/cbook/deprecation.py: F401 - lib/matplotlib/font_manager.py: E221, E251, E501 + lib/matplotlib/font_manager.py: E501 lib/matplotlib/image.py: F401, F403 - lib/matplotlib/lines.py: F401 lib/matplotlib/mathtext.py: E221, E251 lib/matplotlib/pylab.py: F401, F403 lib/matplotlib/pyplot.py: F401, F811 - lib/matplotlib/style/__init__.py: F401 - lib/matplotlib/testing/conftest.py: F401 - lib/matplotlib/tests/conftest.py: F401 - lib/matplotlib/tests/test_backend_qt.py: F401 lib/matplotlib/tests/test_mathtext.py: E501 - lib/matplotlib/text.py: F401 lib/matplotlib/transforms.py: E201, E202, E203 - lib/matplotlib/tri/__init__.py: F401, F403 - lib/matplotlib/tri/triinterpolate.py: E201, E221 - lib/mpl_toolkits/axes_grid/*: F401, F403 - lib/mpl_toolkits/axes_grid1/__init__.py: F401 + lib/matplotlib/tri/_triinterpolate.py: E201, E221 lib/mpl_toolkits/axes_grid1/axes_size.py: E272 lib/mpl_toolkits/axisartist/__init__.py: F401 lib/mpl_toolkits/axisartist/angle_helper.py: E221 - lib/mpl_toolkits/axisartist/axes_divider.py: F401 - lib/mpl_toolkits/axisartist/axes_rgb.py: F401 - lib/mpl_toolkits/axisartist/axislines.py: F401 - lib/mpl_toolkits/mplot3d/__init__.py: F401 - lib/mpl_toolkits/tests/conftest.py: F401 lib/pylab.py: F401, F403 - doc/conf.py: E402, E501 + doc/conf.py: E402 tutorials/advanced/path_tutorial.py: E402 tutorials/advanced/patheffects_guide.py: E402 tutorials/advanced/transforms_tutorial.py: E402, E501 @@ -97,15 +75,16 @@ per-file-ignores = tutorials/colors/colormap-manipulation.py: E402 tutorials/intermediate/artists.py: E402 tutorials/intermediate/constrainedlayout_guide.py: E402 - tutorials/intermediate/gridspec.py: E402 tutorials/intermediate/legend_guide.py: E402 tutorials/intermediate/tight_layout_guide.py: E402 tutorials/introductory/customizing.py: E501 tutorials/introductory/images.py: E402, E501 tutorials/introductory/pyplot.py: E402, E501 tutorials/introductory/sample_plots.py: E501 - tutorials/introductory/usage.py: E501 - tutorials/text/annotations.py: E501 + tutorials/introductory/quick_start.py: E703 + tutorials/introductory/animation_tutorial.py: E501 + tutorials/text/annotations.py: E402, E501 + tutorials/text/mathtext.py: E501 tutorials/text/text_intro.py: E402 tutorials/text/text_props.py: E501 tutorials/text/usetex.py: E501 @@ -113,179 +92,27 @@ per-file-ignores = tutorials/toolkits/axisartist.py: E501 examples/animation/frame_grabbing_sgskip.py: E402 - examples/axes_grid1/inset_locator_demo.py: E402 - examples/axes_grid1/scatter_hist_locatable_axes.py: E402 - examples/axisartist/demo_curvelinear_grid.py: E402 - examples/color/color_by_yvalue.py: E402 - examples/color/color_cycle_default.py: E402 - examples/color/color_cycler.py: E402 - examples/color/color_demo.py: E402 - examples/color/colorbar_basics.py: E402 - examples/color/colormap_reference.py: E402 - examples/color/custom_cmap.py: E402 - examples/color/named_colors.py: E402 - examples/images_contours_and_fields/affine_image.py: E402 - examples/images_contours_and_fields/barb_demo.py: E402 - examples/images_contours_and_fields/barcode_demo.py: E402 - examples/images_contours_and_fields/contour_corner_mask.py: E402 - examples/images_contours_and_fields/contour_demo.py: E402 - examples/images_contours_and_fields/contour_image.py: E402 - examples/images_contours_and_fields/contourf_demo.py: E402 - examples/images_contours_and_fields/contourf_hatching.py: E402 - examples/images_contours_and_fields/contourf_log.py: E402 - examples/images_contours_and_fields/demo_bboximage.py: E402 - examples/images_contours_and_fields/image_antialiasing.py: E402 - examples/images_contours_and_fields/image_clip_path.py: E402 - examples/images_contours_and_fields/image_demo.py: E402 - examples/images_contours_and_fields/image_masked.py: E402 - examples/images_contours_and_fields/image_transparency_blend.py: E402 - examples/images_contours_and_fields/image_zcoord.py: E402 - examples/images_contours_and_fields/interpolation_methods.py: E402 - examples/images_contours_and_fields/irregulardatagrid.py: E402 - examples/images_contours_and_fields/layer_images.py: E402 - examples/images_contours_and_fields/matshow.py: E402 - examples/images_contours_and_fields/multi_image.py: E402 - examples/images_contours_and_fields/pcolor_demo.py: E402 - examples/images_contours_and_fields/plot_streamplot.py: E402 - examples/images_contours_and_fields/quadmesh_demo.py: E402 - examples/images_contours_and_fields/quiver_demo.py: E402 - examples/images_contours_and_fields/quiver_simple_demo.py: E402 - examples/images_contours_and_fields/shading_example.py: E402 - examples/images_contours_and_fields/specgram_demo.py: E402 - examples/images_contours_and_fields/spy_demos.py: E402 - examples/images_contours_and_fields/tricontour_demo.py: E201, E402 - examples/images_contours_and_fields/tricontour_smooth_delaunay.py: E402 - examples/images_contours_and_fields/tricontour_smooth_user.py: E402 - examples/images_contours_and_fields/trigradient_demo.py: E402 - examples/images_contours_and_fields/triinterp_demo.py: E402 - examples/images_contours_and_fields/tripcolor_demo.py: E201, E402 - examples/images_contours_and_fields/triplot_demo.py: E201, E402 - examples/images_contours_and_fields/watermark_image.py: E402 - examples/lines_bars_and_markers/curve_error_band.py: E402 - examples/lines_bars_and_markers/errorbar_limits_simple.py: E402 - examples/lines_bars_and_markers/fill.py: E402 - examples/lines_bars_and_markers/fill_between_demo.py: E402 - examples/lines_bars_and_markers/filled_step.py: E402 - examples/lines_bars_and_markers/stairs_demo.py: E402 - examples/lines_bars_and_markers/horizontal_barchart_distribution.py: E402 - examples/lines_bars_and_markers/joinstyle.py: E402 - examples/lines_bars_and_markers/scatter_hist.py: E402 - examples/lines_bars_and_markers/scatter_piecharts.py: E402 - examples/lines_bars_and_markers/scatter_with_legend.py: E402 - examples/lines_bars_and_markers/span_regions.py: E402 - examples/lines_bars_and_markers/stem_plot.py: E402 - examples/lines_bars_and_markers/step_demo.py: E402 - examples/lines_bars_and_markers/timeline.py: E402 - examples/lines_bars_and_markers/xcorr_acorr_demo.py: E402 - examples/misc/agg_buffer.py: E402 - examples/misc/histogram_path.py: E402 + examples/lines_bars_and_markers/marker_reference.py: E402 + examples/images_contours_and_fields/tricontour_demo.py: E201 + examples/images_contours_and_fields/tripcolor_demo.py: E201 + examples/images_contours_and_fields/triplot_demo.py: E201 examples/misc/print_stdout_sgskip.py: E402 - examples/misc/rasterization_demo.py: E402 - examples/misc/svg_filter_line.py: E402 - examples/misc/svg_filter_pie.py: E402 examples/misc/table_demo.py: E201 - examples/mplot3d/surface3d.py: E402 - examples/pie_and_polar_charts/bar_of_pie.py: E402 - examples/pie_and_polar_charts/nested_pie.py: E402 - examples/pie_and_polar_charts/pie_and_donut_labels.py: E402 - examples/pie_and_polar_charts/pie_demo2.py: E402 - examples/pie_and_polar_charts/pie_features.py: E402 - examples/pie_and_polar_charts/polar_bar.py: E402 - examples/pie_and_polar_charts/polar_demo.py: E402 - examples/pie_and_polar_charts/polar_legend.py: E402 - examples/pie_and_polar_charts/polar_scatter.py: E402 - examples/pyplots/align_ylabels.py: E402 - examples/pyplots/annotate_transform.py: E251, E402 - examples/pyplots/annotation_basic.py: E402 - examples/pyplots/annotation_polar.py: E402 - examples/pyplots/auto_subplots_adjust.py: E302, E402 - examples/pyplots/axline.py: E402 - examples/pyplots/boxplot_demo_pyplot.py: E402 - examples/pyplots/dollar_ticks.py: E402 - examples/pyplots/fig_axes_customize_simple.py: E402 - examples/pyplots/fig_axes_labels_simple.py: E402 - examples/pyplots/fig_x.py: E402 - examples/pyplots/pyplot_formatstr.py: E402 - examples/pyplots/pyplot_mathtext.py: E402 - examples/pyplots/pyplot_scales.py: E402 - examples/pyplots/pyplot_simple.py: E402 - examples/pyplots/pyplot_text.py: E402 - examples/pyplots/pyplot_three.py: E402 - examples/pyplots/pyplot_two_subplots.py: E402 - examples/pyplots/text_commands.py: E402 - examples/pyplots/text_layout.py: E402 - examples/pyplots/whats_new_1_subplot3d.py: E402 - examples/pyplots/whats_new_98_4_fill_between.py: E402 - examples/pyplots/whats_new_98_4_legend.py: E402 - examples/pyplots/whats_new_99_axes_grid.py: E402 - examples/pyplots/whats_new_99_mplot3d.py: E402 - examples/pyplots/whats_new_99_spines.py: E402 - examples/scales/power_norm.py: E402 - examples/scales/scales.py: E402 - examples/shapes_and_collections/artist_reference.py: E402 - examples/shapes_and_collections/collections.py: E402 - examples/shapes_and_collections/compound_path.py: E402 - examples/shapes_and_collections/dolphin.py: E402 - examples/shapes_and_collections/donut.py: E402 - examples/shapes_and_collections/ellipse_collection.py: E402 - examples/shapes_and_collections/ellipse_demo.py: E402 - examples/shapes_and_collections/fancybox_demo.py: E402 - examples/shapes_and_collections/hatch_demo.py: E402 - examples/shapes_and_collections/hatch_style_reference.py: E402 - examples/shapes_and_collections/line_collection.py: E402 - examples/shapes_and_collections/marker_path.py: E402 - examples/shapes_and_collections/patch_collection.py: E402 - examples/shapes_and_collections/path_patch.py: E402 - examples/shapes_and_collections/quad_bezier.py: E402 - examples/shapes_and_collections/scatter.py: E402 - examples/showcase/anatomy.py: E402 - examples/showcase/bachelors_degrees_by_gender.py: E402 - examples/showcase/firefox.py: E501 - examples/specialty_plots/anscombe.py: E402 - examples/specialty_plots/radar_chart.py: E402 - examples/specialty_plots/sankey_basics.py: E402 - examples/specialty_plots/sankey_links.py: E402 - examples/specialty_plots/sankey_rankine.py: E402 - examples/specialty_plots/skewt.py: E402 examples/style_sheets/bmh.py: E501 - examples/style_sheets/ggplot.py: E501 examples/style_sheets/plot_solarizedlight2.py: E501 - examples/subplots_axes_and_figures/axes_margins.py: E402 - examples/subplots_axes_and_figures/axes_zoom_effect.py: E402 - examples/subplots_axes_and_figures/custom_figure_class.py: E402 examples/subplots_axes_and_figures/demo_constrained_layout.py: E402 - examples/subplots_axes_and_figures/demo_tight_layout.py: E402, E501 - examples/subplots_axes_and_figures/figure_size_units.py: E402 - examples/subplots_axes_and_figures/secondary_axis.py: E402 - examples/subplots_axes_and_figures/two_scales.py: E402 - examples/subplots_axes_and_figures/zoom_inset_axes.py: E402 - examples/text_labels_and_annotations/date_index_formatter.py: E402 - examples/text_labels_and_annotations/demo_text_rotation_mode.py: E402 examples/text_labels_and_annotations/custom_legends.py: E402 - examples/text_labels_and_annotations/fancyarrow_demo.py: E402 - examples/text_labels_and_annotations/font_family_rc_sgskip.py: E402 - examples/text_labels_and_annotations/font_file.py: E402 - examples/text_labels_and_annotations/legend.py: E402 - examples/text_labels_and_annotations/line_with_text.py: E402 - examples/text_labels_and_annotations/mathtext_asarray.py: E402 - examples/text_labels_and_annotations/tex_demo.py: E402 - examples/text_labels_and_annotations/watermark_text.py: E402 - examples/ticks_and_spines/custom_ticker1.py: E402 - examples/ticks_and_spines/date_concise_formatter.py: E402 - examples/ticks_and_spines/major_minor_demo.py: E402 - examples/ticks_and_spines/tick-formatters.py: E402 - examples/ticks_and_spines/tick_labels_from_values.py: E402 - examples/user_interfaces/canvasagg.py: E402 + examples/ticks/date_concise_formatter.py: E402 + examples/ticks/date_formatters_locators.py: F401 examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py: E402 examples/user_interfaces/embedding_in_gtk3_sgskip.py: E402 - examples/user_interfaces/embedding_in_qt_sgskip.py: E402 - examples/user_interfaces/gtk_spreadsheet_sgskip.py: E402 - examples/user_interfaces/mathtext_wx_sgskip.py: E402 + examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py: E402 + examples/user_interfaces/embedding_in_gtk4_sgskip.py: E402 + examples/user_interfaces/gtk3_spreadsheet_sgskip.py: E402 + examples/user_interfaces/gtk4_spreadsheet_sgskip.py: E402 examples/user_interfaces/mpl_with_glade3_sgskip.py: E402 - examples/user_interfaces/pylab_with_gtk_sgskip.py: E302, E402 + examples/user_interfaces/pylab_with_gtk3_sgskip.py: E402 + examples/user_interfaces/pylab_with_gtk4_sgskip.py: E402 examples/user_interfaces/toolmanager_sgskip.py: E402 - examples/userdemo/connectionstyle_demo.py: E402 - examples/userdemo/custom_boxstyle01.py: E402 examples/userdemo/pgf_preamble_sgskip.py: E402 - examples/widgets/*.py: E402 - examples/statistics/*.py: E402 +force-check = True diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000000..33ff9446d8a6 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,11 @@ +# style: end-of-file-fixer pre-commit hook +c1a33a481b9c2df605bcb9bef9c19fe65c3dac21 + +# style: trailing-whitespace pre-commit hook +213061c0804530d04bbbd5c259f10dc8504e5b2b + +# style: check-docstring-first pre-commit hook +046533797725293dfc2a6edb9f536b25f08aa636 + +# chore: fix spelling errors +686c9e5a413e31c46bb049407d5eca285bcab76d diff --git a/.git_archival.txt b/.git_archival.txt new file mode 100644 index 000000000000..3994ec0a83ea --- /dev/null +++ b/.git_archival.txt @@ -0,0 +1,4 @@ +node: $Format:%H$ +node-date: $Format:%cI$ +describe-name: $Format:%(describe:tags=true)$ +ref-names: $Format:%D$ diff --git a/.gitattributes b/.gitattributes index 8a1657abfab3..a0c2c8627af7 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,6 @@ * text=auto +*.m diff=objc *.ppm binary *.svg binary *.svg linguist-language=true -lib/matplotlib/_version.py export-subst +.git_archival.txt export-subst diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 2bef7ab95a56..5c9afed3c02b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1,3 +1,3 @@ # These are supported funding model platforms -github: [numfocus] +github: [matplotlib, numfocus] custom: https://numfocus.org/donate-to-matplotlib diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index 35a1beb9b2a6..000000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -name: Bug Report -about: Report a bug or issue with Matplotlib ---- - - - - -### Bug report - -**Bug summary** - - - -**Code for reproduction** - - - -```python -# Paste your code here -# -# -``` - -**Actual outcome** - - - -``` -# If applicable, paste the console output here -# -# -``` - -**Expected outcome** - - - - -**Matplotlib version** - - * Operating system: - * Matplotlib version (`import matplotlib; print(matplotlib.__version__)`): - * Matplotlib backend (`print(matplotlib.get_backend())`): - * Python version: - * Jupyter version (if applicable): - * Other libraries: - - - - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 000000000000..985762649b67 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,86 @@ +name: Bug Report +description: Report a bug or issue with Matplotlib. +title: "[Bug]: " +body: + - type: textarea + id: summary + attributes: + label: Bug summary + description: Describe the bug in 1-2 short sentences + placeholder: + value: + validations: + required: true + - type: textarea + id: reproduction + attributes: + label: Code for reproduction + description: | + If possible, please provide a minimum self-contained example. + placeholder: Paste your code here. This field is automatically formatted as Python code. + render: python + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual outcome + description: | + Paste the output produced by the code provided above, e.g. + console output, images/videos produced by the code, any relevant screenshots/screencasts, etc. + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected outcome + description: Describe (or provide a visual example of) the expected outcome from the code snippet. + validations: + required: true + - type: textarea + id: details + attributes: + label: Additional information + description: | + - What are the conditions under which this bug happens? input parameters, edge cases, etc? + - Has this worked in earlier versions? + - Do you know why this bug is happening? + - Do you maybe even know a fix? + - type: input + id: operating-system + attributes: + label: Operating system + description: Windows, OS/X, Arch, Debian, Ubuntu, etc. + - type: input + id: matplotlib-version + attributes: + label: Matplotlib Version + description: "From Python prompt: `import matplotlib; print(matplotlib.__version__)`" + validations: + required: true + - type: input + id: matplotlib-backend + attributes: + label: Matplotlib Backend + description: "From Python prompt: `import matplotlib; print(matplotlib.get_backend())`" + - type: input + id: python-version + attributes: + label: Python version + description: "In console: `python --version`" + - type: input + id: jupyter-version + attributes: + label: Jupyter version + description: "In console: `jupyter notebook --version` or `jupyter lab --version`" + - type: dropdown + id: install + attributes: + label: Installation + description: How did you install matplotlib? + options: + - pip + - conda + - Linux package manager + - from source (.tar.gz) + - git checkout diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.md deleted file mode 100644 index c0857f636db6..000000000000 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ /dev/null @@ -1,35 +0,0 @@ ---- -name: Documentation improvement -about: Create a report to help us improve the documentation -labels: Documentation ---- - - - - -### Problem - - - - -### Suggested Improvement - - - -**Matplotlib version** - - * Operating system: - * Matplotlib version: (`import matplotlib; print(matplotlib.__version__)`) - * Matplotlib documentation version: (is listed under the logo) \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 000000000000..ea0eb385baaf --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,32 @@ +name: Documentation +description: Create a report to help us improve the documentation +title: "[Doc]: " +labels: [Documentation] +body: + - type: input + id: link + attributes: + label: Documentation Link + description: | + Link to any documentation or examples that you are referencing. + Suggested improvements should be based on the development version of the docs: https://matplotlib.org/devdocs/ + placeholder: https://matplotlib.org/devdocs/... + - type: textarea + id: problem + attributes: + label: Problem + description: What is missing, unclear, or wrong in the documentation? + placeholder: | + * I found [...] to be unclear because [...] + * [...] made me think that [...] when really it should be [...] + * There is no example showing how to do [...] + validations: + required: true + - type: textarea + id: improvement + attributes: + label: Suggested improvement + placeholder: | + * This line should be be changed to say [...] + * Include a paragraph explaining [...] + * Add a figure showing [...] diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 6ca57f1ce8fa..000000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -name: Feature Request -about: Suggest something to add to Matplotlib -labels: New feature ---- - - - -### Problem - - - -### Proposed Solution - - - -### Additional context and prior art - - \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 000000000000..5274c287569c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,25 @@ +name: Feature Request +description: Suggest something to add to Matplotlib! +title: "[ENH]: " +labels: [New feature] +body: + - type: markdown + attributes: + value: | + Please search the [issues](https://github.com/matplotlib/matplotlib/issues) for relevant feature requests before creating a new feature request. + - type: textarea + id: problem + attributes: + label: Problem + description: Briefly describe the problem this feature will solve. (2-4 sentences) + placeholder: | + * I'm always frustrated when [...] because [...] + * I would like it if [...] happened when I [...] because [...] + * Here is a sample image of what I am asking for [...] + validations: + required: true + - type: textarea + id: solution + attributes: + label: Proposed solution + description: Describe a way to accomplish the goals of this feature request. diff --git a/.github/ISSUE_TEMPLATE/maintenance.md b/.github/ISSUE_TEMPLATE/maintenance.md deleted file mode 100644 index a72282892d85..000000000000 --- a/.github/ISSUE_TEMPLATE/maintenance.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: Maintenance -about: Help improve performance, usability and/or consistency. -labels: Maintenance ---- - -### Describe the issue - -**Summary** - - - -### Proposed fix - diff --git a/.github/ISSUE_TEMPLATE/maintenance.yml b/.github/ISSUE_TEMPLATE/maintenance.yml new file mode 100644 index 000000000000..746ab55ef0e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/maintenance.yml @@ -0,0 +1,17 @@ +name: Maintenance +description: Help improve performance, usability and/or consistency. +title: "[MNT]: " +labels: [Maintenance] +body: + - type: textarea + id: summary + attributes: + label: Summary + description: Please provide 1-2 short sentences that succinctly describes what could be improved. + validations: + required: true + - type: textarea + id: fix + attributes: + label: Proposed fix + description: Please describe how you think this could be improved. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 6c3479d74741..9b2f5b5c7275 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -4,13 +4,15 @@ -- [ ] Has pytest style unit tests (and `pytest` passes). -- [ ] Is [Flake 8](https://flake8.pycqa.org/en/latest/) compliant (run `flake8` on changed files to check). -- [ ] New features are documented, with examples if plot related. +**Documentation and Tests** +- [ ] Has pytest style unit tests (and `pytest` passes) - [ ] Documentation is sphinx and numpydoc compliant (the docs should [build](https://matplotlib.org/devel/documenting_mpl.html#building-the-docs) without error). -- [ ] Conforms to Matplotlib style conventions (install `flake8-docstrings` and run `flake8 --docstring-convention=all`). -- [ ] New features have an entry in `doc/users/next_whats_new/` (follow instructions in README.rst there). -- [ ] API changes documented in `doc/api/next_api_changes/` (follow instructions in README.rst there). +- [ ] New plotting related features are documented with examples. + +**Release Notes** +- [ ] New features are marked with a `.. versionadded::` directive in the docstring and documented in `doc/users/next_whats_new/` +- [ ] API changes are marked with a `.. versionchanged::` directive in the docstring and documented in `doc/api/next_api_changes/` +- [ ] Release notes conform with instructions in `next_whats_new/README.rst` or `next_api_changes/README.rst` + + + + + + + + image/svg+xml + + + + + + + + width + + + + + + + + + + + + + + + + + + + headaxislength + headlength + + + + + + + + + + + + headwidth + + + + + + + + + length + + + + + + + + + diff --git a/doc/_static/switcher.json b/doc/_static/switcher.json new file mode 100644 index 000000000000..b96977bdd725 --- /dev/null +++ b/doc/_static/switcher.json @@ -0,0 +1,37 @@ +[ + { + "name": "3.7 (stable)", + "version": "stable", + "url": "https://matplotlib.org/stable/" + }, + { + "name": "3.8 (dev)", + "version": "dev", + "url": "https://matplotlib.org/devdocs/" + }, + { + "name": "3.6", + "version": "3.6.3", + "url": "https://matplotlib.org/3.6.3/" + }, + { + "name": "3.5", + "version": "3.5.3", + "url": "https://matplotlib.org/3.5.3/" + }, + { + "name": "3.4", + "version": "3.4.3", + "url": "https://matplotlib.org/3.4.3/" + }, + { + "name": "3.3", + "version": "3.3.4", + "url": "https://matplotlib.org/3.3.4/" + }, + { + "name": "2.2", + "version": "2.2.4", + "url": "https://matplotlib.org/2.2.4/" + } +] diff --git a/doc/_static/zenodo_cache/5194481.svg b/doc/_static/zenodo_cache/5194481.svg new file mode 100644 index 000000000000..728ae0c15a6a --- /dev/null +++ b/doc/_static/zenodo_cache/5194481.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.5194481 + + + 10.5281/zenodo.5194481 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/5706396.svg b/doc/_static/zenodo_cache/5706396.svg new file mode 100644 index 000000000000..54718543c9c8 --- /dev/null +++ b/doc/_static/zenodo_cache/5706396.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.5706396 + + + 10.5281/zenodo.5706396 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/5773480.svg b/doc/_static/zenodo_cache/5773480.svg new file mode 100644 index 000000000000..431dbd803973 --- /dev/null +++ b/doc/_static/zenodo_cache/5773480.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.5773480 + + + 10.5281/zenodo.5773480 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/6513224.svg b/doc/_static/zenodo_cache/6513224.svg new file mode 100644 index 000000000000..fd54dfcb9abb --- /dev/null +++ b/doc/_static/zenodo_cache/6513224.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.6513224 + + + 10.5281/zenodo.6513224 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/6982547.svg b/doc/_static/zenodo_cache/6982547.svg new file mode 100644 index 000000000000..6eb000d892da --- /dev/null +++ b/doc/_static/zenodo_cache/6982547.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.6982547 + + + 10.5281/zenodo.6982547 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/7084615.svg b/doc/_static/zenodo_cache/7084615.svg new file mode 100644 index 000000000000..9bb362063414 --- /dev/null +++ b/doc/_static/zenodo_cache/7084615.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.7084615 + + + 10.5281/zenodo.7084615 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/7162185.svg b/doc/_static/zenodo_cache/7162185.svg new file mode 100644 index 000000000000..ea0966377194 --- /dev/null +++ b/doc/_static/zenodo_cache/7162185.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.7162185 + + + 10.5281/zenodo.7162185 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/7275322.svg b/doc/_static/zenodo_cache/7275322.svg new file mode 100644 index 000000000000..2d0fd408b504 --- /dev/null +++ b/doc/_static/zenodo_cache/7275322.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.7275322 + + + 10.5281/zenodo.7275322 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/7527665.svg b/doc/_static/zenodo_cache/7527665.svg new file mode 100644 index 000000000000..3c3e0b7a8b2a --- /dev/null +++ b/doc/_static/zenodo_cache/7527665.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.7527665 + + + 10.5281/zenodo.7527665 + + + \ No newline at end of file diff --git a/doc/_static/zenodo_cache/7637593.svg b/doc/_static/zenodo_cache/7637593.svg new file mode 100644 index 000000000000..4e91dea5e805 --- /dev/null +++ b/doc/_static/zenodo_cache/7637593.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + DOI + + + DOI + + + 10.5281/zenodo.7637593 + + + 10.5281/zenodo.7637593 + + + \ No newline at end of file diff --git a/doc/_templates/automodule.rst b/doc/_templates/automodule.rst index e9f2a755d413..984c12e00d03 100644 --- a/doc/_templates/automodule.rst +++ b/doc/_templates/automodule.rst @@ -5,7 +5,7 @@ treat this separately (sphinx-doc/sphinx/issues/4874) .. automodule:: {{ fullname }} :members: - + {% else %} .. automodule:: {{ fullname }} @@ -18,7 +18,7 @@ Classes ------- -.. autosummary:: +.. autosummary:: :template: autosummary.rst :toctree: {% for item in classes %}{% if item not in ['zip', 'map', 'reduce'] %} @@ -32,7 +32,7 @@ Classes Functions --------- -.. autosummary:: +.. autosummary:: :template: autosummary.rst :toctree: diff --git a/doc/_templates/autosummary.rst b/doc/_templates/autosummary.rst index bf318f1d9aff..c5f90e87f016 100644 --- a/doc/_templates/autosummary.rst +++ b/doc/_templates/autosummary.rst @@ -7,7 +7,7 @@ {% if objtype in ['class'] %} .. auto{{ objtype }}:: {{ objname }} :show-inheritance: - :special-members: + :special-members: __call__ {% else %} .. auto{{ objtype }}:: {{ objname }} diff --git a/doc/_templates/autosummary_class_only.rst b/doc/_templates/autosummary_class_only.rst new file mode 100644 index 000000000000..6611f04f5c0d --- /dev/null +++ b/doc/_templates/autosummary_class_only.rst @@ -0,0 +1,11 @@ +{{ fullname | escape | underline }} + + +.. currentmodule:: {{ module }} + + +{% if objtype in ['class'] %} +.. auto{{ objtype }}:: {{ objname }} + :no-members: + +{% endif %} diff --git a/doc/_templates/cheatsheet_sidebar.html b/doc/_templates/cheatsheet_sidebar.html new file mode 100644 index 000000000000..3f2b7c4f4db1 --- /dev/null +++ b/doc/_templates/cheatsheet_sidebar.html @@ -0,0 +1,9 @@ + + diff --git a/doc/_templates/donate_sidebar.html b/doc/_templates/donate_sidebar.html index fc7310b70088..071c92888c3c 100644 --- a/doc/_templates/donate_sidebar.html +++ b/doc/_templates/donate_sidebar.html @@ -1,6 +1,5 @@ - - - - - -
-

Matplotlib cheatsheets

- - Matplotlib cheatsheets - -
diff --git a/doc/api/_enums_api.rst b/doc/api/_enums_api.rst index c9e283305967..c38d535f3573 100644 --- a/doc/api/_enums_api.rst +++ b/doc/api/_enums_api.rst @@ -12,4 +12,3 @@ .. autoclass:: CapStyle :members: demo :exclude-members: butt, round, projecting, input_description - diff --git a/doc/api/afm_api.rst b/doc/api/afm_api.rst index 7e6d307cca33..bcae04150909 100644 --- a/doc/api/afm_api.rst +++ b/doc/api/afm_api.rst @@ -2,7 +2,12 @@ ``matplotlib.afm`` ****************** -.. automodule:: matplotlib.afm +.. attention:: + This module is considered internal. + + Its use is deprecated and it will be removed in a future version. + +.. automodule:: matplotlib._afm :members: :undoc-members: :show-inheritance: diff --git a/doc/api/animation_api.rst b/doc/api/animation_api.rst index c8edde884046..d1b81e20b5c8 100644 --- a/doc/api/animation_api.rst +++ b/doc/api/animation_api.rst @@ -11,12 +11,16 @@ :local: :backlinks: entry + Animation ========= -The easiest way to make a live animation in matplotlib is to use one of the +The easiest way to make a live animation in Matplotlib is to use one of the `Animation` classes. +.. inheritance-diagram:: matplotlib.animation.FuncAnimation matplotlib.animation.ArtistAnimation + :parts: 1 + .. autosummary:: :toctree: _as_gen :nosignatures: @@ -29,10 +33,11 @@ In both cases it is critical to keep a reference to the instance object. The animation is advanced by a timer (typically from the host GUI framework) which the `Animation` object holds the only reference to. If you do not hold a reference to the `Animation` object, it (and -hence the timers), will be garbage collected which will stop the +hence the timers) will be garbage collected which will stop the animation. -To save an animation to disk use `Animation.save` or `Animation.to_html5_video` +To save an animation use `Animation.save`, `Animation.to_html5_video`, +or `Animation.to_jshtml`. See :ref:`ani_writer_classes` below for details about what movie formats are supported. @@ -46,9 +51,9 @@ supported. The inner workings of `FuncAnimation` is more-or-less:: for d in frames: - artists = func(d, *fargs) - fig.canvas.draw_idle() - fig.canvas.start_event_loop(interval) + artists = func(d, *fargs) + fig.canvas.draw_idle() + fig.canvas.start_event_loop(interval) with details to handle 'blitting' (to dramatically improve the live performance), to be non-blocking, not repeatedly start/stop the GUI @@ -92,13 +97,18 @@ this hopefully minimalist example gives a sense of how ``init_func`` and ``func`` are used inside of `FuncAnimation` and the theory of how 'blitting' works. +.. note:: + + The zorder of artists is not taken into account when 'blitting' + because the 'blitted' artists are always drawn on top. + The expected signature on ``func`` and ``init_func`` is very simple to keep `FuncAnimation` out of your book keeping and plotting logic, but this means that the callable objects you pass in must know what artists they should be working on. There are several approaches to handling this, of varying complexity and encapsulation. The simplest approach, which works quite well in the case of a script, is to define the -artist at a global scope and let Python sort things out. For example :: +artist at a global scope and let Python sort things out. For example:: import numpy as np import matplotlib.pyplot as plt @@ -106,7 +116,7 @@ artist at a global scope and let Python sort things out. For example :: fig, ax = plt.subplots() xdata, ydata = [], [] - ln, = plt.plot([], [], 'ro') + ln, = ax.plot([], [], 'ro') def init(): ax.set_xlim(0, 2*np.pi) @@ -123,8 +133,36 @@ artist at a global scope and let Python sort things out. For example :: init_func=init, blit=True) plt.show() -The second method is to use `functools.partial` to 'bind' artists to -function. A third method is to use closures to build up the required +The second method is to use `functools.partial` to pass arguments to the +function:: + + import numpy as np + import matplotlib.pyplot as plt + from matplotlib.animation import FuncAnimation + from functools import partial + + fig, ax = plt.subplots() + line1, = ax.plot([], [], 'ro') + + def init(): + ax.set_xlim(0, 2*np.pi) + ax.set_ylim(-1, 1) + return line1, + + def update(frame, ln, x, y): + x.append(frame) + y.append(np.sin(frame)) + ln.set_data(x, y) + return ln, + + ani = FuncAnimation( + fig, partial(update, ln=line1, x=[], y=[]), + frames=np.linspace(0, 2*np.pi, 128), + init_func=init, blit=True) + + plt.show() + +A third method is to use closures to build up the required artists and functions. A fourth method is to create a class. Examples @@ -157,6 +195,10 @@ Examples Writer Classes ============== +.. inheritance-diagram:: matplotlib.animation.FFMpegFileWriter matplotlib.animation.FFMpegWriter matplotlib.animation.ImageMagickFileWriter matplotlib.animation.ImageMagickWriter matplotlib.animation.PillowWriter matplotlib.animation.HTMLWriter + :top-classes: matplotlib.animation.AbstractMovieWriter + :parts: 1 + The provided writers fall into a few broad categories. The Pillow writer relies on the Pillow library to write the animation, keeping @@ -186,7 +228,6 @@ on all systems. FFMpegWriter ImageMagickWriter - AVConvWriter The file-based writers save temporary files for each frame which are stitched into a single file at the end. Although slower, these writers can be easier to @@ -198,18 +239,19 @@ debug. FFMpegFileWriter ImageMagickFileWriter - AVConvFileWriter -Fundamentally, a `MovieWriter` provides a way to grab sequential frames -from the same underlying `~matplotlib.figure.Figure` object. The base -class `MovieWriter` implements 3 methods and a context manager. The -only difference between the pipe-based and file-based writers is in the -arguments to their respective ``setup`` methods. +The writer classes provide a way to grab sequential frames from the same +underlying `~matplotlib.figure.Figure`. They all provide three methods that +must be called in sequence: -The ``setup()`` method is used to prepare the writer (possibly opening -a pipe), successive calls to ``grab_frame()`` capture a single frame -at a time and ``finish()`` finalizes the movie and writes the output -file to disk. For example :: +- `~.AbstractMovieWriter.setup` prepares the writer (e.g. opening a pipe). + Pipe-based and file-based writers take different arguments to ``setup()``. +- `~.AbstractMovieWriter.grab_frame` can then be called as often as + needed to capture a single frame at a time +- `~.AbstractMovieWriter.finish` finalizes the movie and writes the output + file to disk. + +Example:: moviewriter = MovieWriter(...) moviewriter.setup(fig, 'my_movie.ext', dpi=100) @@ -219,14 +261,14 @@ file to disk. For example :: moviewriter.finish() If using the writer classes directly (not through `Animation.save`), it is -strongly encouraged to use the `~MovieWriter.saving` context manager :: +strongly encouraged to use the `~.AbstractMovieWriter.saving` context manager:: with moviewriter.saving(fig, 'myfile.mp4', dpi=100): for j in range(n): update_figure(j) moviewriter.grab_frame() -to ensures that setup and cleanup are performed as necessary. +to ensure that setup and cleanup are performed as necessary. Examples -------- @@ -283,21 +325,9 @@ and mixins :toctree: _as_gen :nosignatures: - AVConvBase FFMpegBase ImageMagickBase are provided. See the source code for how to easily implement new `MovieWriter` classes. - -Inheritance Diagrams -==================== - -.. inheritance-diagram:: matplotlib.animation.FuncAnimation matplotlib.animation.ArtistAnimation - :private-bases: - :parts: 1 - -.. inheritance-diagram:: matplotlib.animation.AVConvFileWriter matplotlib.animation.AVConvWriter matplotlib.animation.FFMpegFileWriter matplotlib.animation.FFMpegWriter matplotlib.animation.ImageMagickFileWriter matplotlib.animation.ImageMagickWriter - :private-bases: - :parts: 1 diff --git a/doc/api/api_changes.rst b/doc/api/api_changes.rst deleted file mode 100644 index d64664e2ffdb..000000000000 --- a/doc/api/api_changes.rst +++ /dev/null @@ -1,42 +0,0 @@ - -=========== -API Changes -=========== - -If updating Matplotlib breaks your scripts, this list may help you figure out -what caused the breakage and how to fix it by updating your code. - -For API changes in older versions see - -.. toctree:: - :maxdepth: 1 - - api_changes_old - -Changes for the latest version are listed below. For new features that were -added to Matplotlib, see :ref:`whats-new` - -.. ifconfig:: releaselevel == 'dev' - - .. note:: - - The list below is a table of contents of individual files from the - most recent :file:`api_changes_X.Y` folder. - - When a release is made - - - The include directive below should be changed to point to the new file - created in the previous step. - - - .. toctree:: - :glob: - :maxdepth: 1 - - next_api_changes/behavior/* - next_api_changes/deprecations/* - next_api_changes/development/* - next_api_changes/removals/* - -.. include:: prev_api_changes/api_changes_3.4.2.rst -.. include:: prev_api_changes/api_changes_3.4.0.rst diff --git a/doc/api/api_changes_old.rst b/doc/api/api_changes_old.rst deleted file mode 100644 index ab9381680498..000000000000 --- a/doc/api/api_changes_old.rst +++ /dev/null @@ -1,11 +0,0 @@ - -================ - Old API Changes -================ - -.. toctree:: - :glob: - :reversed: - :maxdepth: 1 - - prev_api_changes/* diff --git a/doc/api/artist_api.rst b/doc/api/artist_api.rst index d77f27f0960f..3903bbd5924d 100644 --- a/doc/api/artist_api.rst +++ b/doc/api/artist_api.rst @@ -4,16 +4,17 @@ ``matplotlib.artist`` ********************* -.. inheritance-diagram:: matplotlib.axes._axes.Axes matplotlib.axes._base._AxesBase matplotlib.axis.Axis matplotlib.axis.Tick matplotlib.axis.XAxis matplotlib.axis.XTick matplotlib.axis.YAxis matplotlib.axis.YTick matplotlib.collections.AsteriskPolygonCollection matplotlib.collections.BrokenBarHCollection matplotlib.collections.CircleCollection matplotlib.collections.Collection matplotlib.collections.EllipseCollection matplotlib.collections.EventCollection matplotlib.collections.LineCollection matplotlib.collections.PatchCollection matplotlib.collections.PathCollection matplotlib.collections.PolyCollection matplotlib.collections.QuadMesh matplotlib.collections.RegularPolyCollection matplotlib.collections.StarPolygonCollection matplotlib.collections.TriMesh matplotlib.collections._CollectionWithSizes matplotlib.contour.ClabelText matplotlib.figure.Figure matplotlib.image.AxesImage matplotlib.image.BboxImage matplotlib.image.FigureImage matplotlib.image.NonUniformImage matplotlib.image.PcolorImage matplotlib.image._ImageBase matplotlib.legend.Legend matplotlib.lines.Line2D matplotlib.offsetbox.AnchoredOffsetbox matplotlib.offsetbox.AnchoredText matplotlib.offsetbox.AnnotationBbox matplotlib.offsetbox.AuxTransformBox matplotlib.offsetbox.DrawingArea matplotlib.offsetbox.HPacker matplotlib.offsetbox.OffsetBox matplotlib.offsetbox.OffsetImage matplotlib.offsetbox.PackerBase matplotlib.offsetbox.PaddedBox matplotlib.offsetbox.TextArea matplotlib.offsetbox.VPacker matplotlib.patches.Arc matplotlib.patches.Arrow matplotlib.patches.Circle matplotlib.patches.CirclePolygon matplotlib.patches.ConnectionPatch matplotlib.patches.Ellipse matplotlib.patches.FancyArrow matplotlib.patches.FancyArrowPatch matplotlib.patches.FancyBboxPatch matplotlib.patches.Patch matplotlib.patches.PathPatch matplotlib.patches.StepPatch matplotlib.patches.Polygon matplotlib.patches.Rectangle matplotlib.patches.RegularPolygon matplotlib.patches.Shadow matplotlib.patches.Wedge matplotlib.projections.geo.AitoffAxes matplotlib.projections.geo.GeoAxes matplotlib.projections.geo.HammerAxes matplotlib.projections.geo.LambertAxes matplotlib.projections.geo.MollweideAxes matplotlib.projections.polar.PolarAxes matplotlib.quiver.Barbs matplotlib.quiver.Quiver matplotlib.quiver.QuiverKey matplotlib.spines.Spine matplotlib.table.Cell matplotlib.table.CustomCell matplotlib.table.Table matplotlib.text.Annotation matplotlib.text.Text - :parts: 1 - :private-bases: - - - .. automodule:: matplotlib.artist :no-members: :no-undoc-members: +Inheritance Diagrams +==================== + +.. inheritance-diagram:: matplotlib.axes._axes.Axes matplotlib.axes._base._AxesBase matplotlib.axis.Axis matplotlib.axis.Tick matplotlib.axis.XAxis matplotlib.axis.XTick matplotlib.axis.YAxis matplotlib.axis.YTick matplotlib.collections.AsteriskPolygonCollection matplotlib.collections.BrokenBarHCollection matplotlib.collections.CircleCollection matplotlib.collections.Collection matplotlib.collections.EllipseCollection matplotlib.collections.EventCollection matplotlib.collections.LineCollection matplotlib.collections.PatchCollection matplotlib.collections.PathCollection matplotlib.collections.PolyCollection matplotlib.collections.QuadMesh matplotlib.collections.RegularPolyCollection matplotlib.collections.StarPolygonCollection matplotlib.collections.TriMesh matplotlib.collections._CollectionWithSizes matplotlib.contour.ClabelText matplotlib.contour.ContourSet matplotlib.contour.QuadContourSet matplotlib.figure.FigureBase matplotlib.figure.Figure matplotlib.figure.SubFigure matplotlib.image.AxesImage matplotlib.image.BboxImage matplotlib.image.FigureImage matplotlib.image.NonUniformImage matplotlib.image.PcolorImage matplotlib.image._ImageBase matplotlib.legend.Legend matplotlib.lines.Line2D matplotlib.offsetbox.AnchoredOffsetbox matplotlib.offsetbox.AnchoredText matplotlib.offsetbox.AnnotationBbox matplotlib.offsetbox.AuxTransformBox matplotlib.offsetbox.DrawingArea matplotlib.offsetbox.HPacker matplotlib.offsetbox.OffsetBox matplotlib.offsetbox.OffsetImage matplotlib.offsetbox.PackerBase matplotlib.offsetbox.PaddedBox matplotlib.offsetbox.TextArea matplotlib.offsetbox.VPacker matplotlib.patches.Annulus matplotlib.patches.Arc matplotlib.patches.Arrow matplotlib.patches.Circle matplotlib.patches.CirclePolygon matplotlib.patches.ConnectionPatch matplotlib.patches.Ellipse matplotlib.patches.FancyArrow matplotlib.patches.FancyArrowPatch matplotlib.patches.FancyBboxPatch matplotlib.patches.Patch matplotlib.patches.PathPatch matplotlib.patches.Polygon matplotlib.patches.Rectangle matplotlib.patches.RegularPolygon matplotlib.patches.Shadow matplotlib.patches.StepPatch matplotlib.patches.Wedge matplotlib.projections.geo.AitoffAxes matplotlib.projections.geo.GeoAxes matplotlib.projections.geo.HammerAxes matplotlib.projections.geo.LambertAxes matplotlib.projections.geo.MollweideAxes matplotlib.projections.polar.PolarAxes matplotlib.projections.polar.RadialAxis matplotlib.projections.polar.RadialTick matplotlib.projections.polar.ThetaAxis matplotlib.projections.polar.ThetaTick matplotlib.quiver.Barbs matplotlib.quiver.Quiver matplotlib.quiver.QuiverKey matplotlib.spines.Spine matplotlib.table.Cell matplotlib.table.Table matplotlib.text.Annotation matplotlib.text.Text matplotlib.tri.TriContourSet + :parts: 1 + :private-bases: + ``Artist`` class ================ @@ -26,6 +27,7 @@ Interactive ----------- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -34,10 +36,10 @@ Interactive Artist.pchanged Artist.get_cursor_data Artist.format_cursor_data + Artist.set_mouseover + Artist.get_mouseover Artist.mouseover Artist.contains - Artist.set_contains - Artist.get_contains Artist.pick Artist.pickable Artist.set_picker @@ -47,6 +49,7 @@ Clipping -------- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -61,6 +64,7 @@ Bulk Properties --------------- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -73,6 +77,7 @@ Drawing ------- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -100,12 +105,14 @@ Drawing Artist.get_agg_filter Artist.get_window_extent + Artist.get_tightbbox Artist.get_transformed_clip_path_and_affine Figure and Axes --------------- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -120,6 +127,7 @@ Children -------- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -130,6 +138,7 @@ Transform --------- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -141,6 +150,7 @@ Units ----- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -152,6 +162,7 @@ Metadata -------- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -166,6 +177,7 @@ Miscellaneous ------------- .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: @@ -178,6 +190,7 @@ Functions ========= .. autosummary:: + :template: autosummary.rst :toctree: _as_gen :nosignatures: diff --git a/doc/api/axes_api.rst b/doc/api/axes_api.rst index 6024a79bd81c..8d0951626f72 100644 --- a/doc/api/axes_api.rst +++ b/doc/api/axes_api.rst @@ -2,6 +2,10 @@ ``matplotlib.axes`` ******************* +The `~.axes.Axes` class represents one (sub-)plot in a figure. It contains the +plotted data, axis ticks, labels, title, legend, etc. Its methods are the main +interface for manipulating the plot. + .. currentmodule:: matplotlib.axes .. contents:: Table of Contents @@ -14,30 +18,15 @@ :no-members: :no-undoc-members: -Inheritance -=========== -.. inheritance-diagram:: matplotlib.axes.Axes - :private-bases: - The Axes class ============== -.. autoclass:: Axes - :no-members: - :no-undoc-members: - :show-inheritance: - - -Subplots -======== - .. autosummary:: :toctree: _as_gen - :template: autosummary.rst + :template: autosummary_class_only.rst :nosignatures: - SubplotBase - subplot_class_factory + Axes Plotting ======== @@ -295,7 +284,6 @@ Axis limits and direction Axes.get_ylim Axes.update_datalim - Axes.update_datalim_bounds Axes.set_xbound Axes.get_xbound @@ -314,6 +302,7 @@ Axis labels, title, and legend Axes.get_xlabel Axes.set_ylabel Axes.get_ylabel + Axes.label_outer Axes.set_title Axes.get_title @@ -485,6 +474,9 @@ Axes position Axes.get_axes_locator Axes.set_axes_locator + Axes.get_subplotspec + Axes.set_subplotspec + Axes.reset_position Axes.get_position @@ -607,3 +599,6 @@ Other Axes.get_default_bbox_extra_artists Axes.get_transformed_clip_path_and_affine Axes.has_data + Axes.set + +.. autoclass:: matplotlib.axes.Axes.ArtistList diff --git a/doc/api/axis_api.rst b/doc/api/axis_api.rst index 4bc9ad0c6e89..e7da26a11706 100644 --- a/doc/api/axis_api.rst +++ b/doc/api/axis_api.rst @@ -41,7 +41,6 @@ Inheritance :nosignatures: Axis.clear - Axis.cla Axis.get_scale @@ -101,6 +100,7 @@ Ticks, tick labels and Offset text Axis.get_offset_text Axis.get_tick_padding + Axis.get_tick_params Axis.get_ticklabels Axis.get_ticklines Axis.get_ticklocs @@ -151,6 +151,7 @@ Interactive :nosignatures: Axis.contains + Axis.pickradius Axis.get_pickradius Axis.set_pickradius @@ -169,17 +170,6 @@ Units Axis.update_units -Incremental navigation ----------------------- - -.. autosummary:: - :toctree: _as_gen - :template: autosummary.rst - :nosignatures: - - Axis.pan - Axis.zoom - XAxis Specific -------------- @@ -192,6 +182,7 @@ XAxis Specific XAxis.get_text_heights XAxis.get_ticks_position XAxis.set_ticks_position + XAxis.set_label_position XAxis.tick_bottom XAxis.tick_top @@ -208,6 +199,7 @@ YAxis Specific YAxis.get_ticks_position YAxis.set_offset_position YAxis.set_ticks_position + YAxis.set_label_position YAxis.tick_left YAxis.tick_right @@ -266,8 +258,6 @@ specify a matching series of labels. Calling ``set_ticks`` makes a :template: autosummary.rst :nosignatures: - - Tick.apply_tickdir Tick.get_loc Tick.get_pad Tick.get_pad_pixels @@ -277,4 +267,5 @@ specify a matching series of labels. Calling ``set_ticks`` makes a Tick.set_label1 Tick.set_label2 Tick.set_pad + Tick.set_url Tick.update_position diff --git a/doc/api/backend_agg_api.rst b/doc/api/backend_agg_api.rst index 40c8cd4bce6a..752f348f8747 100644 --- a/doc/api/backend_agg_api.rst +++ b/doc/api/backend_agg_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backends.backend_agg` -====================================== +*********************************** +``matplotlib.backends.backend_agg`` +*********************************** .. automodule:: matplotlib.backends.backend_agg :members: diff --git a/doc/api/backend_bases_api.rst b/doc/api/backend_bases_api.rst index 990a1a091f81..c98a6af3e05e 100644 --- a/doc/api/backend_bases_api.rst +++ b/doc/api/backend_bases_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backend_bases` -================================ +**************************** +``matplotlib.backend_bases`` +**************************** .. automodule:: matplotlib.backend_bases :members: diff --git a/doc/api/backend_cairo_api.rst b/doc/api/backend_cairo_api.rst index 2623270c6781..66371ec6895c 100644 --- a/doc/api/backend_cairo_api.rst +++ b/doc/api/backend_cairo_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backends.backend_cairo` -======================================== +************************************* +``matplotlib.backends.backend_cairo`` +************************************* .. automodule:: matplotlib.backends.backend_cairo :members: diff --git a/doc/api/backend_gtk3_api.rst b/doc/api/backend_gtk3_api.rst index 5e17df66d602..66c247555df4 100644 --- a/doc/api/backend_gtk3_api.rst +++ b/doc/api/backend_gtk3_api.rst @@ -1,16 +1,12 @@ -**NOTE** These backends are not documented here, to avoid adding a dependency -to building the docs. +********************************************************************************** +``matplotlib.backends.backend_gtk3agg``, ``matplotlib.backends.backend_gtk3cairo`` +********************************************************************************** + +**NOTE** These :ref:`backends` are not documented here, to avoid adding a +dependency to building the docs. .. redirect-from:: /api/backend_gtk3agg_api .. redirect-from:: /api/backend_gtk3cairo_api - -:mod:`matplotlib.backends.backend_gtk3agg` -========================================== - .. module:: matplotlib.backends.backend_gtk3agg - -:mod:`matplotlib.backends.backend_gtk3cairo` -============================================ - .. module:: matplotlib.backends.backend_gtk3cairo diff --git a/doc/api/backend_gtk4_api.rst b/doc/api/backend_gtk4_api.rst new file mode 100644 index 000000000000..8f2e38d885a8 --- /dev/null +++ b/doc/api/backend_gtk4_api.rst @@ -0,0 +1,12 @@ +********************************************************************************** +``matplotlib.backends.backend_gtk4agg``, ``matplotlib.backends.backend_gtk4cairo`` +********************************************************************************** + +**NOTE** These :ref:`backends` are not documented here, to avoid adding a +dependency to building the docs. + +.. redirect-from:: /api/backend_gtk4agg_api +.. redirect-from:: /api/backend_gtk4cairo_api + +.. module:: matplotlib.backends.backend_gtk4agg +.. module:: matplotlib.backends.backend_gtk4cairo diff --git a/doc/api/backend_managers_api.rst b/doc/api/backend_managers_api.rst index faf4eda18de3..3e77e89dbbce 100644 --- a/doc/api/backend_managers_api.rst +++ b/doc/api/backend_managers_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backend_managers` -================================== +******************************* +``matplotlib.backend_managers`` +******************************* .. automodule:: matplotlib.backend_managers :members: diff --git a/doc/api/backend_mixed_api.rst b/doc/api/backend_mixed_api.rst index 7457f6684f94..61d770e56ccf 100644 --- a/doc/api/backend_mixed_api.rst +++ b/doc/api/backend_mixed_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backends.backend_mixed` -======================================== +************************************* +``matplotlib.backends.backend_mixed`` +************************************* .. automodule:: matplotlib.backends.backend_mixed :members: diff --git a/doc/api/backend_nbagg_api.rst b/doc/api/backend_nbagg_api.rst index 977eabce8db0..6596f461bbf0 100644 --- a/doc/api/backend_nbagg_api.rst +++ b/doc/api/backend_nbagg_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backends.backend_nbagg` -======================================== +************************************* +``matplotlib.backends.backend_nbagg`` +************************************* .. automodule:: matplotlib.backends.backend_nbagg :members: diff --git a/doc/api/backend_pdf_api.rst b/doc/api/backend_pdf_api.rst index ded143ddcf8d..014c3e6e5017 100644 --- a/doc/api/backend_pdf_api.rst +++ b/doc/api/backend_pdf_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backends.backend_pdf` -====================================== +*********************************** +``matplotlib.backends.backend_pdf`` +*********************************** .. automodule:: matplotlib.backends.backend_pdf :members: diff --git a/doc/api/backend_pgf_api.rst b/doc/api/backend_pgf_api.rst index ec7440080eb0..9f90beb72a1b 100644 --- a/doc/api/backend_pgf_api.rst +++ b/doc/api/backend_pgf_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backends.backend_pgf` -====================================== +*********************************** +``matplotlib.backends.backend_pgf`` +*********************************** .. automodule:: matplotlib.backends.backend_pgf :members: diff --git a/doc/api/backend_ps_api.rst b/doc/api/backend_ps_api.rst index 9d585be7a0ad..d9b07d961b4b 100644 --- a/doc/api/backend_ps_api.rst +++ b/doc/api/backend_ps_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backends.backend_ps` -===================================== +********************************** +``matplotlib.backends.backend_ps`` +********************************** .. automodule:: matplotlib.backends.backend_ps :members: diff --git a/doc/api/backend_qt_api.rst b/doc/api/backend_qt_api.rst index 90fe9bb95539..ebfeedceb6e1 100644 --- a/doc/api/backend_qt_api.rst +++ b/doc/api/backend_qt_api.rst @@ -1,27 +1,71 @@ -**NOTE** These backends are not documented here, to avoid adding a dependency -to building the docs. +****************************************************************************** +``matplotlib.backends.backend_qtagg``, ``matplotlib.backends.backend_qtcairo`` +****************************************************************************** + +**NOTE** These :ref:`backends` are not (auto) documented here, to avoid adding +a dependency to building the docs. .. redirect-from:: /api/backend_qt4agg_api .. redirect-from:: /api/backend_qt4cairo_api .. redirect-from:: /api/backend_qt5agg_api .. redirect-from:: /api/backend_qt5cairo_api -:mod:`matplotlib.backends.backend_qt4agg` -========================================= +.. module:: matplotlib.backends.qt_compat +.. module:: matplotlib.backends.backend_qt +.. module:: matplotlib.backends.backend_qtagg +.. module:: matplotlib.backends.backend_qtcairo +.. module:: matplotlib.backends.backend_qt5agg +.. module:: matplotlib.backends.backend_qt5cairo + +.. _QT_bindings: -.. module:: matplotlib.backends.backend_qt4agg +Qt Bindings +----------- -:mod:`matplotlib.backends.backend_qt4cairo` -=========================================== +There are currently 2 actively supported Qt versions, Qt5 and Qt6, and two +supported Python bindings per version -- `PyQt5 +`_ and `PySide2 +`_ for Qt5 and `PyQt6 +`_ and `PySide6 +`_ for Qt6 [#]_. Matplotlib's +qtagg and qtcairo backends (``matplotlib.backends.backend_qtagg`` and +``matplotlib.backend.backend_qtcairo``) support all these bindings, with common +parts factored out in the ``matplotlib.backends.backend_qt`` module. -.. module:: matplotlib.backends.backend_qt4cairo +At runtime, these backends select the actual binding used as follows: -:mod:`matplotlib.backends.backend_qt5agg` -========================================= +1. If a binding's ``QtCore`` subpackage is already imported, that binding is + selected (the order for the check is ``PyQt6``, ``PySide6``, ``PyQt5``, + ``PySide2``). +2. If the :envvar:`QT_API` environment variable is set to one of "PyQt6", + "PySide6", "PyQt5", "PySide2" (case-insensitive), that binding is selected. + (See also the documentation on :ref:`environment-variables`.) +3. Otherwise, the first available backend in the order ``PyQt6``, ``PySide6``, + ``PyQt5``, ``PySide2`` is selected. -.. module:: matplotlib.backends.backend_qt5agg +In the past, Matplotlib used to have separate backends for each version of Qt +(e.g. qt4agg/``matplotlib.backends.backend_qt4agg`` and +qt5agg/``matplotlib.backends.backend_qt5agg``). This scheme was dropped when +support for Qt6 was added. For back-compatibility, qt5agg/``backend_qt5agg`` +and qt5cairo/``backend_qt5cairo`` remain available; selecting one of these +backends forces the use of a Qt5 binding. Their use is discouraged and +``backend_qtagg`` or ``backend_qtcairo`` should be preferred instead. However, +these modules will not be deprecated until we drop support for Qt5. -:mod:`matplotlib.backends.backend_qt5cairo` -=========================================== +While both PyQt +and Qt for Python (aka PySide) closely mirror the underlying C++ API they are +wrapping, they are not drop-in replacements for each other [#]_. To account +for this, Matplotlib has an internal API compatibility layer in +`matplotlib.backends.qt_compat` which covers our needs. Despite being a public +module, we do not consider this to be a stable user-facing API and it may +change without warning [#]_. -.. module:: matplotlib.backends.backend_qt5cairo +.. [#] There is also `PyQt4 + `_ and `PySide + `_ for Qt4 but these are no + longer supported by Matplotlib and upstream support for Qt4 ended + in 2015. +.. [#] Despite the slight API differences, the more important distinction + between the PyQt and Qt for Python series of bindings is licensing. +.. [#] If you are looking for a general purpose compatibility library please + see `qtpy `_. diff --git a/doc/api/backend_svg_api.rst b/doc/api/backend_svg_api.rst index 0b26d11e8818..2e7c1c9f5db1 100644 --- a/doc/api/backend_svg_api.rst +++ b/doc/api/backend_svg_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backends.backend_svg` -====================================== +*********************************** +``matplotlib.backends.backend_svg`` +*********************************** .. automodule:: matplotlib.backends.backend_svg :members: diff --git a/doc/api/backend_template_api.rst b/doc/api/backend_template_api.rst index 892f5b696d93..8198eeae121e 100644 --- a/doc/api/backend_template_api.rst +++ b/doc/api/backend_template_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backends.backend_template` -=========================================== +**************************************** +``matplotlib.backends.backend_template`` +**************************************** .. automodule:: matplotlib.backends.backend_template :members: diff --git a/doc/api/backend_tk_api.rst b/doc/api/backend_tk_api.rst index 48131a48ce46..08abf603fd91 100644 --- a/doc/api/backend_tk_api.rst +++ b/doc/api/backend_tk_api.rst @@ -1,15 +1,12 @@ - -:mod:`matplotlib.backends.backend_tkagg` -======================================== +****************************************************************************** +``matplotlib.backends.backend_tkagg``, ``matplotlib.backends.backend_tkcairo`` +****************************************************************************** .. automodule:: matplotlib.backends.backend_tkagg :members: :undoc-members: :show-inheritance: -:mod:`matplotlib.backends.backend_tkcairo` -========================================== - .. automodule:: matplotlib.backends.backend_tkcairo :members: :undoc-members: diff --git a/doc/api/backend_tools_api.rst b/doc/api/backend_tools_api.rst index 7e3d5619cc35..994f32ac854e 100644 --- a/doc/api/backend_tools_api.rst +++ b/doc/api/backend_tools_api.rst @@ -1,6 +1,6 @@ - -:mod:`matplotlib.backend_tools` -=============================== +**************************** +``matplotlib.backend_tools`` +**************************** .. automodule:: matplotlib.backend_tools :members: diff --git a/doc/api/backend_webagg_api.rst b/doc/api/backend_webagg_api.rst index c71f84e31606..ced3533da249 100644 --- a/doc/api/backend_webagg_api.rst +++ b/doc/api/backend_webagg_api.rst @@ -1,9 +1,8 @@ +************************************** +``matplotlib.backends.backend_webagg`` +************************************** -:mod:`matplotlib.backends.backend_webagg` -========================================= - -.. note:: - The WebAgg backend is not documented here, in order to avoid adding Tornado - to the doc build requirements. - -.. module:: matplotlib.backends.backend_webagg +.. automodule:: matplotlib.backends.backend_webagg + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/backend_wx_api.rst b/doc/api/backend_wx_api.rst index 3ae3bc502e69..abf506a161be 100644 --- a/doc/api/backend_wx_api.rst +++ b/doc/api/backend_wx_api.rst @@ -1,14 +1,11 @@ -**NOTE** These backends are not documented here, to avoid adding a dependency -to building the docs. +****************************************************************************** +``matplotlib.backends.backend_wxagg``, ``matplotlib.backends.backend_wxcairo`` +****************************************************************************** -.. redirect-from:: /api/backend_wxagg_api +**NOTE** These :ref:`backends` are not documented here, to avoid adding a +dependency to building the docs. -:mod:`matplotlib.backends.backend_wxagg` -======================================== +.. redirect-from:: /api/backend_wxagg_api .. module:: matplotlib.backends.backend_wxagg - -:mod:`matplotlib.backends.backend_wxcairo` -========================================== - .. module:: matplotlib.backends.backend_wxcairo diff --git a/doc/api/blocking_input_api.rst b/doc/api/blocking_input_api.rst deleted file mode 100644 index 6ba612682ac4..000000000000 --- a/doc/api/blocking_input_api.rst +++ /dev/null @@ -1,8 +0,0 @@ -***************************** -``matplotlib.blocking_input`` -***************************** - -.. automodule:: matplotlib.blocking_input - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/colors_api.rst b/doc/api/colors_api.rst index 4302e289530c..970986ff4438 100644 --- a/doc/api/colors_api.rst +++ b/doc/api/colors_api.rst @@ -2,8 +2,11 @@ ``matplotlib.colors`` ********************* -The Color :ref:`tutorials ` and :ref:`examples -` demonstrate how to set colors and colormaps. +.. note:: + + The Color :ref:`tutorials ` and :ref:`examples + ` demonstrate how to set colors and colormaps. You may want + to read those instead. .. currentmodule:: matplotlib.colors @@ -11,26 +14,44 @@ The Color :ref:`tutorials ` and :ref:`examples :no-members: :no-inherited-members: -Classes -------- +Color norms +----------- .. autosummary:: :toctree: _as_gen/ :template: autosummary.rst + Normalize + NoNorm + AsinhNorm BoundaryNorm - Colormap CenteredNorm - LightSource - LinearSegmentedColormap - ListedColormap + FuncNorm LogNorm - NoNorm - Normalize PowerNorm SymLogNorm TwoSlopeNorm - FuncNorm + +Colormaps +--------- + +.. autosummary:: + :toctree: _as_gen/ + :template: autosummary.rst + + Colormap + LinearSegmentedColormap + ListedColormap + +Other classes +------------- + +.. autosummary:: + :toctree: _as_gen/ + :template: autosummary.rst + + ColorSequenceRegistry + LightSource Functions --------- @@ -49,3 +70,4 @@ Functions is_color_like same_color get_named_colors_mapping + make_norm_from_scale diff --git a/doc/api/dates_api.rst b/doc/api/dates_api.rst index 1afeaaeac3cd..7a3e3bcf4a95 100644 --- a/doc/api/dates_api.rst +++ b/doc/api/dates_api.rst @@ -4,8 +4,10 @@ .. inheritance-diagram:: matplotlib.dates :parts: 1 + :top-classes: matplotlib.ticker.Formatter, matplotlib.ticker.Locator .. automodule:: matplotlib.dates :members: :undoc-members: + :exclude-members: rrule :show-inheritance: diff --git a/doc/api/docstring_api.rst b/doc/api/docstring_api.rst index 853ff93494cf..38a73a2e83d1 100644 --- a/doc/api/docstring_api.rst +++ b/doc/api/docstring_api.rst @@ -2,7 +2,12 @@ ``matplotlib.docstring`` ************************ -.. automodule:: matplotlib.docstring +.. attention:: + This module is considered internal. + + Its use is deprecated and it will be removed in a future version. + +.. automodule:: matplotlib._docstring :members: :undoc-members: :show-inheritance: diff --git a/doc/api/font_manager_api.rst b/doc/api/font_manager_api.rst index 24bfefe00d32..3e043112380b 100644 --- a/doc/api/font_manager_api.rst +++ b/doc/api/font_manager_api.rst @@ -7,5 +7,9 @@ :undoc-members: :show-inheritance: + .. data:: fontManager + The global instance of `FontManager`. +.. autoclass:: FontEntry + :no-undoc-members: diff --git a/doc/api/fontconfig_pattern_api.rst b/doc/api/fontconfig_pattern_api.rst deleted file mode 100644 index 772900035df7..000000000000 --- a/doc/api/fontconfig_pattern_api.rst +++ /dev/null @@ -1,8 +0,0 @@ -********************************* -``matplotlib.fontconfig_pattern`` -********************************* - -.. automodule:: matplotlib.fontconfig_pattern - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/ft2font.rst b/doc/api/ft2font.rst new file mode 100644 index 000000000000..a1f984abdda5 --- /dev/null +++ b/doc/api/ft2font.rst @@ -0,0 +1,8 @@ +********************** +``matplotlib.ft2font`` +********************** + +.. automodule:: matplotlib.ft2font + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/hatch_api.rst b/doc/api/hatch_api.rst new file mode 100644 index 000000000000..b706be379a15 --- /dev/null +++ b/doc/api/hatch_api.rst @@ -0,0 +1,8 @@ +******************** +``matplotlib.hatch`` +******************** + +.. automodule:: matplotlib.hatch + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/api/index.rst b/doc/api/index.rst index c247a169e304..2e8c33fa05e5 100644 --- a/doc/api/index.rst +++ b/doc/api/index.rst @@ -1,44 +1,52 @@ -API Overview -============ +API Reference +============= -.. toctree:: - :hidden: +When using the library you will typically create +:doc:`Figure ` and :doc:`Axes ` objects and +call their methods to add content and modify the appearance. - api_changes +- :mod:`matplotlib.figure`: axes creation, figure-level content +- :mod:`matplotlib.axes`: most plotting methods, Axes labels, access to axis + styling, etc. -.. contents:: :local: +Example: We create a Figure ``fig`` and Axes ``ax``. Then we call +methods on them to plot data, add axis labels and a figure title. -See also the :doc:`api_changes`. +.. plot:: + :include-source: + :align: center -Usage patterns --------------- + import matplotlib.pyplot as plt + import numpy as np -Below we describe several common approaches to plotting with Matplotlib. + x = np.arange(0, 4, 0.05) + y = np.sin(x*np.pi) -The pyplot API -^^^^^^^^^^^^^^ + fig, ax = plt.subplots(figsize=(3,2), constrained_layout=True) + ax.plot(x, y) + ax.set_xlabel('t [s]') + ax.set_ylabel('S [V]') + ax.set_title('Sine wave') + fig.set_facecolor('lightsteelblue') -`matplotlib.pyplot` is a collection of command style functions that make -Matplotlib work like MATLAB. Each pyplot function makes some change to a -figure: e.g., creates a figure, creates a plotting area in a figure, plots -some lines in a plotting area, decorates the plot with labels, etc. -`.pyplot` is mainly intended for interactive plots and simple cases of -programmatic plot generation. -Further reading: +.. _usage_patterns: -- The `matplotlib.pyplot` function reference -- :doc:`/tutorials/introductory/pyplot` -- :ref:`Pyplot examples ` +Usage patterns +-------------- + +Below we describe several common approaches to plotting with Matplotlib. See +:ref:`api_interfaces` for an explanation of the trade-offs between the supported user +APIs. -.. _api-index: -The object-oriented API -^^^^^^^^^^^^^^^^^^^^^^^ +The explicit API +^^^^^^^^^^^^^^^^ -At its core, Matplotlib is object-oriented. We recommend directly working -with the objects, if you need more control and customization of your plots. +At its core, Matplotlib is an object-oriented library. We recommend directly +working with the objects if you need more control and customization of your +plots. In many cases you will create a `.Figure` and one or more `~matplotlib.axes.Axes` using `.pyplot.subplots` and from then on only work @@ -52,7 +60,27 @@ Further reading: - Most of the :ref:`examples ` use the object-oriented approach (except for the pyplot section) -The pylab API (disapproved) + +The implicit API +^^^^^^^^^^^^^^^^ + +`matplotlib.pyplot` is a collection of functions that make +Matplotlib work like MATLAB. Each pyplot function makes some change to a +figure: e.g., creates a figure, creates a plotting area in a figure, plots +some lines in a plotting area, decorates the plot with labels, etc. + +`.pyplot` is mainly intended for interactive plots and simple cases of +programmatic plot generation. + +Further reading: + +- The `matplotlib.pyplot` function reference +- :doc:`/tutorials/introductory/pyplot` +- :ref:`Pyplot examples ` + +.. _api-index: + +The pylab API (discouraged) ^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. automodule:: pylab @@ -61,7 +89,7 @@ The pylab API (disapproved) Modules ------- -Matplotlib consists of the following submodules: +Alphabetical list of modules: .. toctree:: :maxdepth: 1 @@ -77,7 +105,6 @@ Matplotlib consists of the following submodules: backend_tools_api.rst index_backend_api.rst bezier_api.rst - blocking_input_api.rst category_api.rst cbook_api.rst cm_api.rst @@ -91,9 +118,11 @@ Matplotlib consists of the following submodules: dviread.rst figure_api.rst font_manager_api.rst - fontconfig_pattern_api.rst + ft2font.rst gridspec_api.rst + hatch_api.rst image_api.rst + layout_engine_api.rst legend_api.rst legend_handler_api.rst lines_api.rst @@ -110,6 +139,7 @@ Matplotlib consists of the following submodules: rcsetup_api.rst sankey_api.rst scale_api.rst + sphinxext_mathmpl_api.rst sphinxext_plot_directive_api.rst spines_api.rst style_api.rst @@ -117,7 +147,6 @@ Matplotlib consists of the following submodules: testing_api.rst text_api.rst texmanager_api.rst - textpath_api.rst ticker_api.rst tight_bbox_api.rst tight_layout_api.rst @@ -128,22 +157,6 @@ Matplotlib consists of the following submodules: widgets_api.rst _api_api.rst _enums_api.rst - -Toolkits --------- - -:ref:`toolkits-index` are collections of application-specific functions that extend -Matplotlib. The following toolkits are included: - -.. toctree:: - :hidden: - - toolkits/index.rst - -.. toctree:: - :maxdepth: 1 - toolkits/mplot3d.rst toolkits/axes_grid1.rst toolkits/axisartist.rst - toolkits/axes_grid.rst diff --git a/doc/api/index_backend_api.rst b/doc/api/index_backend_api.rst index ad2febf8dc38..639d96a9a0dd 100644 --- a/doc/api/index_backend_api.rst +++ b/doc/api/index_backend_api.rst @@ -2,6 +2,8 @@ ``matplotlib.backends`` *********************** +.. module:: matplotlib.backends + .. toctree:: :maxdepth: 1 @@ -10,6 +12,7 @@ backend_agg_api.rst backend_cairo_api.rst backend_gtk3_api.rst + backend_gtk4_api.rst backend_nbagg_api.rst backend_pdf_api.rst backend_pgf_api.rst diff --git a/doc/api/layout_engine_api.rst b/doc/api/layout_engine_api.rst new file mode 100644 index 000000000000..8890061e0979 --- /dev/null +++ b/doc/api/layout_engine_api.rst @@ -0,0 +1,9 @@ +**************************** +``matplotlib.layout_engine`` +**************************** + +.. currentmodule:: matplotlib.layout_engine + +.. automodule:: matplotlib.layout_engine + :members: + :inherited-members: diff --git a/doc/api/lines_api.rst b/doc/api/lines_api.rst index 808df726d118..4cde67c7e656 100644 --- a/doc/api/lines_api.rst +++ b/doc/api/lines_api.rst @@ -26,4 +26,3 @@ Functions :template: autosummary.rst segment_hits - \ No newline at end of file diff --git a/doc/api/mathtext_api.rst b/doc/api/mathtext_api.rst index c0f4941414ed..295ed0382c61 100644 --- a/doc/api/mathtext_api.rst +++ b/doc/api/mathtext_api.rst @@ -9,4 +9,3 @@ :members: :undoc-members: :show-inheritance: - :exclude-members: Box, Char, ComputerModernFontConstants, DejaVuSansFontConstants, DejaVuSerifFontConstants, FontConstantsBase, Fonts, Glue, Kern, Node, Parser, STIXFontConstants, STIXSansFontConstants, Ship, StandardPsFonts, TruetypeFonts diff --git a/doc/api/matplotlib_configuration_api.rst b/doc/api/matplotlib_configuration_api.rst index 5fa27bbc6723..d5dc60c80613 100644 --- a/doc/api/matplotlib_configuration_api.rst +++ b/doc/api/matplotlib_configuration_api.rst @@ -4,6 +4,11 @@ .. py:currentmodule:: matplotlib +.. automodule:: matplotlib + :no-members: + :no-undoc-members: + :noindex: + Backend management ================== @@ -26,6 +31,7 @@ Default values and styling :no-members: .. automethod:: find_all + .. automethod:: copy .. autofunction:: rc_context @@ -52,7 +58,18 @@ Logging .. autofunction:: set_loglevel +Colormaps and color sequences +============================= + +.. autodata:: colormaps + :no-value: + +.. autodata:: color_sequences + :no-value: + Miscellaneous ============= +.. autoclass:: MatplotlibDeprecationWarning + .. autofunction:: get_cachedir diff --git a/doc/api/next_api_changes.rst b/doc/api/next_api_changes.rst new file mode 100644 index 000000000000..d33c8014f735 --- /dev/null +++ b/doc/api/next_api_changes.rst @@ -0,0 +1,15 @@ + +================ +Next API changes +================ + +.. ifconfig:: releaselevel == 'dev' + + .. toctree:: + :glob: + :maxdepth: 1 + + next_api_changes/behavior/* + next_api_changes/deprecations/* + next_api_changes/development/* + next_api_changes/removals/* diff --git a/doc/api/next_api_changes/behavior/00001-ABC.rst b/doc/api/next_api_changes/behavior/00001-ABC.rst index 236f672c1123..f6d8c1d8b351 100644 --- a/doc/api/next_api_changes/behavior/00001-ABC.rst +++ b/doc/api/next_api_changes/behavior/00001-ABC.rst @@ -1,7 +1,7 @@ -Behavior Change template +Behavior change template ~~~~~~~~~~~~~~~~~~~~~~~~ Enter description here.... Please rename file with PR number and your initials i.e. "99999-ABC.rst" -and ``git add`` the new file. +and ``git add`` the new file. diff --git a/doc/api/next_api_changes/development/00001-ABC.rst b/doc/api/next_api_changes/development/00001-ABC.rst index 4c60d3db185b..6db90a13e44c 100644 --- a/doc/api/next_api_changes/development/00001-ABC.rst +++ b/doc/api/next_api_changes/development/00001-ABC.rst @@ -1,7 +1,7 @@ -Development Change template +Development change template ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Enter description here.... Please rename file with PR number and your initials i.e. "99999-ABC.rst" -and ``git add`` the new file. +and ``git add`` the new file. diff --git a/doc/api/next_api_changes/removals/00001-ABC.rst b/doc/api/next_api_changes/removals/00001-ABC.rst index 5c68eda9698a..3cc5b6344f7f 100644 --- a/doc/api/next_api_changes/removals/00001-ABC.rst +++ b/doc/api/next_api_changes/removals/00001-ABC.rst @@ -1,7 +1,7 @@ -Removal Change template +Removal change template ~~~~~~~~~~~~~~~~~~~~~~~ Enter description of methods/classes removed here.... Please rename file with PR number and your initials i.e. "99999-ABC.rst" -and ``git add`` the new file. +and ``git add`` the new file. diff --git a/doc/api/patches_api.rst b/doc/api/patches_api.rst index ac21a644ccab..5b1eefa91971 100644 --- a/doc/api/patches_api.rst +++ b/doc/api/patches_api.rst @@ -2,7 +2,9 @@ ``matplotlib.patches`` ********************** -.. currentmodule:: matplotlib.patches + +.. inheritance-diagram:: matplotlib.patches + :parts: 1 .. automodule:: matplotlib.patches :no-members: @@ -15,6 +17,7 @@ Classes :toctree: _as_gen/ :template: autosummary.rst + Annulus Arc Arrow ArrowStyle @@ -45,4 +48,3 @@ Functions bbox_artist draw_bbox - diff --git a/doc/api/prev_api_changes/api_changes_0.65.rst b/doc/api/prev_api_changes/api_changes_0.65.rst index 43fffb1bcf4e..f9b9af732010 100644 --- a/doc/api/prev_api_changes/api_changes_0.65.rst +++ b/doc/api/prev_api_changes/api_changes_0.65.rst @@ -8,5 +8,5 @@ Changes for 0.65 connect and disconnect Did away with the text methods for angle since they were ambiguous. - fontangle could mean fontstyle (obligue, etc) or the rotation of the + fontangle could mean fontstyle (oblique, etc) or the rotation of the text. Use style and rotation instead. diff --git a/doc/api/prev_api_changes/api_changes_0.70.rst b/doc/api/prev_api_changes/api_changes_0.70.rst index b8094658b249..e30dfbb64954 100644 --- a/doc/api/prev_api_changes/api_changes_0.70.rst +++ b/doc/api/prev_api_changes/api_changes_0.70.rst @@ -6,4 +6,4 @@ Changes for 0.70 MplEvent factored into a base class Event and derived classes MouseEvent and KeyEvent - Removed definct set_measurement in wx toolbar + Removed defunct set_measurement in wx toolbar diff --git a/doc/api/prev_api_changes/api_changes_0.72.rst b/doc/api/prev_api_changes/api_changes_0.72.rst index 9529e396f356..bfb6fc124658 100644 --- a/doc/api/prev_api_changes/api_changes_0.72.rst +++ b/doc/api/prev_api_changes/api_changes_0.72.rst @@ -6,7 +6,7 @@ Changes for 0.72 - Line2D, Text, and Patch copy_properties renamed update_from and moved into artist base class - - LineCollecitons.color renamed to LineCollections.set_color for + - LineCollections.color renamed to LineCollections.set_color for consistency with set/get introspection mechanism, - pylab figure now defaults to num=None, which creates a new figure diff --git a/doc/api/prev_api_changes/api_changes_0.91.0.rst b/doc/api/prev_api_changes/api_changes_0.91.0.rst index b33fcb50f0ad..32760554522a 100644 --- a/doc/api/prev_api_changes/api_changes_0.91.0.rst +++ b/doc/api/prev_api_changes/api_changes_0.91.0.rst @@ -25,18 +25,18 @@ Changes for 0.91.0 * The :mod:`matplotlib.dviread` file now has a parser for files like psfonts.map and pdftex.map, to map TeX font names to external files. -* The file :mod:`matplotlib.type1font` contains a new class for Type 1 +* The file ``matplotlib.type1font`` contains a new class for Type 1 fonts. Currently it simply reads pfa and pfb format files and stores the data in a way that is suitable for embedding in pdf files. In the future the class might actually parse the font to allow e.g., subsetting. -* :mod:`matplotlib.ft2font` now supports ``FT_Attach_File``. In +* ``matplotlib.ft2font`` now supports ``FT_Attach_File``. In practice this can be used to read an afm file in addition to a pfa/pfb file, to get metrics and kerning information for a Type 1 font. -* The :class:`.AFM` class now supports querying CapHeight and stem +* The ``AFM`` class now supports querying CapHeight and stem widths. The get_name_char method now has an isord kwarg like get_width_char. diff --git a/doc/api/prev_api_changes/api_changes_0.98.0.rst b/doc/api/prev_api_changes/api_changes_0.98.0.rst index 0c73628e1651..bb9f3e6585af 100644 --- a/doc/api/prev_api_changes/api_changes_0.98.0.rst +++ b/doc/api/prev_api_changes/api_changes_0.98.0.rst @@ -12,7 +12,7 @@ Changes for 0.98.0 rather than custom callback handling. Any users of ``matplotlib.cm.ScalarMappable.add_observer`` of the :class:`~matplotlib.cm.ScalarMappable` should use the - :attr:`matplotlib.cm.ScalarMappable.callbacksSM` + ``matplotlib.cm.ScalarMappable.callbacksSM`` :class:`~matplotlib.cbook.CallbackRegistry` instead. * New axes function and Axes method provide control over the plot @@ -126,8 +126,8 @@ with a verb in the present tense. | ``lbwh_to_bbox(l, b, w, h)`` | `Bbox.from_bounds(x0, y0, w, h) <.Bbox.from_bounds>` | | | [It is a staticmethod.] | +--------------------------------------------+------------------------------------------------------+ -| ``inverse_transform_bbox(trans, bbox)`` | `Bbox.inverse_transformed(trans) | -| | <.BboxBase.inverse_transformed>` | +| ``inverse_transform_bbox(trans, bbox)`` | ``bbox.inverse_transformed(trans)`` | +| | | +--------------------------------------------+------------------------------------------------------+ | ``Interval.contains_open(v)`` | `interval_contains_open(tuple, v) | | | <.interval_contains_open>` | @@ -181,7 +181,7 @@ The ``Polar`` class has moved to :mod:`matplotlib.projections.polar`. .. [3] :meth:`matplotlib.axes.Axes.set_position` now accepts either four scalars or a :class:`matplotlib.transforms.Bbox` instance. -.. [4] Since the recfactoring allows for more than two scale types +.. [4] Since the refactoring allows for more than two scale types ('log' or 'linear'), it no longer makes sense to have a toggle. ``Axes.toggle_log_lineary()`` has been removed. diff --git a/doc/api/prev_api_changes/api_changes_0.98.x.rst b/doc/api/prev_api_changes/api_changes_0.98.x.rst index 41ee63502254..053fd908a03e 100644 --- a/doc/api/prev_api_changes/api_changes_0.98.x.rst +++ b/doc/api/prev_api_changes/api_changes_0.98.x.rst @@ -63,8 +63,8 @@ Changes for 0.98.x :meth:`matplotlib.axes.Axes.set_ylim` now return a copy of the ``viewlim`` array to avoid modify-in-place surprises. -* :meth:`matplotlib.afm.AFM.get_fullname` and - :meth:`matplotlib.afm.AFM.get_familyname` no longer raise an +* ``matplotlib.afm.AFM.get_fullname`` and + ``matplotlib.afm.AFM.get_familyname`` no longer raise an exception if the AFM file does not specify these optional attributes, but returns a guess based on the required FontName attribute. @@ -87,13 +87,13 @@ Changes for 0.98.x :class:`~matplotlib.collections.Collection` base class. * ``matplotlib.figure.Figure.figurePatch`` renamed - :attr:`matplotlib.figure.Figure.patch`; + ``matplotlib.figure.Figure.patch``; ``matplotlib.axes.Axes.axesPatch`` renamed - :attr:`matplotlib.axes.Axes.patch`; + ``matplotlib.axes.Axes.patch``; ``matplotlib.axes.Axes.axesFrame`` renamed - :attr:`matplotlib.axes.Axes.frame`. + ``matplotlib.axes.Axes.frame``. ``matplotlib.axes.Axes.get_frame``, which returns - :attr:`matplotlib.axes.Axes.patch`, is deprecated. + ``matplotlib.axes.Axes.patch``, is deprecated. * Changes in the :class:`matplotlib.contour.ContourLabeler` attributes (:func:`matplotlib.pyplot.clabel` function) so that they all have a diff --git a/doc/api/prev_api_changes/api_changes_0.99.x.rst b/doc/api/prev_api_changes/api_changes_0.99.x.rst index 03596e93dac3..4736d066d43e 100644 --- a/doc/api/prev_api_changes/api_changes_0.99.x.rst +++ b/doc/api/prev_api_changes/api_changes_0.99.x.rst @@ -21,8 +21,8 @@ Changes beyond 0.99.x on or off, and applies it. + :meth:`matplotlib.axes.Axes.margins` sets margins used to - autoscale the :attr:`matplotlib.axes.Axes.viewLim` based on - the :attr:`matplotlib.axes.Axes.dataLim`. + autoscale the ``matplotlib.axes.Axes.viewLim`` based on + the ``matplotlib.axes.Axes.dataLim``. + :meth:`matplotlib.axes.Axes.locator_params` allows one to adjust axes locator parameters such as *nbins*. diff --git a/doc/api/prev_api_changes/api_changes_1.1.x.rst b/doc/api/prev_api_changes/api_changes_1.1.x.rst index 8320e2c4fc09..790b669081b7 100644 --- a/doc/api/prev_api_changes/api_changes_1.1.x.rst +++ b/doc/api/prev_api_changes/api_changes_1.1.x.rst @@ -1,6 +1,6 @@ -Changes in 1.1.x -================ +API Changes in 1.1.x +==================== * Added new :class:`matplotlib.sankey.Sankey` for generating Sankey diagrams. diff --git a/doc/api/prev_api_changes/api_changes_1.2.x.rst b/doc/api/prev_api_changes/api_changes_1.2.x.rst index cb6b2071f79b..45a2f35cf29e 100644 --- a/doc/api/prev_api_changes/api_changes_1.2.x.rst +++ b/doc/api/prev_api_changes/api_changes_1.2.x.rst @@ -1,5 +1,5 @@ -Changes in 1.2.x -================ +API Changes in 1.2.x +==================== * The ``classic`` option of the rc parameter ``toolbar`` is deprecated and will be removed in the next release. @@ -66,7 +66,7 @@ Changes in 1.2.x This change means that third party objects can expose themselves as Matplotlib axes by providing a ``_as_mpl_axes`` method. See - :ref:`adding-new-scales` for more detail. + :mod:`matplotlib.projections` for more detail. * A new keyword *extendfrac* in :meth:`~matplotlib.pyplot.colorbar` and :class:`~matplotlib.colorbar.ColorbarBase` allows one to control the size of diff --git a/doc/api/prev_api_changes/api_changes_1.3.x.rst b/doc/api/prev_api_changes/api_changes_1.3.x.rst index 5b596d83b5e2..553f4d7118c7 100644 --- a/doc/api/prev_api_changes/api_changes_1.3.x.rst +++ b/doc/api/prev_api_changes/api_changes_1.3.x.rst @@ -1,8 +1,8 @@ .. _changes_in_1_3: -Changes in 1.3.x -================ +API Changes in 1.3.x +==================== Changes in 1.3.1 ---------------- @@ -33,7 +33,7 @@ Code removal functionality on `matplotlib.path.Path.contains_point` and friends instead. - - Instead of ``axes.Axes.get_frame``, use `.axes.Axes.patch`. + - Instead of ``axes.Axes.get_frame``, use ``axes.Axes.patch``. - The following keyword arguments to the `~.axes.Axes.legend` function have been renamed: @@ -102,7 +102,7 @@ Code deprecation be used. In previous Matplotlib versions this attribute was an undocumented tuple of ``(colorbar_instance, colorbar_axes)`` but is now just ``colorbar_instance``. To get the colorbar axes it is possible to just use - the :attr:`~matplotlib.colorbar.ColorbarBase.ax` attribute on a colorbar + the ``matplotlib.colorbar.ColorbarBase.ax`` attribute on a colorbar instance. * The ``matplotlib.mpl`` module is now deprecated. Those who relied on this @@ -192,7 +192,7 @@ Code changes by ``self.vline`` for vertical cursors lines and ``self.hline`` is added for the horizontal cursors lines. -* On POSIX platforms, the :func:`~matplotlib.cbook.report_memory` function +* On POSIX platforms, the ``matplotlib.cbook.report_memory`` function raises :class:`NotImplementedError` instead of :class:`OSError` if the :command:`ps` command cannot be run. diff --git a/doc/api/prev_api_changes/api_changes_1.4.x.rst b/doc/api/prev_api_changes/api_changes_1.4.x.rst index 2d49b4b6651a..c12d40a67991 100644 --- a/doc/api/prev_api_changes/api_changes_1.4.x.rst +++ b/doc/api/prev_api_changes/api_changes_1.4.x.rst @@ -1,5 +1,5 @@ -Changes in 1.4.x -================ +API Changes in 1.4.x +==================== Code changes ------------ @@ -82,7 +82,7 @@ original location: * The artist used to draw the outline of a `.Figure.colorbar` has been changed from a `matplotlib.lines.Line2D` to `matplotlib.patches.Polygon`, thus - `.colorbar.ColorbarBase.outline` is now a `matplotlib.patches.Polygon` + ``colorbar.ColorbarBase.outline`` is now a `matplotlib.patches.Polygon` object. * The legend handler interface has changed from a callable, to any object @@ -149,9 +149,9 @@ original location: ``drawRect`` from ``FigureCanvasQTAgg``; they were always an implementation detail of the (preserved) ``drawRectangle()`` function. -* The function signatures of `.tight_bbox.adjust_bbox` and - `.tight_bbox.process_figure_for_rasterizing` have been changed. A new - *fixed_dpi* parameter allows for overriding the ``figure.dpi`` setting +* The function signatures of ``matplotlib.tight_bbox.adjust_bbox`` and + ``matplotlib.tight_bbox.process_figure_for_rasterizing`` have been changed. + A new *fixed_dpi* parameter allows for overriding the ``figure.dpi`` setting instead of trying to deduce the intended behaviour from the file format. * Added support for horizontal/vertical axes padding to diff --git a/doc/api/prev_api_changes/api_changes_1.5.0.rst b/doc/api/prev_api_changes/api_changes_1.5.0.rst index 4ebb5c182c4b..1248b1dfd394 100644 --- a/doc/api/prev_api_changes/api_changes_1.5.0.rst +++ b/doc/api/prev_api_changes/api_changes_1.5.0.rst @@ -1,6 +1,6 @@ -Changes in 1.5.0 -================ +API Changes in 1.5.0 +==================== Code Changes ------------ @@ -41,7 +41,7 @@ fully delegate to `.Quiver`). Previously any input matching 'mid.*' would be interpreted as 'middle', 'tip.*' as 'tip' and any string not matching one of those patterns as 'tail'. -The value of `.Quiver.pivot` is normalized to be in the set {'tip', 'tail', +The value of ``Quiver.pivot`` is normalized to be in the set {'tip', 'tail', 'middle'} in `.Quiver`. Reordered ``Axes.get_children`` @@ -60,7 +60,7 @@ by the new keyword argument *corner_mask*, or if this is not specified then the new :rc:`contour.corner_mask` instead. The new default behaviour is equivalent to using ``corner_mask=True``; the previous behaviour can be obtained using ``corner_mask=False`` or by changing the rcParam. The example -http://matplotlib.org/examples/pylab_examples/contour_corner_mask.html +https://matplotlib.org/examples/pylab_examples/contour_corner_mask.html demonstrates the difference. Use of the old contouring algorithm, which is obtained with ``corner_mask='legacy'``, is now deprecated. @@ -116,10 +116,10 @@ In either case to update the data in the `.Line2D` object you must update both the ``x`` and ``y`` data. -Removed *args* and *kwargs* from `.MicrosecondLocator.__call__` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Removed *args* and *kwargs* from ``MicrosecondLocator.__call__`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The call signature of :meth:`~matplotlib.dates.MicrosecondLocator.__call__` +The call signature of ``matplotlib.dates.MicrosecondLocator.__call__`` has changed from ``__call__(self, *args, **kwargs)`` to ``__call__(self)``. This is consistent with the superclass :class:`~matplotlib.ticker.Locator` and also all the other Locators derived from this superclass. @@ -374,7 +374,7 @@ directly. patheffects.svg ~~~~~~~~~~~~~~~ - - remove ``get_proxy_renderer`` method from ``AbstarctPathEffect`` class + - remove ``get_proxy_renderer`` method from ``AbstractPathEffect`` class - remove ``patch_alpha`` and ``offset_xy`` from ``SimplePatchShadow`` diff --git a/doc/api/prev_api_changes/api_changes_1.5.2.rst b/doc/api/prev_api_changes/api_changes_1.5.2.rst index d2ee33546314..85c504fa6f12 100644 --- a/doc/api/prev_api_changes/api_changes_1.5.2.rst +++ b/doc/api/prev_api_changes/api_changes_1.5.2.rst @@ -1,5 +1,5 @@ -Changes in 1.5.2 -================ +API Changes in 1.5.2 +==================== Default Behavior Changes diff --git a/doc/api/prev_api_changes/api_changes_1.5.3.rst b/doc/api/prev_api_changes/api_changes_1.5.3.rst index 0dc025111eae..ff5d6a9cf996 100644 --- a/doc/api/prev_api_changes/api_changes_1.5.3.rst +++ b/doc/api/prev_api_changes/api_changes_1.5.3.rst @@ -1,5 +1,5 @@ -Changes in 1.5.3 -================ +API Changes in 1.5.3 +==================== ``ax.plot(..., marker=None)`` gives default marker -------------------------------------------------- diff --git a/doc/api/prev_api_changes/api_changes_2.1.0.rst b/doc/api/prev_api_changes/api_changes_2.1.0.rst index 90f9e00eecd1..39ea78bdf587 100644 --- a/doc/api/prev_api_changes/api_changes_2.1.0.rst +++ b/doc/api/prev_api_changes/api_changes_2.1.0.rst @@ -20,7 +20,7 @@ Previously they were clipped to a very small number and shown. Matplotlib uses instances of :obj:`~matplotlib.cbook.CallbackRegistry` as a bridge between user input event from the GUI and user callbacks. Previously, any exceptions raised in a user call back would bubble out -of of the ``process`` method, which is typically in the GUI event +of the ``process`` method, which is typically in the GUI event loop. Most GUI frameworks simple print the traceback to the screen and continue as there is not always a clear method of getting the exception back to the user. However PyQt5 now exits the process when @@ -422,8 +422,8 @@ The ``shading`` kwarg to `~matplotlib.axes.Axes.pcolor` has been removed. Set ``edgecolors`` appropriately instead. -Functions removed from the `.lines` module -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Functions removed from the ``lines`` module +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The :mod:`matplotlib.lines` module no longer imports the ``pts_to_prestep``, ``pts_to_midstep`` and ``pts_to_poststep`` diff --git a/doc/api/prev_api_changes/api_changes_2.2.0.rst b/doc/api/prev_api_changes/api_changes_2.2.0.rst index 29ed03649fd8..f13fe2a246f0 100644 --- a/doc/api/prev_api_changes/api_changes_2.2.0.rst +++ b/doc/api/prev_api_changes/api_changes_2.2.0.rst @@ -74,7 +74,7 @@ rcParam. Deprecated ``Axis.unit_data`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Use `.Axis.units` (which has long existed) instead. +Use ``Axis.units`` (which has long existed) instead. Removals @@ -159,7 +159,7 @@ If `.MovieWriterRegistry` can't find the requested `.MovieWriter`, a more helpful `RuntimeError` message is now raised instead of the previously raised `KeyError`. -`~.tight_layout.auto_adjust_subplotpars` now raises `ValueError` +``matplotlib.tight_layout.auto_adjust_subplotpars`` now raises `ValueError` instead of `RuntimeError` when sizes of input lists don't match @@ -169,7 +169,7 @@ instead of `RuntimeError` when sizes of input lists don't match `matplotlib.figure.Figure.set_figwidth` and `matplotlib.figure.Figure.set_figheight` had the keyword argument ``forward=False`` by default, but `.figure.Figure.set_size_inches` now defaults -to ``forward=True``. This makes these functions conistent. +to ``forward=True``. This makes these functions consistent. Do not truncate svg sizes to nearest point @@ -198,11 +198,11 @@ Changes to Qt backend class MRO To support both Agg and cairo rendering for Qt backends all of the non-Agg specific code previously in ``backend_qt5agg.FigureCanvasQTAggBase`` has been -moved to :class:`.backend_qt5.FigureCanvasQT` so it can be shared with the +moved to ``backend_qt5.FigureCanvasQT`` so it can be shared with the cairo implementation. The ``FigureCanvasQTAggBase.paintEvent``, ``FigureCanvasQTAggBase.blit``, and ``FigureCanvasQTAggBase.print_figure`` -methods have moved to :meth:`.FigureCanvasQTAgg.paintEvent`, -:meth:`.FigureCanvasQTAgg.blit`, and :meth:`.FigureCanvasQTAgg.print_figure`. +methods have moved to ``FigureCanvasQTAgg.paintEvent``, +``FigureCanvasQTAgg.blit``, and ``FigureCanvasQTAgg.print_figure``. The first two methods assume that the instance is also a ``QWidget`` so to use ``FigureCanvasQTAggBase`` it was required to multiple inherit from a ``QWidget`` sub-class. @@ -210,9 +210,9 @@ The first two methods assume that the instance is also a ``QWidget`` so to use Having moved all of its methods either up or down the class hierarchy ``FigureCanvasQTAggBase`` has been deprecated. To do this without warning and to preserve as much API as possible, ``.backend_qt5agg.FigureCanvasQTAggBase`` -now inherits from :class:`.backend_qt5.FigureCanvasQTAgg`. +now inherits from ``backend_qt5.FigureCanvasQTAgg``. -The MRO for :class:`.FigureCanvasQTAgg` and ``FigureCanvasQTAggBase`` used to +The MRO for ``FigureCanvasQTAgg`` and ``FigureCanvasQTAggBase`` used to be :: diff --git a/doc/api/prev_api_changes/api_changes_3.0.0.rst b/doc/api/prev_api_changes/api_changes_3.0.0.rst index a6224c352179..89a588d8f9e3 100644 --- a/doc/api/prev_api_changes/api_changes_3.0.0.rst +++ b/doc/api/prev_api_changes/api_changes_3.0.0.rst @@ -106,7 +106,7 @@ Different exception types for undocumented options - Passing the undocumented ``xmin`` or ``xmax`` arguments to :meth:`~matplotlib.axes.Axes.set_xlim` would silently override the ``left`` and ``right`` arguments. :meth:`~matplotlib.axes.Axes.set_ylim` and the - 3D equivalents (e.g. `~.Axes3D.set_zlim3d`) had a + 3D equivalents (e.g. `~.Axes3D.set_zlim`) had a corresponding problem. A ``TypeError`` will be raised if they would override the earlier limit arguments. In 3.0 these were kwargs were deprecated, but in 3.1 @@ -197,7 +197,7 @@ Contour color autoscaling improvements Selection of contour levels is now the same for contour and contourf; previously, for contour, levels outside the data range were deleted. (Exception: if no contour levels are found within the -data range, the `levels` attribute is replaced with a list holding +data range, the ``levels`` attribute is replaced with a list holding only the minimum of the data range.) When contour is called with levels specified as a target number rather @@ -297,7 +297,7 @@ Blacklisted rcparams no longer updated by `~matplotlib.rcdefaults`, `~matplotlib The rc modifier functions `~matplotlib.rcdefaults`, `~matplotlib.rc_file_defaults` and `~matplotlib.rc_file` -now ignore rcParams in the `matplotlib.style.core.STYLE_BLACKLIST` set. In +now ignore rcParams in the ``matplotlib.style.core.STYLE_BLACKLIST`` set. In particular, this prevents the ``backend`` and ``interactive`` rcParams from being incorrectly modified by these functions. @@ -324,12 +324,12 @@ instead of ``\usepackage{ucs}\usepackage[utf8x]{inputenc}``. Return type of ArtistInspector.get_aliases changed -------------------------------------------------- -`ArtistInspector.get_aliases` previously returned the set of aliases as +``ArtistInspector.get_aliases`` previously returned the set of aliases as ``{fullname: {alias1: None, alias2: None, ...}}``. The dict-to-None mapping was used to simulate a set in earlier versions of Python. It has now been replaced by a set, i.e. ``{fullname: {alias1, alias2, ...}}``. -This value is also stored in `.ArtistInspector.aliasd`, which has likewise +This value is also stored in ``ArtistInspector.aliasd``, which has likewise changed. @@ -471,7 +471,7 @@ Hold machinery Setting or unsetting ``hold`` (:ref:`deprecated in version 2.0`) has now been completely removed. Matplotlib now always behaves as if ``hold=True``. To clear an axes you can manually use :meth:`~.axes.Axes.cla()`, -or to clear an entire figure use :meth:`~.figure.Figure.clf()`. +or to clear an entire figure use :meth:`~.figure.Figure.clear()`. Removal of deprecated backends diff --git a/doc/api/prev_api_changes/api_changes_3.0.1.rst b/doc/api/prev_api_changes/api_changes_3.0.1.rst index d214ae9e6652..4b203cd04596 100644 --- a/doc/api/prev_api_changes/api_changes_3.0.1.rst +++ b/doc/api/prev_api_changes/api_changes_3.0.1.rst @@ -1,10 +1,10 @@ API Changes for 3.0.1 ===================== -`.tight_layout.auto_adjust_subplotpars` can return ``None`` now if the new -subplotparams will collapse axes to zero width or height. This prevents -``tight_layout`` from being executed. Similarly -`.tight_layout.get_tight_layout_figure` will return None. +``matplotlib.tight_layout.auto_adjust_subplotpars`` can return ``None`` now if +the new subplotparams will collapse axes to zero width or height. +This prevents ``tight_layout`` from being executed. Similarly +``matplotlib.tight_layout.get_tight_layout_figure`` will return None. To improve import (startup) time, private modules are now imported lazily. These modules are no longer available at these locations: 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 f3737889841f..3e67af8f64cf 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 @@ -293,9 +293,9 @@ where the `.cm.ScalarMappable` passed to `matplotlib.colorbar.Colorbar` (`~.Figure.colorbar`) had a ``set_norm`` method, as did the colorbar. The colorbar is now purely a follower to the `.ScalarMappable` norm and colormap, and the old inherited methods -`~matplotlib.colorbar.ColorbarBase.set_norm`, -`~matplotlib.colorbar.ColorbarBase.set_cmap`, -`~matplotlib.colorbar.ColorbarBase.set_clim` are deprecated, as are +``matplotlib.colorbar.ColorbarBase.set_norm``, +``matplotlib.colorbar.ColorbarBase.set_cmap``, +``matplotlib.colorbar.ColorbarBase.set_clim`` are deprecated, as are the getter versions of those calls. To set the norm associated with a colorbar do ``colorbar.mappable.set_norm()`` etc. @@ -308,7 +308,7 @@ FreeType or libpng are not in the compiler or linker's default path, set the standard environment variables ``CFLAGS``/``LDFLAGS`` on Linux or OSX, or ``CL``/``LINK`` on Windows, to indicate the relevant paths. -See details in :doc:`/users/installing`. +See details in :doc:`/users/installing/index`. Setting artist properties twice or more in the same call ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -337,7 +337,7 @@ match the array value type of the ``Path.codes`` array. LaTeX code in matplotlibrc file ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Previously, the rc file keys ``pgf.preamble`` and ``text.latex.preamble`` were -parsed using commmas as separators. This would break valid LaTeX code, such as:: +parsed using commas as separators. This would break valid LaTeX code, such as:: \usepackage[protrusion=true, expansion=false]{microtype} @@ -389,16 +389,16 @@ consistent with the behavior on Py2, where a buffer object was returned. -`matplotlib.font_manager.win32InstalledFonts` return type -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -`matplotlib.font_manager.win32InstalledFonts` returns an empty list instead +``matplotlib.font_manager.win32InstalledFonts`` return type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``matplotlib.font_manager.win32InstalledFonts`` returns an empty list instead of None if no fonts are found. -`.Axes.fmt_xdata` and `.Axes.fmt_ydata` error handling -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``Axes.fmt_xdata`` and ``Axes.fmt_ydata`` error handling +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Previously, if the user provided a `.Axes.fmt_xdata` or -`.Axes.fmt_ydata` function that raised a `TypeError` (or set them to a +Previously, if the user provided a ``Axes.fmt_xdata`` or +``Axes.fmt_ydata`` function that raised a `TypeError` (or set them to a non-callable), the exception would be silently ignored and the default formatter be used instead. This is no longer the case; the exception is now propagated out. @@ -476,7 +476,7 @@ Exception changes - `.Axes.streamplot` does not support irregularly gridded ``x`` and ``y`` values. So far, it used to silently plot an incorrect result. This has been changed to raise a `ValueError` instead. -- The `.streamplot.Grid` class, which is internally used by streamplot +- The ``streamplot.Grid`` class, which is internally used by streamplot code, also throws a `ValueError` when irregularly gridded values are passed in. @@ -496,7 +496,7 @@ Classes and methods - ``backend_wx.Toolbar`` (use ``backend_wx.NavigationToolbar2Wx`` instead) - ``cbook.align_iterators`` (no replacement) - ``contour.ContourLabeler.get_real_label_width`` (no replacement) -- ``legend.Legend.draggable`` (use `legend.Legend.set_draggable()` instead) +- ``legend.Legend.draggable`` (use `.legend.Legend.set_draggable()` instead) - ``texmanager.TexManager.postscriptd``, ``texmanager.TexManager.pscnt``, ``texmanager.TexManager.make_ps``, ``texmanager.TexManager.get_ps_bbox`` (no replacements) @@ -566,7 +566,7 @@ in Matplotlib 2.2 has been removed. See below for a list: - ``mlab.safe_isnan`` (use `numpy.isnan` instead) - ``mlab.cohere_pairs`` (use `scipy.signal.coherence` instead) - ``mlab.entropy`` (use `scipy.stats.entropy` instead) -- ``mlab.normpdf`` (use `scipy.stats.norm.pdf` instead) +- ``mlab.normpdf`` (use ``scipy.stats.norm.pdf`` instead) - ``mlab.find`` (use ``np.nonzero(np.ravel(condition))`` instead) - ``mlab.longest_contiguous_ones`` - ``mlab.longest_ones`` @@ -652,7 +652,7 @@ no longer available in the `pylab` module: - ``longest_ones`` - ``movavg`` - ``norm_flat`` (use ``numpy.linalg.norm(a.flat, ord=2)`` instead) -- ``normpdf`` (use `scipy.stats.norm.pdf` instead) +- ``normpdf`` (use ``scipy.stats.norm.pdf`` instead) - ``path_length`` - ``poly_below`` - ``poly_between`` @@ -705,7 +705,7 @@ now a no-op). The image comparison test decorators now skip (rather than xfail) the test for uncomparable formats. The affected decorators are `~.image_comparison` and -`~.check_figures_equal`. The deprecated `~.ImageComparisonTest` class is +`~.check_figures_equal`. The deprecated ``ImageComparisonTest`` class is likewise changed. Dependency changes @@ -825,7 +825,7 @@ This has not been used in the codebase since its addition in 2009. This has never been used internally, there is no equivalent method exists on the 2D Axis classes, and despite the similar name, it has a completely - different behavior from the 2D Axis' `axis.Axis.get_ticks_position` method. + different behavior from the 2D Axis' ``axis.Axis.get_ticks_position`` method. - ``.backend_pgf.LatexManagerFactory`` - ``mpl_toolkits.axisartist.axislines.SimpleChainedObjects`` @@ -936,8 +936,8 @@ Axes3D - `.axes3d.Axes3D.w_yaxis` - `.axes3d.Axes3D.w_zaxis` -Use `.axes3d.Axes3D.xaxis`, `.axes3d.Axes3D.yaxis` and `.axes3d.Axes3D.zaxis` -instead. +Use ``axes3d.Axes3D.xaxis``, ``axes3d.Axes3D.yaxis`` and +``axes3d.Axes3D.zaxis`` instead. Testing ~~~~~~~ @@ -945,7 +945,7 @@ Testing - ``matplotlib.testing.decorators.switch_backend`` decorator Test functions should use ``pytest.mark.backend``, and the mark will be -picked up by the `matplotlib.testing.conftest.mpl_test_settings` fixture. +picked up by the ``matplotlib.testing.conftest.mpl_test_settings`` fixture. Quiver ~~~~~~ @@ -1069,7 +1069,7 @@ Axis - ``Axis.iter_ticks`` -This only served as a helper to the private `.Axis._update_ticks` +This only served as a helper to the private ``Axis._update_ticks`` Undeprecations @@ -1123,10 +1123,10 @@ The `.Formatter` class gained a new `~.Formatter.format_ticks` method, which takes the list of all tick locations as a single argument and returns the list of all formatted values. It is called by the axis tick handling code and, by default, first calls `~.Formatter.set_locs` with all locations, then repeatedly -calls `~.Formatter.__call__` for each location. +calls ``Formatter.__call__`` for each location. Tick-handling code in the codebase that previously performed this sequence -(`~.Formatter.set_locs` followed by repeated `~.Formatter.__call__`) have been +(`~.Formatter.set_locs` followed by repeated ``Formatter.__call__``) have been updated to use `~.Formatter.format_ticks`. `~.Formatter.format_ticks` is intended to be overridden by `.Formatter` diff --git a/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst b/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst index 8e76a047e348..dc47740890be 100644 --- a/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst +++ b/doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst @@ -46,9 +46,9 @@ highest. Code that worked around the normalization between 0 and 1 will need to be modified. -`MovieWriterRegistry` -~~~~~~~~~~~~~~~~~~~~~ -`MovieWriterRegistry` now always checks the availability of the writer classes +``MovieWriterRegistry`` +~~~~~~~~~~~~~~~~~~~~~~~ +`.MovieWriterRegistry` now always checks the availability of the writer classes before returning them. If one wishes, for example, to get the first available writer, without performing the availability check on subsequent writers, it is now possible to iterate over the registry, which will yield the names of the @@ -88,13 +88,13 @@ enough to be accommodated by the default data limit margins. While the new behavior is algorithmically simpler, it is conditional on properties of the `.Collection` object: - 1. ``offsets = None``, ``transform`` is a child of `.Axes.transData`: use the paths + 1. ``offsets = None``, ``transform`` is a child of ``Axes.transData``: use the paths for the automatic limits (i.e. for `.LineCollection` in `.Axes.streamplot`). - 2. ``offsets != None``, and ``offset_transform`` is child of `.Axes.transData`: + 2. ``offsets != None``, and ``offset_transform`` is child of ``Axes.transData``: - a) ``transform`` is child of `.Axes.transData`: use the ``path + offset`` for + a) ``transform`` is child of ``Axes.transData``: use the ``path + offset`` for limits (i.e., for `.Axes.bar`). - b) ``transform`` is not a child of `.Axes.transData`: just use the offsets + b) ``transform`` is not a child of ``Axes.transData``: just use the offsets for the limits (i.e. for scatter) 3. otherwise return a null `.Bbox`. @@ -294,7 +294,7 @@ Exception changes ~~~~~~~~~~~~~~~~~ Various APIs that raised a `ValueError` for incorrectly typed inputs now raise `TypeError` instead: `.backend_bases.GraphicsContextBase.set_clip_path`, -`.blocking_input.BlockingInput.__call__`, `.cm.register_cmap`, `.dviread.DviFont`, +``blocking_input.BlockingInput.__call__``, `.cm.register_cmap`, `.dviread.DviFont`, `.rcsetup.validate_hatch`, ``.rcsetup.validate_animation_writer_path``, `.spines.Spine`, many classes in the :mod:`matplotlib.transforms` module and :mod:`matplotlib.tri` package, and Axes methods that take a ``norm`` parameter. diff --git a/doc/api/prev_api_changes/api_changes_3.2.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.2.0/deprecations.rst index 42de9ffccd77..65b72c7e0558 100644 --- a/doc/api/prev_api_changes/api_changes_3.2.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.2.0/deprecations.rst @@ -118,7 +118,7 @@ The unused ``Locator.autoscale`` method is deprecated (pass the axis limits to Animation ~~~~~~~~~ -The following methods and attributes of the `MovieWriterRegistry` class are +The following methods and attributes of the `.MovieWriterRegistry` class are deprecated: ``set_dirty``, ``ensure_not_dirty``, ``reset_available_writers``, ``avail``. diff --git a/doc/api/prev_api_changes/api_changes_3.2.0/development.rst b/doc/api/prev_api_changes/api_changes_3.2.0/development.rst index 470b594d522c..9af7fb8fb561 100644 --- a/doc/api/prev_api_changes/api_changes_3.2.0/development.rst +++ b/doc/api/prev_api_changes/api_changes_3.2.0/development.rst @@ -22,7 +22,7 @@ is desired. Packaging DLLs ~~~~~~~~~~~~~~ -Previously, it was possible to package Windows DLLs into the Maptlotlib +Previously, it was possible to package Windows DLLs into the Matplotlib wheel (or sdist) by copying them into the source tree and setting the ``package_data.dlls`` entry in ``setup.cfg``. diff --git a/doc/api/prev_api_changes/api_changes_3.3.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.3.0/behaviour.rst index 2b21794ede6b..65807a184fbc 100644 --- a/doc/api/prev_api_changes/api_changes_3.3.0/behaviour.rst +++ b/doc/api/prev_api_changes/api_changes_3.3.0/behaviour.rst @@ -262,7 +262,7 @@ most common operations remain available), and the list-of-one `.Polygon` is returned as is. This makes the `repr` of the returned artist more accurate: it is now :: - # "bar", "barstacked" + # "bar", "barstacked" [] # "step", "stepfilled" instead of :: diff --git a/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst index 4eb73b16dd54..322f6df40d42 100644 --- a/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.3.0/deprecations.rst @@ -72,7 +72,7 @@ Revert deprecation \*min, \*max keyword arguments to ``set_x/y/zlim_3d()`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These keyword arguments were deprecated in 3.0, alongside with the respective parameters in ``set_xlim()`` / ``set_ylim()``. The deprecations of the 2D -versions were already reverted in in 3.1. +versions were already reverted in 3.1. ``cbook.local_over_kwdict`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -93,7 +93,7 @@ Parameters *norm* and *vmin*/*vmax* should not be used simultaneously ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Passing parameters *norm* and *vmin*/*vmax* simultaneously to functions using colormapping such as ``scatter()`` and ``imshow()`` is deprecated. -Inestead of ``norm=LogNorm(), vmin=min_val, vmax=max_val`` pass +Instead of ``norm=LogNorm(), vmin=min_val, vmax=max_val`` pass ``norm=LogNorm(min_val, max_val)``. *vmin* and *vmax* should only be used without setting *norm*. @@ -162,7 +162,7 @@ The ``on_mappable_changed`` and ``update_bruteforce`` methods of `~matplotlib.colorbar.Colorbar` are deprecated; both can be replaced by calls to `~matplotlib.colorbar.Colorbar.update_normal`. -``OldScalarFormatter``, ``IndexFormatter`` and ``DateIndexFormatter`` +``OldScalarFormatter``, ``IndexFormatter`` and ``IndexDateFormatter`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These formatters are deprecated. Their functionality can be implemented using e.g. `.FuncFormatter`. @@ -238,7 +238,7 @@ The following validators, defined in `.rcsetup`, are deprecated: ``validate_axes_titlelocation``, ``validate_toolbar``, ``validate_ps_papersize``, ``validate_legend_loc``, ``validate_bool_maybe_none``, ``validate_hinting``, -``validate_movie_writers``, ``validate_webagg_address``, +``validate_movie_writer``, ``validate_webagg_address``, ``validate_nseq_float``, ``validate_nseq_int``. To test whether an rcParam value would be acceptable, one can test e.g. ``rc = RcParams(); rc[k] = v`` raises an exception. @@ -273,13 +273,13 @@ mathtext glues The *copy* parameter of ``mathtext.Glue`` is deprecated (the underlying glue spec is now immutable). ``mathtext.GlueSpec`` is deprecated. -Signatures of `.Artist.draw` and `.Axes.draw` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The *inframe* parameter to `.Axes.draw` is deprecated. Use +Signatures of `.Artist.draw` and `matplotlib.axes.Axes.draw` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The *inframe* parameter to `matplotlib.axes.Axes.draw` is deprecated. Use `.Axes.redraw_in_frame` instead. -Not passing the *renderer* parameter to `.Axes.draw` is deprecated. Use -``axes.draw_artist(axes)`` instead. +Not passing the *renderer* parameter to `matplotlib.axes.Axes.draw` is +deprecated. Use ``axes.draw_artist(axes)`` instead. These changes make the signature of the ``draw`` (``artist.draw(renderer)``) method consistent across all artists; thus, additional parameters to @@ -328,7 +328,7 @@ are deprecated. Panning and zooming are now implemented using the Passing None to various Axes subclass factories ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Support for passing ``None`` as base class to `.axes.subplot_class_factory`, +Support for passing ``None`` as base class to ``axes.subplot_class_factory``, ``axes_grid1.parasite_axes.host_axes_class_factory``, ``axes_grid1.parasite_axes.host_subplot_class_factory``, ``axes_grid1.parasite_axes.parasite_axes_class_factory``, and @@ -351,7 +351,7 @@ PGF backend cleanups ~~~~~~~~~~~~~~~~~~~~ The *dummy* parameter of `.RendererPgf` is deprecated. -`.GraphicsContextPgf` is deprecated (use `.GraphicsContextBase` instead). +``GraphicsContextPgf`` is deprecated (use `.GraphicsContextBase` instead). ``set_factor`` method of :mod:`mpl_toolkits.axisartist` locators ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -545,8 +545,8 @@ experimental and may change in the future. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ... is deprecated. -`.epoch2num` and `.num2epoch` are deprecated -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``epoch2num`` and ``num2epoch`` are deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ These are unused and can be easily reproduced by other date tools. `.get_epoch` will return Matplotlib's epoch. @@ -575,7 +575,7 @@ This is deprecated; pass keys as a list of strings instead. Statusbar classes and attributes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The ``statusbar`` attribute of `.FigureManagerBase`, `.StatusbarBase` and all +The ``statusbar`` attribute of `.FigureManagerBase`, ``StatusbarBase`` and all its subclasses, and ``StatusBarWx``, are deprecated, as messages are now displayed in the toolbar instead. @@ -609,8 +609,8 @@ accepted. Qt modifier keys ~~~~~~~~~~~~~~~~ The ``MODIFIER_KEYS``, ``SUPER``, ``ALT``, ``CTRL``, and ``SHIFT`` -global variables of the :mod:`matplotlib.backends.backend_qt4agg`, -:mod:`matplotlib.backends.backend_qt4cairo`, +global variables of the ``matplotlib.backends.backend_qt4agg``, +``matplotlib.backends.backend_qt4cairo``, :mod:`matplotlib.backends.backend_qt5agg` and :mod:`matplotlib.backends.backend_qt5cairo` modules are deprecated. diff --git a/doc/api/prev_api_changes/api_changes_3.3.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.3.0/removals.rst index 3f7c232e9800..36b63c6dcfc8 100644 --- a/doc/api/prev_api_changes/api_changes_3.3.0/removals.rst +++ b/doc/api/prev_api_changes/api_changes_3.3.0/removals.rst @@ -202,7 +202,7 @@ Arguments renamed to ``manage_ticks``. - The ``normed`` parameter of `~.Axes.hist2d` has been renamed to ``density``. - The ``s`` parameter of `.Annotation` has been renamed to ``text``. -- For all functions in `.bezier` that supported a ``tolerence`` parameter, this +- For all functions in `.bezier` that supported a ``tolerance`` parameter, this parameter has been renamed to ``tolerance``. - ``axis("normal")`` is not supported anymore. Use the equivalent ``axis("auto")`` instead. diff --git a/doc/api/prev_api_changes/api_changes_3.3.1.rst b/doc/api/prev_api_changes/api_changes_3.3.1.rst index b3383a4e5fd2..3eda8a9a3a1a 100644 --- a/doc/api/prev_api_changes/api_changes_3.3.1.rst +++ b/doc/api/prev_api_changes/api_changes_3.3.1.rst @@ -15,7 +15,7 @@ reverts the deprecation. Functions ``epoch2num`` and ``dates.julian2num`` use ``date.epoch`` rcParam ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now `~.dates.epoch2num` and (undocumented) ``julian2num`` return floating point +Now ``epoch2num`` and (undocumented) ``julian2num`` return floating point days since `~.dates.get_epoch` as set by :rc:`date.epoch`, instead of floating point days since the old epoch of "0000-12-31T00:00:00". If needed, you can translate from the new to old values as diff --git a/doc/api/prev_api_changes/api_changes_3.4.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.4.0/deprecations.rst index 3de8959bb3ef..9e09f3febe64 100644 --- a/doc/api/prev_api_changes/api_changes_3.4.0/deprecations.rst +++ b/doc/api/prev_api_changes/api_changes_3.4.0/deprecations.rst @@ -38,8 +38,8 @@ Subplot-related attributes and methods Some ``SubplotBase`` methods and attributes have been deprecated and/or moved to `.SubplotSpec`: -- ``get_geometry`` (use `.SubplotBase.get_subplotspec` instead), -- ``change_geometry`` (use `.SubplotBase.set_subplotspec` instead), +- ``get_geometry`` (use ``SubplotBase.get_subplotspec`` instead), +- ``change_geometry`` (use ``SubplotBase.set_subplotspec`` instead), - ``is_first_row``, ``is_last_row``, ``is_first_col``, ``is_last_col`` (use the corresponding methods on the `.SubplotSpec` instance instead), - ``update_params`` (now a no-op), diff --git a/doc/api/prev_api_changes/api_changes_3.4.0/development.rst b/doc/api/prev_api_changes/api_changes_3.4.0/development.rst index ab5e118de9e8..982046c3869e 100644 --- a/doc/api/prev_api_changes/api_changes_3.4.0/development.rst +++ b/doc/api/prev_api_changes/api_changes_3.4.0/development.rst @@ -4,7 +4,7 @@ Development changes Increase to minimum supported versions of Python and dependencies ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For Maptlotlib 3.4, the :ref:`minimum supported versions ` are +For Matplotlib 3.4, the :ref:`minimum supported versions ` are being bumped: +------------+-----------------+---------------+ diff --git a/doc/api/prev_api_changes/api_changes_3.5.0.rst b/doc/api/prev_api_changes/api_changes_3.5.0.rst new file mode 100644 index 000000000000..890484bcd19a --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.5.0.rst @@ -0,0 +1,14 @@ +API Changes for 3.5.0 +===================== + +.. contents:: + :local: + :depth: 1 + +.. include:: /api/prev_api_changes/api_changes_3.5.0/behaviour.rst + +.. include:: /api/prev_api_changes/api_changes_3.5.0/deprecations.rst + +.. include:: /api/prev_api_changes/api_changes_3.5.0/removals.rst + +.. include:: /api/prev_api_changes/api_changes_3.5.0/development.rst diff --git a/doc/api/prev_api_changes/api_changes_3.5.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.5.0/behaviour.rst new file mode 100644 index 000000000000..69e38270ca76 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.5.0/behaviour.rst @@ -0,0 +1,247 @@ +Behaviour changes +----------------- + +First argument to ``subplot_mosaic`` renamed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Both `.FigureBase.subplot_mosaic`, and `.pyplot.subplot_mosaic` have had the +first positional argument renamed from *layout* to *mosaic*. As we have +consolidated the *constrained_layout* and *tight_layout* keyword arguments in +the Figure creation functions of `.pyplot` into a single *layout* keyword +argument, the original ``subplot_mosaic`` argument name would collide. + +As this API is provisional, we are changing this argument name with no +deprecation period. + +.. _Behavioural API Changes 3.5 - Axes children combined: + +``Axes`` children are no longer separated by type +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Formerly, `.axes.Axes` children were separated by `.Artist` type, into sublists +such as ``Axes.lines``. For methods that produced multiple elements (such as +`.Axes.errorbar`), though individual parts would have similar *zorder*, this +separation might cause them to be drawn at different times, causing +inconsistent results when overlapping other Artists. + +Now, the children are no longer separated by type, and the sublist properties +are generated dynamically when accessed. Consequently, Artists will now always +appear in the correct sublist; e.g., if `.axes.Axes.add_line` is called on a +`.Patch`, it will appear in the ``Axes.patches`` sublist, *not* ``Axes.lines``. +The ``Axes.add_*`` methods will now warn if passed an unexpected type. + +Modification of the following sublists is still accepted, but deprecated: + +* ``Axes.artists`` +* ``Axes.collections`` +* ``Axes.images`` +* ``Axes.lines`` +* ``Axes.patches`` +* ``Axes.tables`` +* ``Axes.texts`` + +To remove an Artist, use its `.Artist.remove` method. To add an Artist, use the +corresponding ``Axes.add_*`` method. + +``MatplotlibDeprecationWarning`` now subclasses ``DeprecationWarning`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Historically, it has not been possible to filter +`~matplotlib.MatplotlibDeprecationWarning`\s by checking for +`DeprecationWarning`, since we subclass `UserWarning` directly. + +The decision to not subclass `DeprecationWarning` has to do with a decision +from core Python in the 2.x days to not show `DeprecationWarning`\s to users. +However, there is now a more sophisticated filter in place (see +https://www.python.org/dev/peps/pep-0565/). + +Users will now see `~matplotlib.MatplotlibDeprecationWarning` only during +interactive sessions, and these can be silenced by the standard mechanism: + +.. code:: python + + warnings.filterwarnings("ignore", category=DeprecationWarning) + +Library authors must now enable `DeprecationWarning`\s explicitly in order for +(non-interactive) CI/CD pipelines to report back these warnings, as is standard +for the rest of the Python ecosystem: + +.. code:: python + + warnings.filterwarnings("always", DeprecationWarning) + +``Artist.set`` applies artist properties in the order in which they are given +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The change only affects the interaction between the *color*, *edgecolor*, +*facecolor*, and (for `.Collection`\s) *alpha* properties: the *color* property +now needs to be passed first in order not to override the other properties. +This is consistent with e.g. `.Artist.update`, which did not reorder the +properties passed to it. + +``pcolor(mesh)`` shading defaults to auto +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *shading* keyword argument for `.Axes.pcolormesh` and `.Axes.pcolor` +default has been changed to 'auto'. + +Passing ``Z(M, N)``, ``x(N)``, ``y(M)`` to ``pcolormesh`` with +``shading='flat'`` will now raise a `TypeError`. Use ``shading='auto'`` or +``shading='nearest'`` for ``x`` and ``y`` to be treated as cell centers, or +drop the last column and row of ``Z`` to get the old behaviour with +``shading='flat'``. + +Colorbars now have pan and zoom functionality +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Interactive plots with colorbars can now be zoomed and panned on the colorbar +axis. This adjusts the *vmin* and *vmax* of the `.ScalarMappable` associated +with the colorbar. This is currently only enabled for continuous norms. Norms +used with ``contourf`` and categoricals, such as `.BoundaryNorm` and `.NoNorm`, +have the interactive capability disabled by default. `cb.ax.set_navigate() +<.Axes.set_navigate>` can be used to set whether a colorbar axes is interactive +or not. + +Colorbar lines no longer clipped +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If a colorbar has lines added to it (e.g. for contour lines), these will no +longer be clipped. This is an improvement for lines on the edge of the +colorbar, but could lead to lines off the colorbar if the limits of the +colorbar are changed. + +``Figure.suppressComposite`` now also controls compositing of Axes images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The output of ``NonUniformImage`` and ``PcolorImage`` has changed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Pixel-level differences may be observed in images generated using +`.NonUniformImage` or `.PcolorImage`, typically for pixels exactly at the +boundary between two data cells (no user-facing axes method currently generates +`.NonUniformImage`\s, and only `.pcolorfast` can generate `.PcolorImage`\s). +These artists are also now slower, normally by ~1.5x but sometimes more (in +particular for ``NonUniformImage(interpolation="bilinear")``. This slowdown +arises from fixing occasional floating point inaccuracies. + +Change of the (default) legend handler for ``Line2D`` instances +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The default legend handler for Line2D instances (`.HandlerLine2D`) now +consistently exposes all the attributes and methods related to the line marker +(:ghissue:`11358`). This makes it easy to change the marker features after +instantiating a legend. + +.. code:: + + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + + ax.plot([1, 3, 2], marker="s", label="Line", color="pink", mec="red", ms=8) + leg = ax.legend() + + leg.legendHandles[0].set_color("lightgray") + leg.legendHandles[0].set_mec("black") # marker edge color + +The former legend handler for Line2D objects has been renamed +`.HandlerLine2DCompound`. To revert to the previous behaviour, one can use + +.. code:: + + import matplotlib.legend as mlegend + from matplotlib.legend_handler import HandlerLine2DCompound + from matplotlib.lines import Line2D + + mlegend.Legend.update_default_handler_map({Line2D: HandlerLine2DCompound()}) + +Setting ``Line2D`` marker edge/face color to *None* use rcParams +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Line2D.set_markeredgecolor(None)`` and ``Line2D.set_markerfacecolor(None)`` +now set the line property using the corresponding rcParam +(:rc:`lines.markeredgecolor` and :rc:`lines.markerfacecolor`). This is +consistent with other `.Line2D` property setters. + +Default theta tick locations for wedge polar plots have changed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For polar plots that don't cover a full circle, the default theta tick +locations are now at multiples of 10°, 15°, 30°, 45°, 90°, rather than using +values that mostly make sense for linear plots (20°, 25°, etc.). + +``axvspan`` now plots full wedges in polar plots +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... rather than triangles. + +Convenience converter from ``Scale`` to ``Normalize`` now public +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Downstream libraries can take advantage of `.colors.make_norm_from_scale` to +create a `~.colors.Normalize` subclass directly from an existing scale. +Usually norms have a scale, and the advantage of having a `~.scale.ScaleBase` +attached to a norm is to provide a scale, and associated tick locators and +formatters, for the colorbar. + +``ContourSet`` always use ``PathCollection`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to correct rendering issues with closed loops, the `.ContourSet` now +creates a `.PathCollection` instead of a `.LineCollection` for line contours. +This type matches the artist used for filled contours. + +This affects `.ContourSet` itself and its subclasses, `.QuadContourSet` +(returned by `.Axes.contour`), and `.TriContourSet` (returned by +`.Axes.tricontour`). + +``hatch.SmallFilledCircles`` inherits from ``hatch.Circles`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `.hatch.SmallFilledCircles` class now inherits from `.hatch.Circles` rather +than from `.hatch.SmallCircles`. + +hexbin with a log norm +~~~~~~~~~~~~~~~~~~~~~~ + +`~.axes.Axes.hexbin` no longer (incorrectly) adds 1 to every bin value if a log +norm is being used. + +Setting invalid ``rcParams["date.converter"]`` now raises ValueError +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, invalid values passed to :rc:`date.converter` would be ignored with +a `UserWarning`, but now raise `ValueError`. + +``Text`` and ``TextBox`` added *parse_math* option +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`.Text` and `.TextBox` objects now allow a *parse_math* keyword-only argument +which controls whether math should be parsed from the displayed string. If +*True*, the string will be parsed as a math text object. If *False*, the string +will be considered a literal and no parsing will occur. + +For `.Text`, this argument defaults to *True*. For `.TextBox` this argument +defaults to *False*. + +``Type1Font`` objects now decrypt the encrypted part +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Type 1 fonts have a large part of their code encrypted as an obsolete +copy-protection measure. This part is now available decrypted as the +``decrypted`` attribute of ``matplotlib.type1font.Type1Font``. This decrypted +data is not yet parsed, but this is a prerequisite for implementing subsetting. + +3D contourf polygons placed between levels +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The polygons used in a 3D `~.Axes3D.contourf` plot are now +placed halfway between the contour levels, as each polygon represents the +location of values that lie between two levels. + +``AxesDivider`` now defaults to rcParams-specified pads +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`.AxesDivider.append_axes`, ``AxesDivider.new_horizontal``, and +``AxesDivider.new_vertical`` now default to paddings specified by +:rc:`figure.subplot.wspace` and :rc:`figure.subplot.hspace` rather than zero. diff --git a/doc/api/prev_api_changes/api_changes_3.5.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.5.0/deprecations.rst new file mode 100644 index 000000000000..7bb9009fbe77 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.5.0/deprecations.rst @@ -0,0 +1,379 @@ +Deprecations +------------ + +Discouraged: ``Figure`` parameters *tight_layout* and *constrained_layout* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``Figure`` parameters *tight_layout* and *constrained_layout* are +triggering competing layout mechanisms and thus should not be used together. + +To make the API clearer, we've merged them under the new parameter *layout* +with values 'constrained' (equal to ``constrained_layout=True``), 'tight' +(equal to ``tight_layout=True``). If given, *layout* takes precedence. + +The use of *tight_layout* and *constrained_layout* is discouraged in favor of +*layout*. However, these parameters will stay available for backward +compatibility. + +Modification of ``Axes`` children sublists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +See :ref:`Behavioural API Changes 3.5 - Axes children combined` for more +information; modification of the following sublists is deprecated: + +* ``Axes.artists`` +* ``Axes.collections`` +* ``Axes.images`` +* ``Axes.lines`` +* ``Axes.patches`` +* ``Axes.tables`` +* ``Axes.texts`` + +To remove an Artist, use its `.Artist.remove` method. To add an Artist, use the +corresponding ``Axes.add_*`` method. + +Passing incorrect types to ``Axes.add_*`` methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following ``Axes.add_*`` methods will now warn if passed an unexpected +type. See their documentation for the types they expect. + +- `.Axes.add_collection` +- `.Axes.add_image` +- `.Axes.add_line` +- `.Axes.add_patch` +- `.Axes.add_table` + +Discouraged: ``plot_date`` +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The use of `~.Axes.plot_date` is discouraged. This method exists for historic +reasons and may be deprecated in the future. + +- ``datetime``-like data should directly be plotted using `~.Axes.plot`. +- If you need to plot plain numeric data as :ref:`date-format` or + need to set a timezone, call ``ax.xaxis.axis_date`` / ``ax.yaxis.axis_date`` + before `~.Axes.plot`. See `.Axis.axis_date`. + +``epoch2num`` and ``num2epoch`` are deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These methods convert from unix timestamps to matplotlib floats, but are not +used internally to matplotlib, and should not be needed by end users. To +convert a unix timestamp to datetime, simply use +`datetime.datetime.utcfromtimestamp`, or to use NumPy `~numpy.datetime64` +``dt = np.datetime64(e*1e6, 'us')``. + +Auto-removal of grids by `~.Axes.pcolor` and `~.Axes.pcolormesh` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.Axes.pcolor` and `~.Axes.pcolormesh` currently remove any visible axes major +grid. This behavior is deprecated; please explicitly call ``ax.grid(False)`` to +remove the grid. + +The first parameter of ``Axes.grid`` and ``Axis.grid`` has been renamed to *visible* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The parameter was previously named *b*. This deprecation only matters if that +parameter was passed using a keyword argument, e.g. ``grid(b=False)``. + +Unification and cleanup of Selector widget API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The API for Selector widgets has been unified to use: + +- *props* for the properties of the Artist representing the selection. +- *handle_props* for the Artists representing handles for modifying the + selection. +- *grab_range* for the maximal tolerance to grab a handle with the mouse. + +Additionally, several internal parameters and attribute have been deprecated +with the intention of keeping them private. + +RectangleSelector and EllipseSelector +..................................... + +The *drawtype* keyword argument to `~matplotlib.widgets.RectangleSelector` is +deprecated. In the future the only behaviour will be the default behaviour of +``drawtype='box'``. + +Support for ``drawtype=line`` will be removed altogether as it is not clear +which points are within and outside a selector that is just a line. As a +result, the *lineprops* keyword argument to +`~matplotlib.widgets.RectangleSelector` is also deprecated. + +To retain the behaviour of ``drawtype='none'``, use ``rectprops={'visible': +False}`` to make the drawn `~matplotlib.patches.Rectangle` invisible. + +Cleaned up attributes and arguments are: + +- The ``active_handle`` attribute has been privatized and deprecated. +- The ``drawtype`` attribute has been privatized and deprecated. +- The ``eventpress`` attribute has been privatized and deprecated. +- The ``eventrelease`` attribute has been privatized and deprecated. +- The ``interactive`` attribute has been privatized and deprecated. +- The *marker_props* argument is deprecated, use *handle_props* instead. +- The *maxdist* argument is deprecated, use *grab_range* instead. +- The *rectprops* argument is deprecated, use *props* instead. +- The ``rectprops`` attribute has been privatized and deprecated. +- The ``state`` attribute has been privatized and deprecated. +- The ``to_draw`` attribute has been privatized and deprecated. + +PolygonSelector +............... + +- The *line* attribute is deprecated. If you want to change the selector artist + properties, use the ``set_props`` or ``set_handle_props`` methods. +- The *lineprops* argument is deprecated, use *props* instead. +- The *markerprops* argument is deprecated, use *handle_props* instead. +- The *maxdist* argument and attribute is deprecated, use *grab_range* instead. +- The *vertex_select_radius* argument and attribute is deprecated, use + *grab_range* instead. + +SpanSelector +............ + +- The ``active_handle`` attribute has been privatized and deprecated. +- The ``eventpress`` attribute has been privatized and deprecated. +- The ``eventrelease`` attribute has been privatized and deprecated. +- The *maxdist* argument and attribute is deprecated, use *grab_range* instead. +- The ``pressv`` attribute has been privatized and deprecated. +- The ``prev`` attribute has been privatized and deprecated. +- The ``rect`` attribute has been privatized and deprecated. +- The *rectprops* argument is deprecated, use *props* instead. +- The ``rectprops`` attribute has been privatized and deprecated. +- The *span_stays* argument is deprecated, use the *interactive* argument + instead. +- The ``span_stays`` attribute has been privatized and deprecated. +- The ``state`` attribute has been privatized and deprecated. + +LassoSelector +............. + +- The *lineprops* argument is deprecated, use *props* instead. +- The ``onpress`` and ``onrelease`` methods are deprecated. They are straight + aliases for ``press`` and ``release``. + +``ConversionInterface.convert`` no longer needs to accept unitless values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, custom subclasses of `.units.ConversionInterface` needed to +implement a ``convert`` method that not only accepted instances of the unit, +but also unitless values (which are passed through as is). This is no longer +the case (``convert`` is never called with a unitless value), and such support +in `.StrCategoryConverter` is deprecated. Likewise, the +``.ConversionInterface.is_numlike`` helper is deprecated. + +Consider calling `.Axis.convert_units` instead, which still supports unitless +values. + +Locator and Formatter wrapper methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``set_view_interval``, ``set_data_interval`` and ``set_bounds`` methods of +`.Locator`\s and `.Formatter`\s (and their common base class, TickHelper) are +deprecated. Directly manipulate the view and data intervals on the underlying +axis instead. + +Unused positional parameters to ``print_`` methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +None of the ``print_`` methods implemented by canvas subclasses used +positional arguments other that the first (the output filename or file-like), +so these extra parameters are deprecated. + +``QuadMesh`` signature +~~~~~~~~~~~~~~~~~~~~~~ + +The `.QuadMesh` signature :: + + def __init__(meshWidth, meshHeight, coordinates, + antialiased=True, shading='flat', **kwargs) + +is deprecated and replaced by the new signature :: + + def __init__(coordinates, *, antialiased=True, shading='flat', **kwargs) + +In particular: + +- The *coordinates* argument must now be a (M, N, 2) array-like. Previously, + the grid shape was separately specified as (*meshHeight* + 1, *meshWidth* + + 1) and *coordinates* could be an array-like of any shape with M * N * 2 + elements. +- All parameters except *coordinates* are keyword-only now. + +rcParams will no longer cast inputs to str +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +After a deprecation period, rcParams that expect a (non-pathlike) str will no +longer cast non-str inputs using `str`. This will avoid confusing errors in +subsequent code if e.g. a list input gets implicitly cast to a str. + +Case-insensitive scales +~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, scales could be set case-insensitively (e.g., +``set_xscale("LoG")``). This is deprecated; all builtin scales use lowercase +names. + +Interactive cursor details +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Setting a mouse cursor on a window has been moved from the toolbar to the +canvas. Consequently, several implementation details on toolbars and within +backends have been deprecated. + +``NavigationToolbar2.set_cursor`` and ``backend_tools.SetCursorBase.set_cursor`` +................................................................................ + +Instead, use the `.FigureCanvasBase.set_cursor` method on the canvas (available +as the ``canvas`` attribute on the toolbar or the Figure.) + +``backend_tools.SetCursorBase`` and subclasses +.............................................. + +``backend_tools.SetCursorBase`` was subclassed to provide backend-specific +implementations of ``set_cursor``. As that is now deprecated, the subclassing +is no longer necessary. Consequently, the following subclasses are also +deprecated: + +- ``matplotlib.backends.backend_gtk3.SetCursorGTK3`` +- ``matplotlib.backends.backend_qt5.SetCursorQt`` +- ``matplotlib.backends._backend_tk.SetCursorTk`` +- ``matplotlib.backends.backend_wx.SetCursorWx`` + +Instead, use the `.backend_tools.ToolSetCursor` class. + +``cursord`` in GTK, Qt, and wx backends +....................................... + +The ``backend_gtk3.cursord``, ``backend_qt.cursord``, and +``backend_wx.cursord`` dictionaries are deprecated. This makes the GTK module +importable on headless environments. + +Miscellaneous deprecations +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- ``is_url`` and ``URL_REGEX`` are deprecated. (They were previously defined in + the toplevel :mod:`matplotlib` module.) +- The ``ArrowStyle.beginarrow`` and ``ArrowStyle.endarrow`` attributes are + deprecated; use the ``arrow`` attribute to define the desired heads and tails + of the arrow. +- ``backend_pgf.LatexManager.str_cache`` is deprecated. +- ``backends.qt_compat.ETS`` and ``backends.qt_compat.QT_RC_MAJOR_VERSION`` are + deprecated, with no replacement. +- The ``blocking_input`` module has been deprecated. Instead, use + ``canvas.start_event_loop()`` and ``canvas.stop_event_loop()`` while + connecting event callbacks as needed. +- ``cbook.report_memory`` is deprecated; use ``psutil.virtual_memory`` instead. +- ``cm.LUTSIZE`` is deprecated. Use :rc:`image.lut` instead. This value only + affects colormap quantization levels for default colormaps generated at + module import time. +- ``Collection.__init__`` previously ignored *transOffset* without *offsets* also + being specified. In the future, *transOffset* will begin having an effect + regardless of *offsets*. In the meantime, if you wish to set *transOffset*, + call `.Collection.set_offset_transform` explicitly. +- ``Colorbar.patch`` is deprecated; this attribute is not correctly updated + anymore. +- ``ContourLabeler.get_label_width`` is deprecated. +- ``dviread.PsfontsMap`` now raises LookupError instead of KeyError for missing + fonts. +- ``Dvi.baseline`` is deprecated (with no replacement). +- The *format* parameter of ``dviread.find_tex_file`` is deprecated (with no + replacement). +- ``FancyArrowPatch.get_path_in_displaycoord`` and + ``ConnectionPath.get_path_in_displaycoord`` are deprecated. The path in + display coordinates can still be obtained, as for other patches, using + ``patch.get_transform().transform_path(patch.get_path())``. +- The ``font_manager.win32InstalledFonts`` and + ``font_manager.get_fontconfig_fonts`` helper functions have been deprecated. +- All parameters of ``imshow`` starting from *aspect* will become keyword-only. +- ``QuadMesh.convert_mesh_to_paths`` and ``QuadMesh.convert_mesh_to_triangles`` + are deprecated. ``QuadMesh.get_paths()`` can be used as an alternative for + the former; there is no replacement for the latter. +- ``ScalarMappable.callbacksSM`` is deprecated. Use + ``ScalarMappable.callbacks`` instead. +- ``streamplot.get_integrator`` is deprecated. +- ``style.core.STYLE_FILE_PATTERN``, ``style.core.load_base_library``, and + ``style.core.iter_user_libraries`` are deprecated. +- ``SubplotParams.validate`` is deprecated. Use `.SubplotParams.update` to + change `.SubplotParams` while always keeping it in a valid state. +- The ``grey_arrayd``, ``font_family``, ``font_families``, and ``font_info`` + attributes of `.TexManager` are deprecated. +- ``Text.get_prop_tup`` is deprecated with no replacements (because the `.Text` + class cannot know whether a backend needs to update cache e.g. when the + text's color changes). +- ``Tick.apply_tickdir`` didn't actually update the tick markers on the + existing Line2D objects used to draw the ticks and is deprecated; use + `.Axis.set_tick_params` instead. +- ``tight_layout.auto_adjust_subplotpars`` is deprecated. + +- The ``grid_info`` attribute of ``axisartist`` classes has been deprecated. +- ``axisartist.clip_path`` is deprecated with no replacement. +- ``axes_grid1.axes_grid.CbarAxes`` and ``axes_grid1.axisartist.CbarAxes`` are + deprecated (they are now dynamically generated based on the owning axes + class). +- The ``axes_grid1.Divider.get_vsize_hsize`` and + ``axes_grid1.Grid.get_vsize_hsize`` methods are deprecated. Copy their + implementations if needed. +- ``AxesDivider.append_axes(..., add_to_figure=False)`` is deprecated. Use + ``ax.remove()`` to remove the Axes from the figure if needed. +- ``FixedAxisArtistHelper.change_tick_coord`` is deprecated with no + replacement. +- ``floating_axes.GridHelperCurveLinear.get_boundary`` is deprecated, with no + replacement. +- ``ParasiteAxesBase.get_images_artists`` has been deprecated. + +- The "units finalize" signal (previously emitted by Axis instances) is + deprecated. Connect to "units" instead. +- Passing formatting parameters positionally to ``stem()`` is deprecated + +``plot_directive`` deprecations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``:encoding:`` option to ``.. plot`` directive has had no effect since +Matplotlib 1.3.1, and is now deprecated. + +The following helpers in `matplotlib.sphinxext.plot_directive` are deprecated: + +- ``unescape_doctest`` (use `doctest.script_from_examples` instead), +- ``split_code_at_show``, +- ``run_code``. + +Testing support +~~~~~~~~~~~~~~~ + +``matplotlib.test()`` is deprecated +................................... + +Run tests using ``pytest`` from the commandline instead. The variable +``matplotlib.default_test_modules`` is only used for ``matplotlib.test()`` and +is thus deprecated as well. + +To test an installed copy, be sure to specify both ``matplotlib`` and +``mpl_toolkits`` with ``--pyargs``:: + + python -m pytest --pyargs matplotlib.tests mpl_toolkits.tests + +See :ref:`testing` for more details. + +Unused pytest fixtures and markers +.................................. + +The fixture ``matplotlib.testing.conftest.mpl_image_comparison_parameters`` is +not used internally by Matplotlib. If you use this please copy it into your +code base. + +The ``@pytest.mark.style`` marker is deprecated; use ``@mpl.style.context``, +which has the same effect. + +Support for ``nx1 = None`` or ``ny1 = None`` in ``AxesLocator`` and ``Divider.locate`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In `.axes_grid1.axes_divider`, various internal APIs will stop supporting +passing ``nx1 = None`` or ``ny1 = None`` to mean ``nx + 1`` or ``ny + 1``, in +preparation for a possible future API which allows indexing and slicing of +dividers (possibly ``divider[a:b] == divider.new_locator(a, b)``, but also +``divider[a:] == divider.new_locator(a, )``). The user-facing +`.Divider.new_locator` API is unaffected -- it correctly normalizes ``nx1 = +None`` and ``ny1 = None`` as needed. diff --git a/doc/api/prev_api_changes/api_changes_3.5.0/development.rst b/doc/api/prev_api_changes/api_changes_3.5.0/development.rst new file mode 100644 index 000000000000..2db21237a699 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.5.0/development.rst @@ -0,0 +1,82 @@ +Development changes +------------------- + +Increase to minimum supported versions of dependencies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For Matplotlib 3.5, the :ref:`minimum supported versions ` and +some :ref:`optional dependencies ` are being bumped: + ++---------------+---------------+---------------+ +| Dependency | min in mpl3.4 | min in mpl3.5 | ++===============+===============+===============+ +| NumPy | 1.16 | 1.17 | ++---------------+---------------+---------------+ +| Tk (optional) | 8.3 | 8.4 | ++---------------+---------------+---------------+ + +This is consistent with our :ref:`min_deps_policy` and `NEP29 +`__ + +New wheel architectures +~~~~~~~~~~~~~~~~~~~~~~~ + +Wheels have been added for: + +- Python 3.10 +- PyPy 3.7 +- macOS on Apple Silicon (both arm64 and universal2) + +New build dependencies +~~~~~~~~~~~~~~~~~~~~~~ + +Versioning has been switched from bundled versioneer to `setuptools-scm +`__ using the +``release-branch-semver`` version scheme. The latter is well-maintained, but +may require slight modification to packaging scripts. + +The `setuptools-scm-git-archive +`__ plugin is also used +for consistent version export. + +Data directory is no longer optional +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Historically, the ``mpl-data`` directory has been optional (example files were +unnecessary, and fonts could be deleted if a suitable dependency on a system +font were provided). Though example files are still optional, they have been +substantially pared down, and we now consider the directory to be required. + +Specifically, the ``matplotlibrc`` file found there is used for runtime +verifications and must exist. Packagers may still symlink fonts to system +versions if needed. + +New runtime dependencies +~~~~~~~~~~~~~~~~~~~~~~~~ + +fontTools for type 42 subsetting +................................ + +A new dependency `fontTools `_ is integrated +into Matplotlib 3.5. It is designed to be used with PS/EPS and PDF documents; +and handles Type 42 font subsetting. + +Underscore support in LaTeX +........................... + +The `underscore `_ package is now a +requirement to improve support for underscores in LaTeX. + +This is consistent with our :ref:`min_deps_policy`. + +Matplotlib-specific build options moved from ``setup.cfg`` to ``mplsetup.cfg`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In order to avoid conflicting with the use of :file:`setup.cfg` by +``setuptools``, the Matplotlib-specific build options have moved from +``setup.cfg`` to ``mplsetup.cfg``. The :file:`setup.cfg.template` has been +correspondingly been renamed to :file:`mplsetup.cfg.template`. + +Note that the path to this configuration file can still be set via the +:envvar:`MPLSETUPCFG` environment variable, which allows one to keep using the +same file before and after this change. diff --git a/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst new file mode 100644 index 000000000000..45b574e04cf5 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.5.0/removals.rst @@ -0,0 +1,365 @@ +Removals +-------- + +The following deprecated APIs have been removed: + +Removed behaviour +~~~~~~~~~~~~~~~~~ + +Stricter validation of function parameters +.......................................... + +- Calling `.Figure.add_axes` with no arguments will raise an error. Adding a + free-floating axes needs a position rectangle. If you want a figure-filling + single axes, use `.Figure.add_subplot` instead. +- `.Figure.add_subplot` validates its inputs; in particular, for + ``add_subplot(rows, cols, index)``, all parameters must be integral. + Previously strings and floats were accepted and converted to int. +- Passing *None* as the *which* argument to ``autofmt_xdate`` is no longer + supported; use its more explicit synonym, ``which="major"``, instead. +- Setting the *orientation* of an ``eventplot()`` or `.EventCollection` to + "none" or *None* is no longer supported; set it to "horizontal" instead. + Moreover, the two orientations ("horizontal" and "vertical") are now + case-sensitive. +- Passing parameters *norm* and *vmin*/*vmax* simultaneously to functions using + colormapping such as ``scatter()`` and ``imshow()`` is no longer supported. + Instead of ``norm=LogNorm(), vmin=min_val, vmax=max_val`` pass + ``norm=LogNorm(min_val, max_val)``. *vmin* and *vmax* should only be used + without setting *norm*. +- Passing *None* as either the *radius* or *startangle* arguments of an + `.Axes.pie` is no longer accepted; use the explicit defaults of 1 and 0, + respectively, instead. +- Passing *None* as the *normalize* argument of `.Axes.pie` (the former + default) is no longer accepted, and the pie will always be normalized by + default. If you wish to plot an incomplete pie, explicitly pass + ``normalize=False``. +- Support for passing *None* to ``subplot_class_factory`` has been removed. + Explicitly pass in the base `~matplotlib.axes.Axes` class instead. +- Passing multiple keys as a single comma-separated string or multiple + arguments to `.ToolManager.update_keymap` is no longer supported; pass keys + as a list of strings instead. +- Passing the dash offset as *None* is no longer accepted, as this was never + universally implemented, e.g. for vector output. Set the offset to 0 instead. +- Setting a custom method overriding `.Artist.contains` using + ``Artist.set_contains`` has been removed, as has ``Artist.get_contains``. + There is no replacement, but you may still customize pick events using + `.Artist.set_picker`. +- `~.Axes.semilogx`, `~.Axes.semilogy`, `~.Axes.loglog`, `.LogScale`, and + `.SymmetricalLogScale` used to take keyword arguments that depends on the + axis orientation ("basex" vs "basey", "subsx" vs "subsy", "nonposx" vs + "nonposy"); these parameter names have been removed in favor of "base", + "subs", "nonpositive". This removal also affects e.g. ``ax.set_yscale("log", + basey=...)`` which must now be spelled ``ax.set_yscale("log", base=...)``. + + The change from "nonpos" to "nonpositive" also affects + `~.scale.LogTransform`, `~.scale.InvertedLogTransform`, + `~.scale.SymmetricalLogTransform`, etc. + + To use *different* bases for the x-axis and y-axis of a `~.Axes.loglog` plot, + use e.g. ``ax.set_xscale("log", base=10); ax.set_yscale("log", base=2)``. +- Passing *None*, or no argument, to ``parasite_axes_class_factory``, + ``parasite_axes_auxtrans_class_factory``, ``host_axes_class_factory`` is no + longer accepted; pass an explicit base class instead. + +Case-sensitivity is now enforced more +...................................... + +- Upper or mixed-case property names are no longer normalized to lowercase in + `.Artist.set` and `.Artist.update`. This allows one to pass names such as + *patchA* or *UVC*. +- Case-insensitive capstyles and joinstyles are no longer lower-cased; please + pass capstyles ("miter", "round", "bevel") and joinstyles ("butt", "round", + "projecting") as lowercase. +- Saving metadata in PDF with the PGF backend no longer changes keys to + lowercase. Only the canonically cased keys listed in the PDF specification + (and the `~.backend_pgf.PdfPages` documentation) are accepted. + +No implicit initialization of ``Tick`` attributes +................................................. + +The `.Tick` constructor no longer initializes the attributes ``tick1line``, +``tick2line``, ``gridline``, ``label1``, and ``label2`` via ``_get_tick1line``, +``_get_tick2line``, ``_get_gridline``, ``_get_text1``, and ``_get_text2``. +Please directly set the attribute in the subclass' ``__init__`` instead. + +``NavigationToolbar2`` subclass changes +....................................... + +Overriding the ``_init_toolbar`` method of `.NavigationToolbar2` to initialize +third-party toolbars is no longer supported. Instead, the toolbar should be +initialized in the ``__init__`` method of the subclass (which should call the +base-class' ``__init__`` as appropriate). + +The ``press`` and ``release`` methods of `.NavigationToolbar2` were called when +pressing or releasing a mouse button, but *only* when an interactive pan or +zoom was occurring (contrary to what the docs stated). They are no longer +called; if you write a backend which needs to customize such events, please +directly override ``press_pan``/``press_zoom``/``release_pan``/``release_zoom`` +instead. + +Removal of old file mode flag +............................. + +Flags containing "U" passed to `.cbook.to_filehandle` and `.cbook.open_file_cm` +are no longer accepted. This is consistent with their removal from `open` in +Python 3.9. + +Keymaps toggling ``Axes.get_navigate`` have been removed +........................................................ + +This includes numeric key events and rcParams. + +The ``TTFPATH`` and ``AFMPATH`` environment variables +..................................................... + +Support for the (undocumented) ``TTFPATH`` and ``AFMPATH`` environment +variables has been removed. Register additional fonts using +``matplotlib.font_manager.fontManager.addfont()``. + +Modules +~~~~~~~ + +- ``matplotlib.backends.qt_editor.formsubplottool``; use + ``matplotlib.backends.backend_qt.SubplotToolQt`` instead. +- ``matplotlib.compat`` +- ``matplotlib.ttconv`` +- The Qt4-based backends, ``qt4agg`` and ``qt4cairo``, have been removed. Qt4 + has reached its end-of-life in 2015 and there are no releases of either PyQt4 + or PySide for recent versions of Python. Please use one of the Qt5 or Qt6 + backends. + +Classes, methods and attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following module-level classes/variables have been removed: + +- ``backend_bases.StatusbarBase`` and all its subclasses, and ``StatusBarWx``; + messages are displayed in the toolbar +- ``backend_pgf.GraphicsContextPgf`` +- ``MODIFIER_KEYS``, ``SUPER``, ``ALT``, ``CTRL``, and ``SHIFT`` of + `matplotlib.backends.backend_qt5agg` and + `matplotlib.backends.backend_qt5cairo` +- ``backend_wx.DEBUG_MSG`` +- ``dviread.Encoding`` +- ``Fil``, ``Fill``, ``Filll``, ``NegFil``, ``NegFill``, ``NegFilll``, and + ``SsGlue`` from `.mathtext`; directly construct glue instances with + ``Glue("fil")``, etc. +- ``mathtext.GlueSpec`` +- ``OldScalarFormatter``, ``IndexFormatter`` and ``IndexDateFormatter``; use + `.FuncFormatter` instead +- ``OldAutoLocator`` +- ``AVConvBase``, ``AVConvWriter`` and ``AVConvFileWriter``. Debian 8 (2015, + EOL 06/2020) and Ubuntu 14.04 (EOL 04/2019) were the last versions of Debian + and Ubuntu to ship avconv. It remains possible to force the use of avconv by + using the FFmpeg-based writers with :rc:`animation.ffmpeg_path` set to + "avconv". +- ``matplotlib.axes._subplots._subplot_classes`` +- ``axes_grid1.axes_rgb.RGBAxesBase``; use ``RGBAxes`` instead + +The following class attributes have been removed: + +- ``backend_pgf.LatexManager.latex_stdin_utf8`` +- ``backend_pgf.PdfPages.metadata`` +- ``ContourSet.ax`` and ``Quiver.ax``; use ``ContourSet.axes`` or + ``Quiver.axes`` as with other artists +- ``DateFormatter.illegal_s`` +- ``dates.YearLocator.replaced``; `.YearLocator` is now a subclass of + `.RRuleLocator`, and the attribute ``YearLocator.replaced`` has been removed. + For tick locations that required modifying this, a custom rrule and + `.RRuleLocator` can be used instead. +- ``FigureManagerBase.statusbar``; messages are displayed in the toolbar +- ``FileMovieWriter.clear_temp`` +- ``mathtext.Glue.glue_subtype`` +- ``MovieWriter.args_key``, ``MovieWriter.exec_key``, and + ``HTMLWriter.args_key`` +- ``NavigationToolbar2QT.basedir``; the base directory to the icons is + ``os.path.join(mpl.get_data_path(), "images")`` +- ``NavigationToolbar2QT.ctx`` +- ``NavigationToolbar2QT.parent``; to access the parent window, use + ``toolbar.canvas.parent()`` or ``toolbar.parent()`` +- ``prevZoomRect``, ``retinaFix``, ``savedRetinaImage``, ``wxoverlay``, + ``zoomAxes``, ``zoomStartX``, and ``zoomStartY`` attributes of + ``NavigationToolbar2Wx`` +- ``NonUniformImage.is_grayscale``, ``PcolorImage.is_grayscale``, for + consistency with ``AxesImage.is_grayscale``. (Note that previously, these + attributes were only available *after rendering the image*). +- ``RendererCairo.fontweights``, ``RendererCairo.fontangles`` +- ``used_characters`` of `.RendererPdf`, `.PdfFile`, and `.RendererPS` +- ``LogScale.LogTransform``, ``LogScale.InvertedLogTransform``, + ``SymmetricalScale.SymmetricalTransform``, and + ``SymmetricalScale.InvertedSymmetricalTransform``; directly access the + transform classes from `matplotlib.scale` +- ``cachedir``, ``rgba_arrayd``, ``serif``, ``sans_serif``, ``cursive``, and + ``monospace`` attributes of `.TexManager` +- ``axleft``, ``axright``, ``axbottom``, ``axtop``, ``axwspace``, and + ``axhspace`` attributes of `.widgets.SubplotTool`; access the ``ax`` + attribute of the corresponding slider +- ``widgets.TextBox.params_to_disable`` +- ``angle_helper.LocatorBase.den``; it has been renamed to *nbins* +- ``axes_grid.CbarAxesBase.cbid`` and ``axes_grid.CbarAxesBase.locator``; use + ``mappable.colorbar_cid`` or ``colorbar.locator`` instead + +The following class methods have been removed: + +- ``Axes.update_datalim_bounds``; use ``ax.dataLim.set(Bbox.union([ax.dataLim, + bounds]))`` +- ``pan`` and ``zoom`` methods of `~.axis.Axis` and `~.ticker.Locator` have + been removed; panning and zooming are now implemented using the + ``start_pan``, ``drag_pan``, and ``end_pan`` methods of `~.axes.Axes` +- ``.BboxBase.inverse_transformed``; call `.BboxBase.transformed` on the + `~.Transform.inverted()` transform +- ``Collection.set_offset_position`` and ``Collection.get_offset_position`` + have been removed; the ``offset_position`` of the `.Collection` class is now + "screen" +- ``Colorbar.on_mappable_changed`` and ``Colorbar.update_bruteforce``; use + ``Colorbar.update_normal()`` instead +- ``docstring.Substitution.from_params`` has been removed; directly assign to + ``params`` of ``docstring.Substitution`` instead +- ``DraggableBase.artist_picker``; set the artist's picker instead +- ``DraggableBase.on_motion_blit``; use `.DraggableBase.on_motion` instead +- ``FigureCanvasGTK3._renderer_init`` +- ``Locator.refresh()`` and the associated helper methods + ``NavigationToolbar2.draw()`` and ``ToolViewsPositions.refresh_locators()`` +- ``track_characters`` and ``merge_used_characters`` of `.RendererPdf`, + `.PdfFile`, and `.RendererPS` +- ``RendererWx.get_gc`` +- ``SubplotSpec.get_rows_columns``; use the ``GridSpec.nrows``, + ``GridSpec.ncols``, ``SubplotSpec.rowspan``, and ``SubplotSpec.colspan`` + properties instead. +- ``ScalarMappable.update_dict``, ``ScalarMappable.add_checker()``, and + ``ScalarMappable.check_update()``; register a callback in + ``ScalarMappable.callbacks`` to be notified of updates +- ``TexManager.make_tex_preview`` and ``TexManager.make_dvi_preview`` +- ``funcleft``, ``funcright``, ``funcbottom``, ``functop``, ``funcwspace``, and + ``funchspace`` methods of `.widgets.SubplotTool` + +- ``axes_grid1.axes_rgb.RGBAxes.add_RGB_to_figure`` +- ``axisartist.axis_artist.AxisArtist.dpi_transform`` +- ``axisartist.grid_finder.MaxNLocator.set_factor`` and + ``axisartist.grid_finder.FixedLocator.set_factor``; the factor is always 1 + now + +Functions +~~~~~~~~~ + +- ``bezier.make_path_regular`` has been removed; use ``Path.cleaned()`` (or + ``Path.cleaned(curves=True)``, etc.) instead, but note that these methods add + a ``STOP`` code at the end of the path. +- ``bezier.concatenate_paths`` has been removed; use + ``Path.make_compound_path()`` instead. +- ``cbook.local_over_kwdict`` has been removed; use `.cbook.normalize_kwargs` + instead. +- ``qt_compat.is_pyqt5`` has been removed due to the release of PyQt6. The Qt + version can be checked using ``QtCore.qVersion()``. +- ``testing.compare.make_external_conversion_command`` has been removed. +- ``axes_grid1.axes_rgb.imshow_rgb`` has been removed; use + ``imshow(np.dstack([r, g, b]))`` instead. + +Arguments +~~~~~~~~~ + +- The *s* parameter to `.Axes.annotate` and `.pyplot.annotate` is no longer + supported; use the new name *text*. +- The *inframe* parameter to `matplotlib.axes.Axes.draw` has been removed; use + `.Axes.redraw_in_frame` instead. +- The *required*, *forbidden* and *allowed* parameters of + `.cbook.normalize_kwargs` have been removed. +- The *ismath* parameter of the ``draw_tex`` method of all renderer classes has + been removed (as a call to ``draw_tex`` — not to be confused with + ``draw_text``! — means that the entire string should be passed to the + ``usetex`` machinery anyways). Likewise, the text machinery will no longer + pass the *ismath* parameter when calling ``draw_tex`` (this should only + matter for backend implementers). +- The *quality*, *optimize*, and *progressive* parameters of `.Figure.savefig` + (which only affected JPEG output) have been removed, as well as from the + corresponding ``print_jpg`` methods. JPEG output options can be set by + directly passing the relevant parameters in *pil_kwargs*. +- The *clear_temp* parameter of `.FileMovieWriter` has been removed; files + placed in a temporary directory (using ``frame_prefix=None``, the default) + will be cleared; files placed elsewhere will not. +- The *copy* parameter of ``mathtext.Glue`` has been removed. +- The *quantize* parameter of `.Path.cleaned()` has been removed. +- The *dummy* parameter of `.RendererPgf` has been removed. +- The *props* parameter of `.Shadow` has been removed; use keyword arguments + instead. +- The *recursionlimit* parameter of ``matplotlib.test`` has been removed. +- The *label* parameter of `.Tick` has no effect and has been removed. +- `~.ticker.MaxNLocator` no longer accepts a positional parameter and the + keyword argument *nbins* simultaneously because they specify the same + quantity. +- The *add_all* parameter to ``axes_grid.Grid``, ``axes_grid.ImageGrid``, + ``axes_rgb.make_rgb_axes``, and ``axes_rgb.RGBAxes`` have been removed; the + APIs always behave as if ``add_all=True``. +- The *den* parameter of ``axisartist.angle_helper.LocatorBase`` has been + removed; use *nbins* instead. + +- The *s* keyword argument to `.AnnotationBbox.get_fontsize` has no effect and + has been removed. +- The *offset_position* keyword argument of the `.Collection` class has been + removed; the ``offset_position`` now "screen". +- Arbitrary keyword arguments to ``StreamplotSet`` have no effect and have been + removed. + +- The *fontdict* and *minor* parameters of `.Axes.set_xticklabels` / + `.Axes.set_yticklabels` are now keyword-only. +- All parameters of `.Figure.subplots` except *nrows* and *ncols* are now + keyword-only; this avoids typing e.g. ``subplots(1, 1, 1)`` when meaning + ``subplot(1, 1, 1)``, but actually getting ``subplots(1, 1, sharex=1)``. +- All parameters of `.pyplot.tight_layout` are now keyword-only, to be + consistent with `.Figure.tight_layout`. +- ``ColorbarBase`` only takes a single positional argument now, the ``Axes`` to + create it in, with all other options required to be keyword arguments. The + warning for keyword arguments that were overridden by the mappable is now + removed. + +- Omitting the *renderer* parameter to `matplotlib.axes.Axes.draw` is no longer + supported; use ``axes.draw_artist(axes)`` instead. +- Passing ``ismath="TeX!"`` to `.RendererAgg.get_text_width_height_descent` is + no longer supported; pass ``ismath="TeX"`` instead, +- Changes to the signature of the `matplotlib.axes.Axes.draw` method make it + consistent with all other artists; thus additional parameters to + `.Artist.draw` have also been removed. + +rcParams +~~~~~~~~ + +- The ``animation.avconv_path`` and ``animation.avconv_args`` rcParams have + been removed. +- The ``animation.html_args`` rcParam has been removed. +- The ``keymap.all_axes`` rcParam has been removed. +- The ``mathtext.fallback_to_cm`` rcParam has been removed. Use + :rc:`mathtext.fallback` instead. +- The ``savefig.jpeg_quality`` rcParam has been removed. +- The ``text.latex.preview`` rcParam has been removed. +- The following deprecated rcParams validators, defined in `.rcsetup`, have + been removed: + + - ``validate_alignment`` + - ``validate_axes_titlelocation`` + - ``validate_axis_locator`` + - ``validate_bool_maybe_none`` + - ``validate_fontset`` + - ``validate_grid_axis`` + - ``validate_hinting`` + - ``validate_legend_loc`` + - ``validate_mathtext_default`` + - ``validate_movie_frame_fmt`` + - ``validate_movie_html_fmt`` + - ``validate_movie_writer`` + - ``validate_nseq_float`` + - ``validate_nseq_int`` + - ``validate_orientation`` + - ``validate_pgf_texsystem`` + - ``validate_ps_papersize`` + - ``validate_svg_fontset`` + - ``validate_toolbar`` + - ``validate_webagg_address`` + +- Some rcParam validation has become stricter: + + - :rc:`axes.axisbelow` no longer accepts strings starting with "line" + (case-insensitive) as "line"; use "line" (case-sensitive) instead. + - :rc:`text.latex.preamble` and :rc:`pdf.preamble` no longer accept + non-string values. + - All ``*.linestyle`` rcParams no longer accept ``offset = None``; set the + offset to 0 instead. diff --git a/doc/api/prev_api_changes/api_changes_3.5.2.rst b/doc/api/prev_api_changes/api_changes_3.5.2.rst new file mode 100644 index 000000000000..47b000de0350 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.5.2.rst @@ -0,0 +1,13 @@ +API Changes for 3.5.2 +===================== + +.. contents:: + :local: + :depth: 1 + +QuadMesh mouseover defaults to False +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +New in 3.5, `.QuadMesh.get_cursor_data` allows display of data values +under the cursor. However, this can be very slow for large meshes, so +by ``.QuadMesh.set_mouseover`` defaults to *False*. diff --git a/doc/api/prev_api_changes/api_changes_3.5.3.rst b/doc/api/prev_api_changes/api_changes_3.5.3.rst new file mode 100644 index 000000000000..03d1f476513e --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.5.3.rst @@ -0,0 +1,13 @@ +API Changes for 3.5.3 +===================== + +.. contents:: + :local: + :depth: 1 + +Passing *linefmt* positionally is undeprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Positional use of all formatting parameters in `~.Axes.stem` has been +deprecated since Matplotlib 3.5. This deprecation is relaxed so that one can +still pass *linefmt* positionally, i.e. ``stem(x, y, 'r')``. diff --git a/doc/api/prev_api_changes/api_changes_3.6.0.rst b/doc/api/prev_api_changes/api_changes_3.6.0.rst new file mode 100644 index 000000000000..1bba4506fd7d --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.6.0.rst @@ -0,0 +1,14 @@ +API Changes for 3.6.0 +===================== + +.. contents:: + :local: + :depth: 1 + +.. include:: /api/prev_api_changes/api_changes_3.6.0/behaviour.rst + +.. include:: /api/prev_api_changes/api_changes_3.6.0/deprecations.rst + +.. include:: /api/prev_api_changes/api_changes_3.6.0/removals.rst + +.. include:: /api/prev_api_changes/api_changes_3.6.0/development.rst diff --git a/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst new file mode 100644 index 000000000000..a35584b04961 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.6.0/behaviour.rst @@ -0,0 +1,248 @@ +Behaviour changes +----------------- + +``plt.get_cmap`` and ``matplotlib.cm.get_cmap`` return a copy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Formerly, `~.pyplot.get_cmap` and `.cm.get_cmap` returned a global version of a +`.Colormap`. This was prone to errors as modification of the colormap would +propagate from one location to another without warning. Now, a new copy of the +colormap is returned. + +Large ``imshow`` images are now downsampled +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When showing an image using `~matplotlib.axes.Axes.imshow` that has more than +:math:`2^{24}` columns or :math:`2^{23}` rows, the image will now be +downsampled to below this resolution before being resampled for display by the +AGG renderer. Previously such a large image would be shown incorrectly. To +prevent this downsampling and the warning it raises, manually downsample your +data before handing it to `~matplotlib.axes.Axes.imshow`. + +Default date limits changed to 1970-01-01 – 1970-01-02 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously the default limits for an empty axis set up for dates +(`.Axis.axis_date`) was 2000-01-01 to 2010-01-01. This has been changed to +1970-01-01 to 1970-01-02. With the default epoch, this makes the numeric limit +for date axes the same as for other axes (0.0-1.0), and users are less likely +to set a locator with far too many ticks. + +*markerfmt* argument to ``stem`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The behavior of the *markerfmt* parameter of `~.Axes.stem` has changed: + +- If *markerfmt* does not contain a color, the color is taken from *linefmt*. +- If *markerfmt* does not contain a marker, the default is 'o'. + +Before, *markerfmt* was passed unmodified to ``plot(..., fmt)``, which had a +number of unintended side-effects; e.g. only giving a color switched to a solid +line without markers. + +For a simple call ``stem(x, y)`` without parameters, the new rules still +reproduce the old behavior. + +``get_ticklabels`` now always populates labels +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously `.Axis.get_ticklabels` (and `.Axes.get_xticklabels`, +`.Axes.get_yticklabels`) would only return empty strings unless a draw had +already been performed. Now the ticks and their labels are updated when the +labels are requested. + +Warning when scatter plot color settings discarded +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When making an animation of a scatter plot, if you don't set *c* (the color +value parameter) when initializing the artist, the color settings are ignored. +`.Axes.scatter` now raises a warning if color-related settings are changed +without setting *c*. + +3D ``contourf`` polygons placed between levels +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The polygons used in a 3D `~.Axes3D.contourf` plot are now placed halfway +between the contour levels, as each polygon represents the location of values +that lie between two levels. + +Axes title now avoids y-axis offset +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, Axes titles could overlap the y-axis offset text, which is often in +the upper left corner of the axes. Now titles are moved above the offset text +if overlapping when automatic title positioning is in effect (i.e. if *y* in +`.Axes.set_title` is *None* and :rc:`axes.titley` is also *None*). + +Dotted operators gain extra space in mathtext +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In mathtext, ``\doteq \doteqdot \dotminus \dotplus \dots`` are now surrounded +by extra space because they are correctly treated as relational or binary +operators. + +*math* parameter of ``mathtext.get_unicode_index`` defaults to False +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In math mode, ASCII hyphens (U+002D) are now replaced by Unicode minus signs +(U+2212) at the parsing stage. + +``ArtistList`` proxies copy contents on iteration +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When iterating over the contents of the dynamically generated proxy lists for +the Artist-type accessors (see :ref:`Behavioural API Changes 3.5 - Axes +children combined`), a copy of the contents is made. This ensure that artists +can safely be added or removed from the Axes while iterating over their +children. + +This is a departure from the expected behavior of mutable iterable data types +in Python — iterating over a list while mutating it has surprising consequences +and dictionaries will error if they change size during iteration. Because all +of the accessors are filtered views of the same underlying list, it is possible +for seemingly unrelated changes, such as removing a Line, to affect the +iteration over any of the other accessors. In this case, we have opted to make +a copy of the relevant children before yielding them to the user. + +This change is also consistent with our plan to make these accessors immutable +in Matplotlib 3.7. + +``AxesImage`` string representation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The string representation of `.AxesImage` changes from stating the position in +the figure ``"AxesImage(80,52.8;496x369.6)"`` to giving the number of pixels +``"AxesImage(size=(300, 200))"``. + +Improved autoscaling for Bézier curves +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Bézier curves are now autoscaled to their extents - previously they were +autoscaled to their ends and control points, which in some cases led to +unnecessarily large limits. + +``QuadMesh`` mouseover defaults to False +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +New in 3.5, `.QuadMesh.get_cursor_data` allows display of data values under the +cursor. However, this can be very slow for large meshes, so mouseover now +defaults to *False*. + +Changed pgf backend document class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The pgf backend now uses the ``article`` document class as basis for +compilation. + +``MathtextBackendAgg.get_results`` no longer returns ``used_characters`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The last item (``used_characters``) in the tuple returned by +``MathtextBackendAgg.get_results`` has been removed. In order to unpack this +tuple in a backward and forward-compatible way, use e.g. ``ox, oy, width, +height, descent, image, *_ = parse(...)``, which will ignore +``used_characters`` if it was present. + +``Type1Font`` objects include more properties +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``matplotlib._type1font.Type1Font.prop`` dictionary now includes more keys, +such as ``CharStrings`` and ``Subrs``. The value of the ``Encoding`` key is now +a dictionary mapping codes to glyph names. The +``matplotlib._type1font.Type1Font.transform`` method now correctly removes +``UniqueID`` properties from the font. + +``rcParams.copy()`` returns ``RcParams`` rather than ``dict`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Returning an `.RcParams` instance from `.RcParams.copy` makes the copy still +validate inputs, and additionally avoids emitting deprecation warnings when +using a previously copied instance to update the global instance (even if some +entries are deprecated). + +``rc_context`` no longer resets the value of ``'backend'`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`matplotlib.rc_context` incorrectly reset the value of :rc:`backend` if backend +resolution was triggered in the context. This affected only the value. The +actual backend was not changed. Now, `matplotlib.rc_context` does not reset +:rc:`backend` anymore. + +Default ``rcParams["animation.convert_args"]`` changed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It now defaults to ``["-layers", "OptimizePlus"]`` to try to generate smaller +GIFs. Set it back to an empty list to recover the previous behavior. + +Style file encoding now specified to be UTF-8 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It has been impossible to import Matplotlib with a non UTF-8 compatible locale +encoding because we read the style library at import time. This change is +formalizing and documenting the status quo so there is no deprecation period. + +MacOSX backend uses sRGB instead of GenericRGB color space +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +MacOSX backend now display sRGB tagged image instead of GenericRGB which is an +older (now deprecated) Apple color space. This is the source color space used +by ColorSync to convert to the current display profile. + +Renderer optional for ``get_tightbbox`` and ``get_window_extent`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `.Artist.get_tightbbox` and `.Artist.get_window_extent` methods no longer +require the *renderer* keyword argument, saving users from having to query it +from ``fig.canvas.get_renderer``. If the *renderer* keyword argument is not +supplied, these methods first check if there is a cached renderer from a +previous draw and use that. If there is no cached renderer, then the methods +will use ``fig.canvas.get_renderer()`` as a fallback. + +``FigureFrameWx`` constructor, subclasses, and ``get_canvas`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``FigureCanvasWx`` constructor gained a *canvas_class* keyword-only +parameter which specifies the canvas class that should be used. This parameter +will become required in the future. The ``get_canvas`` method, which was +previously used to customize canvas creation, is deprecated. The +``FigureFrameWxAgg`` and ``FigureFrameWxCairo`` subclasses, which overrode +``get_canvas``, are deprecated. + +``FigureFrameWx.sizer`` +~~~~~~~~~~~~~~~~~~~~~~~ + +... has been removed. The frame layout is no longer based on a sizer, as the +canvas is now the sole child widget; the toolbar is now a regular toolbar added +using ``SetToolBar``. + +Incompatible layout engines raise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You cannot switch between ``tight_layout`` and ``constrained_layout`` if a +colorbar has already been added to a figure. Invoking the incompatible layout +engine used to warn, but now raises with a `RuntimeError`. + +``CallbackRegistry`` raises on unknown signals +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When Matplotlib instantiates a `.CallbackRegistry`, it now limits callbacks to +the signals that the registry knows about. In practice, this means that calling +`~.FigureCanvasBase.mpl_connect` with an invalid signal name now raises a +`ValueError`. + +Changed exception type for incorrect SVG date metadata +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Providing date metadata with incorrect type to the SVG backend earlier resulted +in a `ValueError`. Now, a `TypeError` is raised instead. + +Specified exception types in ``Grid`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In a few cases an `Exception` was thrown when an incorrect argument value was +set in the `mpl_toolkits.axes_grid1.axes_grid.Grid` (= +`mpl_toolkits.axisartist.axes_grid.Grid`) constructor. These are replaced as +follows: + +* Providing an incorrect value for *ngrids* now raises a `ValueError` +* Providing an incorrect type for *rect* now raises a `TypeError` diff --git a/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst new file mode 100644 index 000000000000..3a9e91e12289 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.6.0/deprecations.rst @@ -0,0 +1,414 @@ +Deprecations +------------ + +Parameters to ``plt.figure()`` and the ``Figure`` constructor +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +All parameters to `.pyplot.figure` and the `.Figure` constructor, other than +*num*, *figsize*, and *dpi*, will become keyword-only after a deprecation +period. + +Deprecation aliases in cbook +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The module ``matplotlib.cbook.deprecation`` was previously deprecated in +Matplotlib 3.4, along with deprecation-related API in ``matplotlib.cbook``. Due +to technical issues, ``matplotlib.cbook.MatplotlibDeprecationWarning`` and +``matplotlib.cbook.mplDeprecation`` did not raise deprecation warnings on use. +Changes in Python have now made it possible to warn when these aliases are +being used. + +In order to avoid downstream breakage, these aliases will now warn, and their +removal has been pushed from 3.6 to 3.8 to give time to notice said warnings. +As replacement, please use `matplotlib.MatplotlibDeprecationWarning`. + +``Axes`` subclasses should override ``clear`` instead of ``cla`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For clarity, `.axes.Axes.clear` is now preferred over `.Axes.cla`. However, for +backwards compatibility, the latter will remain as an alias for the former. + +For additional compatibility with third-party libraries, Matplotlib will +continue to call the ``cla`` method of any `~.axes.Axes` subclasses if they +define it. In the future, this will no longer occur, and Matplotlib will only +call the ``clear`` method in `~.axes.Axes` subclasses. + +It is recommended to define only the ``clear`` method when on Matplotlib 3.6, +and only ``cla`` for older versions. + +Pending deprecation top-level cmap registration and access functions in ``mpl.cm`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As part of a `multi-step process +`_ we are refactoring +the global state for managing the registered colormaps. + +In Matplotlib 3.5 we added a `.ColormapRegistry` class and exposed an instance +at the top level as ``matplotlib.colormaps``. The existing top level functions +in `matplotlib.cm` (``get_cmap``, ``register_cmap``, ``unregister_cmap``) were +changed to be aliases around the same instance. + +In Matplotlib 3.6 we have marked those top level functions as pending +deprecation with the intention of deprecation in Matplotlib 3.7. The following +functions have been marked for pending deprecation: + +- ``matplotlib.cm.get_cmap``; use ``matplotlib.colormaps[name]`` instead if you + have a `str`. + + **Added 3.6.1** Use `matplotlib.cm.ColormapRegistry.get_cmap` if you + have a string, `None` or a `matplotlib.colors.Colormap` object that you want + to convert to a `matplotlib.colors.Colormap` instance. +- ``matplotlib.cm.register_cmap``; use `matplotlib.colormaps.register + <.ColormapRegistry.register>` instead +- ``matplotlib.cm.unregister_cmap``; use `matplotlib.colormaps.unregister + <.ColormapRegistry.unregister>` instead +- ``matplotlib.pyplot.register_cmap``; use `matplotlib.colormaps.register + <.ColormapRegistry.register>` instead + +The `matplotlib.pyplot.get_cmap` function will stay available for backward +compatibility. + +Pending deprecation of layout methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The methods `~.Figure.set_tight_layout`, `~.Figure.set_constrained_layout`, are +discouraged, and now emit a `PendingDeprecationWarning` in favor of explicitly +referencing the layout engine via ``figure.set_layout_engine('tight')`` and +``figure.set_layout_engine('constrained')``. End users should not see the +warning, but library authors should adjust. + +The methods `~.Figure.set_constrained_layout_pads` and +`~.Figure.get_constrained_layout_pads` are will be deprecated in favor of +``figure.get_layout_engine().set()`` and ``figure.get_layout_engine().get()``, +and currently emit a `PendingDeprecationWarning`. + +seaborn styles renamed +~~~~~~~~~~~~~~~~~~~~~~ + +Matplotlib currently ships many style files inspired from the seaborn library +("seaborn", "seaborn-bright", "seaborn-colorblind", etc.) but they have gone +out of sync with the library itself since the release of seaborn 0.9. To +prevent confusion, the style files have been renamed "seaborn-v0_8", +"seaborn-v0_8-bright", "seaborn-v0_8-colorblind", etc. Users are encouraged to +directly use seaborn to access the up-to-date styles. + +Auto-removal of overlapping Axes by ``plt.subplot`` and ``plt.subplot2grid`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, `.pyplot.subplot` and `.pyplot.subplot2grid` would automatically +remove preexisting Axes that overlap with the newly added Axes. This behavior +was deemed confusing, and is now deprecated. Explicitly call ``ax.remove()`` on +Axes that need to be removed. + +Passing *linefmt* positionally to ``stem`` is undeprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Positional use of all formatting parameters in `~.Axes.stem` has been +deprecated since Matplotlib 3.5. This deprecation is relaxed so that one can +still pass *linefmt* positionally, i.e. ``stem(x, y, 'r')``. + +``stem(..., use_line_collection=False)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated with no replacement. This was a compatibility fallback to a +former more inefficient representation of the stem lines. + +Positional / keyword arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Passing all but the very few first arguments positionally in the constructors +of Artists is deprecated. Most arguments will become keyword-only in a future +version. + +Passing too many positional arguments to ``tripcolor`` is now deprecated (extra +arguments were previously silently ignored). + +Passing *emit* and *auto* parameters of ``set_xlim``, ``set_ylim``, +``set_zlim``, ``set_rlim`` positionally is deprecated; they will become +keyword-only in a future release. + +The *transOffset* parameter of `.Collection.set_offset_transform` and the +various ``create_collection`` methods of legend handlers has been renamed to +*offset_transform* (consistently with the property name). + +Calling ``MarkerStyle()`` with no arguments or ``MarkerStyle(None)`` is +deprecated; use ``MarkerStyle("")`` to construct an empty marker style. + +``Axes.get_window_extent`` / ``Figure.get_window_extent`` accept only +*renderer*. This aligns the API with the general `.Artist.get_window_extent` +API. All other parameters were ignored anyway. + +The *cleared* parameter of ``get_renderer``, which only existed for AGG-based +backends, has been deprecated. Use ``renderer.clear()`` instead to explicitly +clear the renderer buffer. + +Methods to set parameters in ``LogLocator`` and ``LogFormatter*`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In `~.LogFormatter` and derived subclasses, the methods ``base`` and +``label_minor`` for setting the respective parameter are deprecated and +replaced by ``set_base`` and ``set_label_minor``, respectively. + +In `~.LogLocator`, the methods ``base`` and ``subs`` for setting the respective +parameter are deprecated. Instead, use ``set_params(base=..., subs=...)``. + +``Axes.get_renderer_cache`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The canvas now takes care of the renderer and whether to cache it or not. The +alternative is to call ``axes.figure.canvas.get_renderer()``. + +Groupers from ``get_shared_x_axes`` / ``get_shared_y_axes`` will be immutable +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Modifications to the Groupers returned by ``get_shared_x_axes`` and +``get_shared_y_axes`` are deprecated. In the future, these methods will return +immutable views on the grouper structures. Note that previously, calling e.g. +``join()`` would already fail to set up the correct structures for sharing +axes; use `.Axes.sharex` or `.Axes.sharey` instead. + +Unused methods in ``Axis``, ``Tick``, ``XAxis``, and ``YAxis`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``Tick.label`` has been pending deprecation since 3.1 and is now deprecated. +Use ``Tick.label1`` instead. + +The following methods are no longer used and deprecated without a replacement: + +- ``Axis.get_ticklabel_extents`` +- ``Tick.get_pad_pixels`` +- ``XAxis.get_text_heights`` +- ``YAxis.get_text_widths`` + +``mlab.stride_windows`` +~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated. Use ``np.lib.stride_tricks.sliding_window_view`` instead (or +``np.lib.stride_tricks.as_strided`` on NumPy < 1.20). + +Event handlers +~~~~~~~~~~~~~~ + +The ``draw_event``, ``resize_event``, ``close_event``, ``key_press_event``, +``key_release_event``, ``pick_event``, ``scroll_event``, +``button_press_event``, ``button_release_event``, ``motion_notify_event``, +``enter_notify_event`` and ``leave_notify_event`` methods of +`.FigureCanvasBase` are deprecated. They had inconsistent signatures across +backends, and made it difficult to improve event metadata. + +In order to trigger an event on a canvas, directly construct an `.Event` object +of the correct class and call ``canvas.callbacks.process(event.name, event)``. + +Widgets +~~~~~~~ + +All parameters to ``MultiCursor`` starting from *useblit* are becoming +keyword-only (passing them positionally is deprecated). + +The ``canvas`` and ``background`` attributes of ``MultiCursor`` are deprecated +with no replacement. + +The *visible* attribute of Selector widgets has been deprecated; use +``set_visible`` or ``get_visible`` instead. + +The *state_modifier_keys* attribute of Selector widgets has been privatized and +the modifier keys must be set when creating the widget. + +``Axes3D.dist`` +~~~~~~~~~~~~~~~ + +... has been privatized. Use the *zoom* keyword argument in +`.Axes3D.set_box_aspect` instead. + +3D Axis +~~~~~~~ + +The previous constructor of `.axis3d.Axis`, with signature ``(self, adir, +v_intervalx, d_intervalx, axes, *args, rotate_label=None, **kwargs)`` is +deprecated in favor of a new signature closer to the one of 2D Axis; it is now +``(self, axes, *, rotate_label=None, **kwargs)`` where ``kwargs`` are forwarded +to the 2D Axis constructor. The axis direction is now inferred from the axis +class' ``axis_name`` attribute (as in the 2D case); the ``adir`` attribute is +deprecated. + +The ``init3d`` method of 3D Axis is also deprecated; all the relevant +initialization is done as part of the constructor. + +The ``d_interval`` and ``v_interval`` attributes of 3D Axis are deprecated; use +``get_data_interval`` and ``get_view_interval`` instead. + +The ``w_xaxis``, ``w_yaxis``, and ``w_zaxis`` attributes of ``Axis3D`` have +been pending deprecation since 3.1. They are now deprecated. Instead use +``xaxis``, ``yaxis``, and ``zaxis``. + +``mplot3d.axis3d.Axis.set_pane_pos`` is deprecated. This is an internal method +where the provided values are overwritten during drawing. Hence, it does not +serve any purpose to be directly accessible. + +The two helper functions ``mplot3d.axis3d.move_from_center`` and +``mplot3d.axis3d.tick_update_position`` are considered internal and deprecated. +If these are required, please vendor the code from the corresponding private +methods ``_move_from_center`` and ``_tick_update_position``. + +``Figure.callbacks`` is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The Figure ``callbacks`` property is deprecated. The only signal was +"dpi_changed", which can be replaced by connecting to the "resize_event" on the +canvas ``figure.canvas.mpl_connect("resize_event", func)`` instead. + +``FigureCanvas`` without a ``required_interactive_framework`` attribute +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Support for such canvas classes is deprecated. Note that canvas classes which +inherit from ``FigureCanvasBase`` always have such an attribute. + +Backend-specific deprecations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +- ``backend_gtk3.FigureManagerGTK3Agg`` and + ``backend_gtk4.FigureManagerGTK4Agg``; directly use + ``backend_gtk3.FigureManagerGTK3`` and ``backend_gtk4.FigureManagerGTK4`` + instead. +- The *window* parameter to ``backend_gtk3.NavigationToolbar2GTK3`` had no + effect, and is now deprecated. +- ``backend_gtk3.NavigationToolbar2GTK3.win`` +- ``backend_gtk3.RendererGTK3Cairo`` and ``backend_gtk4.RendererGTK4Cairo``; + use `.RendererCairo` instead, which has gained the ``set_context`` method, + which also auto-infers the size of the underlying surface. +- ``backend_cairo.RendererCairo.set_ctx_from_surface`` and + ``backend_cairo.RendererCairo.set_width_height`` in favor of + `.RendererCairo.set_context`. +- ``backend_gtk3.error_msg_gtk`` +- ``backend_gtk3.icon_filename`` and ``backend_gtk3.window_icon`` +- ``backend_macosx.NavigationToolbar2Mac.prepare_configure_subplots`` has been + replaced by ``configure_subplots()``. +- ``backend_pdf.Name.hexify`` +- ``backend_pdf.Operator`` and ``backend_pdf.Op.op`` are deprecated in favor of + a single standard `enum.Enum` interface on `.backend_pdf.Op`. +- ``backend_pdf.fill``; vendor the code of the similarly named private + functions if you rely on these functions. +- ``backend_pgf.LatexManager.texcommand`` and + ``backend_pgf.LatexManager.latex_header`` +- ``backend_pgf.NO_ESCAPE`` +- ``backend_pgf.common_texification`` +- ``backend_pgf.get_fontspec`` +- ``backend_pgf.get_preamble`` +- ``backend_pgf.re_mathsep`` +- ``backend_pgf.writeln`` +- ``backend_ps.convert_psfrags`` +- ``backend_ps.quote_ps_string``; vendor the code of the similarly named + private functions if you rely on it. +- ``backend_qt.qApp``; use ``QtWidgets.QApplication.instance()`` instead. +- ``backend_svg.escape_attrib``; vendor the code of the similarly named private + functions if you rely on it. +- ``backend_svg.escape_cdata``; vendor the code of the similarly named private + functions if you rely on it. +- ``backend_svg.escape_comment``; vendor the code of the similarly named + private functions if you rely on it. +- ``backend_svg.short_float_fmt``; vendor the code of the similarly named + private functions if you rely on it. +- ``backend_svg.generate_transform`` and ``backend_svg.generate_css`` +- ``backend_tk.NavigationToolbar2Tk.lastrect`` and + ``backend_tk.RubberbandTk.lastrect`` +- ``backend_tk.NavigationToolbar2Tk.window``; use ``toolbar.master`` instead. +- ``backend_tools.ToolBase.destroy``; To run code upon tool removal, connect to + the ``tool_removed_event`` event. +- ``backend_wx.RendererWx.offset_text_height`` +- ``backend_wx.error_msg_wx`` + +- ``FigureCanvasBase.pick``; directly call `.Figure.pick`, which has taken over + the responsibility of checking the canvas widget lock as well. +- ``FigureCanvasBase.resize``, which has no effect; use + ``FigureManagerBase.resize`` instead. + +- ``FigureManagerMac.close`` + +- ``FigureFrameWx.sizer``; use ``frame.GetSizer()`` instead. +- ``FigureFrameWx.figmgr`` and ``FigureFrameWx.get_figure_manager``; use + ``frame.canvas.manager`` instead. +- ``FigureFrameWx.num``; use ``frame.canvas.manager.num`` instead. +- ``FigureFrameWx.toolbar``; use ``frame.GetToolBar()`` instead. +- ``FigureFrameWx.toolmanager``; use ``frame.canvas.manager.toolmanager`` + instead. + +Modules +~~~~~~~ + +The modules ``matplotlib.afm``, ``matplotlib.docstring``, +``matplotlib.fontconfig_pattern``, ``matplotlib.tight_bbox``, +``matplotlib.tight_layout``, and ``matplotlib.type1font`` are considered +internal and public access is deprecated. + +``checkdep_usetex`` deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This method was only intended to disable tests in case no latex install was +found. As such, it is considered to be private and for internal use only. + +Please vendor the code if you need this. + +``date_ticker_factory`` deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``date_ticker_factory`` method in the `matplotlib.dates` module is +deprecated. Instead use `~.AutoDateLocator` and `~.AutoDateFormatter` for a +more flexible and scalable locator and formatter. + +If you need the exact ``date_ticker_factory`` behavior, please copy the code. + +``dviread.find_tex_file`` will raise ``FileNotFoundError`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the future, ``dviread.find_tex_file`` will raise a `FileNotFoundError` for +missing files. Previously, it would return an empty string in such cases. +Raising an exception allows attaching a user-friendly message instead. During +the transition period, a warning is raised. + +``transforms.Affine2D.identity()`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated in favor of directly calling the `.Affine2D` constructor with +no arguments. + +Deprecations in ``testing.decorators`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The unused class ``CleanupTestCase`` and decorator ``cleanup`` are deprecated +and will be removed. Vendor the code, including the private function +``_cleanup_cm``. + +The function ``check_freetype_version`` is considered internal and deprecated. +Vendor the code of the private function ``_check_freetype_version``. + +``text.get_rotation()`` +~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated with no replacement. Copy the original implementation if +needed. + +Miscellaneous internals +~~~~~~~~~~~~~~~~~~~~~~~ + +- ``axes_grid1.axes_size.AddList``; use ``sum(sizes, start=Fixed(0))`` (for + example) to sum multiple size objects. +- ``axes_size.Padded``; use ``size + pad`` instead +- ``axes_size.SizeFromFunc``, ``axes_size.GetExtentHelper`` +- ``AxisArtistHelper.delta1`` and ``AxisArtistHelper.delta2`` +- ``axislines.GridHelperBase.new_gridlines`` and + ``axislines.Axes.new_gridlines`` +- ``cbook.maxdict``; use the standard library ``functools.lru_cache`` instead. +- ``_DummyAxis.dataLim`` and ``_DummyAxis.viewLim``; use + ``get_data_interval()``, ``set_data_interval()``, ``get_view_interval()``, + and ``set_view_interval()`` instead. +- ``GridSpecBase.get_grid_positions(..., raw=True)`` +- ``ImageMagickBase.delay`` and ``ImageMagickBase.output_args`` +- ``MathtextBackend``, ``MathtextBackendAgg``, ``MathtextBackendPath``, + ``MathTextWarning`` +- ``TexManager.get_font_config``; it previously returned an internal hashed key + for used for caching purposes. +- ``TextToPath.get_texmanager``; directly construct a `.texmanager.TexManager` + instead. +- ``ticker.is_close_to_int``; use ``math.isclose(x, round(x))`` instead. +- ``ticker.is_decade``; use ``y = numpy.log(x)/numpy.log(base); + numpy.isclose(y, numpy.round(y))`` instead. diff --git a/doc/api/prev_api_changes/api_changes_3.6.0/development.rst b/doc/api/prev_api_changes/api_changes_3.6.0/development.rst new file mode 100644 index 000000000000..fb9f1f3e21c5 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.6.0/development.rst @@ -0,0 +1,42 @@ +Development changes +------------------- + +Increase to minimum supported versions of dependencies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For Matplotlib 3.6, the :ref:`minimum supported versions ` are +being bumped: + ++------------+-----------------+---------------+ +| Dependency | min in mpl3.5 | min in mpl3.6 | ++============+=================+===============+ +| Python | 3.7 | 3.8 | ++------------+-----------------+---------------+ +| NumPy | 1.17 | 1.19 | ++------------+-----------------+---------------+ + +This is consistent with our :ref:`min_deps_policy` and `NEP29 +`__ + +Build setup options changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``gui_support.macosx`` setup option has been renamed to +``packages.macosx``. + +New wheel architectures +~~~~~~~~~~~~~~~~~~~~~~~ + +Wheels have been added for: + +- Python 3.11 +- PyPy 3.8 and 3.9 + +Increase to required versions of documentation dependencies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`sphinx`_ >= 3.0 and `numpydoc`_ >= 1.0 are now required for building the +documentation. + +.. _numpydoc: https://pypi.org/project/numpydoc/ +.. _sphinx: https://pypi.org/project/Sphinx/ diff --git a/doc/api/prev_api_changes/api_changes_3.6.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.6.0/removals.rst new file mode 100644 index 000000000000..b261fdb30596 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.6.0/removals.rst @@ -0,0 +1,222 @@ +Removals +-------- + +The following deprecated APIs have been removed: + +Removed behaviour +~~~~~~~~~~~~~~~~~ + +Stricter validation of function parameters +.......................................... + +- Unknown keyword arguments to `.Figure.savefig`, `.pyplot.savefig`, and the + ``FigureCanvas.print_*`` methods now raise a `TypeError`, instead of being + ignored. +- Extra parameters to the `~.axes.Axes` constructor, i.e., those other than + *fig* and *rect*, are now keyword only. +- Passing arguments not specifically listed in the signatures of + `.Axes3D.plot_surface` and `.Axes3D.plot_wireframe` is no longer supported; + pass any extra arguments as keyword arguments instead. +- Passing positional arguments to `.LineCollection` has been removed; use + specific keyword argument names now. + +``imread`` no longer accepts URLs +................................. + +Passing a URL to `~.pyplot.imread()` has been removed. Please open the URL for +reading and directly use the Pillow API (e.g., +``PIL.Image.open(urllib.request.urlopen(url))``, or +``PIL.Image.open(io.BytesIO(requests.get(url).content))``) instead. + +MarkerStyle is immutable +........................ + +The methods ``MarkerStyle.set_fillstyle`` and ``MarkerStyle.set_marker`` have +been removed. Create a new `.MarkerStyle` with the respective parameters +instead. + +Passing bytes to ``FT2Font.set_text`` +..................................... + +... is no longer supported. Pass `str` instead. + +Support for passing tool names to ``ToolManager.add_tool`` +.......................................................... + +... has been removed. The second parameter to `.ToolManager.add_tool` must now +always be a tool class. + +``backend_tools.ToolFullScreen`` now inherits from ``ToolBase``, not from ``ToolToggleBase`` +............................................................................................ + +`.ToolFullScreen` can only switch between the non-fullscreen and fullscreen +states, but not unconditionally put the window in a given state; hence the +``enable`` and ``disable`` methods were misleadingly named. Thus, the +`.ToolToggleBase`-related API (``enable``, ``disable``, etc.) was removed. + +``BoxStyle._Base`` and ``transmute`` method of box styles +......................................................... + +... have been removed. Box styles implemented as classes no longer need to +inherit from a base class. + +Loaded modules logging +...................... + +The list of currently loaded modules is no longer logged at the DEBUG level at +Matplotlib import time, because it can produce extensive output and make other +valuable DEBUG statements difficult to find. If you were relying on this +output, please arrange for your own logging (the built-in `sys.modules` can be +used to get the currently loaded modules). + +Modules +~~~~~~~ + +- The ``cbook.deprecation`` module has been removed from the public API as it + is considered internal. +- The ``mpl_toolkits.axes_grid`` module has been removed. All functionality from + ``mpl_toolkits.axes_grid`` can be found in either `mpl_toolkits.axes_grid1` + or `mpl_toolkits.axisartist`. Axes classes from ``mpl_toolkits.axes_grid`` + based on ``Axis`` from `mpl_toolkits.axisartist` can be found in + `mpl_toolkits.axisartist`. + +Classes, methods and attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following module-level classes/variables have been removed: + +- ``cm.cmap_d`` +- ``colorbar.colorbar_doc``, ``colorbar.colorbar_kw_doc`` +- ``ColorbarPatch`` +- ``mathtext.Fonts`` and all its subclasses +- ``mathtext.FontConstantsBase`` and all its subclasses +- ``mathtext.latex_to_bakoma``, ``mathtext.latex_to_cmex``, + ``mathtext.latex_to_standard`` +- ``mathtext.MathtextBackendPdf``, ``mathtext.MathtextBackendPs``, + ``mathtext.MathtextBackendSvg``, ``mathtext.MathtextBackendCairo``; use + `.MathtextBackendPath` instead. +- ``mathtext.Node`` and all its subclasses +- ``mathtext.NUM_SIZE_LEVELS`` +- ``mathtext.Parser`` +- ``mathtext.Ship`` +- ``mathtext.SHRINK_FACTOR`` and ``mathtext.GROW_FACTOR`` +- ``mathtext.stix_virtual_fonts``, +- ``mathtext.tex2uni`` +- ``backend_pgf.TmpDirCleaner`` +- ``backend_ps.GraphicsContextPS``; use ``GraphicsContextBase`` instead. +- ``backend_wx.IDLE_DELAY`` +- ``axes_grid1.parasite_axes.ParasiteAxesAuxTransBase``; use + `.ParasiteAxesBase` instead. +- ``axes_grid1.parasite_axes.ParasiteAxesAuxTrans``; use `.ParasiteAxes` + instead. + +The following class attributes have been removed: + +- ``Line2D.validCap`` and ``Line2D.validJoin``; validation is centralized in + ``rcsetup``. +- ``Patch.validCap`` and ``Patch.validJoin``; validation is centralized in + ``rcsetup``. +- ``renderer.M``, ``renderer.eye``, ``renderer.vvec``, + ``renderer.get_axis_position`` placed on the Renderer during 3D Axes draw; + these attributes are all available via `.Axes3D`, which can be accessed via + ``self.axes`` on all `.Artist`\s. +- ``RendererPdf.mathtext_parser``, ``RendererPS.mathtext_parser``, + ``RendererSVG.mathtext_parser``, ``RendererCairo.mathtext_parser`` +- ``StandardPsFonts.pswriter`` +- ``Subplot.figbox``; use `.Axes.get_position` instead. +- ``Subplot.numRows``; ``ax.get_gridspec().nrows`` instead. +- ``Subplot.numCols``; ``ax.get_gridspec().ncols`` instead. +- ``SubplotDivider.figbox`` +- ``cids``, ``cnt``, ``observers``, ``change_observers``, and + ``submit_observers`` on all `.Widget`\s + +The following class methods have been removed: + +- ``Axis.cla()``; use `.Axis.clear` instead. +- ``RadialAxis.cla()`` and ``ThetaAxis.cla()``; use `.RadialAxis.clear` or + `.ThetaAxis.clear` instead. +- ``Spine.cla()``; use `.Spine.clear` instead. +- ``ContourLabeler.get_label_coords()``; there is no replacement as it was + considered an internal helper. +- ``FancyArrowPatch.get_dpi_cor`` and ``FancyArrowPatch.set_dpi_cor`` + +- ``FigureCanvas.get_window_title()`` and ``FigureCanvas.set_window_title()``; + use `.FigureManagerBase.get_window_title` or + `.FigureManagerBase.set_window_title` if using pyplot, or use GUI-specific + methods if embedding. +- ``FigureManager.key_press()`` and ``FigureManager.button_press()``; trigger + the events directly on the canvas using + ``canvas.callbacks.process(event.name, event)`` for key and button events. + +- ``RendererAgg.get_content_extents()`` and + ``RendererAgg.tostring_rgba_minimized()`` +- ``NavigationToolbar2Wx.get_canvas()`` + +- ``ParasiteAxesBase.update_viewlim()``; use ``ParasiteAxesBase.apply_aspect`` + instead. +- ``Subplot.get_geometry()``; use ``SubplotBase.get_subplotspec`` instead. +- ``Subplot.change_geometry()``; use ``SubplotBase.set_subplotspec`` instead. +- ``Subplot.update_params()``; this method did nothing. +- ``Subplot.is_first_row()``; use ``ax.get_subplotspec().is_first_row`` + instead. +- ``Subplot.is_first_col()``; use ``ax.get_subplotspec().is_first_col`` + instead. +- ``Subplot.is_last_row()``; use ``ax.get_subplotspec().is_last_row`` instead. +- ``Subplot.is_last_col()``; use ``ax.get_subplotspec().is_last_col`` instead. +- ``SubplotDivider.change_geometry()``; use `.SubplotDivider.set_subplotspec` + instead. +- ``SubplotDivider.get_geometry()``; use `.SubplotDivider.get_subplotspec` + instead. +- ``SubplotDivider.update_params()`` +- ``get_depth``, ``parse``, ``to_mask``, ``to_rgba``, and ``to_png`` of + `.MathTextParser`; use `.mathtext.math_to_image` instead. + +- ``MovieWriter.cleanup()``; the cleanup logic is instead fully implemented in + `.MovieWriter.finish` and ``cleanup`` is no longer called. + +Functions +~~~~~~~~~ + +The following functions have been removed; + +- ``backend_template.new_figure_manager()``, + ``backend_template.new_figure_manager_given_figure()``, and + ``backend_template.draw_if_interactive()`` have been removed, as part of the + introduction of the simplified backend API. +- Deprecation-related re-imports ``cbook.deprecated()``, and + ``cbook.warn_deprecated()``. +- ``colorbar.colorbar_factory()``; use `.Colorbar` instead. + ``colorbar.make_axes_kw_doc()`` +- ``mathtext.Error()`` +- ``mathtext.ship()`` +- ``mathtext.tex2uni()`` +- ``axes_grid1.parasite_axes.parasite_axes_auxtrans_class_factory()``; use + `.parasite_axes_class_factory` instead. +- ``sphinext.plot_directive.align()``; use + ``docutils.parsers.rst.directives.images.Image.align`` instead. + +Arguments +~~~~~~~~~ + +The following arguments have been removed: + +- *dpi* from ``print_ps()`` in the PS backend and ``print_pdf()`` in the PDF + backend. Instead, the methods will obtain the DPI from the ``savefig`` + machinery. +- *dpi_cor* from `~.FancyArrowPatch` +- *minimum_descent* from ``TextArea``; it is now effectively always True +- *origin* from ``FigureCanvasWx.gui_repaint()`` +- *project* from ``Line3DCollection.draw()`` +- *renderer* from `.Line3DCollection.do_3d_projection`, + `.Patch3D.do_3d_projection`, `.PathPatch3D.do_3d_projection`, + `.Path3DCollection.do_3d_projection`, `.Patch3DCollection.do_3d_projection`, + `.Poly3DCollection.do_3d_projection` +- *resize_callback* from the Tk backend; use + ``get_tk_widget().bind('', ..., True)`` instead. +- *return_all* from ``gridspec.get_position()`` +- Keyword arguments to ``gca()``; there is no replacement. + +rcParams +~~~~~~~~ + +The setting :rc:`ps.useafm` no longer has any effect on `matplotlib.mathtext`. diff --git a/doc/api/prev_api_changes/api_changes_3.6.1.rst b/doc/api/prev_api_changes/api_changes_3.6.1.rst new file mode 100644 index 000000000000..ad929d426885 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.6.1.rst @@ -0,0 +1,15 @@ +API Changes for 3.6.1 +===================== + +Deprecations +------------ + +Colorbars for orphaned mappables are deprecated, but no longer raise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before 3.6.0, Colorbars for mappables that do not have a parent Axes would +steal space from the current Axes. 3.6.0 raised an error on this, but without a +deprecation cycle. For 3.6.1 this is reverted; the current Axes is used, but a +deprecation warning is shown instead. In this undetermined case, users and +libraries should explicitly specify what Axes they want space to be stolen +from: ``fig.colorbar(mappable, ax=plt.gca())``. diff --git a/doc/api/prev_api_changes/api_changes_3.7.0.rst b/doc/api/prev_api_changes/api_changes_3.7.0.rst new file mode 100644 index 000000000000..932a4ba34452 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.7.0.rst @@ -0,0 +1,14 @@ +API Changes for 3.7.0 +===================== + +.. contents:: + :local: + :depth: 1 + +.. include:: /api/prev_api_changes/api_changes_3.7.0/behaviour.rst + +.. include:: /api/prev_api_changes/api_changes_3.7.0/deprecations.rst + +.. include:: /api/prev_api_changes/api_changes_3.7.0/removals.rst + +.. include:: /api/prev_api_changes/api_changes_3.7.0/development.rst diff --git a/doc/api/prev_api_changes/api_changes_3.7.0/behaviour.rst b/doc/api/prev_api_changes/api_changes_3.7.0/behaviour.rst new file mode 100644 index 000000000000..6057bfa9af4c --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.7.0/behaviour.rst @@ -0,0 +1,136 @@ +Behaviour Changes +----------------- + +All Axes have ``get_subplotspec`` and ``get_gridspec`` methods now, which returns None for Axes not positioned via a gridspec +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, this method was only present for Axes positioned via a gridspec. +Following this change, checking ``hasattr(ax, "get_gridspec")`` should now be +replaced by ``ax.get_gridspec() is not None``. For compatibility with older +Matplotlib releases, one can also check +``hasattr(ax, "get_gridspec") and ax.get_gridspec() is not None``. + +``HostAxesBase.get_aux_axes`` now defaults to using the same base axes class as the host axes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If using an ``mpl_toolkits.axisartist``-based host Axes, the parasite Axes will +also be based on ``mpl_toolkits.axisartist``. This behavior is consistent with +``HostAxesBase.twin``, ``HostAxesBase.twinx``, and ``HostAxesBase.twiny``. + +``plt.get_cmap`` and ``matplotlib.cm.get_cmap`` return a copy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Formerly, `~.pyplot.get_cmap` and `.cm.get_cmap` returned a global version of a +`.Colormap`. This was prone to errors as modification of the colormap would +propagate from one location to another without warning. Now, a new copy of the +colormap is returned. + +``TrapezoidMapTriFinder`` uses different random number generator +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The random number generator used to determine the order of insertion of +triangle edges in ``TrapezoidMapTriFinder`` has changed. This can result in a +different triangle index being returned for a point that lies exactly on an +edge between two triangles. This can also affect triangulation interpolation +and refinement algorithms that use ``TrapezoidMapTriFinder``. + +``FuncAnimation(save_count=None)`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Passing ``save_count=None`` to `.FuncAnimation` no longer limits the number +of frames to 100. Make sure that it either can be inferred from *frames* +or provide an integer *save_count*. + +``CenteredNorm`` halfrange is not modified when vcenter changes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, the **halfrange** would expand in proportion to the +amount that **vcenter** was moved away from either **vmin** or **vmax**. +Now, the halfrange remains fixed when vcenter is changed, and **vmin** and +**vmax** are updated based on the **vcenter** and **halfrange** values. + +For example, this is what the values were when changing vcenter previously. + +.. code-block:: + + norm = CenteredNorm(vcenter=0, halfrange=1) + # Move vcenter up by one + norm.vcenter = 1 + # updates halfrange and vmax (vmin stays the same) + # norm.halfrange == 2, vmin == -1, vmax == 3 + +and now, with that same example + +.. code-block:: + + norm = CenteredNorm(vcenter=0, halfrange=1) + norm.vcenter = 1 + # updates vmin and vmax (halfrange stays the same) + # norm.halfrange == 1, vmin == 0, vmax == 2 + +The **halfrange** can be set manually or ``norm.autoscale()`` +can be used to automatically set the limits after setting **vcenter**. + +``fig.subplot_mosaic`` no longer passes the ``gridspec_kw`` args to nested gridspecs. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For nested `.Figure.subplot_mosaic` layouts, it is almost always +inappropriate for *gridspec_kw* arguments to be passed to lower nest +levels, and these arguments are incompatible with the lower levels in +many cases. This dictionary is no longer passed to the inner +layouts. Users who need to modify *gridspec_kw* at multiple levels +should use `.Figure.subfigures` to get nesting, and construct the +inner layouts with `.Figure.subplots` or `.Figure.subplot_mosaic`. + +``HPacker`` alignment with **bottom** or **top** are now correct +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, the **bottom** and **top** alignments were swapped. +This has been corrected so that the alignments correspond appropriately. + +On Windows only fonts known to the registry will be discovered +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, Matplotlib would recursively walk user and system font directories +to discover fonts, however this lead to a number of undesirable behaviors +including finding deleted fonts. Now Matplotlib will only find fonts that are +known to the Windows registry. + +This means that any user installed fonts must go through the Windows font +installer rather than simply being copied to the correct folder. + +This only impacts the set of fonts Matplotlib will consider when using +`matplotlib.font_manager.findfont`. To use an arbitrary font, directly pass the +path to a font as shown in +:doc:`/gallery/text_labels_and_annotations/font_file`. + +``QuadMesh.set_array`` now always raises ``ValueError`` for inputs with incorrect shapes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It could previously also raise `TypeError` in some cases. + +``contour`` and ``contourf`` auto-select suitable levels when given boolean inputs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the height array given to `.Axes.contour` or `.Axes.contourf` is of bool +dtype and *levels* is not specified, *levels* now defaults to ``[0.5]`` for +`~.Axes.contour` and ``[0, 0.5, 1]`` for `.Axes.contourf`. + +``contour`` no longer warns if no contour lines are drawn. +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This can occur if the user explicitly passes a ``levels`` array with no values + +``AxesImage.set_extent`` now raises ``TypeError`` for unknown keyword arguments +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It previously raised a `ValueError`. + +etween ``z.min()`` and ``z.max()``; or if ``z`` has the same value everywhere. + +Change of ``legend(loc="best")`` behavior +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The algorithm of the auto-legend locator has been tweaked to better handle +non rectangular patches. Additional details on this change can be found in +:ghissue:`9580` and :ghissue:`9598`. diff --git a/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst b/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst new file mode 100644 index 000000000000..dd6d9d8e0894 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.7.0/deprecations.rst @@ -0,0 +1,291 @@ +Deprecations +------------ + +``Axes`` subclasses should override ``clear`` instead of ``cla`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For clarity, `.axes.Axes.clear` is now preferred over `.Axes.cla`. However, for +backwards compatibility, the latter will remain as an alias for the former. + +For additional compatibility with third-party libraries, Matplotlib will +continue to call the ``cla`` method of any `~.axes.Axes` subclasses if they +define it. In the future, this will no longer occur, and Matplotlib will only +call the ``clear`` method in `~.axes.Axes` subclasses. + +It is recommended to define only the ``clear`` method when on Matplotlib 3.6, +and only ``cla`` for older versions. + +rcParams type +~~~~~~~~~~~~~ + +Relying on ``rcParams`` being a ``dict`` subclass is deprecated. + +Nothing will change for regular users because ``rcParams`` will continue to +be dict-like (technically fulfill the ``MutableMapping`` interface). + +The `.RcParams` class does validation checking on calls to +``.RcParams.__getitem__`` and ``.RcParams.__setitem__``. However, there are rare +cases where we want to circumvent the validation logic and directly access the +underlying data values. Previously, this could be accomplished via a call to +the parent methods ``dict.__getitem__(rcParams, key)`` and +``dict.__setitem__(rcParams, key, val)``. + +Matplotlib 3.7 introduces ``rcParams._set(key, val)`` and +``rcParams._get(key)`` as a replacement to calling the parent methods. They are +intentionally marked private to discourage external use; However, if direct +`.RcParams` data access is needed, please switch from the dict functions to the +new ``_get()`` and ``_set()``. Even though marked private, we guarantee API +stability for these methods and they are subject to Matplotlib's API and +deprecation policy. + +Please notify the Matplotlib developers if you rely on ``rcParams`` being a +dict subclass in any other way, for which there is no migration path yet. + +Deprecation aliases in cbook +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The module ``matplotlib.cbook.deprecation`` was previously deprecated in +Matplotlib 3.4, along with deprecation-related API in ``matplotlib.cbook``. Due +to technical issues, ``matplotlib.cbook.MatplotlibDeprecationWarning`` and +``matplotlib.cbook.mplDeprecation`` did not raise deprecation warnings on use. +Changes in Python have now made it possible to warn when these aliases are +being used. + +In order to avoid downstream breakage, these aliases will now warn, and their +removal has been pushed from 3.6 to 3.8 to give time to notice said warnings. +As replacement, please use `matplotlib.MatplotlibDeprecationWarning`. + +``draw_gouraud_triangle`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated as in most backends this is a redundant call. Use +`~.RendererBase.draw_gouraud_triangles` instead. A ``draw_gouraud_triangle`` +call in a custom `~matplotlib.artist.Artist` can readily be replaced as:: + + self.draw_gouraud_triangles(gc, points.reshape((1, 3, 2)), + colors.reshape((1, 3, 4)), trans) + +A `~.RendererBase.draw_gouraud_triangles` method can be implemented from an +existing ``draw_gouraud_triangle`` method as:: + + transform = transform.frozen() + for tri, col in zip(triangles_array, colors_array): + self.draw_gouraud_triangle(gc, tri, col, transform) + +``matplotlib.pyplot.get_plot_commands`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is a pending deprecation. This is considered internal and no end-user +should need it. + +``matplotlib.tri`` submodules are deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``matplotlib.tri.*`` submodules are deprecated. All functionality is +available in ``matplotlib.tri`` directly and should be imported from there. + +Passing undefined *label_mode* to ``Grid`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated. This includes `mpl_toolkits.axes_grid1.axes_grid.Grid`, +`mpl_toolkits.axes_grid1.axes_grid.AxesGrid`, and +`mpl_toolkits.axes_grid1.axes_grid.ImageGrid` as well as the corresponding +classes imported from `mpl_toolkits.axisartist.axes_grid`. + +Pass ``label_mode='keep'`` instead to get the previous behavior of not modifying labels. + +Colorbars for orphaned mappables are deprecated, but no longer raise +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Before 3.6.0, Colorbars for mappables that do not have a parent axes would +steal space from the current Axes. 3.6.0 raised an error on this, but without +a deprecation cycle. For 3.6.1 this is reverted, the current axes is used, +but a deprecation warning is shown instead. In this undetermined case users +and libraries should explicitly specify what axes they want space to be stolen +from: ``fig.colorbar(mappable, ax=plt.gca())``. + +``Animation`` attributes +~~~~~~~~~~~~~~~~~~~~~~~~ + +The attributes ``repeat`` of `.TimedAnimation` and subclasses and +``save_count`` of `.FuncAnimation` are considered private and deprecated. + +``contour.ClabelText`` and ``ContourLabeler.set_label_props`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are deprecated. + +Use ``Text(..., transform_rotates_text=True)`` as a replacement for +``contour.ClabelText(...)`` and ``text.set(text=text, color=color, +fontproperties=labeler.labelFontProps, clip_box=labeler.axes.bbox)`` as a +replacement for the ``ContourLabeler.set_label_props(label, text, color)``. + +``ContourLabeler`` attributes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``labelFontProps``, ``labelFontSizeList``, and ``labelTextsList`` +attributes of `.ContourLabeler` have been deprecated. Use the ``labelTexts`` +attribute and the font properties of the corresponding text objects instead. + +``backend_ps.PsBackendHelper`` and ``backend_ps.ps_backend_helper`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... are deprecated with no replacement. + +``backend_webagg.ServerThread`` is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... with no replacement. + +``parse_fontconfig_pattern`` will no longer ignore unknown constant names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, in a fontconfig pattern like ``DejaVu Sans:foo``, the unknown +``foo`` constant name would be silently ignored. This now raises a warning, +and will become an error in the future. + +``BufferRegion.to_string`` and ``BufferRegion.to_string_argb`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... are deprecated. Use ``np.asarray(buffer_region)`` to get an array view on +a buffer region without making a copy; to convert that view from RGBA (the +default) to ARGB, use ``np.take(..., [2, 1, 0, 3], axis=2)``. + +``num2julian``, ``julian2num`` and ``JULIAN_OFFSET`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... of the `.dates` module are deprecated without replacements. These are +undocumented and not exported. If you rely on these, please make a local copy. + +``unit_cube``, ``tunit_cube``, and ``tunit_edges`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... of `.Axes3D` are deprecated without replacements. If you rely on them, +please copy the code of the corresponding private function (name starting +with ``_``). + +Most arguments to widgets have been made keyword-only +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Passing all but the very few first arguments positionally in the constructors +of Widgets is deprecated. Most arguments will become keyword-only in a future +version. + +``SimpleEvent`` +~~~~~~~~~~~~~~~ + +The ``SimpleEvent`` nested class (previously accessible via the public +subclasses of ``ConnectionStyle._Base``, such as `.ConnectionStyle.Arc`, has +been deprecated. + +``RadioButtons.circles`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated. (RadioButtons now draws itself using `~.Axes.scatter`.) + +``CheckButtons.rectangles`` and ``CheckButtons.lines`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``CheckButtons.rectangles`` and ``CheckButtons.lines`` are deprecated. +(``CheckButtons`` now draws itself using `~.Axes.scatter`.) + +``OffsetBox.get_extent_offsets`` and ``OffsetBox.get_extent`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... are deprecated; these methods are also deprecated on all subclasses of +`.OffsetBox`. + +To get the offsetbox extents, instead of ``get_extent``, use +`.OffsetBox.get_bbox`, which directly returns a `.Bbox` instance. + +To also get the child offsets, instead of ``get_extent_offsets``, separately +call `~.OffsetBox.get_offset` on each children after triggering a draw. + +``legend.legendHandles`` +~~~~~~~~~~~~~~~~~~~~~~~~ + +... was undocumented and has been renamed to ``legend_handles``. Using ``legendHandles`` is deprecated. + +``ticklabels`` parameter of `.Axis.set_ticklabels` renamed to ``labels`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``offsetbox.bbox_artist`` +~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is deprecated. This is just a wrapper to call `.patches.bbox_artist` if a +flag is set in the file, so use that directly if you need the behavior. + +``Quiver.quiver_doc`` and ``Barbs.barbs_doc`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... are deprecated. These are the doc-string and should not be accessible as +a named class member. + +Deprecate unused parameter *x* to ``TextBox.begin_typing`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This parameter was unused in the method, but was a required argument. + +Deprecation of top-level cmap registration and access functions in ``mpl.cm`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +As part of a `multi-step process +`_ we are refactoring +the global state for managing the registered colormaps. + +In Matplotlib 3.5 we added a `.ColormapRegistry` class and exposed an instance +at the top level as ``matplotlib.colormaps``. The existing top level functions +in `matplotlib.cm` (``get_cmap``, ``register_cmap``, ``unregister_cmap``) were +changed to be aliases around the same instance. In Matplotlib 3.6 we have +marked those top level functions as pending deprecation. + +In Matplotlib 3.7, the following functions have been marked for deprecation: + +- ``matplotlib.cm.get_cmap``; use ``matplotlib.colormaps[name]`` instead if you + have a `str`. + + **Added 3.6.1** Use `matplotlib.cm.ColormapRegistry.get_cmap` if you + have a string, `None` or a `matplotlib.colors.Colormap` object that you want + to convert to a `matplotlib.colors.Colormap` instance. +- ``matplotlib.cm.register_cmap``; use `matplotlib.colormaps.register + <.ColormapRegistry.register>` instead +- ``matplotlib.cm.unregister_cmap``; use `matplotlib.colormaps.unregister + <.ColormapRegistry.unregister>` instead +- ``matplotlib.pyplot.register_cmap``; use `matplotlib.colormaps.register + <.ColormapRegistry.register>` instead + +The `matplotlib.pyplot.get_cmap` function will stay available for backward +compatibility. + +``BrokenBarHCollection`` is deprecated +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +It was just a thin wrapper inheriting from `.PolyCollection`; +`~.Axes.broken_barh` has now been changed to return a `.PolyCollection` +instead. + +The ``BrokenBarHCollection.span_where`` helper is likewise deprecated; for the +duration of the deprecation it has been moved to the parent `.PolyCollection` +class. Use `~.Axes.fill_between` as a replacement; see +:doc:`/gallery/lines_bars_and_markers/span_regions` for an example. + +Passing inconsistent ``loc`` and ``nth_coord`` to axisartist helpers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Trying to construct for example a "top y-axis" or a "left x-axis" is now +deprecated. + +``passthru_pt`` +~~~~~~~~~~~~~~~ + +This attribute of ``AxisArtistHelper``\s is deprecated. + +``axes3d.vvec``, ``axes3d.eye``, ``axes3d.sx``, and ``axes3d.sy`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... are deprecated without replacement. + +``Line2D`` +~~~~~~~~~~ + +When creating a Line2D or using `.Line2D.set_xdata` and `.Line2D.set_ydata`, +passing x/y data as non sequence is deprecated. diff --git a/doc/api/prev_api_changes/api_changes_3.7.0/development.rst b/doc/api/prev_api_changes/api_changes_3.7.0/development.rst new file mode 100644 index 000000000000..c2ae35970524 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.7.0/development.rst @@ -0,0 +1,49 @@ +Development changes +------------------- + + +Windows wheel runtime bundling +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Wheels built for Windows now bundle the MSVC runtime DLL ``msvcp140.dll``. This +enables importing Matplotlib on systems that do not have the runtime installed. + + +Increase to minimum supported versions of dependencies +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +For Matplotlib 3.7, the :ref:`minimum supported versions ` are +being bumped: + ++------------+-----------------+---------------+ +| Dependency | min in mpl3.6 | min in mpl3.7 | ++============+=================+===============+ +| NumPy | 1.19 | 1.20 | ++------------+-----------------+---------------+ +| pyparsing | 2.2.1 | 2.3.1 | ++------------+-----------------+---------------+ +| Qt | | 5.10 | ++------------+-----------------+---------------+ + +- There are no wheels or conda packages that support both Qt 5.9 (or older) and + Python 3.8 (or newer). + +This is consistent with our :ref:`min_deps_policy` and `NEP29 +`__ + + +New dependencies +~~~~~~~~~~~~~~~~ + +* `importlib-resources `_ + (>= 3.2.0; only required on Python < 3.10) + +Maximum line length increased to 88 characters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The maximum line length for new contributions has been extended from 79 characters to +88 characters. +This change provides an extra 9 characters to allow code which is a single idea to fit +on fewer lines (often a single line). +The chosen length is the same as `black `_. diff --git a/doc/api/prev_api_changes/api_changes_3.7.0/removals.rst b/doc/api/prev_api_changes/api_changes_3.7.0/removals.rst new file mode 100644 index 000000000000..c8f499666525 --- /dev/null +++ b/doc/api/prev_api_changes/api_changes_3.7.0/removals.rst @@ -0,0 +1,369 @@ +Removals +-------- + +``epoch2num`` and ``num2epoch`` are removed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These methods convert from unix timestamps to matplotlib floats, but are not +used internally to Matplotlib, and should not be needed by end users. To +convert a unix timestamp to datetime, simply use +`datetime.datetime.utcfromtimestamp`, or to use NumPy `~numpy.datetime64` +``dt = np.datetime64(e*1e6, 'us')``. + +Locator and Formatter wrapper methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``set_view_interval``, ``set_data_interval`` and ``set_bounds`` methods of +`.Locator`\s and `.Formatter`\s (and their common base class, TickHelper) are +removed. Directly manipulate the view and data intervals on the underlying +axis instead. + +Interactive cursor details +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Setting a mouse cursor on a window has been moved from the toolbar to the +canvas. Consequently, several implementation details on toolbars and within +backends have been removed. + +``NavigationToolbar2.set_cursor`` and ``backend_tools.SetCursorBase.set_cursor`` +................................................................................ + +Instead, use the `.FigureCanvasBase.set_cursor` method on the canvas (available +as the ``canvas`` attribute on the toolbar or the Figure.) + +``backend_tools.SetCursorBase`` and subclasses +.............................................. + +``backend_tools.SetCursorBase`` was subclassed to provide backend-specific +implementations of ``set_cursor``. As that is now removed, the subclassing +is no longer necessary. Consequently, the following subclasses are also +removed: + +- ``matplotlib.backends.backend_gtk3.SetCursorGTK3`` +- ``matplotlib.backends.backend_qt5.SetCursorQt`` +- ``matplotlib.backends._backend_tk.SetCursorTk`` +- ``matplotlib.backends.backend_wx.SetCursorWx`` + +Instead, use the `.backend_tools.ToolSetCursor` class. + +``cursord`` in GTK and wx backends +.................................. + +The ``backend_gtk3.cursord`` and ``backend_wx.cursord`` dictionaries are +removed. This makes the GTK module importable on headless environments. + +``auto_add_to_figure=True`` for ``Axes3D`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +... is no longer supported. Instead use ``fig.add_axes(ax)``. + +The first parameter of ``Axes.grid`` and ``Axis.grid`` has been renamed to *visible* +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The parameter was previously named *b*. This name change only matters if that +parameter was passed using a keyword argument, e.g. ``grid(b=False)``. + +Removal of deprecations in the Selector widget API +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +RectangleSelector and EllipseSelector +..................................... + +The *drawtype* keyword argument to `~matplotlib.widgets.RectangleSelector` is +removed. From now on, the only behaviour will be ``drawtype='box'``. + +Support for ``drawtype=line`` is removed altogether. As a +result, the *lineprops* keyword argument to +`~matplotlib.widgets.RectangleSelector` is also removed. + +To retain the behaviour of ``drawtype='none'``, use ``rectprops={'visible': +False}`` to make the drawn `~matplotlib.patches.Rectangle` invisible. + +Cleaned up attributes and arguments are: + +- The ``active_handle`` attribute has been privatized and removed. +- The ``drawtype`` attribute has been privatized and removed. +- The ``eventpress`` attribute has been privatized and removed. +- The ``eventrelease`` attribute has been privatized and removed. +- The ``interactive`` attribute has been privatized and removed. +- The *marker_props* argument is removed, use *handle_props* instead. +- The *maxdist* argument is removed, use *grab_range* instead. +- The *rectprops* argument is removed, use *props* instead. +- The ``rectprops`` attribute has been privatized and removed. +- The ``state`` attribute has been privatized and removed. +- The ``to_draw`` attribute has been privatized and removed. + +PolygonSelector +............... + +- The *line* attribute is removed. If you want to change the selector artist + properties, use the ``set_props`` or ``set_handle_props`` methods. +- The *lineprops* argument is removed, use *props* instead. +- The *markerprops* argument is removed, use *handle_props* instead. +- The *maxdist* argument and attribute is removed, use *grab_range* instead. +- The *vertex_select_radius* argument and attribute is removed, use + *grab_range* instead. + +SpanSelector +............ + +- The ``active_handle`` attribute has been privatized and removed. +- The ``eventpress`` attribute has been privatized and removed. +- The ``eventrelease`` attribute has been privatized and removed. +- The ``pressv`` attribute has been privatized and removed. +- The ``prev`` attribute has been privatized and removed. +- The ``rect`` attribute has been privatized and removed. +- The *rectprops* parameter has been renamed to *props*. +- The ``rectprops`` attribute has been privatized and removed. +- The *span_stays* parameter has been renamed to *interactive*. +- The ``span_stays`` attribute has been privatized and removed. +- The ``state`` attribute has been privatized and removed. + +LassoSelector +............. + +- The *lineprops* argument is removed, use *props* instead. +- The ``onpress`` and ``onrelease`` methods are removed. They are straight + aliases for ``press`` and ``release``. +- The ``matplotlib.widgets.TextBox.DIST_FROM_LEFT`` attribute has been + removed. It was marked as private in 3.5. + +``backend_template.show`` +~~~~~~~~~~~~~~~~~~~~~~~~~ +... has been removed, in order to better demonstrate the new backend definition +API. + +Unused positional parameters to ``print_`` methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +None of the ``print_`` methods implemented by canvas subclasses used +positional arguments other that the first (the output filename or file-like), +so these extra parameters are removed. + +``QuadMesh`` signature +~~~~~~~~~~~~~~~~~~~~~~ + +The `.QuadMesh` signature :: + + def __init__(meshWidth, meshHeight, coordinates, + antialiased=True, shading='flat', **kwargs) + +is removed and replaced by the new signature :: + + def __init__(coordinates, *, antialiased=True, shading='flat', **kwargs) + +In particular: + +- The *coordinates* argument must now be a (M, N, 2) array-like. Previously, + the grid shape was separately specified as (*meshHeight* + 1, *meshWidth* + + 1) and *coordinates* could be an array-like of any shape with M * N * 2 + elements. +- All parameters except *coordinates* are keyword-only now. + +Expiration of ``FancyBboxPatch`` deprecations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `.FancyBboxPatch` constructor no longer accepts the *bbox_transmuter* +parameter, nor can the *boxstyle* parameter be set to "custom" -- instead, +directly set *boxstyle* to the relevant boxstyle instance. The +*mutation_scale* and *mutation_aspect* parameters have also become +keyword-only. + +The *mutation_aspect* parameter is now handled internally and no longer passed +to the boxstyle callables when mutating the patch path. + +Testing support +~~~~~~~~~~~~~~~ + +``matplotlib.test()`` has been removed +...................................... + +Run tests using ``pytest`` from the commandline instead. The variable +``matplotlib.default_test_modules`` was only used for ``matplotlib.test()`` and +is thus removed as well. + +To test an installed copy, be sure to specify both ``matplotlib`` and +``mpl_toolkits`` with ``--pyargs``:: + + python -m pytest --pyargs matplotlib.tests mpl_toolkits.tests + +See :ref:`testing` for more details. + +Auto-removal of grids by `~.Axes.pcolor` and `~.Axes.pcolormesh` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`~.Axes.pcolor` and `~.Axes.pcolormesh` previously remove any visible axes +major grid. This behavior is removed; please explicitly call ``ax.grid(False)`` +to remove the grid. + +Modification of ``Axes`` children sublists +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +See :ref:`Behavioural API Changes 3.5 - Axes children combined` for more +information; modification of the following sublists is no longer supported: + +* ``Axes.artists`` +* ``Axes.collections`` +* ``Axes.images`` +* ``Axes.lines`` +* ``Axes.patches`` +* ``Axes.tables`` +* ``Axes.texts`` + +To remove an Artist, use its `.Artist.remove` method. To add an Artist, use the +corresponding ``Axes.add_*`` method. + +Passing incorrect types to ``Axes.add_*`` methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The following ``Axes.add_*`` methods will now raise if passed an unexpected +type. See their documentation for the types they expect. + +- `.Axes.add_collection` +- `.Axes.add_image` +- `.Axes.add_line` +- `.Axes.add_patch` +- `.Axes.add_table` + + +``ConversionInterface.convert`` no longer accepts unitless values +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, custom subclasses of `.units.ConversionInterface` needed to +implement a ``convert`` method that not only accepted instances of the unit, +but also unitless values (which are passed through as is). This is no longer +the case (``convert`` is never called with a unitless value), and such support +in ``.StrCategoryConverter`` is removed. Likewise, the +``.ConversionInterface.is_numlike`` helper is removed. + +Consider calling `.Axis.convert_units` instead, which still supports unitless +values. + + +Normal list of `.Artist` objects now returned by `.HandlerLine2D.create_artists` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For Matplotlib 3.5 and 3.6 a proxy list was returned that simulated the return +of `.HandlerLine2DCompound.create_artists`. Now a list containing only the +single artist is return. + + +rcParams will no longer cast inputs to str +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +rcParams that expect a (non-pathlike) str no longer cast non-str inputs using +`str`. This will avoid confusing errors in subsequent code if e.g. a list input +gets implicitly cast to a str. + +Case-insensitive scales +~~~~~~~~~~~~~~~~~~~~~~~ + +Previously, scales could be set case-insensitively (e.g., +``set_xscale("LoG")``). Now all builtin scales use lowercase names. + +Support for ``nx1 = None`` or ``ny1 = None`` in ``AxesLocator`` and ``Divider.locate`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In `.axes_grid1.axes_divider`, various internal APIs no longer supports +passing ``nx1 = None`` or ``ny1 = None`` to mean ``nx + 1`` or ``ny + 1``, in +preparation for a possible future API which allows indexing and slicing of +dividers (possibly ``divider[a:b] == divider.new_locator(a, b)``, but also +``divider[a:] == divider.new_locator(a, )``). The user-facing +`.Divider.new_locator` API is unaffected -- it correctly normalizes ``nx1 = +None`` and ``ny1 = None`` as needed. + + +change signature of ``.FigureCanvasBase.enter_notify_event`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *xy* parameter is now required and keyword only. This was deprecated in +3.0 and originally slated to be removed in 3.5. + +``Colorbar`` tick update parameters +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The *update_ticks* parameter of `.Colorbar.set_ticks` and +`.Colorbar.set_ticklabels` was ignored since 3.5 and has been removed. + +plot directive removals +~~~~~~~~~~~~~~~~~~~~~~~ + +The public methods: + +- ``matplotlib.sphinxext.split_code_at_show`` +- ``matplotlib.sphinxext.unescape_doctest`` +- ``matplotlib.sphinxext.run_code`` + +have been removed. + +The deprecated *encoding* option to the plot directive has been removed. + +Miscellaneous removals +~~~~~~~~~~~~~~~~~~~~~~ + +- ``is_url`` and ``URL_REGEX`` are removed. (They were previously defined in + the toplevel :mod:`matplotlib` module.) +- The ``ArrowStyle.beginarrow`` and ``ArrowStyle.endarrow`` attributes are + removed; use the ``arrow`` attribute to define the desired heads and tails + of the arrow. +- ``backend_pgf.LatexManager.str_cache`` is removed. +- ``backends.qt_compat.ETS`` and ``backends.qt_compat.QT_RC_MAJOR_VERSION`` are + removed, with no replacement. +- The ``blocking_input`` module is removed. Instead, use + ``canvas.start_event_loop()`` and ``canvas.stop_event_loop()`` while + connecting event callbacks as needed. +- ``cbook.report_memory`` is removed; use ``psutil.virtual_memory`` instead. +- ``cm.LUTSIZE`` is removed. Use :rc:`image.lut` instead. This value only + affects colormap quantization levels for default colormaps generated at + module import time. +- ``Colorbar.patch`` is removed; this attribute was not correctly updated + anymore. +- ``ContourLabeler.get_label_width`` is removed. +- ``Dvi.baseline`` is removed (with no replacement). +- The *format* parameter of ``dviread.find_tex_file`` is removed (with no + replacement). +- ``FancyArrowPatch.get_path_in_displaycoord`` and + ``ConnectionPath.get_path_in_displaycoord`` are removed. The path in + display coordinates can still be obtained, as for other patches, using + ``patch.get_transform().transform_path(patch.get_path())``. +- The ``font_manager.win32InstalledFonts`` and + ``font_manager.get_fontconfig_fonts`` helper functions are removed. +- All parameters of ``imshow`` starting from *aspect* are keyword-only. +- ``QuadMesh.convert_mesh_to_paths`` and ``QuadMesh.convert_mesh_to_triangles`` + are removed. ``QuadMesh.get_paths()`` can be used as an alternative for the + former; there is no replacement for the latter. +- ``ScalarMappable.callbacksSM`` is removed. Use + ``ScalarMappable.callbacks`` instead. +- ``streamplot.get_integrator`` is removed. +- ``style.core.STYLE_FILE_PATTERN``, ``style.core.load_base_library``, and + ``style.core.iter_user_libraries`` are removed. +- ``SubplotParams.validate`` is removed. Use `.SubplotParams.update` to + change `.SubplotParams` while always keeping it in a valid state. +- The ``grey_arrayd``, ``font_family``, ``font_families``, and ``font_info`` + attributes of `.TexManager` are removed. +- ``Text.get_prop_tup`` is removed with no replacements (because the `.Text` + class cannot know whether a backend needs to update cache e.g. when the + text's color changes). +- ``Tick.apply_tickdir`` didn't actually update the tick markers on the + existing Line2D objects used to draw the ticks and is removed; use + `.Axis.set_tick_params` instead. +- ``tight_layout.auto_adjust_subplotpars`` is removed. +- The ``grid_info`` attribute of ``axisartist`` classes has been removed. +- ``axes_grid1.axes_grid.CbarAxes`` and ``axisartist.axes_grid.CbarAxes`` are + removed (they are now dynamically generated based on the owning axes + class). +- The ``axes_grid1.Divider.get_vsize_hsize`` and + ``axes_grid1.Grid.get_vsize_hsize`` methods are removed. +- ``AxesDivider.append_axes(..., add_to_figure=False)`` is removed. Use + ``ax.remove()`` to remove the Axes from the figure if needed. +- ``FixedAxisArtistHelper.change_tick_coord`` is removed with no + replacement. +- ``floating_axes.GridHelperCurveLinear.get_boundary`` is removed with no + replacement. +- ``ParasiteAxesBase.get_images_artists`` is removed. +- The "units finalize" signal (previously emitted by Axis instances) is + removed. Connect to "units" instead. +- Passing formatting parameters positionally to ``stem()`` is no longer + possible. +- ``axisartist.clip_path`` is removed with no replacement. + diff --git a/doc/api/projections/geo.rst b/doc/api/projections/geo.rst new file mode 100644 index 000000000000..beaa7ec343f3 --- /dev/null +++ b/doc/api/projections/geo.rst @@ -0,0 +1,7 @@ +****************************** +``matplotlib.projections.geo`` +****************************** + +.. automodule:: matplotlib.projections.geo + :members: + :show-inheritance: diff --git a/doc/api/projections/polar.rst b/doc/api/projections/polar.rst new file mode 100644 index 000000000000..3491fd92d16e --- /dev/null +++ b/doc/api/projections/polar.rst @@ -0,0 +1,7 @@ +******************************** +``matplotlib.projections.polar`` +******************************** + +.. automodule:: matplotlib.projections.polar + :members: + :show-inheritance: diff --git a/doc/api/projections_api.rst b/doc/api/projections_api.rst index e7c807957925..f0c742c241e7 100644 --- a/doc/api/projections_api.rst +++ b/doc/api/projections_api.rst @@ -6,11 +6,13 @@ :members: :show-inheritance: +Built-in projections +==================== +Matplotlib has built-in support for polar and some geographic projections. +See the following pages for more information: -******************************** -``matplotlib.projections.polar`` -******************************** +.. toctree:: + :maxdepth: 1 -.. automodule:: matplotlib.projections.polar - :members: - :show-inheritance: + projections/polar + projections/geo diff --git a/doc/api/pyplot_summary.rst b/doc/api/pyplot_summary.rst index f3f4c88b78e8..616e9c257aa5 100644 --- a/doc/api/pyplot_summary.rst +++ b/doc/api/pyplot_summary.rst @@ -2,31 +2,337 @@ ``matplotlib.pyplot`` ********************* -Pyplot function overview +.. currentmodule:: matplotlib.pyplot + +.. automodule:: matplotlib.pyplot + :no-members: + :no-undoc-members: + + +Managing Figure and Axes ------------------------ -.. currentmodule:: matplotlib +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + axes + cla + clf + close + delaxes + fignum_exists + figure + gca + gcf + get_figlabels + get_fignums + sca + subplot + subplot2grid + subplot_mosaic + subplots + twinx + twiny + + +Adding data to the plot +----------------------- + +Basic +^^^^^ .. autosummary:: :toctree: _as_gen - :template: autofunctions.rst + :template: autosummary.rst + :nosignatures: - pyplot + plot + errorbar + scatter + plot_date + step + loglog + semilogx + semilogy + fill_between + fill_betweenx + bar + barh + bar_label + stem + eventplot + pie + stackplot + broken_barh + vlines + hlines + fill + polar -.. currentmodule:: matplotlib.pyplot -.. autofunction:: plotting +Spans +^^^^^ +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: -Colors in Matplotlib --------------------- + axhline + axhspan + axvline + axvspan + axline -There are many colormaps you can use to map data onto color values. -Below we list several ways in which color can be utilized in Matplotlib. -For a more in-depth look at colormaps, see the -:doc:`/tutorials/colors/colormaps` tutorial. +Spectral +^^^^^^^^ -.. currentmodule:: matplotlib.pyplot +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + acorr + angle_spectrum + cohere + csd + magnitude_spectrum + phase_spectrum + psd + specgram + xcorr + + +Statistics +^^^^^^^^^^ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + boxplot + violinplot + + +Binned +^^^^^^ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + hexbin + hist + hist2d + stairs + + +Contours +^^^^^^^^ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + clabel + contour + contourf + + +2D arrays +^^^^^^^^^ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + imshow + matshow + pcolor + pcolormesh + spy + figimage + + +Unstructured triangles +^^^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + triplot + tripcolor + tricontour + tricontourf + + +Text and annotations +^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + annotate + text + figtext + table + arrow + figlegend + legend + + +Vector fields +^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + barbs + quiver + quiverkey + streamplot + + +Axis configuration +------------------ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + autoscale + axis + box + grid + locator_params + minorticks_off + minorticks_on + rgrids + thetagrids + tick_params + ticklabel_format + xlabel + xlim + xscale + xticks + ylabel + ylim + yscale + yticks + suptitle + title + + +Layout +------ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + margins + subplots_adjust + subplot_tool + tight_layout + + +Colormapping +------------ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + clim + colorbar + gci + sci + get_cmap + set_cmap + imread + imsave + +Colormaps are available via the colormap registry `matplotlib.colormaps`. For +convenience this registry is available in ``pyplot`` as + +.. autodata:: colormaps + :no-value: + +Additionally, there are shortcut functions to set builtin colormaps; e.g. +``plt.viridis()`` is equivalent to ``plt.set_cmap('viridis')``. + + +.. autodata:: color_sequences + :no-value: + + +Configuration +------------- + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + rc + rc_context + rcdefaults + + +Output +------ + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + draw + draw_if_interactive + ioff + ion + install_repl_displayhook + isinteractive + pause + savefig + show + switch_backend + uninstall_repl_displayhook + + +Other +----- + +.. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: -.. autofunction:: colormaps + connect + disconnect + findobj + get + getp + get_current_fig_manager + ginput + new_figure_manager + set_loglevel + setp + waitforbuttonpress + xkcd diff --git a/doc/api/sphinxext_mathmpl_api.rst b/doc/api/sphinxext_mathmpl_api.rst new file mode 100644 index 000000000000..839334ca39fe --- /dev/null +++ b/doc/api/sphinxext_mathmpl_api.rst @@ -0,0 +1,7 @@ +================================ +``matplotlib.sphinxext.mathmpl`` +================================ + +.. automodule:: matplotlib.sphinxext.mathmpl + :exclude-members: latex_math + :no-undoc-members: diff --git a/doc/api/testing_api.rst b/doc/api/testing_api.rst index 808d2b870109..7731d4510b27 100644 --- a/doc/api/testing_api.rst +++ b/doc/api/testing_api.rst @@ -3,11 +3,6 @@ ********************** -:func:`matplotlib.test` -======================= - -.. autofunction:: matplotlib.test - :mod:`matplotlib.testing` ========================= diff --git a/doc/api/text_api.rst b/doc/api/text_api.rst index c88d45f2832b..af37e5c526a3 100644 --- a/doc/api/text_api.rst +++ b/doc/api/text_api.rst @@ -2,7 +2,32 @@ ``matplotlib.text`` ******************* +.. redirect-from:: /api/textpath_api + .. automodule:: matplotlib.text + :no-members: + +.. autoclass:: matplotlib.text.Text + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: matplotlib.text.Annotation + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: matplotlib.text.OffsetFrom + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: matplotlib.text.TextPath + :members: + :undoc-members: + :show-inheritance: + +.. autoclass:: matplotlib.text.TextToPath :members: :undoc-members: :show-inheritance: diff --git a/doc/api/textpath_api.rst b/doc/api/textpath_api.rst deleted file mode 100644 index 875e4b376867..000000000000 --- a/doc/api/textpath_api.rst +++ /dev/null @@ -1,8 +0,0 @@ -*********************** -``matplotlib.textpath`` -*********************** - -.. automodule:: matplotlib.textpath - :members: - :undoc-members: - :show-inheritance: diff --git a/doc/api/tight_bbox_api.rst b/doc/api/tight_bbox_api.rst index 3a96b5b6d027..9e8dd2fa66f9 100644 --- a/doc/api/tight_bbox_api.rst +++ b/doc/api/tight_bbox_api.rst @@ -2,7 +2,12 @@ ``matplotlib.tight_bbox`` ************************* -.. automodule:: matplotlib.tight_bbox +.. attention:: + This module is considered internal. + + Its use is deprecated and it will be removed in a future version. + +.. automodule:: matplotlib._tight_bbox :members: :undoc-members: :show-inheritance: diff --git a/doc/api/tight_layout_api.rst b/doc/api/tight_layout_api.rst index 1f1a32281aa0..35f92e3ddced 100644 --- a/doc/api/tight_layout_api.rst +++ b/doc/api/tight_layout_api.rst @@ -2,7 +2,12 @@ ``matplotlib.tight_layout`` *************************** -.. automodule:: matplotlib.tight_layout +.. attention:: + This module is considered internal. + + Its use is deprecated and it will be removed in a future version. + +.. automodule:: matplotlib._tight_layout :members: :undoc-members: :show-inheritance: diff --git a/doc/api/toolkits/axes_grid.rst b/doc/api/toolkits/axes_grid.rst deleted file mode 100644 index 991b0ff6813a..000000000000 --- a/doc/api/toolkits/axes_grid.rst +++ /dev/null @@ -1,26 +0,0 @@ -.. _axes_grid-api-index: - -Matplotlib axes_grid Toolkit -============================ - -.. currentmodule:: mpl_toolkits - - -.. note:: - AxesGrid toolkit has been a part of matplotlib since v - 0.99. Originally, the toolkit had a single namespace of - *axes_grid*. In more recent version, the toolkit - has divided into two separate namespace (*axes_grid1* and *axisartist*). - While *axes_grid* namespace is maintained for the backward compatibility, - use of *axes_grid1* and *axisartist* is recommended. - For the documentation on ``axes_grid``, - see the `previous version of the docs - `_. - -.. toctree:: - :maxdepth: 1 - - axes_grid1 - axisartist - - diff --git a/doc/api/toolkits/axes_grid1.rst b/doc/api/toolkits/axes_grid1.rst index 3abbaf8f22c0..c48a6a31af90 100644 --- a/doc/api/toolkits/axes_grid1.rst +++ b/doc/api/toolkits/axes_grid1.rst @@ -1,13 +1,14 @@ .. module:: mpl_toolkits.axes_grid1 -Matplotlib axes_grid1 Toolkit -============================= +.. redirect-from:: /api/toolkits/axes_grid -The matplotlib :mod:`mpl_toolkits.axes_grid1` toolkit is a collection of -helper classes to ease displaying multiple images in matplotlib. While the -aspect parameter in matplotlib adjust the position of the single axes, -axes_grid1 toolkit provides a framework to adjust the position of -multiple axes according to their aspects. +``mpl_toolkits.axes_grid1`` +=========================== + +:mod:`mpl_toolkits.axes_grid1` provides a framework of helper classes to adjust +the positioning of multiple fixed-aspect Axes (e.g., displaying images). It +can be contrasted with the ``aspect`` property of Matplotlib Axes, which +adjusts the position of a single Axes. See :ref:`axes_grid1_users-guide-index` for a guide on the usage of axes_grid1. @@ -16,6 +17,13 @@ See :ref:`axes_grid1_users-guide-index` for a guide on the usage of axes_grid1. :align: center :scale: 50 +.. note:: + + This module contains classes and function that were formerly part of the + ``mpl_toolkits.axes_grid`` module that was removed in 3.6. Additional + classes from that older module may also be found in + `mpl_toolkits.axisartist`. + .. currentmodule:: mpl_toolkits **The submodules of the axes_grid1 API are:** @@ -32,5 +40,3 @@ See :ref:`axes_grid1_users-guide-index` for a guide on the usage of axes_grid1. axes_grid1.inset_locator axes_grid1.mpl_axes axes_grid1.parasite_axes - - diff --git a/doc/api/toolkits/axisartist.rst b/doc/api/toolkits/axisartist.rst index f18246fef128..8cac4d68a266 100644 --- a/doc/api/toolkits/axisartist.rst +++ b/doc/api/toolkits/axisartist.rst @@ -1,15 +1,15 @@ .. module:: mpl_toolkits.axisartist -Matplotlib axisartist Toolkit -============================= +``mpl_toolkits.axisartist`` +=========================== -The *axisartist* namespace includes a derived Axes implementation ( -:class:`mpl_toolkits.axisartist.Axes`). The -biggest difference is that the artists that are responsible for drawing -axis lines, ticks, ticklabels, and axis labels are separated out from the -mpl's Axis class. This change was strongly motivated to support curvilinear grid. +The *axisartist* namespace provides a derived Axes implementation +(:class:`mpl_toolkits.axisartist.Axes`), designed to support curvilinear +grids. The biggest difference is that the artists that are responsible for +drawing axis lines, ticks, ticklabels, and axis labels are separated out from +Matplotlib's Axis class. -You can find a tutorial describing usage of axisartist at the +You can find a tutorial describing usage of axisartist at the :ref:`axisartist_users-guide-index` user guide. .. figure:: ../../gallery/axisartist/images/sphx_glr_demo_curvelinear_grid_001.png @@ -17,6 +17,13 @@ You can find a tutorial describing usage of axisartist at the :align: center :scale: 50 +.. note:: + + This module contains classes and function that were formerly part of the + ``mpl_toolkits.axes_grid`` module that was removed in 3.6. Additional + classes from that older module may also be found in + `mpl_toolkits.axes_grid1`. + .. currentmodule:: mpl_toolkits **The submodules of the axisartist API are:** @@ -32,9 +39,7 @@ You can find a tutorial describing usage of axisartist at the axisartist.axis_artist axisartist.axisline_style axisartist.axislines - axisartist.clip_path axisartist.floating_axes axisartist.grid_finder axisartist.grid_helper_curvelinear axisartist.parasite_axes - diff --git a/doc/api/toolkits/index.rst b/doc/api/toolkits/index.rst deleted file mode 100644 index 59c01ab21a69..000000000000 --- a/doc/api/toolkits/index.rst +++ /dev/null @@ -1,46 +0,0 @@ -.. _toolkits-index: - -.. _toolkits: - -######## -Toolkits -######## - -Toolkits are collections of application-specific functions that extend -Matplotlib. - -.. _toolkit_mplot3d: - -mplot3d -======= - -:mod:`mpl_toolkits.mplot3d` provides some basic 3D -plotting (scatter, surf, line, mesh) tools. Not the fastest or most feature -complete 3D library out there, but it ships with Matplotlib and thus may be a -lighter weight solution for some use cases. Check out the -:doc:`mplot3d tutorial ` for more -information. - -.. figure:: ../../gallery/mplot3d/images/sphx_glr_contourf3d_2_001.png - :target: ../../gallery/mplot3d/contourf3d_2.html - :align: center - :scale: 50 - -.. toctree:: - :maxdepth: 2 - - mplot3d/index.rst - mplot3d/faq.rst - -Links ------ -* mpl3d API: :ref:`toolkit_mplot3d-api` - -.. include:: axes_grid1.rst - :start-line: 1 - -.. include:: axisartist.rst - :start-line: 1 - -.. include:: axes_grid.rst - :start-line: 1 diff --git a/doc/api/toolkits/mplot3d.rst b/doc/api/toolkits/mplot3d.rst index 97d3bf13246f..fc6c4cbad6d1 100644 --- a/doc/api/toolkits/mplot3d.rst +++ b/doc/api/toolkits/mplot3d.rst @@ -1,8 +1,35 @@ -.. _toolkit_mplot3d-api: +.. _toolkit_mplot3d-index: +.. currentmodule:: mpl_toolkits.mplot3d + +************************ +``mpl_toolkits.mplot3d`` +************************ + +The mplot3d toolkit adds simple 3D plotting capabilities (scatter, surface, +line, mesh, etc.) to Matplotlib by supplying an Axes object that can create +a 2D projection of a 3D scene. The resulting graph will have the same look +and feel as regular 2D plots. Not the fastest or most feature complete 3D +library out there, but it ships with Matplotlib and thus may be a lighter +weight solution for some use cases. + +See the :doc:`mplot3d tutorial ` for +more information. + +.. image:: /_static/demo_mplot3d.png + :align: center -*********** -mplot3d API -*********** +The interactive backends also provide the ability to rotate and zoom the 3D +scene. One can rotate the 3D scene by simply clicking-and-dragging the scene. +Panning is done by clicking the middle mouse button, and zooming is done by +right-clicking the scene and dragging the mouse up and down. Unlike 2D plots, +the toolbar pan and zoom buttons are not used. + +.. toctree:: + :maxdepth: 2 + + mplot3d/faq.rst + mplot3d/view_angles.rst + mplot3d/axes3d.rst .. note:: `.pyplot` cannot be used to add content to 3D plots, because its function @@ -25,11 +52,8 @@ mplot3d API Please report any functions that do not behave as expected as a bug. In addition, help and patches would be greatly appreciated! -.. autosummary:: - :toctree: ../_as_gen - :template: autosummary.rst - axes3d.Axes3D +`axes3d.Axes3D` (fig[, rect, elev, azim, roll, ...]) 3D Axes object. .. module:: mpl_toolkits.mplot3d.axis3d diff --git a/doc/api/toolkits/mplot3d/axes3d.rst b/doc/api/toolkits/mplot3d/axes3d.rst new file mode 100644 index 000000000000..e334fee2fea5 --- /dev/null +++ b/doc/api/toolkits/mplot3d/axes3d.rst @@ -0,0 +1,306 @@ +mpl\_toolkits.mplot3d.axes3d.Axes3D +=================================== + + +.. currentmodule:: mpl_toolkits.mplot3d.axes3d + + +.. autoclass:: Axes3D + :no-members: + :no-undoc-members: + :show-inheritance: + + +.. currentmodule:: mpl_toolkits.mplot3d.axes3d.Axes3D + + +Plotting +-------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + plot + scatter + bar + bar3d + + plot_surface + plot_wireframe + plot_trisurf + + clabel + contour + tricontour + contourf + tricontourf + + quiver + voxels + errorbar + stem + + +Text and annotations +-------------------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + text + text2D + + +Clearing +-------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + clear + + +Appearance +---------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + set_axis_off + set_axis_on + grid + get_frame_on + set_frame_on + + +Axis +---- + +Axis limits and direction +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + get_zaxis + get_xlim + get_ylim + get_zlim + set_zlim + get_w_lims + invert_zaxis + zaxis_inverted + get_zbound + set_zbound + + +Axis labels and title +^^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + set_zlabel + get_zlabel + set_title + + +Axis scales +^^^^^^^^^^^ + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + set_xscale + set_yscale + set_zscale + get_zscale + + +Autoscaling and margins +^^^^^^^^^^^^^^^^^^^^^^^ + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + set_zmargin + margins + autoscale + autoscale_view + set_autoscalez_on + get_autoscalez_on + auto_scale_xyz + + +Aspect ratio +^^^^^^^^^^^^ + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + set_aspect + set_box_aspect + apply_aspect + + +Ticks +^^^^^ + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + tick_params + set_zticks + get_zticks + set_zticklabels + get_zticklines + get_zgridlines + get_zminorticklabels + get_zmajorticklabels + zaxis_date + + +Units +----- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + convert_zunits + + +Adding artists +-------------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + add_collection3d + + +Sharing +------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + sharez + + +Interactive +----------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + can_zoom + can_pan + disable_mouse_rotation + mouse_init + drag_pan + format_zdata + format_coord + + +Projection and perspective +-------------------------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + view_init + set_proj_type + get_proj + set_top_view + + +Drawing +------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + draw + get_tightbbox + + +Aliases and deprecated methods +------------------------------ + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + set_zlim3d + stem3D + text3D + tunit_cube + tunit_edges + unit_cube + w_xaxis + w_yaxis + w_zaxis + + +Other +----- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + get_axis_position + add_contour_set + add_contourf_set + update_datalim + + +.. currentmodule:: mpl_toolkits.mplot3d + +Sample 3D data +-------------- + +.. autosummary:: + :toctree: ../../_as_gen + :template: autosummary.rst + :nosignatures: + + axes3d.get_test_data + + +.. minigallery:: mpl_toolkits.mplot3d.axes3d.Axes3D + :add-heading: diff --git a/doc/api/toolkits/mplot3d/faq.rst b/doc/api/toolkits/mplot3d/faq.rst index dfc23b55e069..7e53cabc6e9a 100644 --- a/doc/api/toolkits/mplot3d/faq.rst +++ b/doc/api/toolkits/mplot3d/faq.rst @@ -49,4 +49,3 @@ Work is being done to eliminate this issue. For matplotlib v1.1.0, there is a semi-official manner to modify these parameters. See the note in the :mod:`.mplot3d.axis3d` section of the mplot3d API documentation for more information. - diff --git a/doc/api/toolkits/mplot3d/index.rst b/doc/api/toolkits/mplot3d/index.rst deleted file mode 100644 index 8b153c06903f..000000000000 --- a/doc/api/toolkits/mplot3d/index.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _toolkit_mplot3d-index: -.. currentmodule:: mpl_toolkits.mplot3d - -******* -mplot3d -******* - -Matplotlib mplot3d toolkit -========================== -The mplot3d toolkit adds simple 3D plotting capabilities to matplotlib by -supplying an axes object that can create a 2D projection of a 3D scene. -The resulting graph will have the same look and feel as regular 2D plots. - -See the :doc:`mplot3d tutorial ` for -more information on how to use this toolkit. - -.. image:: /_static/demo_mplot3d.png - -The interactive backends also provide the ability to rotate and zoom -the 3D scene. One can rotate the 3D scene by simply clicking-and-dragging -the scene. Zooming is done by right-clicking the scene and dragging the -mouse up and down. Note that one does not use the zoom button like one -would use for regular 2D plots. - -.. toctree:: - :maxdepth: 2 - - faq.rst diff --git a/doc/api/toolkits/mplot3d/view_angles.rst b/doc/api/toolkits/mplot3d/view_angles.rst new file mode 100644 index 000000000000..10d4fac39e8c --- /dev/null +++ b/doc/api/toolkits/mplot3d/view_angles.rst @@ -0,0 +1,40 @@ +.. _toolkit_mplot3d-view-angles: + +******************* +mplot3d View Angles +******************* + +How to define the view angle +============================ + +The position of the viewport "camera" in a 3D plot is defined by three angles: +*elevation*, *azimuth*, and *roll*. From the resulting position, it always +points towards the center of the plot box volume. The angle direction is a +common convention, and is shared with +`PyVista `_ and +`MATLAB `_ +(though MATLAB lacks a roll angle). Note that a positive roll angle rotates the +viewing plane clockwise, so the 3d axes will appear to rotate +counter-clockwise. + +.. image:: /_static/mplot3d_view_angles.png + :align: center + :scale: 50 + +Rotating the plot using the mouse will control only the azimuth and elevation, +but all three angles can be set programmatically:: + + import matplotlib.pyplot as plt + ax = plt.figure().add_subplot(projection='3d') + ax.view_init(elev=30, azim=45, roll=15) + + +Primary view planes +=================== + +To look directly at the primary view planes, the required elevation, azimuth, +and roll angles are shown in the diagram of an "unfolded" plot below. These are +further documented in the `.mplot3d.axes3d.Axes3D.view_init` API. + +.. plot:: gallery/mplot3d/view_planes_3d.py + :align: center diff --git a/doc/api/transformations.rst b/doc/api/transformations.rst index 58c29598704c..186db9aea728 100644 --- a/doc/api/transformations.rst +++ b/doc/api/transformations.rst @@ -2,7 +2,7 @@ ``matplotlib.transforms`` ************************* -.. inheritance-diagram:: matplotlib.transforms matplotlib.path +.. inheritance-diagram:: matplotlib.transforms :parts: 1 .. automodule:: matplotlib.transforms @@ -15,4 +15,3 @@ interval_contains, interval_contains_open :show-inheritance: :special-members: - diff --git a/doc/api/tri_api.rst b/doc/api/tri_api.rst index 9205e34ff93b..0b4e046eec08 100644 --- a/doc/api/tri_api.rst +++ b/doc/api/tri_api.rst @@ -2,7 +2,9 @@ ``matplotlib.tri`` ****************** -.. automodule:: matplotlib.tri +Unstructured triangular grid functions. + +.. py:module:: matplotlib.tri .. autoclass:: matplotlib.tri.Triangulation :members: @@ -17,7 +19,7 @@ :show-inheritance: .. autoclass:: matplotlib.tri.TriInterpolator - + .. autoclass:: matplotlib.tri.LinearTriInterpolator :members: __call__, gradient :show-inheritance: @@ -30,7 +32,7 @@ .. autoclass:: matplotlib.tri.UniformTriRefiner :show-inheritance: - :members: + :members: .. autoclass:: matplotlib.tri.TriAnalyzer - :members: + :members: diff --git a/doc/api/type1font.rst b/doc/api/type1font.rst index 2cb2a68eb5d5..00ef38f4d447 100644 --- a/doc/api/type1font.rst +++ b/doc/api/type1font.rst @@ -2,7 +2,12 @@ ``matplotlib.type1font`` ************************ -.. automodule:: matplotlib.type1font +.. attention:: + This module is considered internal. + + Its use is deprecated and it will be removed in a future version. + +.. automodule:: matplotlib._type1font :members: :undoc-members: :show-inheritance: diff --git a/doc/citing.rst b/doc/citing.rst deleted file mode 100644 index c1a1b5fe679d..000000000000 --- a/doc/citing.rst +++ /dev/null @@ -1,150 +0,0 @@ -:orphan: - -Citing Matplotlib -================= - -If Matplotlib contributes to a project that leads to a scientific publication, -please acknowledge this fact by citing `J. D. Hunter, "Matplotlib: A 2D -Graphics Environment", Computing in Science & Engineering, vol. 9, no. 3, -pp. 90-95, 2007 `_. - -.. literalinclude:: MCSE.2007.55.bib - :language: bibtex - -.. container:: sphx-glr-download - - :download:`Download BibTeX bibliography file: MCSE.2007.55.bib ` - -DOIs ----- - -The following DOI represents *all* Matplotlib versions. Please select a more -specific DOI from the list below, referring to the version used for your publication. - - .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.592536.svg - :target: https://doi.org/10.5281/zenodo.592536 - -By version -~~~~~~~~~~ -.. START OF AUTOGENERATED - - -v3.4.2 - .. image:: _static/zenodo_cache/4743323.svg - :target: https://doi.org/10.5281/zenodo.4743323 -v3.4.1 - .. image:: _static/zenodo_cache/4649959.svg - :target: https://doi.org/10.5281/zenodo.4649959 -v3.4.0 - .. image:: _static/zenodo_cache/4638398.svg - :target: https://doi.org/10.5281/zenodo.4638398 -v3.3.4 - .. image:: _static/zenodo_cache/4475376.svg - :target: https://doi.org/10.5281/zenodo.4475376 -v3.3.3 - .. image:: _static/zenodo_cache/4268928.svg - :target: https://doi.org/10.5281/zenodo.4268928 -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 -v3.3.0 - .. image:: _static/zenodo_cache/3948793.svg - :target: https://doi.org/10.5281/zenodo.3948793 -v3.2.2 - .. image:: _static/zenodo_cache/3898017.svg - :target: https://doi.org/10.5281/zenodo.3898017 -v3.2.1 - .. image:: _static/zenodo_cache/3714460.svg - :target: https://doi.org/10.5281/zenodo.3714460 -v3.2.0 - .. image:: _static/zenodo_cache/3695547.svg - :target: https://doi.org/10.5281/zenodo.3695547 -v3.1.3 - .. image:: _static/zenodo_cache/3633844.svg - :target: https://doi.org/10.5281/zenodo.3633844 -v3.1.2 - .. image:: _static/zenodo_cache/3563226.svg - :target: https://doi.org/10.5281/zenodo.3563226 -v3.1.1 - .. image:: _static/zenodo_cache/3264781.svg - :target: https://doi.org/10.5281/zenodo.3264781 -v3.1.0 - .. image:: _static/zenodo_cache/2893252.svg - :target: https://doi.org/10.5281/zenodo.2893252 -v3.0.3 - .. image:: _static/zenodo_cache/2577644.svg - :target: https://doi.org/10.5281/zenodo.2577644 -v3.0.2 - .. image:: _static/zenodo_cache/1482099.svg - :target: https://doi.org/10.5281/zenodo.1482099 -v3.0.1 - .. image:: _static/zenodo_cache/1482098.svg - :target: https://doi.org/10.5281/zenodo.1482098 -v2.2.5 - .. image:: _static/zenodo_cache/3633833.svg - :target: https://doi.org/10.5281/zenodo.3633833 -v3.0.0 - .. image:: _static/zenodo_cache/1420605.svg - :target: https://doi.org/10.5281/zenodo.1420605 -v2.2.4 - .. image:: _static/zenodo_cache/2669103.svg - :target: https://doi.org/10.5281/zenodo.2669103 -v2.2.3 - .. image:: _static/zenodo_cache/1343133.svg - :target: https://doi.org/10.5281/zenodo.1343133 -v2.2.2 - .. image:: _static/zenodo_cache/1202077.svg - :target: https://doi.org/10.5281/zenodo.1202077 -v2.2.1 - .. image:: _static/zenodo_cache/1202050.svg - :target: https://doi.org/10.5281/zenodo.1202050 -v2.2.0 - .. image:: _static/zenodo_cache/1189358.svg - :target: https://doi.org/10.5281/zenodo.1189358 -v2.1.2 - .. image:: _static/zenodo_cache/1154287.svg - :target: https://doi.org/10.5281/zenodo.1154287 -v2.1.1 - .. image:: _static/zenodo_cache/1098480.svg - :target: https://doi.org/10.5281/zenodo.1098480 -v2.1.0 - .. image:: _static/zenodo_cache/1004650.svg - :target: https://doi.org/10.5281/zenodo.1004650 -v2.0.2 - .. image:: _static/zenodo_cache/573577.svg - :target: https://doi.org/10.5281/zenodo.573577 -v2.0.1 - .. image:: _static/zenodo_cache/570311.svg - :target: https://doi.org/10.5281/zenodo.570311 -v2.0.0 - .. image:: _static/zenodo_cache/248351.svg - :target: https://doi.org/10.5281/zenodo.248351 -v1.5.3 - .. image:: _static/zenodo_cache/61948.svg - :target: https://doi.org/10.5281/zenodo.61948 -v1.5.2 - .. image:: _static/zenodo_cache/56926.svg - :target: https://doi.org/10.5281/zenodo.56926 -v1.5.1 - .. image:: _static/zenodo_cache/44579.svg - :target: https://doi.org/10.5281/zenodo.44579 -v1.5.0 - .. image:: _static/zenodo_cache/32914.svg - :target: https://doi.org/10.5281/zenodo.32914 -v1.4.3 - .. image:: _static/zenodo_cache/15423.svg - :target: https://doi.org/10.5281/zenodo.15423 -v1.4.2 - .. image:: _static/zenodo_cache/12400.svg - :target: https://doi.org/10.5281/zenodo.12400 -v1.4.1 - .. image:: _static/zenodo_cache/12287.svg - :target: https://doi.org/10.5281/zenodo.12287 -v1.4.0 - .. image:: _static/zenodo_cache/11451.svg - :target: https://doi.org/10.5281/zenodo.11451 - -.. END OF AUTOGENERATED diff --git a/doc/conf.py b/doc/conf.py index f78cb851b27b..acb36254db82 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,26 +1,43 @@ # Matplotlib documentation build configuration file, created by # sphinx-quickstart on Fri May 2 12:33:25 2008. # -# This file is execfile()d with the current directory set to its containing dir. +# This file is execfile()d with the current directory set to its containing +# dir. # # The contents of this file are pickled, so don't put values in the namespace -# that aren't pickleable (module imports are okay, they're removed automatically). +# that aren't picklable (module imports are okay, they're removed +# automatically). # # All configuration values have a default value; values that are commented out # serve to show the default value. +import logging import os from pathlib import Path import shutil import subprocess import sys +from urllib.parse import urlsplit, urlunsplit import warnings import matplotlib -from matplotlib._api import MatplotlibDeprecationWarning -import sphinx from datetime import datetime +import time + +# debug that building expected version +print(f"Building Documentation for Matplotlib: {matplotlib.__version__}") + +# Release mode enables optimizations and other related options. +is_release_build = tags.has('release') # noqa + +# are we running circle CI? +CIRCLECI = 'CIRCLECI' in os.environ + +# Parse year using SOURCE_DATE_EPOCH, falling back to current time. +# https://reproducible-builds.org/specs/source-date-epoch/ +sourceyear = datetime.utcfromtimestamp( + int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))).year # If your extensions are in another directory, add it here. If the directory # is relative to the documentation root, use os.path.abspath to make it @@ -36,20 +53,14 @@ # usage in the gallery. warnings.filterwarnings('error', append=True) -# Strip backslashes 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 = [ 'sphinx.ext.autodoc', 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', 'sphinx.ext.inheritance_diagram', 'sphinx.ext.intersphinx', 'sphinx.ext.ifconfig', - 'sphinx.ext.viewcode', 'IPython.sphinxext.ipython_console_highlighting', 'IPython.sphinxext.ipython_directive', 'numpydoc', # Needs to be loaded *after* autodoc. @@ -65,24 +76,21 @@ 'sphinxext.skip_deprecated', 'sphinxext.redirect_from', 'sphinx_copybutton', + 'sphinx_design', ] exclude_patterns = [ 'api/prev_api_changes/api_changes_*/*', - # Be sure to update users/whats_new.rst: - 'users/prev_whats_new/whats_new_3.4.0.rst', ] def _check_dependencies(): names = { + **{ext: ext.split(".")[0] for ext in extensions}, + # Explicitly list deps that are not extensions, or whose PyPI package + # name does not match the (toplevel) module name. "colorspacious": 'colorspacious', - "IPython.sphinxext.ipython_console_highlighting": 'ipython', - "matplotlib": 'matplotlib', - "numpydoc": 'numpydoc', - "PIL.Image": 'pillow', - "sphinx_copybutton": 'sphinx_copybutton', - "sphinx_gallery": 'sphinx_gallery', + "mpl_sphinx_theme": 'mpl_sphinx_theme', "sphinxcontrib.inkscapeconverter": 'sphinxcontrib-svg2pdfconverter', } missing = [] @@ -107,6 +115,7 @@ def _check_dependencies(): # gallery_order.py from the sphinxext folder provides the classes that # allow custom ordering of sections and subsections of the gallery import sphinxext.gallery_order as gallery_order + # The following import is only necessary to monkey patch the signature later on from sphinx_gallery import gen_rst @@ -117,7 +126,7 @@ def _check_dependencies(): # we should ignore warnings coming from importing deprecated modules for # autodoc purposes, as this will disappear automatically when they are removed -warnings.filterwarnings('ignore', category=MatplotlibDeprecationWarning, +warnings.filterwarnings('ignore', category=DeprecationWarning, module='importlib', # used by sphinx.autodoc.importer message=r'(\n|.)*module was deprecated.*') @@ -126,12 +135,10 @@ def _check_dependencies(): # make sure to ignore warnings that stem from simply inspecting deprecated # class-level attributes -warnings.filterwarnings('ignore', category=MatplotlibDeprecationWarning, +warnings.filterwarnings('ignore', category=DeprecationWarning, module='sphinx.util.inspect') -# missing-references names matches sphinx>=3 behavior, so we can't be nitpicky -# for older sphinxes. -nitpicky = sphinx.version_info >= (3,) +nitpicky = True # change this to True to update the allowed failures missing_references_write_json = False missing_references_warn_unused_ignores = False @@ -145,42 +152,102 @@ def _check_dependencies(): 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), 'pytest': ('https://pytest.org/en/stable/', None), 'python': ('https://docs.python.org/3/', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference/', None), + 'scipy': ('https://docs.scipy.org/doc/scipy/', None), + 'tornado': ('https://www.tornadoweb.org/en/stable/', None), + 'xarray': ('https://docs.xarray.dev/en/stable/', None), } # Sphinx gallery configuration + +def matplotlib_reduced_latex_scraper(block, block_vars, gallery_conf, + **kwargs): + """ + Reduce srcset when creating a PDF. + + Because sphinx-gallery runs *very* early, we cannot modify this even in the + earliest builder-inited signal. Thus we do it at scraping time. + """ + from sphinx_gallery.scrapers import matplotlib_scraper + + if gallery_conf['builder_name'] == 'latex': + gallery_conf['image_srcset'] = [] + return matplotlib_scraper(block, block_vars, gallery_conf, **kwargs) + + sphinx_gallery_conf = { - 'examples_dirs': ['../examples', '../tutorials'], - 'filename_pattern': '^((?!sgskip).)*$', - 'gallery_dirs': ['gallery', 'tutorials'], - 'doc_module': ('matplotlib', 'mpl_toolkits'), - 'reference_url': { - 'matplotlib': None, - 'numpy': 'https://docs.scipy.org/doc/numpy/', - 'scipy': 'https://docs.scipy.org/doc/scipy/reference/', - }, 'backreferences_dir': Path('api') / Path('_as_gen'), - 'subsection_order': gallery_order.sectionorder, - 'within_subsection_order': gallery_order.subsectionorder, - 'remove_config_comments': True, + # Compression is a significant effort that we skip for local and CI builds. + 'compress_images': ('thumbnails', 'images') if is_release_build else (), + 'doc_module': ('matplotlib', 'mpl_toolkits'), + 'examples_dirs': ['../examples', '../tutorials', '../plot_types'], + 'filename_pattern': '^((?!sgskip).)*$', + 'gallery_dirs': ['gallery', 'tutorials', 'plot_types'], + 'image_scrapers': (matplotlib_reduced_latex_scraper, ), + 'image_srcset': ["2x"], + 'junit': '../test-results/sphinx-gallery/junit.xml' if CIRCLECI else '', + 'matplotlib_animations': True, 'min_reported_time': 1, + 'plot_gallery': 'True', # sphinx-gallery/913 + 'reference_url': {'matplotlib': None}, + 'remove_config_comments': True, + 'reset_modules': ( + 'matplotlib', + # clear basic_units module to re-register with unit registry on import + lambda gallery_conf, fname: sys.modules.pop('basic_units', None) + ), + 'subsection_order': gallery_order.sectionorder, 'thumbnail_size': (320, 224), - 'compress_images': ('thumbnails', 'images'), - 'matplotlib_animations': True, + 'within_subsection_order': gallery_order.subsectionorder, + 'capture_repr': (), } -plot_gallery = 'True' +if 'plot_gallery=0' in sys.argv: + # Gallery images are not created. Suppress warnings triggered where other + # parts of the documentation link to these images. + + def gallery_image_warning_filter(record): + msg = record.msg + for gallery_dir in sphinx_gallery_conf['gallery_dirs']: + if msg.startswith(f'image file not readable: {gallery_dir}'): + return False + + if msg == 'Could not obtain image size. :scale: option is ignored.': + return False + + return True + + logger = logging.getLogger('sphinx') + logger.addFilter(gallery_image_warning_filter) + + +mathmpl_fontsize = 11.0 +mathmpl_srcset = ['2x'] + +# Monkey-patching gallery header to include search keywords +gen_rst.EXAMPLE_HEADER = """ +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "{0}" +.. LINE NUMBERS ARE GIVEN BELOW. -# Monkey-patching gallery signature to include search keywords -gen_rst.SPHX_GLR_SIG = """\n .. only:: html - .. rst-class:: sphx-glr-signature + .. meta:: + :keywords: codex + + .. note:: + :class: sphx-glr-download-link-note + + Click :ref:`here ` + to download the full example code{2} + +.. rst-class:: sphx-glr-example-title - Keywords: matplotlib code example, codex, python plot, pyplot - `Gallery generated by Sphinx-Gallery - `_\n""" +.. _sphx_glr_{1}: + +""" # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -191,29 +258,29 @@ def _check_dependencies(): # This is the default encoding, but it doesn't hurt to be explicit source_encoding = "utf-8" -# The master toctree document. -master_doc = 'contents' +# The toplevel toctree document (renamed to root_doc in Sphinx 4.0) +root_doc = master_doc = 'users/index' # General substitutions. try: SHA = subprocess.check_output( ['git', 'describe', '--dirty']).decode('utf-8').strip() -# Catch the case where git is not installed locally, and use the versioneer +# Catch the case where git is not installed locally, and use the setuptools_scm # version number instead except (subprocess.CalledProcessError, FileNotFoundError): SHA = matplotlib.__version__ + html_context = { - 'sha': SHA, - # This will disable any analytics in the HTML templates (currently Google - # Analytics.) - 'include_analytics': False, + "doc_version": SHA, } project = 'Matplotlib' -copyright = ('2002 - 2012 John Hunter, Darren Dale, Eric Firing, ' - 'Michael Droettboom and the Matplotlib development ' - f'team; 2012 - {datetime.now().year} The Matplotlib development team') +copyright = ( + '2002–2012 John Hunter, Darren Dale, Eric Firing, Michael Droettboom ' + 'and the Matplotlib development team; ' + f'2012–{sourceyear} The Matplotlib development team' +) # The default replacements for |version| and |release|, also used in various @@ -227,7 +294,7 @@ def _check_dependencies(): # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. today_fmt = '%B %d, %Y' @@ -235,15 +302,15 @@ def _check_dependencies(): unused_docs = [] # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' @@ -253,28 +320,104 @@ def _check_dependencies(): # Plot directive configuration # ---------------------------- -plot_formats = [('png', 100), ('pdf', 100)] +# For speedup, decide which plot_formats to build based on build targets: +# html only -> png +# latex only -> pdf +# all other cases, including html + latex -> png, pdf +# For simplicity, we assume that the build targets appear in the command line. +# We're falling back on using all formats in case that assumption fails. +formats = {'html': ('png', 100), 'latex': ('pdf', 100)} +plot_formats = [formats[target] for target in ['html', 'latex'] + if target in sys.argv] or list(formats.values()) + # GitHub extension github_project_url = "https://github.com/matplotlib/matplotlib/" + # Options for HTML output # ----------------------- +def add_html_cache_busting(app, pagename, templatename, context, doctree): + """ + Add cache busting query on CSS and JavaScript assets. + + This adds the Matplotlib version as a query to the link reference in the + HTML, if the path is not absolute (i.e., it comes from the `_static` + directory) and doesn't already have a query. + """ + from sphinx.builders.html import Stylesheet, JavaScript + + css_tag = context['css_tag'] + js_tag = context['js_tag'] + + def css_tag_with_cache_busting(css): + if isinstance(css, Stylesheet) and css.filename is not None: + url = urlsplit(css.filename) + if not url.netloc and not url.query: + url = url._replace(query=SHA) + css = Stylesheet(urlunsplit(url), priority=css.priority, + **css.attributes) + return css_tag(css) + + def js_tag_with_cache_busting(js): + if isinstance(js, JavaScript) and js.filename is not None: + url = urlsplit(js.filename) + if not url.netloc and not url.query: + url = url._replace(query=SHA) + js = JavaScript(urlunsplit(url), priority=js.priority, + **js.attributes) + return js_tag(js) + + context['css_tag'] = css_tag_with_cache_busting + context['js_tag'] = js_tag_with_cache_busting + + # The style sheet to use for HTML and HTML Help pages. A file of that name # must exist either in Sphinx' static/ path, or in one of the custom paths # given in html_static_path. -#html_style = 'matplotlib.css' -html_style = f'mpl.css?{SHA}' +html_css_files = [ + "mpl.css", +] + +html_theme = "mpl_sphinx_theme" # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # The name of an image file (within the static path) to place at the top of # the sidebar. -#html_logo = 'logo.png' +html_logo = "_static/logo2.svg" +html_theme_options = { + "navbar_links": "internal", + # collapse_navigation in pydata-sphinx-theme is slow, so skipped for local + # and CI builds https://github.com/pydata/pydata-sphinx-theme/pull/386 + "collapse_navigation": not is_release_build, + "show_prev_next": False, + "switcher": { + # Add a unique query to the switcher.json url. This will be ignored by + # the server, but will be used as part of the key for caching by browsers + # so when we do a new minor release the switcher will update "promptly" on + # the stable and devdocs. + "json_url": f"https://matplotlib.org/devdocs/_static/switcher.json?{SHA}", + "version_match": ( + # The start version to show. This must be in switcher.json. + # We either go to 'stable' or to 'devdocs' + 'stable' if matplotlib.__version_info__.releaselevel == 'final' + else 'devdocs') + }, + "logo": {"link": "index", + "image_light": "images/logo2.svg", + "image_dark": "images/logo_dark.svg"}, + "navbar_end": ["theme-switcher", "version-switcher", "mpl_icon_links"], + "secondary_sidebar_items": "page-toc.html", + "footer_items": ["copyright", "sphinx-version", "doc_version"], +} +include_analytics = is_release_build +if include_analytics: + html_theme_options["analytics"] = {"google_analytics_id": "UA-55954603-1"} # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -285,6 +428,9 @@ def _check_dependencies(): # default is ``".html"``. html_file_suffix = '.html' +# this makes this the canonical link for all the pages on the site... +html_baseurl = 'https://matplotlib.org/stable/' + # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. html_last_updated_fmt = '%b %d, %Y' @@ -293,27 +439,36 @@ def _check_dependencies(): html_index = 'index.html' # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Custom sidebar templates, maps page names to templates. html_sidebars = { - 'index': [ + "index": [ # 'sidebar_announcement.html', - 'sidebar_versions.html', - 'donate_sidebar.html'], - '**': ['localtoc.html', 'pagesource.html'] + "sidebar_versions.html", + "cheatsheet_sidebar.html", + "donate_sidebar.html", + ], + # '**': ['localtoc.html', 'pagesource.html'] } -# If false, no module index is generated. -#html_use_modindex = True -html_domain_indices = ["py-modindex"] +# Copies only relevant code, not the '>>>' prompt +copybutton_prompt_text = r'>>> |\.\.\. ' +copybutton_prompt_is_regexp = True + +# If true, add an index to the HTML documents. +html_use_index = False + +# If true, generate domain-specific indices in addition to the general index. +# For e.g. the Python domain, this is the global module index. +html_domain_index = False # If true, the reST sources are included in the HTML build as _sources/. -#html_copy_source = True +# html_copy_source = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. -html_use_opensearch = 'False' +html_use_opensearch = 'https://matplotlib.org/stable' # Output file base name for HTML help builder. htmlhelp_basename = 'Matplotlibdoc' @@ -330,11 +485,13 @@ def _check_dependencies(): # The paper size ('letter' or 'a4'). latex_paper_size = 'letter' -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, document class [howto/manual]). +# Grouping the document tree into LaTeX files. +# List of tuples: +# (source start file, target name, title, author, +# document class [howto/manual]) latex_documents = [ - ('contents', 'Matplotlib.tex', 'Matplotlib', + (root_doc, 'Matplotlib.tex', 'Matplotlib', 'John Hunter\\and Darren Dale\\and Eric Firing\\and Michael Droettboom' '\\and and the matplotlib development team', 'manual'), ] @@ -364,7 +521,7 @@ def _check_dependencies(): # Sphinx 2.0 adopts GNU FreeFont by default, but it does not have all # the Unicode codepoints needed for the section about Mathtext # "Writing mathematical expressions" -fontpkg = r""" +latex_elements['fontpkg'] = r""" \IfFontExistsTF{XITS}{ \setmainfont{XITS} }{ @@ -404,12 +561,7 @@ def _check_dependencies(): Extension = .otf, ]} """ -latex_elements['fontpkg'] = fontpkg -# Sphinx <1.8.0 or >=2.0.0 does this by default, but the 1.8.x series -# did not for latex_engine = 'xelatex' (as it used Latin Modern font). -# We need this for code-blocks as FreeMono has wide glyphs. -latex_elements['fvset'] = r'\fvset{fontsize=\small}' # Fix fancyhdr complaining about \headheight being too small latex_elements['passoptionstopackages'] = r""" \PassOptionsToPackage{headheight=14pt}{geometry} @@ -417,6 +569,8 @@ def _check_dependencies(): # Additional stuff for the LaTeX preamble. latex_elements['preamble'] = r""" + % Show Parts and Chapters in Table of Contents + \setcounter{tocdepth}{0} % One line per author on title page \DeclareRobustCommand{\and}% {\end{tabular}\kern-\tabcolsep\\\begin{tabular}[t]{c}}% @@ -464,7 +618,7 @@ def _check_dependencies(): autoclass_content = 'both' texinfo_documents = [ - ("contents", 'matplotlib', 'Matplotlib Documentation', + (root_doc, 'matplotlib', 'Matplotlib Documentation', 'John Hunter@*Darren Dale@*Eric Firing@*Michael Droettboom@*' 'The matplotlib development team', 'Matplotlib', "Python plotting package", 'Programming', @@ -475,19 +629,97 @@ def _check_dependencies(): numpydoc_show_class_members = False -html4_writer = True - -inheritance_node_attrs = dict(fontsize=16) +# We want to prevent any size limit, as we'll add scroll bars with CSS. +inheritance_graph_attrs = dict(dpi=100, size='1000.0', splines='polyline') +# Also remove minimum node dimensions, and increase line size a bit. +inheritance_node_attrs = dict(height=0.02, margin=0.055, penwidth=1, + width=0.01) +inheritance_edge_attrs = dict(penwidth=1) graphviz_dot = shutil.which('dot') # Still use PNG until SVG linking is fixed # https://github.com/sphinx-doc/sphinx/issues/3176 # graphviz_output_format = 'svg' +# ----------------------------------------------------------------------------- +# Source code links +# ----------------------------------------------------------------------------- +link_github = True +# You can add build old with link_github = False + +if link_github: + import inspect + from packaging.version import parse + + extensions.append('sphinx.ext.linkcode') + + def linkcode_resolve(domain, info): + """ + Determine the URL corresponding to Python object + """ + if domain != 'py': + return None + + modname = info['module'] + fullname = info['fullname'] + + submod = sys.modules.get(modname) + if submod is None: + return None + + obj = submod + for part in fullname.split('.'): + try: + obj = getattr(obj, part) + except AttributeError: + return None + + if inspect.isfunction(obj): + obj = inspect.unwrap(obj) + try: + fn = inspect.getsourcefile(obj) + except TypeError: + fn = None + if not fn or fn.endswith('__init__.py'): + try: + fn = inspect.getsourcefile(sys.modules[obj.__module__]) + except (TypeError, AttributeError, KeyError): + fn = None + if not fn: + return None + + try: + source, lineno = inspect.getsourcelines(obj) + except (OSError, TypeError): + lineno = None + + linespec = (f"#L{lineno:d}-L{lineno + len(source) - 1:d}" + if lineno else "") + + startdir = Path(matplotlib.__file__).parent.parent + try: + fn = os.path.relpath(fn, start=startdir).replace(os.path.sep, '/') + except ValueError: + return None + + if not fn.startswith(('matplotlib/', 'mpl_toolkits/')): + return None + + version = parse(matplotlib.__version__) + tag = 'main' if version.is_devrelease else f'v{version.public}' + return ("https://github.com/matplotlib/matplotlib/blob" + f"/{tag}/lib/{fn}{linespec}") +else: + extensions.append('sphinx.ext.viewcode') + +# ----------------------------------------------------------------------------- +# Sphinx setup +# ----------------------------------------------------------------------------- def setup(app): - if any(st in version for st in ('post', 'alpha', 'beta')): + if any(st in version for st in ('post', 'dev', 'alpha', 'beta')): bld_type = 'dev' else: bld_type = 'rel' app.add_config_value('releaselevel', bld_type, 'env') + app.connect('html-page-context', add_html_cache_busting, priority=1000) diff --git a/doc/contents.rst b/doc/contents.rst deleted file mode 100644 index 407f81e53b9d..000000000000 --- a/doc/contents.rst +++ /dev/null @@ -1,28 +0,0 @@ - - -Overview -======== - -.. only:: html - - :Release: |version| - :Date: |today| - - Download `PDF `_ - - -.. toctree:: - :maxdepth: 2 - - users/index.rst - faq/index.rst - api/index.rst - resources/index.rst - thirdpartypackages/index.rst - devel/index.rst - -.. only:: html - - * :ref:`genindex` - * :ref:`modindex` - * :ref:`search` diff --git a/doc/devel/MEP/MEP08.rst b/doc/devel/MEP/MEP08.rst index 67ce5d3d76ef..18419ac2bf11 100644 --- a/doc/devel/MEP/MEP08.rst +++ b/doc/devel/MEP/MEP08.rst @@ -9,7 +9,10 @@ Status ====== -**Completed** +**Superseded** + +Current guidelines for style, including usage of pep8 are maintained +in `our pull request guidelines `_. We are currently enforcing a sub-set of pep8 on new code contributions. diff --git a/doc/devel/MEP/MEP11.rst b/doc/devel/MEP/MEP11.rst index 9ddd0109c06b..659b7e101480 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 87489393b2f5..009f013e0be9 100644 --- a/doc/devel/MEP/MEP12.rst +++ b/doc/devel/MEP/MEP12.rst @@ -106,7 +106,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`, @@ -142,8 +142,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/MEP/MEP14.rst b/doc/devel/MEP/MEP14.rst index e3e7127abda9..574c733e10bf 100644 --- a/doc/devel/MEP/MEP14.rst +++ b/doc/devel/MEP/MEP14.rst @@ -78,11 +78,11 @@ number of other projects: - `Microsoft DirectWrite`_ - `Apple Core Text`_ -.. _pango: https://www.pango.org/ -.. _harfbuzz: https://www.freedesktop.org/wiki/Software/HarfBuzz/ +.. _pango: https://pango.gnome.org +.. _harfbuzz: https://github.com/harfbuzz/harfbuzz .. _QtTextLayout: https://doc.qt.io/archives/qt-4.8/qtextlayout.html .. _Microsoft DirectWrite: https://docs.microsoft.com/en-ca/windows/win32/directwrite/introducing-directwrite -.. _Apple Core Text: https://developer.apple.com/library/content/documentation/StringsTextFonts/Conceptual/CoreText_Programming/Overview/Overview.html +.. _Apple Core Text: https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/CoreText_Programming/Overview/Overview.html Of the above options, it should be noted that harfbuzz_ is designed from the start as a cross platform option with minimal dependencies, diff --git a/doc/devel/MEP/MEP15.rst b/doc/devel/MEP/MEP15.rst index dc1802e33b8c..8e2f80707429 100644 --- a/doc/devel/MEP/MEP15.rst +++ b/doc/devel/MEP/MEP15.rst @@ -1,6 +1,6 @@ -========================================================================== - MEP15 - Fix axis autoscaling when limits are specified for one axis only -========================================================================== +========================================================================= + MEP15: Fix axis autoscaling when limits are specified for one axis only +========================================================================= .. contents:: :local: diff --git a/doc/devel/MEP/MEP19.rst b/doc/devel/MEP/MEP19.rst index 3fc276a238ee..fd93ba619aed 100644 --- a/doc/devel/MEP/MEP19.rst +++ b/doc/devel/MEP/MEP19.rst @@ -67,7 +67,7 @@ great!]: **Documentation** -Documentation of master is now built by travis and uploaded to http://matplotlib.org/devdocs/index.html +Documentation of main is now built by travis and uploaded to https://matplotlib.org/devdocs/index.html @NelleV, I believe, generates the docs automatically and posts them on the web to chart MEP10 progress. @@ -122,7 +122,7 @@ This section outlines the requirements that we would like to have. #. Make it easy to test a large but sparse matrix of different versions of matplotlib's dependencies. The matplotlib user survey provides some good data as to where to focus our efforts: - https://docs.google.com/spreadsheet/ccc?key=0AjrPjlTMRTwTdHpQS25pcTZIRWdqX0pNckNSU01sMHc + https://docs.google.com/spreadsheets/d/1jbK0J4cIkyBNncnS-gP7pINSliNy9lI-N4JHwxlNSXE/edit #. Nice to have: A decentralized design so that those with more obscure platforms can publish build results to a central dashboard. diff --git a/doc/devel/MEP/MEP22.rst b/doc/devel/MEP/MEP22.rst index d7e93bb5744d..8f8fe69b41a6 100644 --- a/doc/devel/MEP/MEP22.rst +++ b/doc/devel/MEP/MEP22.rst @@ -13,16 +13,18 @@ Status Branches and Pull requests ========================== -Previous work - * https://github.com/matplotlib/matplotlib/pull/1849 - * https://github.com/matplotlib/matplotlib/pull/2557 - * https://github.com/matplotlib/matplotlib/pull/2465 +Previous work: + +* https://github.com/matplotlib/matplotlib/pull/1849 +* https://github.com/matplotlib/matplotlib/pull/2557 +* https://github.com/matplotlib/matplotlib/pull/2465 Pull Requests: - * Removing the NavigationToolbar classes - https://github.com/matplotlib/matplotlib/pull/2740 **CLOSED** - * Keeping the NavigationToolbar classes https://github.com/matplotlib/matplotlib/pull/2759 **CLOSED** - * Navigation by events: https://github.com/matplotlib/matplotlib/pull/3652 + +* Removing the NavigationToolbar classes + https://github.com/matplotlib/matplotlib/pull/2740 **CLOSED** +* Keeping the NavigationToolbar classes https://github.com/matplotlib/matplotlib/pull/2759 **CLOSED** +* Navigation by events: https://github.com/matplotlib/matplotlib/pull/3652 Abstract ======== @@ -39,7 +41,7 @@ reconfiguration. This approach will make easier to create and share tools among users. In the far future, we can even foresee a kind of Marketplace -for ``Tool``\ s where the most popular can be added into the main +for ``Tool``\s where the most popular can be added into the main distribution. Detailed description @@ -55,18 +57,18 @@ https://github.com/matplotlib/matplotlib/issues/2699 The proposed solution is to take the actions out of the ``Toolbar`` and the shortcuts out of the ``Canvas``. The actions and shortcuts will be in the form -of ``Tool``\ s. +of ``Tool``\s. A new class ``Navigation`` will be the bridge between the events from the ``Canvas`` and ``Toolbar`` and redirect them to the appropriate ``Tool``. At the end the user interaction will be divided into three classes: - * NavigationBase: This class is instantiated for each FigureManager - and connect the all user interactions with the Tools - * ToolbarBase: This existing class is relegated only as a GUI access - to Tools. - * ToolBase: Is the basic definition of Tools. +* NavigationBase: This class is instantiated for each FigureManager + and connect the all user interactions with the Tools +* ToolbarBase: This existing class is relegated only as a GUI access + to Tools. +* ToolBase: Is the basic definition of Tools. Implementation @@ -80,37 +82,44 @@ present in the Toolbar as ``Quit``. The `.ToolBase` has the following class attributes for configuration at definition time - * keymap = None: Key(s) to be used to trigger the tool - * description = '': Small description of the tool - * image = None: Image that is used in the toolbar +* keymap = None: Key(s) to be used to trigger the tool +* description = '': Small description of the tool +* image = None: Image that is used in the toolbar The following instance attributes are set at instantiation: - * name - * navigation - -**Methods** - * trigger(self, event): This is the main method of the Tool, it is called when the Tool is triggered by: - * Toolbar button click - * keypress associated with the Tool Keymap - * Call to navigation.trigger_tool(name) - * set_figure(self, figure): Set the figure and navigation attributes - * ``destroy(self, *args)``: Destroy the ``Tool`` graphical interface (if - exists) - -**Available Tools** - * ToolQuit - * ToolEnableAllNavigation - * ToolEnableNavigation - * ToolToggleGrid - * ToolToggleFullScreen - * ToolToggleYScale - * ToolToggleXScale - * ToolHome - * ToolBack - * ToolForward - * SaveFigureBase - * ConfigureSubplotsBase +* name +* navigation + +Methods +~~~~~~~ + +* ``trigger(self, event)``: This is the main method of the Tool, it is called + when the Tool is triggered by: + + * Toolbar button click + * keypress associated with the Tool Keymap + * Call to navigation.trigger_tool(name) + +* ``set_figure(self, figure)``: Set the figure and navigation attributes +* ``destroy(self, *args)``: Destroy the ``Tool`` graphical interface (if + exists) + +Available Tools +~~~~~~~~~~~~~~~ + +* ToolQuit +* ToolEnableAllNavigation +* ToolEnableNavigation +* ToolToggleGrid +* ToolToggleFullScreen +* ToolToggleYScale +* ToolToggleXScale +* ToolHome +* ToolBack +* ToolForward +* SaveFigureBase +* ConfigureSubplotsBase ToolToggleBase(ToolBase) ------------------------ @@ -118,58 +127,63 @@ ToolToggleBase(ToolBase) The `.ToolToggleBase` has the following class attributes for configuration at definition time - * radio_group = None: Attribute to group 'radio' like tools (mutually - exclusive) - * cursor = None: Cursor to use when the tool is active +* radio_group = None: Attribute to group 'radio' like tools (mutually + exclusive) +* cursor = None: Cursor to use when the tool is active The **Toggleable** Tools, can capture keypress, mouse moves, and mouse button press -It defines the following methods - * enable(self, event): Called by `.ToolToggleBase.trigger` method - * disable(self, event): Called when the tool is untoggled - * toggled : **Property** True or False +Methods +~~~~~~~ -**Available Tools** - * ToolZoom - * ToolPan +* ``enable(self, event)``: Called by `.ToolToggleBase.trigger` method +* ``disable(self, event)``: Called when the tool is untoggled +* ``toggled``: **Property** True or False -NavigationBase --------------- +Available Tools +~~~~~~~~~~~~~~~ -Defines the following attributes - * canvas: - * keypresslock: Lock to know if the ``canvas`` ``key_press_event`` is - available and process it - * messagelock: Lock to know if the message is available to write - -Public methods for **User use**: - * nav_connect(self, s, func): Connect to to navigation for events - * nav_disconnect(self, cid): Disconnect from navigation event - * message_event(self, message, sender=None): Emit a - tool_message_event event - * active_toggle(self): **Property** The currently toggled tools or - None - * get_tool_keymap(self, name): Return a list of keys that are - associated with the tool - * set_tool_keymap(self, name, ``*keys``): Set the keys for the given tool - * remove_tool(self, name): Removes tool from the navigation control. - * add_tools(self, tools): Add multiple tools to ``Navigation`` - * add_tool(self, name, tool, group=None, position=None): Add a tool - to the ``Navigation`` - * tool_trigger_event(self, name, sender=None, canvasevent=None, - data=None): Trigger a tool and fire the event - - * tools(self) **Property**: Return a dict with available tools with - corresponding keymaps, descriptions and objects - * get_tool(self, name): Return the tool object +* ToolZoom +* ToolPan +NavigationBase +-------------- +Defines the following attributes: + +* canvas: +* keypresslock: Lock to know if the ``canvas`` ``key_press_event`` is + available and process it +* messagelock: Lock to know if the message is available to write + +Methods (intended for the end user) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* ``nav_connect(self, s, func)``: Connect to navigation for events +* ``nav_disconnect(self, cid)``: Disconnect from navigation event +* ``message_event(self, message, sender=None)``: Emit a + tool_message_event event +* ``active_toggle(self)``: **Property** The currently toggled tools or + None +* ``get_tool_keymap(self, name)``: Return a list of keys that are + associated with the tool +* ``set_tool_keymap(self, name, ``*keys``)``: Set the keys for the given tool +* ``remove_tool(self, name)``: Removes tool from the navigation control. +* ``add_tools(self, tools)``: Add multiple tools to ``Navigation`` +* ``add_tool(self, name, tool, group=None, position=None)``: Add a tool + to the ``Navigation`` +* ``tool_trigger_event(self, name, sender=None, canvasevent=None, + data=None)``: Trigger a tool and fire the event +* ``tools``: **Property** A dict with available tools with + corresponding keymaps, descriptions and objects +* ``get_tool(self, name)``: Return the tool object ToolbarBase ----------- -Methods for **Backend implementation** +Methods (for backend implementation) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * ``add_toolitem(self, name, group, position, image, description, toggle)``: Add a toolitem to the toolbar. This method is a callback from diff --git a/doc/devel/MEP/MEP24.rst b/doc/devel/MEP/MEP24.rst index 53f0609f3e9b..b0620ce3dc8f 100644 --- a/doc/devel/MEP/MEP24.rst +++ b/doc/devel/MEP/MEP24.rst @@ -1,5 +1,5 @@ ======================================= - MEP24: negative radius in polar plots + MEP24: Negative radius in polar plots ======================================= .. contents:: diff --git a/doc/devel/MEP/MEP26.rst b/doc/devel/MEP/MEP26.rst index 929393a683d2..9d3af8f8c703 100644 --- a/doc/devel/MEP/MEP26.rst +++ b/doc/devel/MEP/MEP26.rst @@ -34,7 +34,7 @@ Detailed description ==================== Currently, the look and appearance of existing artist objects (figure, -axes, Line2D etc...) can only be updated via ``set_`` and ``get_`` methods +axes, Line2D, etc.) can only be updated via ``set_`` and ``get_`` methods on the artist object, which is quite laborious, especially if no reference to the artist(s) has been stored. The new style sheets introduced in 1.4 allow styling before a plot is created, but do not @@ -51,7 +51,7 @@ of primitives. The new methodology would require development of a number of steps: - A new stylesheet syntax (likely based on CSS) to allow selection of - artists by type, class, id etc... + artists by type, class, id, etc. - A mechanism by which to parse a stylesheet into a tree - A mechanism by which to translate the parse-tree into something which can be used to update the properties of relevant diff --git a/doc/devel/MEP/MEP27.rst b/doc/devel/MEP/MEP27.rst index 13ed37cb73cb..81eca8f9c53d 100644 --- a/doc/devel/MEP/MEP27.rst +++ b/doc/devel/MEP/MEP27.rst @@ -1,5 +1,5 @@ ====================================== - MEP27: decouple pyplot from backends + MEP27: Decouple pyplot from backends ====================================== .. contents:: @@ -13,9 +13,11 @@ Status Branches and Pull requests ========================== Main PR (including GTK3): + + https://github.com/matplotlib/matplotlib/pull/4143 Backend specific branch diffs: + + https://github.com/OceanWolf/matplotlib/compare/backend-refactor...OceanWolf:backend-refactor-tkagg + https://github.com/OceanWolf/matplotlib/compare/backend-refactor...OceanWolf:backend-refactor-qt + https://github.com/OceanWolf/matplotlib/compare/backend-refactor...backend-refactor-wx @@ -49,15 +51,14 @@ Two main places for generic code appear in the classes derived from 1. ``FigureManagerBase`` has **three** jobs at the moment: - 1. The documentation describes it as a *``Helper class for pyplot - mode, wraps everything up into a neat bundle''* + 1. The documentation describes it as a *Helper class for pyplot + mode, wraps everything up into a neat bundle* 2. But it doesn't just wrap the canvas and toolbar, it also does all of the windowing tasks itself. The conflation of these two - tasks gets seen the best in the following line: ```python - self.set_window_title("Figure %d" % num) ``` This combines + tasks gets seen the best in the following line: + ``self.set_window_title("Figure %d" % num)`` This combines backend specific code ``self.set_window_title(title)`` with matplotlib generic code ``title = "Figure %d" % num``. - 3. Currently the backend specific subclass of ``FigureManager`` decides when to end the mainloop. This also seems very wrong as the figure should have no control over the other figures. @@ -95,7 +96,7 @@ The description of this MEP gives us most of the solution: 1. This allows us to break up the conversion of backends into separate PRs as we can keep the existing ``FigureManagerBase`` class and its dependencies intact. - 2. and this also anticipates MEP22 where the new + 2. And this also anticipates MEP22 where the new ``NavigationBase`` has morphed into a backend independent ``ToolManager``. diff --git a/doc/devel/MEP/MEP28.rst b/doc/devel/MEP/MEP28.rst index 631be1e2b548..07b83c17800e 100644 --- a/doc/devel/MEP/MEP28.rst +++ b/doc/devel/MEP/MEP28.rst @@ -46,7 +46,7 @@ Detailed description Currently, the ``Axes.boxplot`` method accepts parameters that allow the users to specify medians and confidence intervals for each box that -will be drawn in the plot. These were provided so that avdanced users +will be drawn in the plot. These were provided so that advanced users could provide statistics computed in a different fashion that the simple method provided by matplotlib. However, handling this input requires complex logic to make sure that the forms of the data structure match what diff --git a/doc/devel/MEP/MEP29.rst b/doc/devel/MEP/MEP29.rst index ae7eae9fe43e..d937889d55de 100644 --- a/doc/devel/MEP/MEP29.rst +++ b/doc/devel/MEP/MEP29.rst @@ -34,7 +34,7 @@ one has to look at the gallery where one such example is provided: This example takes a list of strings as well as a list of colors which makes it cumbersome to use. An alternative would be to use a restricted set of pango_-like markup and to interpret this markup. -.. _pango: https://developer.gnome.org/pygtk/stable/pango-markup-language.html +.. _pango: https://docs.gtk.org/Pango/pango_markup.html#pango-markup Some markup examples:: @@ -54,7 +54,7 @@ Improvements to use the html.parser from the standard library. * Computation of text fragment positions could benefit from the OffsetFrom - class. See for example item 5 in `Using Complex Coordinates with Annotations `_ + class. See for example item 5 in `Using Complex Coordinates with Annotations `_ Problems -------- diff --git a/doc/devel/MEP/template.rst b/doc/devel/MEP/template.rst index 81191fc44eeb..00bdbc87a95e 100644 --- a/doc/devel/MEP/template.rst +++ b/doc/devel/MEP/template.rst @@ -24,7 +24,7 @@ MEPs go through a number of phases in their lifetime: - **Progress**: Consensus was reached and implementation work has begun. -- **Completed**: The implementation has been merged into master. +- **Completed**: The implementation has been merged into main. - **Superseded**: This MEP has been abandoned in favor of another approach. diff --git a/doc/devel/README.txt b/doc/devel/README.txt index 3fc074035aff..d7636cd4c37c 100644 --- a/doc/devel/README.txt +++ b/doc/devel/README.txt @@ -2,8 +2,8 @@ All documentation in the gitwash directory are automatically generated by runnin script in the project's root directory using the following parameters: python gitwash_dumper.py doc/devel Matplotlib --repo-name=matplotlib --github-user=matplotlib \ - --project-url=http://matplotlib.org \ + --project-url=https://matplotlib.org \ --project-ml-url=https://mail.python.org/mailman/listinfo/matplotlib-devel The script is hosted at https://raw.githubusercontent.com/matthew-brett/gitwash/master/gitwash_dumper.py. -For more information please visit https://github.com/matthew-brett/gitwash \ No newline at end of file +For more information please visit https://github.com/matthew-brett/gitwash diff --git a/doc/devel/add_new_projection.rst b/doc/devel/add_new_projection.rst deleted file mode 100644 index 4eb2f80be490..000000000000 --- a/doc/devel/add_new_projection.rst +++ /dev/null @@ -1,129 +0,0 @@ -.. _adding-new-scales: - -========================================================= -Developer's guide for creating scales and transformations -========================================================= - -.. ::author Michael Droettboom - -Matplotlib supports the addition of custom procedures that transform -the data before it is displayed. - -There is an important distinction between two kinds of -transformations. Separable transformations, working on a single -dimension, are called "scales", and non-separable transformations, -that handle data in two or more dimensions at a time, are called -"projections". - -From the user's perspective, the scale of a plot can be set with -`.Axes.set_xscale` and `.Axes.set_yscale`. Projections can be chosen using the -*projection* keyword argument of functions that create Axes, such as -`.pyplot.subplot` or `.pyplot.axes`, e.g. :: - - plt.subplot(projection="custom") - -This document is intended for developers and advanced users who need -to create new scales and projections for Matplotlib. The necessary -code for scales and projections can be included anywhere: directly -within a plot script, in third-party code, or in the Matplotlib source -tree itself. - -.. _creating-new-scale: - -Creating a new scale -==================== - -Adding a new scale consists of defining a subclass of -:class:`matplotlib.scale.ScaleBase`, that includes the following -elements: - -- A transformation from data coordinates into display coordinates. - -- An inverse of that transformation. This is used, for example, to - convert mouse positions from screen space back into data space. - -- A function to limit the range of the axis to acceptable values - (``limit_range_for_scale()``). A log scale, for instance, would - prevent the range from including values less than or equal to zero. - -- Locators (major and minor) that determine where to place ticks in - the plot, and optionally, how to adjust the limits of the plot to - some "good" values. Unlike ``limit_range_for_scale()``, which is - always enforced, the range setting here is only used when - automatically setting the range of the plot. - -- Formatters (major and minor) that specify how the tick labels - should be drawn. - -Once the class is defined, it must be registered with Matplotlib so -that the user can select it. - -A full-fledged and heavily annotated example is in -:doc:`/gallery/scales/custom_scale`. There are also some classes -in :mod:`matplotlib.scale` that may be used as starting points. - - -.. _creating-new-projection: - -Creating a new projection -========================= - -Adding a new projection consists of defining a projection axes which -subclasses :class:`matplotlib.axes.Axes` and includes the following -elements: - -- A transformation from data coordinates into display coordinates. - -- An inverse of that transformation. This is used, for example, to - convert mouse positions from screen space back into data space. - -- Transformations for the gridlines, ticks and ticklabels. Custom - projections will often need to place these elements in special - locations, and Matplotlib has a facility to help with doing so. - -- Setting up default values (overriding :meth:`~matplotlib.axes.Axes.cla`), - since the defaults for a rectilinear axes may not be appropriate. - -- Defining the shape of the axes, for example, an elliptical axes, that will be - used to draw the background of the plot and for clipping any data elements. - -- Defining custom locators and formatters for the projection. For - example, in a geographic projection, it may be more convenient to - display the grid in degrees, even if the data is in radians. - -- Set up interactive panning and zooming. This is left as an - "advanced" feature left to the reader, but there is an example of - this for polar plots in :mod:`matplotlib.projections.polar`. - -- Any additional methods for additional convenience or features. - -Once the projection axes is defined, it can be used in one of two ways: - -- By defining the class attribute ``name``, the projection axes can be - registered with :func:`matplotlib.projections.register_projection` - and subsequently simply invoked by name:: - - plt.axes(projection='my_proj_name') - -- For more complex, parameterisable projections, a generic "projection" object - may be defined which includes the method ``_as_mpl_axes``. ``_as_mpl_axes`` - should take no arguments and return the projection's axes subclass and a - dictionary of additional arguments to pass to the subclass' ``__init__`` - method. Subsequently a parameterised projection can be initialised with:: - - plt.axes(projection=MyProjection(param1=param1_value)) - - where MyProjection is an object which implements a ``_as_mpl_axes`` method. - - -A full-fledged and heavily annotated example is in -:doc:`/gallery/misc/custom_projection`. The polar plot -functionality in :mod:`matplotlib.projections.polar` may also be of -interest. - -API documentation -================= - -* :mod:`matplotlib.scale` -* :mod:`matplotlib.projections` -* :mod:`matplotlib.projections.polar` diff --git a/doc/devel/coding_guide.rst b/doc/devel/coding_guide.rst index 053527477cf9..d584a1c986e7 100644 --- a/doc/devel/coding_guide.rst +++ b/doc/devel/coding_guide.rst @@ -13,18 +13,103 @@ Pull request guidelines *********************** -Pull requests (PRs) are the mechanism for contributing to Matplotlibs code and -documentation. +`Pull requests (PRs) on GitHub +`__ +are the mechanism for contributing to Matplotlib's code and documentation. + +It is recommended to check that your contribution complies with the following +rules before submitting a pull request: + +* If your pull request addresses an issue, please use the title to describe the + issue (e.g. "Add ability to plot timedeltas") and mention the issue number + in the pull request description to ensure that a link is created to the + original issue (e.g. "Closes #8869" or "Fixes #8869"). This will ensure the + original issue mentioned is automatically closed when your PR is merged. See + `the GitHub documentation + `__ + for more details. + +* Formatting should follow the recommendations of PEP8_, as enforced by + flake8_. Matplotlib modifies PEP8 to extend the maximum line length to 88 + characters. You can check flake8 compliance from the command line with :: + + python -m pip install flake8 + flake8 /path/to/module.py + + or your editor may provide integration with it. Note that Matplotlib + intentionally does not use the black_ auto-formatter (1__), in particular due + to its unability to understand the semantics of mathematical expressions + (2__, 3__). + + .. _PEP8: https://www.python.org/dev/peps/pep-0008/ + .. _flake8: https://flake8.pycqa.org/ + .. _black: https://black.readthedocs.io/ + .. __: https://github.com/matplotlib/matplotlib/issues/18796 + .. __: https://github.com/psf/black/issues/148 + .. __: https://github.com/psf/black/issues/1984 + +* All public methods should have informative docstrings with sample usage when + appropriate. Use the :ref:`docstring standards `. + +* For high-level plotting functions, consider adding a simple example either in + the ``Example`` section of the docstring or the + :ref:`examples gallery `. + +* Changes (both new features and bugfixes) should have good test coverage. See + :ref:`testing` for more details. + +* Import the following modules using the standard scipy conventions:: + + import numpy as np + import numpy.ma as ma + import matplotlib as mpl + import matplotlib.pyplot as plt + import matplotlib.cbook as cbook + import matplotlib.patches as mpatches + + In general, Matplotlib modules should **not** import `.rcParams` using ``from + matplotlib import rcParams``, but rather access it as ``mpl.rcParams``. This + is because some modules are imported very early, before the `.rcParams` + singleton is constructed. + +* If your change is a major new feature, add an entry to the ``What's new`` + section by adding a new file in ``doc/users/next_whats_new`` (see + :file:`doc/users/next_whats_new/README.rst` for more information). + +* If you change the API in a backward-incompatible way, please document it in + :file:`doc/api/next_api_changes/behavior`, by adding a new file with the + naming convention ``99999-ABC.rst`` where the pull request number is followed + by the contributor's initials. (see :file:`doc/api/api_changes.rst` for more + information) + +* See below for additional points about :ref:`keyword-argument-processing`, if + applicable for your pull request. + +.. note:: + + The current state of the Matplotlib code base is not compliant with all + of these guidelines, but we expect that enforcing these constraints on all + new contributions will move the overall code base quality in the right + direction. + + +.. seealso:: + + * :ref:`coding_guidelines` + * :ref:`testing` + * :ref:`documenting-matplotlib` -Summary for PR authors -====================== + + +Summary for pull request authors +================================ .. note:: * We value contributions from people with all levels of experience. In particular if this is your first PR not everything has to be perfect. We'll guide you through the PR process. - * Nevertheless, try to follow the guidelines below as well as you can to + * Nevertheless, please try to follow the guidelines below as well as you can to help make the PR process quick and smooth. * Be patient with reviewers. We try our best to respond quickly, but we have limited bandwidth. If there is no feedback within a couple of days, @@ -34,26 +119,28 @@ When making a PR, pay attention to: .. rst-class:: checklist -* :ref:`Target the master branch `. +* :ref:`Target the main branch `. * Adhere to the :ref:`coding_guidelines`. * Update the :ref:`documentation ` if necessary. * Aim at making the PR as "ready-to-go" as you can. This helps to speed up the review process. * It is ok to open incomplete or work-in-progress PRs if you need help or feedback from the developers. You may mark these as - `draft pull requests `_ + `draft pull requests `_ on GitHub. * When updating your PR, instead of adding new commits to fix something, please consider amending your initial commit(s) to keep the history clean. - You can achieve this using:: + You can achieve this by using + + .. code-block:: bash - git commit --amend --no-edit - git push [your-remote-repo] [your-branch] --force-with-lease + git commit --amend --no-edit + git push [your-remote-repo] [your-branch] --force-with-lease See also :ref:`contributing` for how to make a PR. -Summary for PR reviewers -======================== +Summary for pull request reviewers +================================== .. note:: @@ -70,22 +157,27 @@ Content topics: * Does the PR conform with the :ref:`coding_guidelines`? * Is the :ref:`documentation ` (docstrings, examples, what's new, API changes) updated? +* Is the change purely stylistic? Generally, such changes are discouraged when + not part of other non-stylistic work because it obscures the git history of + functional changes to the code. Reflowing a method or docstring as part of a + larger refactor/rewrite is acceptable. + Organizational topics: .. rst-class:: checklist * Make sure all :ref:`automated tests ` pass. -* The PR should :ref:`target the master branch `. +* The PR should :ref:`target the main branch `. * Tag with descriptive :ref:`labels `. * Set the :ref:`milestone `. * Keep an eye on the :ref:`number of commits `. -* Approve if all of the above topics handled. +* Approve if all of the above topics are handled. * :ref:`Merge ` if a sufficient number of approvals is reached. .. _pr-guidelines-details: -Detailed Guidelines +Detailed guidelines =================== .. _pr-documentation: @@ -106,13 +198,60 @@ Documentation * See :ref:`documenting-matplotlib` for our documentation style guide. -* If your change is a major new feature, add an entry to - :file:`doc/users/whats_new.rst`. +.. _release_notes: + +New features and API changes +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +When adding a major new feature or changing the API in a backward incompatible +way, please document it by including a versioning directive in the docstring +and adding an entry to the folder for either the what's new or API change notes. + ++-------------------+-----------------------------+----------------------------------+ +| for this addition | include this directive | create entry in this folder | ++===================+=============================+==================================+ +| new feature | ``.. versionadded:: 3.N`` | :file:`doc/users/next_whats_new/`| ++-------------------+-----------------------------+----------------------------------+ +| API change | ``.. versionchanged:: 3.N`` | :file:`doc/api/next_api_changes/`| +| | | | +| | | probably in ``behavior/`` | ++-------------------+-----------------------------+----------------------------------+ -* If you change the API in a backward-incompatible way, please - document it by adding a file in the relevant subdirectory of - :file:`doc/api/next_api_changes/`, probably in the ``behavior/`` - subdirectory. +The directives should be placed at the end of a description block. For example:: + + class Foo: + """ + This is the summary. + + Followed by a longer description block. + + Consisting of multiple lines and paragraphs. + + .. versionadded:: 3.5 + + Parameters + ---------- + a : int + The first parameter. + b: bool, default: False + This was added later. + + .. versionadded:: 3.6 + """ + + def set_b(b): + """ + Set b. + + .. versionadded:: 3.6 + + Parameters + ---------- + b: bool + +For classes and functions, the directive should be placed before the +*Parameters* section. For parameters, the directive should be placed at the +end of the parameter description. The patch release version is omitted and +the directive should not be added to entire modules. .. _pr-labels: @@ -121,6 +260,8 @@ Labels * If you have the rights to set labels, tag the PR with descriptive labels. See the `list of labels `__. +* If the PR makes changes to the wheel building Action, add the + "Run cibuildwheel" label to enable testing wheels. .. _pr-milestones: @@ -130,20 +271,20 @@ Milestones * Set the milestone according to these rules: * *New features and API changes* are milestoned for the next minor release - ``v3.X.0``. + ``v3.N.0``. - * *Bugfixes and docstring changes* are milestoned for the next patch - release ``v3.X.Y`` + * *Bugfixes, tests for released code, and docstring changes* are milestoned + for the next patch release ``v3.N.M``. * *Documentation changes* (all .rst files and examples) are milestoned - ``v3.X-doc`` + ``v3.N-doc``. If multiple rules apply, choose the first matching from the above list. Setting a milestone does not imply or guarantee that a PR will be merged for that release, but if it were to be merged what release it would be in. - All of these PRs should target the master branch. The milestone tag triggers + All of these PRs should target the main branch. The milestone tag triggers an :ref:`automatic backport ` for milestones which have a corresponding branch. @@ -159,7 +300,7 @@ Merging core developers (those with commit rights) should review all pull requests. If you are the first to review a PR and approve of the changes use the GitHub `'approve review' - `__ + `__ tool to mark it as such. If you are a subsequent reviewer please approve the review and if you think no more review is needed, merge the PR. @@ -219,6 +360,18 @@ will run on all supported platforms and versions of Python. .. _tox: https://tox.readthedocs.io/ +* If you know your changes do not need to be tested (this is very rare!), all + CIs can be skipped for a given commit by including ``[ci skip]`` or + ``[skip ci]`` in the commit message. If you know only a subset of CIs need + to be run (e.g., if you are changing some block of plain reStructuredText and + want only CircleCI to run to render the result), individual CIs can be + skipped on individual commits as well by using the following substrings + in commit messages: + + - GitHub Actions: ``[skip actions]`` + - AppVeyor: ``[skip appveyor]`` (must be in the first line of the commit) + - Azure Pipelines: ``[skip azp]`` + - CircleCI: ``[skip circle]`` .. _pr-squashing: @@ -246,20 +399,20 @@ Number of commits and squashing .. _branches_and_backports: -Branches and Backports +Branches and backports ====================== Current branches ---------------- The current active branches are -*master* +*main* The current development version. Future minor releases (*v3.N.0*) will be - branched from this. Supports Python 3.7+. + branched from this. *v3.N.x* Maintenance branch for Matplotlib 3.N. Future patch releases will be - branched from this. Supports Python 3.6+. + branched from this. *v3.N.M-doc* Documentation for the current release. On a patch release, this will be @@ -271,7 +424,7 @@ The current active branches are Branch selection for pull requests ---------------------------------- -Generally, all pull requests should target the master branch. +Generally, all pull requests should target the main branch. Other branches are fed through :ref:`automatic ` or :ref:`manual `. Directly @@ -330,7 +483,7 @@ When doing backports please copy the form used by meeseekdev, conflicts make note of them and how you resolved them in the commit message. -We do a backport from master to v2.2.x assuming: +We do a backport from main to v2.2.x assuming: * ``matplotlib`` is a read-only remote branch of the matplotlib/matplotlib repo @@ -340,22 +493,26 @@ the merge notification) or through the git CLI tools. Assuming that you already have a local branch ``v2.2.x`` (if not, then ``git checkout -b v2.2.x``), and that your remote pointing to -``https://github.com/matplotlib/matplotlib`` is called ``upstream``:: +``https://github.com/matplotlib/matplotlib`` is called ``upstream``: + +.. code-block:: bash - git fetch upstream - git checkout v2.2.x # or include -b if you don't already have this. - git reset --hard upstream/v2.2.x - git cherry-pick -m 1 TARGET_SHA - # resolve conflicts and commit if required + git fetch upstream + git checkout v2.2.x # or include -b if you don't already have this. + git reset --hard upstream/v2.2.x + git cherry-pick -m 1 TARGET_SHA + # resolve conflicts and commit if required Files with conflicts can be listed by ``git status``, and will have to be fixed by hand (search on ``>>>>>``). Once the conflict is resolved, you will have to re-add the file(s) to the branch -and then continue the cherry pick:: +and then continue the cherry pick: + +.. code-block:: bash - git add lib/matplotlib/conflicted_file.py - git add lib/matplotlib/conflicted_file2.py - git cherry-pick --continue + git add lib/matplotlib/conflicted_file.py + git add lib/matplotlib/conflicted_file2.py + git cherry-pick --continue Use your discretion to push directly to upstream or to open a PR; be -sure to push or PR against the ``v2.2.x`` upstream branch, not ``master``! +sure to push or PR against the ``v2.2.x`` upstream branch, not ``main``! diff --git a/doc/devel/color_changes.rst b/doc/devel/color_changes.rst index d36a873c7225..f7646ded7c14 100644 --- a/doc/devel/color_changes.rst +++ b/doc/devel/color_changes.rst @@ -1,10 +1,11 @@ .. _color_changes: ********************* -Default Color changes +Default color changes ********************* -As discussed at length elsewhere [insert links], ``jet`` is an +As discussed at length `elsewhere `__ , +``jet`` is an empirically bad colormap and should not be the default colormap. Due to the position that changing the appearance of the plot breaks backward compatibility, this change has been put off for far longer @@ -14,7 +15,7 @@ plots and to adopt a different colormap for filled plots (``imshow``, ``pcolor``, ``contourf``, etc) and for scatter like plots. -Default Heat Map Colormap +Default heat map colormap ------------------------- The choice of a new colormap is fertile ground to bike-shedding ("No, @@ -57,10 +58,10 @@ Nathaniel Smith) to evaluate proposed colormaps. Example script ++++++++++++++ -Proposed Colormaps +Proposed colormaps ++++++++++++++++++ -Default Scatter Colormap +Default scatter colormap ------------------------ For heat-map like applications it can be desirable to cover as much of @@ -99,10 +100,10 @@ Example script qd = np.random.rand(np.prod(X.shape)) Q.set_array(qd) -Proposed Colormaps +Proposed colormaps ++++++++++++++++++ -Color Cycle / Qualitative colormap +Color cycle / qualitative colormap ----------------------------------- When plotting lines it is frequently desirable to plot multiple lines @@ -131,5 +132,5 @@ Example script ax2.set_xlim(0, 2*np.pi) -Proposed Color cycle +Proposed color cycle ++++++++++++++++++++ diff --git a/doc/devel/contributing.rst b/doc/devel/contributing.rst index e0c0e9583bb9..6026c7b4d443 100644 --- a/doc/devel/contributing.rst +++ b/doc/devel/contributing.rst @@ -4,41 +4,100 @@ Contributing ============ -This project is a community effort, and everyone is welcome to -contribute. Everyone within the community -is expected to abide by our -`code of conduct `_. +You've discovered a bug or something else you want to change +in Matplotlib — excellent! -The project is hosted on -https://github.com/matplotlib/matplotlib +You've worked out a way to fix it — even better! -Contributor Incubator -===================== - -If you are interested in becoming a regular contributor to Matplotlib, but -don't know where to start or feel insecure about it, you can join our non-public -communication channel for new contributors. To do so, please go to `gitter -`_ and ask to be added to '#incubator'. -This is a private gitter room moderated by core Matplotlib developers where you can -get guidance and support for your first few PRs. This is a place you can ask questions -about anything: how to use git, github, how our PR review process works, technical questions -about the code, what makes for good documentation or a blog post, how to get involved involved -in community work, or get "pre-review" on your PR. +You want to tell us about it — best of all! +This project is a community effort, and everyone is welcome to +contribute. Everyone within the community +is expected to abide by our +`code of conduct `_. + +Below, you can find a number of ways to contribute, and how to connect with the +Matplotlib community. + +Get Connected +============= + +Do I really have something to contribute to Matplotlib? +------------------------------------------------------- + +100% yes. There are so many ways to contribute to our community. + +When in doubt, we recommend going together! Get connected with our community of +active contributors, many of whom felt just like you when they started out and +are happy to welcome you and support you as you get to know how we work, and +where things are. Take a look at the next sections to learn more. + +Contributor incubator +--------------------- + +The incubator is our non-public communication channel for new contributors. It +is a private gitter room moderated by core Matplotlib developers where you can +get guidance and support for your first few PRs. It's a place you can ask +questions about anything: how to use git, GitHub, how our PR review process +works, technical questions about the code, what makes for good documentation +or a blog post, how to get involved in community work, or get +"pre-review" on your PR. + +To join, please go to our public `gitter +`_ community channel, and ask to be +added to '#incubator'. One of our core developers will see your message and will +add you. + +New Contributors meeting +------------------------ + +Once a month, we host a meeting to discuss topics that interest new +contributors. Anyone can attend, present, or sit in and listen to the call. +Among our attendees are fellow new contributors, as well as maintainers, and +veteran contributors, who are keen to support onboarding of new folks and +share their experience. You can find our community calendar link at the +`Scientific Python website `_, and +you can browse previous meeting notes on `GitHub +`_. +We recommend joining the meeting to clarify any doubts, or lingering +questions you might have, and to get to know a few of the people behind the +GitHub handles 😉. You can reach out to @noatamir on `gitter +`_ for any clarifications or +suggestions. We <3 feedback! .. _new_contributors: -Issues for New Contributors +Issues for new contributors --------------------------- While any contributions are welcome, we have marked some issues as -particularly suited for new contributors by the label -`good first issue `_ -These are well documented issues, that do not require a deep understanding of -the internals of Matplotlib. The issues may additionally be tagged with a -difficulty. ``Difficulty: Easy`` is suited for people with little Python experience. -``Difficulty: Medium`` and ``Difficulty: Hard`` are not trivial to solve and -require more thought and programming experience. +particularly suited for new contributors by the label `good first issue +`_. These +are well documented issues, that do not require a deep understanding of the +internals of Matplotlib. The issues may additionally be tagged with a +difficulty. ``Difficulty: Easy`` is suited for people with little Python +experience. ``Difficulty: Medium`` and ``Difficulty: Hard`` require more +programming experience. This could be for a variety of reasons, among them, +though not necessarily all at the same time: + +- The issue is in areas of the code base which have more interdependencies, + or legacy code. +- It has less clearly defined tasks, which require some independent + exploration, making suggestions, or follow-up discussions to clarify a good + path to resolve the issue. +- It involves Python features such as decorators and context managers, which + have subtleties due to our implementation decisions. + +In general, the Matplotlib project does not assign issues. Issues are +"assigned" or "claimed" by opening a PR; there is no other assignment +mechanism. If you have opened such a PR, please comment on the issue thread to +avoid duplication of work. Please check if there is an existing PR for the +issue you are addressing. If there is, try to work with the author by +submitting reviews of their code or commenting on the PR rather than opening +a new PR; duplicate PRs are subject to being closed. However, if the existing +PR is an outline, unlikely to work, or stalled, and the original author is +unresponsive, feel free to open a new PR referencing the old one. .. _submitting-a-bug-report: @@ -73,11 +132,13 @@ If you are reporting a bug, please do your best to include the following: >>> platform.python_version() '3.9.2' -We have preloaded the issue creation page with a Markdown template that you can +We have preloaded the issue creation page with a Markdown form that you can use to organize this information. Thank you for your help in keeping bug reports complete, targeted and descriptive. +.. _request-a-new-feature: + Requesting a new feature ======================== @@ -117,13 +178,13 @@ A brief overview is: git clone https://github.com//matplotlib.git 4. Enter the directory and install the local version of Matplotlib. - See ref`` for instructions + See :ref:`installing_for_devs` for instructions 5. Create a branch to hold your changes:: - git checkout -b my-feature origin/master + git checkout -b my-feature origin/main - and start making changes. Never work in the ``master`` branch! + and start making changes. Never work in the ``main`` branch! 6. Work on this copy, on your computer, using Git to do the version control. When you're done editing e.g., ``lib/matplotlib/collections.py``, do:: @@ -138,97 +199,8 @@ A brief overview is: Finally, go to the web page of your fork of the Matplotlib repo, and click 'Pull request' to send your changes to the maintainers for review. -.. seealso:: - - * `Git documentation `_ - * `Git-Contributing to a Project `_ - * `Introduction to GitHub `_ - * :ref:`development-workflow` for best practices for Matplotlib - * :ref:`using-git` - -Contributing pull requests --------------------------- - -It is recommended to check that your contribution complies with the following -rules before submitting a pull request: - -* If your pull request addresses an issue, please use the title to describe the - issue and mention the issue number in the pull request description to ensure - that a link is created to the original issue. - -* All public methods should have informative docstrings with sample usage when - appropriate. Use the `numpy docstring standard - `_. - -* Formatting should follow the recommendations of `PEP8 - `__. You should consider - installing/enabling automatic PEP8 checking in your editor. Part of the test - suite is checking PEP8 compliance, things go smoother if the code is mostly - PEP8 compliant to begin with. - -* Each high-level plotting function should have a simple example in the - ``Example`` section of the docstring. This should be as simple as possible - to demonstrate the method. More complex examples should go in the - ``examples`` tree. - -* Changes (both new features and bugfixes) should be tested. See :ref:`testing` - for more details. - -* Import the following modules using the standard scipy conventions:: - - import numpy as np - import numpy.ma as ma - import matplotlib as mpl - import matplotlib.pyplot as plt - import matplotlib.cbook as cbook - import matplotlib.patches as mpatches - - In general, Matplotlib modules should **not** import `.rcParams` using ``from - matplotlib import rcParams``, but rather access it as ``mpl.rcParams``. This - is because some modules are imported very early, before the `.rcParams` - singleton is constructed. - -* If your change is a major new feature, add an entry to the ``What's new`` - section by adding a new file in ``doc/users/next_whats_new`` (see - :file:`doc/users/next_whats_new/README.rst` for more information). - -* If you change the API in a backward-incompatible way, please document it in - :file:`doc/api/next_api_changes/behavior`, by adding a new file with the - naming convention ``99999-ABC.rst`` where the pull request number is followed - by the contributor's initials. (see :file:`doc/api/api_changes.rst` for more - information) - -* See below for additional points about :ref:`keyword-argument-processing`, if - applicable for your pull request. - -In addition, you can check for common programming errors with the following -tools: - -* Code with a good unittest coverage (at least 70%, better 100%), check with:: - - python -m pip install coverage - python -m pytest --cov=matplotlib --showlocals -v - -* No pyflakes warnings, check with:: - - python -m pip install pyflakes - pyflakes path/to/module.py - -.. note:: - - The current state of the Matplotlib code base is not compliant with all - of those guidelines, but we expect that enforcing those constraints on all - new contributions will move the overall code base quality in the right - direction. - - -.. seealso:: - - * :ref:`coding_guidelines` - * :ref:`testing` - * :ref:`documenting-matplotlib` - - +For more detailed instructions on how to set up Matplotlib for development and +best practices for contribution, see :ref:`installing_for_devs`. .. _contributing_documentation: @@ -264,7 +236,7 @@ Other ways to contribute It also helps us if you spread the word: reference the project from your blog and articles or link to it from your website! If Matplotlib contributes to a project that leads to a scientific publication, please follow the -:doc:`/citing` guidelines. +:doc:`/users/project/citing` guidelines. .. _coding_guidelines: @@ -274,30 +246,66 @@ Coding guidelines API changes ----------- -Changes to the public API must follow a standard deprecation procedure to -prevent unexpected breaking of code that uses Matplotlib. - -- Deprecations must be announced via a new file in - a new file in :file:`doc/api/next_api_changes/deprecations/` with - naming convention ``99999-ABC.rst`` where ``99999`` is the pull request - number and ``ABC`` are the contributor's initials. -- Deprecations are targeted at the next point-release (i.e. 3.x.0). -- The deprecated API should, to the maximum extent possible, remain fully - functional during the deprecation period. In cases where this is not - possible, the deprecation must never make a given piece of code do something - different than it was before; at least an exception should be raised. -- If possible, usage of an deprecated API should emit a - `.MatplotlibDeprecationWarning`. There are a number of helper tools for this: - - - Use ``cbook.warn_deprecated()`` for general deprecation warnings. - - Use the decorator ``@cbook.deprecated`` to deprecate classes, functions, - methods, or properties. - - To warn on changes of the function signature, use the decorators - ``@cbook._delete_parameter``, ``@cbook._rename_parameter``, and - ``@cbook._make_keyword_only``. - -- Deprecated API may be removed two point-releases after they were deprecated. - +API consistency and stability are of great value. Therefore, API changes +(e.g. signature changes, behavior changes, removals) will only be conducted +if the added benefit is worth the user effort for adapting. + +Because we are a visualization library our primary output is the final +visualization the user sees. Thus it is our :ref:`long standing +` policy that the appearance of the figure is part of the API +and any changes, either semantic or esthetic, will be treated as a +backwards-incompatible API change. + +API changes in Matplotlib have to be performed following the deprecation process +below, except in very rare circumstances as deemed necessary by the development team. +This ensures that users are notified before the change will take effect and thus +prevents unexpected breaking of code. + +Rules +~~~~~ + +- Deprecations are targeted at the next point.release (e.g. 3.x) +- Deprecated API is generally removed two point-releases after introduction + of the deprecation. Longer deprecations can be imposed by core developers on + a case-by-case basis to give more time for the transition +- The old API must remain fully functional during the deprecation period +- If alternatives to the deprecated API exist, they should be available + during the deprecation period +- If in doubt, decisions about API changes are finally made by the + API consistency lead developer + +Introducing +~~~~~~~~~~~ + +1. Announce the deprecation in a new file + :file:`doc/api/next_api_changes/deprecations/99999-ABC.rst` where ``99999`` + is the pull request number and ``ABC`` are the contributor's initials. +2. If possible, issue a `~matplotlib.MatplotlibDeprecationWarning` when the + deprecated API is used. There are a number of helper tools for this: + + - Use ``_api.warn_deprecated()`` for general deprecation warnings + - Use the decorator ``@_api.deprecated`` to deprecate classes, functions, + methods, or properties + - To warn on changes of the function signature, use the decorators + ``@_api.delete_parameter``, ``@_api.rename_parameter``, and + ``@_api.make_keyword_only`` + + All these helpers take a first parameter *since*, which should be set to + the next point release, e.g. "3.x". + + You can use standard rst cross references in *alternative*. + +Expiring +~~~~~~~~ + +1. Announce the API changes in a new file + :file:`doc/api/next_api_changes/[kind]/99999-ABC.rst` where ``99999`` + is the pull request number and ``ABC`` are the contributor's initials, and + ``[kind]`` is one of the folders :file:`behavior`, :file:`development`, + :file:`removals`. See :file:`doc/api/next_api_changes/README.rst` for more + information. For the content, you can usually copy the deprecation notice + and adapt it slightly. +2. Change the code functionality and remove any related deprecation warnings. Adding new API -------------- @@ -323,7 +331,7 @@ New modules and files: installation * If you have added new files or directories, or reorganized existing ones, make sure the new files are included in the match patterns in - :file:`MANIFEST.in`, and/or in *package_data* in :file:`setup.py`. + in *package_data* in :file:`setupext.py`. C/C++ extensions ---------------- @@ -342,28 +350,33 @@ C/C++ extensions docstrings, and the Numpydoc format is well understood in the scientific Python community. +* C/C++ code in the :file:`extern/` directory is vendored, and should be kept + close to upstream whenever possible. It can be modified to fix bugs or + implement new features only if the required changes cannot be made elsewhere + in the codebase. In particular, avoid making style fixes to it. + .. _keyword-argument-processing: Keyword argument processing --------------------------- Matplotlib makes extensive use of ``**kwargs`` for pass-through customizations -from one function to another. A typical example is in `matplotlib.pyplot.text`. -The definition of the pylab text function is a simple pass-through to -`matplotlib.axes.Axes.text`:: +from one function to another. A typical example is +`~matplotlib.axes.Axes.text`. The definition of `matplotlib.pyplot.text` is a +simple pass-through to `matplotlib.axes.Axes.text`:: - # in pylab.py - def text(*args, **kwargs): - return gca().text(*args, **kwargs) + # in pyplot.py + def text(x, y, s, fontdict=None, **kwargs): + return gca().text(x, y, s, fontdict=fontdict, **kwargs) -`~matplotlib.axes.Axes.text` in simplified form looks like this, i.e., it just +`matplotlib.axes.Axes.text` (simplified for illustration) just passes all ``args`` and ``kwargs`` on to ``matplotlib.text.Text.__init__``:: # in axes/_axes.py - def text(self, x, y, s, fontdict=None, withdash=False, **kwargs): + def text(self, x, y, s, fontdict=None, **kwargs): t = Text(x=x, y=y, text=s, **kwargs) -and ``matplotlib.text.Text.__init__`` (again with liberties for illustration) +and ``matplotlib.text.Text.__init__`` (again, simplified) just passes them on to the `matplotlib.artist.Artist.update` method:: # in text.py @@ -434,11 +447,13 @@ or manually with :: logging.basicConfig(level=logging.DEBUG) import matplotlib.pyplot as plt -Then they will receive messages like:: +Then they will receive messages like + +.. code-block:: none - DEBUG:matplotlib.backends:backend MacOSX version unknown - DEBUG:matplotlib.yourmodulename:Here is some information - DEBUG:matplotlib.yourmodulename:Here is some more detailed information + DEBUG:matplotlib.backends:backend MacOSX version unknown + DEBUG:matplotlib.yourmodulename:Here is some information + DEBUG:matplotlib.yourmodulename:Here is some more detailed information Which logging level to use? ~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -483,14 +498,14 @@ Matplotlib. For example, for the module:: if bottom == top: warnings.warn('Attempting to set identical bottom==top') - running the script:: from matplotlib import my_matplotlib_module - my_matplotlib_module.set_range(0, 0) #set range + my_matplotlib_module.set_range(0, 0) # set range +will display -will display:: +.. code-block:: none UserWarning: Attempting to set identical bottom==top warnings.warn('Attempting to set identical bottom==top') @@ -503,10 +518,12 @@ Modifying the module to use `._api.warn_external`:: if bottom == top: _api.warn_external('Attempting to set identical bottom==top') -and running the same script will display:: +and running the same script will display + +.. code-block:: none - UserWarning: Attempting to set identical bottom==top - my_matplotlib_module.set_range(0, 0) #set range + UserWarning: Attempting to set identical bottom==top + my_matplotlib_module.set_range(0, 0) # set range .. _logging tutorial: https://docs.python.org/3/howto/logging.html#logging-basic-tutorial diff --git a/doc/devel/dependencies.rst b/doc/devel/dependencies.rst index c28283517005..da67cae0061e 100644 --- a/doc/devel/dependencies.rst +++ b/doc/devel/dependencies.rst @@ -4,84 +4,95 @@ Dependencies ============ +Runtime dependencies +==================== + + Mandatory dependencies -====================== +---------------------- When installing through a package manager like ``pip`` or ``conda``, the mandatory dependencies are automatically installed. This list is mainly for reference. -* `Python `_ (>= 3.7) -* `NumPy `_ (>= 1.16) -* `setuptools `_ +* `Python `_ (>= 3.8) +* `contourpy `_ (>= 1.0.1) * `cycler `_ (>= 0.10.0) -* `dateutil `_ (>= 2.7) +* `dateutil `_ (>= 2.7) +* `fontTools `_ (>= 4.22.0) * `kiwisolver `_ (>= 1.0.1) +* `NumPy `_ (>= 1.20) +* `packaging `_ (>= 20.0) * `Pillow `_ (>= 6.2) -* `pyparsing `_ (>=2.2.1) +* `pyparsing `_ (>= 2.3.1) +* `setuptools `_ +* `pyparsing `_ (>= 2.3.1) +* `importlib-resources `_ + (>= 3.2.0; only required on Python < 3.10) .. _optional_dependencies: Optional dependencies -===================== +--------------------- The following packages and tools are not required but extend the capabilities of Matplotlib. Backends --------- +~~~~~~~~ Matplotlib figures can be rendered to various user interfaces. See :ref:`what-is-a-backend` for more details on the optional Matplotlib backends and the capabilities they provide. -* Tk_ (>= 8.3, != 8.6.0 or 8.6.1) [#]_: for the Tk-based backends. -* PyQt4_ (>= 4.6) or PySide_ (>= 1.0.3) [#]_: for the Qt4-based backends. -* PyQt5_ or PySide2_: for the Qt5-based backends. -* PyGObject_: for the GTK3-based backends [#]_. -* wxPython_ (>= 4) [#]_: for the wx-based backends. -* pycairo_ (>= 1.11.0) or cairocffi_ (>= 0.8): for the GTK3 and/or cairo-based - backends. -* Tornado_: for the WebAgg backend. +* Tk_ (>= 8.4, != 8.6.0 or 8.6.1): for the Tk-based backends. Tk is part of + most standard Python installations, but it's not part of Python itself and + thus may not be present in rare cases. +* PyQt6_ (>= 6.1), PySide6_, PyQt5_, or PySide2_: for the Qt-based backends. +* PyGObject_ and pycairo_ (>= 1.14.0): for the GTK-based backends. If using pip + (but not conda or system package manager) PyGObject must be built from + source; see `pygobject documentation + `_. +* pycairo_ (>= 1.14.0) or cairocffi_ (>= 0.8): for cairo-based backends. +* wxPython_ (>= 4): for the wx-based backends. If using pip (but not conda or + system package manager) on Linux wxPython wheels must be manually downloaded + from https://wxpython.org/pages/downloads/. +* Tornado_ (>= 5): for the WebAgg backend. +* ipykernel_: for the nbagg backend. +* macOS (>= 10.12): for the macosx backend. .. _Tk: https://docs.python.org/3/library/tk.html -.. _PyQt4: https://pypi.org/project/PyQt4 -.. _PySide: https://pypi.org/project/PySide -.. _PyQt5: https://pypi.org/project/PyQt5 -.. _PySide2: https://pypi.org/project/PySide2 +.. _PyQt5: https://pypi.org/project/PyQt5/ +.. _PySide2: https://pypi.org/project/PySide2/ +.. _PyQt6: https://pypi.org/project/PyQt6/ +.. _PySide6: https://pypi.org/project/PySide6/ .. _PyGObject: https://pygobject.readthedocs.io/en/latest/ .. _wxPython: https://www.wxpython.org/ .. _pycairo: https://pycairo.readthedocs.io/en/latest/ .. _cairocffi: https://cairocffi.readthedocs.io/en/latest/ -.. _Tornado: https://pypi.org/project/tornado - -.. [#] Tk is part of most standard Python installations, but it's not part of - Python itself and thus may not be present in rare cases. -.. [#] PySide cannot be pip-installed on Linux (but can be conda-installed). -.. [#] If using pip (and not conda), PyGObject must be built from source; see - https://pygobject.readthedocs.io/en/latest/devguide/dev_environ.html. -.. [#] If using pip (and not conda) on Linux, wxPython wheels must be manually - downloaded from https://wxpython.org/pages/downloads/. +.. _Tornado: https://pypi.org/project/tornado/ +.. _ipykernel: https://pypi.org/project/ipykernel/ Animations ----------- +~~~~~~~~~~ * `ffmpeg `_: for saving movies. * `ImageMagick `_: for saving animated gifs. Font handling and rendering ---------------------------- +~~~~~~~~~~~~~~~~~~~~~~~~~~~ * `LaTeX `_ (with `cm-super - `__ ) and `GhostScript (>=9.0) - `_ : for rendering text with LaTeX. + `__ and `underscore + `__) and `GhostScript (>= 9.0) + `_: for rendering text with LaTeX. * `fontconfig `_ (>= 2.7): for detection of system fonts on Linux. C libraries -=========== +----------- Matplotlib brings its own copies of the following libraries: @@ -102,7 +113,7 @@ rasterize characters differently) and of Qhull. As an exception, Matplotlib defaults to the system version of FreeType on AIX. To force Matplotlib to use a copy of FreeType or Qhull already installed in -your system, create a :file:`setup.cfg` file with the following contents: +your system, create a :file:`mplsetup.cfg` file with the following contents: .. code-block:: cfg @@ -161,3 +172,164 @@ If you go this route but need to reset and rebuild to change your settings, remember to clear your artifacts before re-building:: git clean -xfd + + +Minimum pip / manylinux support (linux) +--------------------------------------- + +Matplotlib publishes `manylinux wheels `_ +which have a minimum version of pip which will recognize the wheels + +- Python 3.8: ``manylinx2010`` / pip >= 19.0 +- Python 3.9+: ``manylinx2014`` / pip >= 19.3 + +In all cases the required version of pip is embedded in the CPython source. + + + +.. _development-dependencies: + +Dependencies for building Matplotlib +==================================== + +.. _setup-dependencies: + +Setup dependencies +------------------ + +- `certifi `_ (>= 2020.06.20). Used while + downloading the freetype and QHull source during build. This is not a + runtime dependency. +- `setuptools_scm `_ (>= 7). Used to + update the reported ``mpl.__version__`` based on the current git commit. + Also a runtime dependency for editable installs. +- `NumPy `_ (>= 1.20). Also a runtime dependency. + + +.. _compile-dependencies: + +C++ compiler +------------ + +Matplotlib requires a C++ compiler that supports C++11. + +- `gcc 4.8.1 `_ or higher. For gcc <6.5 you will + need to set ``$CFLAGS=-std=c++11`` to enable C++11 support. + `Installing GCC: Binaries `_. +- `clang 3.3 `_ or higher. + `LLVM Download Page `_. +- `Visual Studio 2015 + `_ + (aka VS 14.0) or higher. A free version of Build Tools for Visual Studio is available for + `download `_. + + +.. _test-dependencies: + +Dependencies for testing Matplotlib +=================================== +This section lists the additional software required for +:ref:`running the tests `. + +Required: + +- pytest_ (>= 3.6) + +Optional: + +In addition to all of the optional dependencies on the main library, for +testing the following will be used if they are installed. + +- Ghostscript_ (>= 9.0, to render PDF files) +- Inkscape_ (to render SVG files) +- nbformat_ and nbconvert_ used to test the notebook backend +- pandas_ used to test compatibility with Pandas +- pikepdf_ used in some tests for the pgf and pdf backends +- psutil_ used in testing the interactive backends +- pytest-cov_ (>= 2.3.1) to collect coverage information +- pytest-flake8_ to test coding standards using flake8_ +- pytest-timeout_ to limit runtime in case of stuck tests +- pytest-xdist_ to run tests in parallel +- pytest-xvfb_ to run tests without windows popping up (Linux) +- pytz_ used to test pytz int +- sphinx_ used to test our sphinx extensions +- WenQuanYi Zen Hei and `Noto Sans CJK `_ + fonts for testing font fallback and non-western fonts +- xarray_ used to test compatibility with xarray + +If any of these dependencies are not discovered the tests that rely on them +will be skipped by pytest. + +.. note:: + + When installing Inkscape on Windows, make sure that you select “Add + Inkscape to system PATHâ€, either for all users or current user, or the + tests will not find it. + +.. _Ghostscript: https://ghostscript.com/ +.. _Inkscape: https://inkscape.org +.. _flake8: https://pypi.org/project/flake8/ +.. _nbconvert: https://pypi.org/project/nbconvert/ +.. _nbformat: https://pypi.org/project/nbformat/ +.. _pandas: https://pypi.org/project/pandas/ +.. _pikepdf: https://pypi.org/project/pikepdf/ +.. _psutil: https://pypi.org/project/psutil/ +.. _pytz: https://fonts.google.com/noto/use#faq +.. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest/ +.. _pytest-flake8: https://pypi.org/project/pytest-flake8/ +.. _pytest-timeout: https://pypi.org/project/pytest-timeout/ +.. _pytest-xdist: https://pypi.org/project/pytest-xdist/ +.. _pytest-xvfb: https://pypi.org/project/pytest-xvfb/ +.. _pytest: http://doc.pytest.org/en/latest/ +.. _sphinx: https://pypi.org/project/Sphinx/ +.. _xarray: https://pypi.org/project/xarray/ + + +.. _doc-dependencies: + +Dependencies for building Matplotlib's documentation +==================================================== + +Python packages +--------------- +The additional Python packages required to build the +:ref:`documentation ` are listed in +:file:`doc-requirements.txt` and can be installed using :: + + pip install -r requirements/doc/doc-requirements.txt + +The content of :file:`doc-requirements.txt` is also shown below: + + .. include:: ../../requirements/doc/doc-requirements.txt + :literal: + +Additional external dependencies +-------------------------------- +Required: + +* a minimal working LaTeX distribution, e.g., `TeX Live `_ or + `MikTeX `_ +* `Graphviz `_ +* the following LaTeX packages (if your OS bundles TeX Live, the + "complete" version of the installer, e.g. "texlive-full" or "texlive-all", + will often automatically include these packages): + + * `cm-super `_ + * `dvipng `_ + * `underscore `_ + +Optional, but recommended: + +* `Inkscape `_ +* `optipng `_ +* the font "Humor Sans" (aka the "XKCD" font), or the free alternative + `Comic Neue `_ +* the font "Times New Roman" + +.. note:: + + The documentation will not build without LaTeX and Graphviz. These are not + Python packages and must be installed separately. The documentation can be + built without Inkscape and optipng, but the build process will raise various + warnings. If the build process warns that you are missing fonts, make sure + your LaTeX distribution bundles cm-super or install it separately. diff --git a/doc/devel/development_setup.rst b/doc/devel/development_setup.rst index a511e4a6aef3..afcd63b3bf15 100644 --- a/doc/devel/development_setup.rst +++ b/doc/devel/development_setup.rst @@ -1,49 +1,127 @@ +.. redirect-from:: /devel/gitwash/configure_git +.. redirect-from:: /devel/gitwash/dot2_dot3 +.. redirect-from:: /devel/gitwash/following_latest +.. redirect-from:: /devel/gitwash/forking_hell +.. redirect-from:: /devel/gitwash/git_development +.. redirect-from:: /devel/gitwash/git_install +.. redirect-from:: /devel/gitwash/git_intro +.. redirect-from:: /devel/gitwash/git_resources +.. redirect-from:: /devel/gitwash/patching +.. redirect-from:: /devel/gitwash/set_up_fork +.. redirect-from:: /devel/gitwash/index + .. _installing_for_devs: ===================================== Setting up Matplotlib for development ===================================== +To set up Matplotlib for development follow these steps: + +.. contents:: + :local: + +Fork the Matplotlib repository +============================== + +Matplotlib is hosted at https://github.com/matplotlib/matplotlib.git. If you +plan on solving issues or submit pull requests to the main Matplotlib +repository, you should first *fork* this repository by visiting +https://github.com/matplotlib/matplotlib.git and clicking on the +``Fork`` button on the top right of the page (see +`the GitHub documentation `__ for more details.) + +Retrieve the latest version of the code +======================================= + +Now that your fork of the repository lives under your GitHub username, you can +retrieve the latest sources with one of the following commands (where your +should replace ```` with your GitHub username): + +.. tab-set:: + + .. tab-item:: https + + .. code-block:: bash + + git clone https://github.com//matplotlib.git + + .. tab-item:: ssh + + .. code-block:: bash + + git clone git@github.com:/matplotlib.git + + This requires you to setup an `SSH key`_ in advance, but saves you from + typing your password at every connection. + + .. _SSH key: https://docs.github.com/en/authentication/connecting-to-github-with-ssh + + +This will place the sources in a directory :file:`matplotlib` below your +current working directory, set up the ``origin`` remote to point to your own +fork, and set up the ``upstream`` remote to point to the Matplotlib main +repository (see also `Managing remote repositories `__.) +Change into this directory before continuing:: + + cd matplotlib + +.. note:: + + For more information on ``git`` and ``GitHub``, check the following resources. + + * `Git documentation `_ + * `GitHub-Contributing to a Project `_ + * `GitHub Skills `_ + * :ref:`using-git` + * :ref:`git-resources` + * `Installing git `_ + * https://tacaswell.github.io/think-like-git.html + * https://tom.preston-werner.com/2009/05/19/the-git-parable.html + .. _dev-environment: -Creating a dedicated environment -================================ +Create a dedicated environment +============================== You should set up a dedicated environment to decouple your Matplotlib development from other Python and Matplotlib installations on your system. -Here we use python's virtual environment `venv`_, but you may also use others -such as conda. + +The simplest way to do this is to use either Python's virtual environment +`venv`_ or `conda`_. .. _venv: https://docs.python.org/3/library/venv.html +.. _conda: https://docs.conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html -A new environment can be set up with :: +.. tab-set:: - python -m venv + .. tab-item:: venv environment -and activated with one of the following:: + Create a new `venv`_ environment with :: - source /bin/activate # Linux/macOS - \Scripts\activate.bat # Windows cmd.exe - \Scripts\Activate.ps1 # Windows PowerShell + python -m venv -Whenever you plan to work on Matplotlib, remember to activate the development -environment in your shell. + and activate it with one of the following :: -Retrieving the latest version of the code -========================================= + source /bin/activate # Linux/macOS + \Scripts\activate.bat # Windows cmd.exe + \Scripts\Activate.ps1 # Windows PowerShell -Matplotlib is hosted at https://github.com/matplotlib/matplotlib.git. + .. tab-item:: conda environment -You can retrieve the latest sources with the command (see -:ref:`set-up-fork` for more details):: + Create a new `conda`_ environment with :: - git clone https://github.com/matplotlib/matplotlib.git + conda env create -f environment.yml -This will place the sources in a directory :file:`matplotlib` below your -current working directory. + You can use ``mamba`` instead of ``conda`` in the above command if + you have `mamba`_ installed. + + .. _mamba: https://mamba.readthedocs.io/en/latest/ + + Activate the environment using :: -If you have the proper privileges, you can use ``git@`` instead of -``https://``, which works through the ssh protocol and might be easier to use -if you are using 2-factor authentication. + conda activate mpl-dev + +Remember to activate the environment whenever you start working on Matplotlib. Installing Matplotlib in editable mode ====================================== @@ -60,76 +138,20 @@ true for ``*.py`` files. If you change the C-extension source (which might also happen if you change branches) you will have to re-run ``python -m pip install -ve .`` -.. _test-dependencies: - -Additional dependencies for testing +Install pre-commit hooks (optional) =================================== -This section lists the additional software required for -:ref:`running the tests `. - -Required: - -- pytest_ (>=3.6) -- Ghostscript_ (>= 9.0, to render PDF files) -- Inkscape_ (to render SVG files) - -Optional: +`pre-commit `_ hooks automatically check flake8 and +other style issues when you run ``git commit``. The hooks are defined in the +top level ``.pre-commit-config.yaml`` file. To install the hooks :: -- pytest-cov_ (>=2.3.1) to collect coverage information -- pytest-flake8_ to test coding standards using flake8_ -- pytest-timeout_ to limit runtime in case of stuck tests -- pytest-xdist_ to run tests in parallel + python -m pip install pre-commit + pre-commit install -.. _pytest: http://doc.pytest.org/en/latest/ -.. _Ghostscript: https://www.ghostscript.com/ -.. _Inkscape: https://inkscape.org -.. _pytest-cov: https://pytest-cov.readthedocs.io/en/latest/ -.. _pytest-flake8: https://pypi.org/project/pytest-flake8/ -.. _pytest-xdist: https://pypi.org/project/pytest-xdist/ -.. _pytest-timeout: https://pypi.org/project/pytest-timeout/ -.. _flake8: https://pypi.org/project/flake8/ +The hooks can also be run manually. All the hooks can be run, in order as +listed in ``.pre-commit-config.yaml``, against the full codebase with :: + pre-commit run --all-files -.. _doc-dependencies: - -Additional dependencies for building documentation -================================================== - -Python packages ---------------- -The additional Python packages required to build the -:ref:`documentation ` are listed in -:file:`doc-requirements.txt` and can be installed using :: - - pip install -r requirements/doc/doc-requirements.txt - -The content of :file:`doc-requirements.txt` is also shown below: - - .. include:: ../../requirements/doc/doc-requirements.txt - :literal: - -Additional external dependencies --------------------------------- -Required: - -* a minimal working LaTeX distribution -* `Graphviz `_ -* the LaTeX packages *cm-super* and *dvipng*. If your OS bundles ``TexLive``, - then often the "complete" version of the installer will automatically include - these packages (e.g. "texlive-full" or "texlive-all"). - -Optional, but recommended: - -* `Inkscape `_ -* `optipng `_ -* the font "Humor Sans" (aka the "XKCD" font), or the free alternative - `Comic Neue `_. -* the font "Times New Roman" - -.. note:: +To run a particular hook manually, run ``pre-commit run`` with the hook id :: - The documentation will not build without LaTeX and Graphviz. These are not - Python packages and must be installed separately. The documentation can be - built without Inkscape and optipng, but the build process will raise various - warnings. If the build process warns that you are missing fonts, make sure - your LaTeX distribution bundles cm-super or install it separately. \ No newline at end of file + pre-commit run --all-files diff --git a/doc/devel/development_workflow.rst b/doc/devel/development_workflow.rst new file mode 100644 index 000000000000..cd902f3f30e5 --- /dev/null +++ b/doc/devel/development_workflow.rst @@ -0,0 +1,430 @@ +.. highlight:: bash + +.. _development-workflow: + +#################### +Development workflow +#################### + +Workflow summary +================ + +To keep your work well organized, with readable history, and in turn make it +easier for project maintainers (that might be you) to see what you've done, and +why you did it, we recommend the following: + +* Don't use your ``main`` branch for anything. Consider deleting it. +* Before starting a new set of changes, fetch all changes from + ``upstream/main``, and start a new *feature branch* from that. +* Make a new branch for each feature or bug fix — "one task, one branch". +* Name your branch for the purpose of the changes - e.g. + ``bugfix-for-issue-14`` or ``refactor-database-code``. +* If you get stuck, reach out on Gitter or + `discourse `__. +* When you're ready or need feedback on your code, open a pull request so that the + Matplotlib developers can give feedback and eventually include your suggested + code into the ``main`` branch. + +.. note:: + + It may sound strange, but deleting your own ``main`` branch can help reduce + confusion about which branch you are on. See `deleting main on GitHub`_ for + details. + +.. _deleting main on GitHub: https://matthew-brett.github.io/pydagogue/gh_delete_master.html + +.. _update-mirror-main: + +Update the ``main`` branch +========================== + +First make sure you have followed :ref:`installing_for_devs`. + +From time to time you should fetch the upstream changes from GitHub:: + + git fetch upstream + +This will pull down any commits you don't have, and set the remote branches to +point to the right commit. + +.. _make-feature-branch: + +Make a new feature branch +========================= + +When you are ready to make some changes to the code, you should start a new +branch. Branches that are for a collection of related edits are often called +'feature branches'. + +Making a new branch for each set of related changes will make it easier for +someone reviewing your branch to see what you are doing. + +Choose an informative name for the branch to remind yourself and the rest of us +what the changes in the branch are for. For example ``add-ability-to-fly``, or +``bugfix-for-issue-42``. + +:: + + # Update the main branch + git fetch upstream + # Make new feature branch starting at current main + git branch my-new-feature upstream/main + git checkout my-new-feature + +Generally, you will want to keep your feature branches on your public GitHub +fork of Matplotlib. To do this, you ``git push`` this new branch up to your +GitHub repo. Generally (if you followed the instructions in these pages, and by +default), git will have a link to your fork of the GitHub repo, called +``origin``. You push up to your own fork with:: + + git push origin my-new-feature + +In git >= 1.7 you can ensure that the link is correctly set by using the +``--set-upstream`` option:: + + git push --set-upstream origin my-new-feature + +From now on git will know that ``my-new-feature`` is related to the +``my-new-feature`` branch in the GitHub repo. + +.. _edit-flow: + +The editing workflow +==================== + +Overview +-------- + +:: + + # hack hack + git add my_new_file + git commit -am 'NF - some message' + git push + +In more detail +-------------- + +#. Make some changes +#. See which files have changed with ``git status``. + You'll see a listing like this one: + + .. code-block:: none + + # On branch ny-new-feature + # Changed but not updated: + # (use "git add ..." to update what will be committed) + # (use "git checkout -- ..." to discard changes in working directory) + # + # modified: README + # + # Untracked files: + # (use "git add ..." to include in what will be committed) + # + # INSTALL + no changes added to commit (use "git add" and/or "git commit -a") + +#. Check what the actual changes are with ``git diff``. +#. Add any new files to version control ``git add new_file_name``. +#. To commit all modified files into the local copy of your repo,, do + ``git commit -am 'A commit message'``. Note the ``-am`` options to + ``commit``. The ``m`` flag just signals that you're going to type a + message on the command line. The ``a`` flag — you can just take on + faith — or see `why the -a flag?`_. The + `git commit `_ manual page might also be + useful. +#. To push the changes up to your forked repo on GitHub, do a ``git + push``. + +.. _why the -a flag?: http://gitready.com/beginner/2009/01/18/the-staging-area.html + + +Open a pull request +=================== + +When you are ready to ask for someone to review your code and consider a merge, +`submit your Pull Request (PR) `_. + +Enter a title for the set of changes with some explanation of what you've done. +Mention anything you'd like particular attention for - such as a +complicated change or some code you are not happy with. + +If you don't think your request is ready to be merged, just say so in your pull +request message and use the "Draft PR" feature of GitHub. This is a good way of +getting some preliminary code review. + +Some other things you might want to do +====================================== + +Explore your repository +----------------------- + +To see a graphical representation of the repository branches and +commits:: + + gitk --all + +To see a linear list of commits for this branch:: + + git log + + +.. _recovering-from-mess-up: + +Recovering from mess-ups +------------------------ + +Sometimes, you mess up merges or rebases. Luckily, in git it is +relatively straightforward to recover from such mistakes. + +If you mess up during a rebase:: + + git rebase --abort + +If you notice you messed up after the rebase:: + + # reset branch back to the saved point + git reset --hard tmp + +If you forgot to make a backup branch:: + + # look at the reflog of the branch + git reflog show cool-feature + + 8630830 cool-feature@{0}: commit: BUG: io: close file handles immediately + 278dd2a cool-feature@{1}: rebase finished: refs/heads/my-feature-branch onto 11ee694744f2552d + 26aa21a cool-feature@{2}: commit: BUG: lib: make seek_gzip_factory not leak gzip obj + ... + + # reset the branch to where it was before the botched rebase + git reset --hard cool-feature@{2} + +.. _rewriting-commit-history: + +Rewriting commit history +------------------------ + +.. note:: + + Do this only for your own feature branches. + +Is there an embarrassing typo in a commit you made? Or perhaps you +made several false starts you don't want posterity to see. + +This can be done via *interactive rebasing*. + +Suppose that the commit history looks like this:: + + git log --oneline + eadc391 Fix some remaining bugs + a815645 Modify it so that it works + 2dec1ac Fix a few bugs + disable + 13d7934 First implementation + 6ad92e5 * masked is now an instance of a new object, MaskedConstant + 29001ed Add pre-nep for a copule of structured_array_extensions. + ... + +and ``6ad92e5`` is the last commit in the ``cool-feature`` branch. Suppose we +want to make the following changes: + +* Rewrite the commit message for ``13d7934`` to something more sensible. +* Combine the commits ``2dec1ac``, ``a815645``, ``eadc391`` into a single one. + +We do as follows:: + + # make a backup of the current state + git branch tmp HEAD + # interactive rebase + git rebase -i 6ad92e5 + +This will open an editor with the following text in it:: + + pick 13d7934 First implementation + pick 2dec1ac Fix a few bugs + disable + pick a815645 Modify it so that it works + pick eadc391 Fix some remaining bugs + + # Rebase 6ad92e5..eadc391 onto 6ad92e5 + # + # Commands: + # p, pick = use commit + # r, reword = use commit, but edit the commit message + # e, edit = use commit, but stop for amending + # s, squash = use commit, but meld into previous commit + # f, fixup = like "squash", but discard this commit's log message + # + # If you remove a line here THAT COMMIT WILL BE LOST. + # However, if you remove everything, the rebase will be aborted. + # + +To achieve what we want, we will make the following changes to it:: + + r 13d7934 First implementation + pick 2dec1ac Fix a few bugs + disable + f a815645 Modify it so that it works + f eadc391 Fix some remaining bugs + +This means that (i) we want to edit the commit message for +``13d7934``, and (ii) collapse the last three commits into one. Now we +save and quit the editor. + +Git will then immediately bring up an editor for editing the commit +message. After revising it, we get the output:: + + [detached HEAD 721fc64] FOO: First implementation + 2 files changed, 199 insertions(+), 66 deletions(-) + [detached HEAD 0f22701] Fix a few bugs + disable + 1 files changed, 79 insertions(+), 61 deletions(-) + Successfully rebased and updated refs/heads/my-feature-branch. + +and now, the history looks like this:: + + 0f22701 Fix a few bugs + disable + 721fc64 ENH: Sophisticated feature + 6ad92e5 * masked is now an instance of a new object, MaskedConstant + +If it went wrong, recovery is again possible as explained :ref:`above +`. + +If you have not yet pushed this branch to github, you can carry on as normal, +however if you *have* already pushed this commit see :ref:`force-push` for how +to replace your already published commits with the new ones. + + +.. _rebase-on-main: + +Rebasing on ``upstream/main`` +----------------------------- + +Let's say you thought of some work you'd like to do. You +:ref:`update-mirror-main` and :ref:`make-feature-branch` called +``cool-feature``. At this stage, ``main`` is at some commit, let's call it E. +Now you make some new commits on your ``cool-feature`` branch, let's call them +A, B, C. Maybe your changes take a while, or you come back to them after a +while. In the meantime, ``main`` has progressed from commit E to commit (say) G: + +.. code-block:: none + + A---B---C cool-feature + / + D---E---F---G main + +At this stage you consider merging ``main`` into your feature branch, and you +remember that this page sternly advises you not to do that, because the +history will get messy. Most of the time, you can just ask for a review without +worrying about whether ``main`` has got a little ahead; however sometimes, the changes in +``main`` might affect your changes, and you need to harmonize them. In this +situation you may prefer to do a rebase. + +``rebase`` takes your changes (A, B, C) and replays them as if they had been +made to the current state of ``main``. In other words, in this case, it takes +the changes represented by A, B, C and replays them on top of G. After the +rebase, your history will look like this: + +.. code-block:: none + + A'--B'--C' cool-feature + / + D---E---F---G main + +See `rebase without tears`_ for more detail. + +.. _rebase without tears: https://matthew-brett.github.io/pydagogue/rebase_without_tears.html + +To do a rebase on ``upstream/main``:: + + # Fetch changes from upstream/main + git fetch upstream + # go to the feature branch + git checkout cool-feature + # make a backup in case you mess up + git branch tmp cool-feature + # rebase cool-feature onto main + git rebase --onto upstream/main upstream/main cool-feature + +In this situation, where you are already on branch ``cool-feature``, the last +command can be written more succinctly as:: + + git rebase upstream/main + +When all looks good, you can delete your backup branch:: + + git branch -D tmp + +If it doesn't look good you may need to have a look at +:ref:`recovering-from-mess-up`. + +If you have made changes to files that have also changed in ``main``, this may +generate merge conflicts that you need to resolve - see the `git rebase`_ man +page for some instructions at the end of the "Description" section. There is +some related help on merging in the git user manual - see `resolving a merge`_. + +.. _git rebase: https://git-scm.com/docs/git-rebase +.. _resolving a merge: https://schacon.github.io/git/user-manual.html#resolving-a-merge + + +If you have not yet pushed this branch to github, you can carry on as normal, +however if you *have* already pushed this commit see :ref:`force-push` for how +to replace your already published commits with the new ones. + + +.. _force-push: + + +Pushing, with force +------------------- + + +If you have in some way re-written already pushed history (e.g. via +:ref:`rewriting-commit-history` or :ref:`rebase-on-main`) leaving you with +a git history that looks something like + +.. code-block:: none + + A'--E cool-feature + / + D---A---B---C origin/cool-feature + +where you have pushed the commits ``A,B,C`` to your fork on GitHub (under the +remote name *origin*) but now have the commits ``A'`` and ``E`` on your local +branch *cool-feature*. If you try to push the new commits to GitHub, it will +fail and show an error that looks like :: + + $ git push + Pushing to github.com:origin/matplotlib.git + To github.com:origin/matplotlib.git + ! [rejected] cool_feature -> cool_feature (non-fast-forward) + error: failed to push some refs to 'github.com:origin/matplotlib.git' + hint: Updates were rejected because the tip of your current branch is behind + hint: its remote counterpart. Integrate the remote changes (e.g. + hint: 'git pull ...') before pushing again. + hint: See the 'Note about fast-forwards' in 'git push --help' for details. + +If this push had succeeded, the commits ``A``, ``B``, and ``C`` would no +longer be referenced by any branch and they would be discarded: + +.. code-block:: none + + D---A'---E cool-feature, origin/cool-feature + +By default ``git push`` helpfully tries to protect you from accidentally +discarding commits by rejecting the push to the remote. When this happens, +GitHub also adds the helpful suggestion to pull the remote changes and then try +pushing again. In some cases, such as if you and a colleague are both +committing and pushing to the same branch, this is a correct course of action. + +However, in the case of having intentionally re-written history, we *want* to +discard the commits on the remote and replace them with the new-and-improved +versions from our local branch. In this case, what we want to do is :: + + $ git push --force-with-lease + +which tells git you are aware of the risks and want to do the push anyway. We +recommend using ``--force-with-lease`` over the ``--force`` flag. The +``--force`` will do the push no matter what, whereas ``--force-with-lease`` +will only do the push if the remote branch is where the local ``git`` client +thought it was. + +Be judicious with force-pushing. It is effectively re-writing published +history, and if anyone has fetched the old commits, it will have a different view +of history which can cause confusion. diff --git a/doc/devel/documenting_mpl.rst b/doc/devel/documenting_mpl.rst index e232900265d2..b814e67cd308 100644 --- a/doc/devel/documenting_mpl.rst +++ b/doc/devel/documenting_mpl.rst @@ -4,47 +4,41 @@ Writing documentation ===================== -.. contents:: Contents - :depth: 3 - :local: - :backlinks: top - :class: multicol-toc - - Getting started =============== General file structure ---------------------- -All documentation is built from the :file:`doc/`, :file:`tutorials/`, and -:file:`examples/` directories. The :file:`doc/` directory contains -configuration files for Sphinx and reStructuredText (ReST_; ``.rst``) files -that are rendered to documentation pages. - - -The main entry point is :file:`doc/index.rst`, which pulls in the -:file:`index.rst` file for the users guide (:file:`doc/users`), developers -guide (:file:`doc/devel`), api reference (:file:`doc/api`), and FAQs -(:file:`doc/faq`). The documentation suite is built as a single document in -order to make the most effective use of cross referencing. +All documentation is built from the :file:`doc/`. The :file:`doc/` +directory contains configuration files for Sphinx and reStructuredText +(ReST_; ``.rst``) files that are rendered to documentation pages. -Sphinx_ also creates ``.rst`` files that are staged in :file:`doc/api` from +Documentation is created in three ways. First, API documentation +(:file:`doc/api`) is created by Sphinx_ from the docstrings of the classes in the Matplotlib library. Except for -:file:`doc/api/api_changes/`, these ``.rst`` files are created when the -documentation is built. - -Similarly, the contents of :file:`doc/gallery` and :file:`doc/tutorials` are -generated by the `Sphinx Gallery`_ from the sources in :file:`examples/` and -:file:`tutorials/`. These sources consist of python scripts that have ReST_ -documentation built into their comments. +:file:`doc/api/api_changes/`, ``.rst`` files in :file:`doc/api` are created +when the documentation is built. See :ref:`writing-docstrings` below. + +Second, the contents of :file:`doc/plot_types`, :file:`doc/gallery` and +:file:`doc/tutorials` are generated by the `Sphinx Gallery`_ from python +files in :file:`plot_types/`, :file:`examples/` and :file:`tutorials/`. +These sources consist of python scripts that have ReST_ documentation built +into their comments. See :ref:`writing-examples-and-tutorials` below. + +Third, Matplotlib has narrative docs written in ReST_ in subdirectories of +:file:`doc/users/`. If you would like to add new documentation that is suited +to an ``.rst`` file rather than a gallery or tutorial example, choose an +appropriate subdirectory to put it in, and add the file to the table of +contents of :file:`index.rst` of the subdirectory. See +:ref:`writing-rest-pages` below. .. note:: - Don't directly edit the ``.rst`` files in :file:`doc/gallery`, - :file:`doc/tutorials`, and :file:`doc/api` (excepting - :file:`doc/api/api_changes/`). Sphinx_ regenerates files in these - directories when building documentation. + Don't directly edit the ``.rst`` files in :file:`doc/plot_types`, + :file:`doc/gallery`, :file:`doc/tutorials`, and :file:`doc/api` + (excepting :file:`doc/api/api_changes/`). Sphinx_ regenerates + files in these directories when building documentation. Setting up the doc build ------------------------ @@ -73,6 +67,10 @@ Other useful invocations include .. code-block:: sh + # Build the html documentation, but skip generation of the gallery images to + # save time. + make html-noplot + # Delete built files. May help if you get errors about missing paths or # broken links. make clean @@ -88,38 +86,47 @@ it, use make SPHINXOPTS= html -On Windows the arguments must be at the end of the statement: - -.. code-block:: bat - - make html SPHINXOPTS= - You can use the ``O`` variable to set additional options: * ``make O=-j4 html`` runs a parallel build with 4 processes. * ``make O=-Dplot_formats=png:100 html`` saves figures in low resolution. -* ``make O=-Dplot_gallery=0 html`` skips the gallery build. -Multiple options can be combined using e.g. ``make O='-j4 -Dplot_gallery=0' +Multiple options can be combined, e.g. ``make O='-j4 -Dplot_formats=png:100' html``. -On Windows, either use the format shown above or set options as environment variables, e.g.: +On Windows, set the options as environment variables, e.g.: .. code-block:: bat - set O=-W --keep-going -j4 - make html + set SPHINXOPTS= & set O=-j4 -Dplot_formats=png:100 & make html + +Showing locally built docs +-------------------------- + +The built docs are available in the folder :file:`build/html`. A shortcut +for opening them in your default browser is: + +.. code-block:: sh + + make show .. _writing-rest-pages: Writing ReST pages ================== -Most documentation is either in the docstring of individual +Most documentation is either in the docstrings of individual classes and methods, in explicit ``.rst`` files, or in examples and tutorials. -All of these use the ReST_ syntax. Users should look at the ReST_ documentation -for a full description. But some specific hints and conventions Matplotlib -uses are useful for creating documentation. +All of these use the ReST_ syntax and are processed by Sphinx_. + +The `Sphinx reStructuredText Primer +`_ is +a good introduction into using ReST. More complete information is available in +the `reStructuredText reference documentation +`_. + +This section contains additional information and conventions how ReST is used +in the Matplotlib documentation. Formatting and style conventions -------------------------------- @@ -127,13 +134,28 @@ Formatting and style conventions It is useful to strive for consistency in the Matplotlib documentation. Here are some formatting and style conventions that are used. -Section name formatting -~~~~~~~~~~~~~~~~~~~~~~~ +Section formatting +~~~~~~~~~~~~~~~~~~ For everything but top-level chapters, use ``Upper lower`` for section titles, e.g., ``Possible hangups`` rather than ``Possible Hangups`` +We aim to follow the recommendations from the +`Python documentation `_ +and the `Sphinx reStructuredText documentation `_ +for section markup characters, i.e.: + +- ``#`` with overline, for parts. This is reserved for the main title in + ``index.rst``. All other pages should start with "chapter" or lower. +- ``*`` with overline, for chapters +- ``=``, for sections +- ``-``, for subsections +- ``^``, for subsubsections +- ``"``, for paragraphs + +This may not yet be applied consistently in existing docs. + Function arguments ~~~~~~~~~~~~~~~~~~ @@ -171,22 +193,22 @@ Documents can be linked with the ``:doc:`` directive: .. code-block:: rst - See the :doc:`/faq/installing_faq` + See the :doc:`/users/installing/index` - See the tutorial :doc:`/tutorials/introductory/sample_plots` + See the tutorial :doc:`/tutorials/introductory/quick_start` See the example :doc:`/gallery/lines_bars_and_markers/simple_plot` will render as: - See the :doc:`/faq/installing_faq` + See the :doc:`/users/installing/index` - See the tutorial :doc:`/tutorials/introductory/sample_plots` + See the tutorial :doc:`/tutorials/introductory/quick_start` See the example :doc:`/gallery/lines_bars_and_markers/simple_plot` Sections can also be given reference names. For instance from the -:doc:`/faq/installing_faq` link: +:doc:`/users/installing/index` link: .. code-block:: rst @@ -243,7 +265,7 @@ generates a link like this: `matplotlib.collections.LineCollection`. have to use qualifiers like ``:class:``, ``:func:``, ``:meth:`` and the likes. Often, you don't want to show the full package and module name. As long as the -target is unanbigous you can simply leave them out: +target is unambiguous you can simply leave them out: .. code-block:: rst @@ -280,8 +302,8 @@ you can check the full list of referenceable objects with the following commands:: python -m sphinx.ext.intersphinx 'https://docs.python.org/3/objects.inv' - python -m sphinx.ext.intersphinx 'https://docs.scipy.org/doc/numpy/objects.inv' - python -m sphinx.ext.intersphinx 'https://docs.scipy.org/doc/scipy/reference/objects.inv' + python -m sphinx.ext.intersphinx 'https://numpy.org/doc/stable/objects.inv' + python -m sphinx.ext.intersphinx 'https://docs.scipy.org/doc/scipy/objects.inv' python -m sphinx.ext.intersphinx 'https://pandas.pydata.org/pandas-docs/stable/objects.inv' .. _rst-figures-and-includes: @@ -290,24 +312,19 @@ Including figures and files --------------------------- Image files can directly included in pages with the ``image::`` directive. -e.g., :file:`thirdpartypackages/index.rst` displays the images for the third-party -packages as static images:: - - .. image:: /_static/toolbar.png +e.g., :file:`tutorials/intermediate/constrainedlayout_guide.py` displays +a couple of static images:: -as rendered on the page: :ref:`thirdparty-index`. + # .. image:: /_static/constrained_layout_1b.png + # :align: center -Files can be included verbatim. For instance the ``matplotlibrc`` file -is important for customizing Matplotlib, and is included verbatim in the -tutorial in :doc:`/tutorials/introductory/customizing`:: - .. literalinclude:: ../../_static/matplotlibrc +Files can be included verbatim. For instance the ``LICENSE`` file is included +at :ref:`license-agreement` using :: -This is rendered at the bottom of :doc:`/tutorials/introductory/customizing`. -Note that this is in a tutorial; see :ref:`writing-examples-and-tutorials` -below. + .. literalinclude:: ../../LICENSE/LICENSE -The examples directory is also copied to :file:`doc/gallery` by sphinx-gallery, +The examples directory is copied to :file:`doc/gallery` by sphinx-gallery, so plots from the examples directory can be included using .. code-block:: rst @@ -347,7 +364,7 @@ An example docstring looks like: .. code-block:: python - def hlines(self, y, xmin, xmax, colors='k', linestyles='solid', + def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', label='', **kwargs): """ Plot horizontal lines at each *y* from *xmin* to *xmax*. @@ -361,24 +378,26 @@ An example docstring looks like: Respective beginning and end of each line. If scalars are provided, all lines will have the same length. - colors : array-like of colors, default: 'k' + colors : list of colors, default: :rc:`lines.color` - linestyles : {'solid', 'dashed', 'dashdot', 'dotted'}, default: 'solid' + linestyles : {'solid', 'dashed', 'dashdot', 'dotted'}, optional label : str, default: '' Returns ------- - lines : `~matplotlib.collections.LineCollection` + `~matplotlib.collections.LineCollection` Other Parameters ---------------- - **kwargs : `~matplotlib.collections.LineCollection` properties + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs : `~matplotlib.collections.LineCollection` properties. - See also + See Also -------- vlines : vertical lines - axhline: horizontal line across the axes + axhline : horizontal line across the Axes """ See the `~.Axes.hlines` documentation for how this renders. @@ -545,10 +564,10 @@ effect. Sphinx automatically links code elements in the definition blocks of ``See also`` sections. No need to use backticks there:: - See also + See Also -------- vlines : vertical lines - axhline: horizontal line across the axes + axhline : horizontal line across the Axes Wrapping parameter lists ~~~~~~~~~~~~~~~~~~~~~~~~ @@ -585,7 +604,7 @@ Setters and getters ------------------- Artist properties are implemented using setter and getter methods (because -Matplotlib predates the introductions of the `property` decorator in Python). +Matplotlib predates the Python `property` decorator). By convention, these setters and getters are named ``set_PROPERTYNAME`` and ``get_PROPERTYNAME``; the list of properties thusly defined on an artist and their values can be listed by the `~.pyplot.setp` and `~.pyplot.getp` functions. @@ -652,7 +671,7 @@ Keyword arguments Since Matplotlib uses a lot of pass-through ``kwargs``, e.g., in every function that creates a line (`~.pyplot.plot`, `~.pyplot.semilogx`, `~.pyplot.semilogy`, -etc...), it can be difficult for the new user to know which ``kwargs`` are +etc.), it can be difficult for the new user to know which ``kwargs`` are supported. Matplotlib uses a docstring interpolation scheme to support documentation of every function that takes a ``**kwargs``. The requirements are: @@ -663,26 +682,14 @@ are: 2. as automated as possible so that as properties change, the docs are updated automatically. -The function `matplotlib.artist.kwdoc` and the decorator -``matplotlib.docstring.dedent_interpd`` facilitate this. They combine Python -string interpolation in the docstring with the Matplotlib artist introspection -facility that underlies ``setp`` and ``getp``. The ``kwdoc`` function gives -the list of properties as a docstring. In order to use this in another -docstring, first update the ``matplotlib.docstring.interpd`` object, as seen in -this example from `matplotlib.lines`: - -.. code-block:: python - - # in lines.py - docstring.interpd.update(Line2D_kwdoc=artist.kwdoc(Line2D)) - -Then in any function accepting `~.Line2D` pass-through ``kwargs``, e.g., -`matplotlib.axes.Axes.plot`: +The ``@_docstring.interpd`` decorator implements this. Any function accepting +`.Line2D` pass-through ``kwargs``, e.g., `matplotlib.axes.Axes.plot`, can list +a summary of the `.Line2D` properties, as follows: .. code-block:: python # in axes.py - @docstring.dedent_interpd + @_docstring.interpd def plot(self, *args, **kwargs): """ Some stuff omitted @@ -707,17 +714,19 @@ Then in any function accepting `~.Line2D` pass-through ``kwargs``, e.g., Here is a list of available `.Line2D` properties: - %(Line2D_kwdoc)s - + %(Line2D:kwdoc)s """ -Note there is a problem for `~matplotlib.artist.Artist` ``__init__`` methods, -e.g., `matplotlib.patches.Patch.__init__`, which supports ``Patch`` ``kwargs``, -since the artist inspector cannot work until the class is fully defined and -we can't modify the ``Patch.__init__.__doc__`` docstring outside the class -definition. There are some some manual hacks in this case, violating the -"single entry point" requirement above -- see the ``docstring.interpd.update`` -calls in `matplotlib.patches`. +The ``%(Line2D:kwdoc)`` syntax makes ``interpd`` lookup an `.Artist` subclass +named ``Line2D``, and call `.artist.kwdoc` on that class. `.artist.kwdoc` +introspects the subclass and summarizes its properties as a substring, which +gets interpolated into the docstring. + +Note that this scheme does not work for decorating an Artist's ``__init__``, as +the subclass and its properties are not defined yet at that point. Instead, +``@_docstring.interpd`` can be used to decorate the class itself -- at that +point, `.kwdoc` can list the properties and interpolate them into +``__init__.__doc__``. Inheriting docstrings @@ -748,7 +757,7 @@ Adding figures -------------- As above (see :ref:`rst-figures-and-includes`), figures in the examples gallery -can be referenced with a ``:plot:`` directive pointing to the python script +can be referenced with a ``.. plot::`` directive pointing to the python script that created the figure. For instance the `~.Axes.legend` docstring references the file :file:`examples/text_labels_and_annotations/legend.py`: @@ -836,7 +845,7 @@ render as comments in :doc:`/gallery/lines_bars_and_markers/simple_plot`. Tutorials are made with the exact same mechanism, except they are longer, and typically have more than one comment block (i.e. -:doc:`/tutorials/introductory/usage`). The first comment block +:doc:`/tutorials/introductory/quick_start`). The first comment block can be the same as the example above. Subsequent blocks of ReST text are delimited by a line of ``###`` characters: @@ -947,63 +956,20 @@ will yield an html file ``/build/html/old_topic/old_info2.html`` that has a (relative) refresh to ``../topic/new_info.html``. Use the full path for this directive, relative to the doc root at -``http://matplotlib.org/stable/``. So ``/old_topic/old_info2`` would be +``https://matplotlib.org/stable/``. So ``/old_topic/old_info2`` would be found by users at ``http://matplotlib.org/stable/old_topic/old_info2``. For clarity, do not use relative links. -Adding animations ------------------ - -Animations are scraped automatically by Sphinx-gallery. If this is not -desired, -there is also a Matplotlib Google/Gmail account with username ``mplgithub`` -which was used to setup the github account but can be used for other -purposes, like hosting Google docs or Youtube videos. You can embed a -Matplotlib animation in the docs by first saving the animation as a -movie using :meth:`matplotlib.animation.Animation.save`, and then -uploading to `Matplotlib's Youtube -channel `_ and inserting the -embedding string youtube provides like: - -.. code-block:: rst - - .. raw:: html - - - -An example save command to generate a movie looks like this - -.. code-block:: python - - ani = animation.FuncAnimation(fig, animate, np.arange(1, len(y)), - interval=25, blit=True, init_func=init) - - ani.save('double_pendulum.mp4', fps=15) - -Contact Michael Droettboom for the login password to upload youtube videos of -google docs to the mplgithub account. - .. _inheritance-diagrams: Generating inheritance diagrams ------------------------------- -Class inheritance diagrams can be generated with the -``inheritance-diagram`` directive. To use it, provide the -directive with a number of class or module names (separated by -whitespace). If a module name is provided, all classes in that module -will be used. All of the ancestors of these classes will be included -in the inheritance diagram. +Class inheritance diagrams can be generated with the Sphinx +`inheritance-diagram`_ directive. -A single option is available: *parts* controls how many of parts in -the path to the class are shown. For example, if *parts* == 1, the -class ``matplotlib.patches.Patch`` is shown as ``Patch``. If *parts* -== 2, it is shown as ``patches.Patch``. If *parts* == 0, the full -path is shown. +.. _inheritance-diagram: https://www.sphinx-doc.org/en/master/usage/extensions/inheritance.html Example: @@ -1015,46 +981,18 @@ Example: .. inheritance-diagram:: matplotlib.patches matplotlib.lines matplotlib.text :parts: 2 -.. _emacs-helpers: - -Emacs helpers -------------- - -There is an emacs mode `rst.el -`_ which -automates many important ReST tasks like building and updating -table-of-contents, and promoting or demoting section headings. Here -is the basic ``.emacs`` configuration: - -.. code-block:: lisp - - (require 'rst) - (setq auto-mode-alist - (append '(("\\.txt$" . rst-mode) - ("\\.rst$" . rst-mode) - ("\\.rest$" . rst-mode)) auto-mode-alist)) - -Some helpful functions:: - - C-c TAB - rst-toc-insert - - Insert table of contents at point - - C-c C-u - rst-toc-update - - Update the table of contents at point - - C-c C-l rst-shift-region-left - - Shift region to the left - C-c C-r rst-shift-region-right +Navbar and style +---------------- - Shift region to the right +Matplotlib has a few subprojects that share the same navbar and style, so these +are centralized as a sphinx theme at +`mpl_sphinx_theme `_. Changes to the +style or topbar should be made there to propagate across all subprojects. .. TODO: Add section about uploading docs -.. _ReST: http://docutils.sourceforge.net/rst.html +.. _ReST: https://docutils.sourceforge.io/rst.html .. _Sphinx: http://www.sphinx-doc.org .. _documentation: https://www.sphinx-doc.org/en/master/contents.html .. _index: http://www.sphinx-doc.org/markup/para.html#index-generating-markup diff --git a/doc/devel/gitwash/branch_dropdown.png b/doc/devel/gitwash/branch_dropdown.png deleted file mode 100644 index 1bb7a577732c..000000000000 Binary files a/doc/devel/gitwash/branch_dropdown.png and /dev/null differ diff --git a/doc/devel/gitwash/configure_git.rst b/doc/devel/gitwash/configure_git.rst deleted file mode 100644 index eb7ce2662c93..000000000000 --- a/doc/devel/gitwash/configure_git.rst +++ /dev/null @@ -1,172 +0,0 @@ -.. highlight:: bash - -.. _configure-git: - -=============== - Configure git -=============== - -.. _git-config-basic: - -Overview -======== - -Your personal git configurations are saved in the ``.gitconfig`` file in -your home directory. - -Here is an example ``.gitconfig`` file: - -.. code-block:: none - - [user] - name = Your Name - email = you@yourdomain.example.com - - [alias] - ci = commit -a - co = checkout - st = status - stat = status - br = branch - wdiff = diff --color-words - - [core] - editor = vim - - [merge] - summary = true - -You can check what is already in your config file using the ``git config --list`` command. You can edit the ``.gitconfig`` file directly or you can use the ``git config --global`` -command.:: - - git config --global user.name "Your Name" - git config --global user.email you@yourdomain.example.com - git config --global alias.ci "commit -a" - git config --global alias.co checkout - git config --global alias.st "status -a" - git config --global alias.stat "status -a" - git config --global alias.br branch - git config --global alias.wdiff "diff --color-words" - git config --global core.editor vim - git config --global merge.summary true - -To set up on another computer, you can copy your ``~/.gitconfig`` file, -or run the commands above. - -In detail -========= - -user.name and user.email ------------------------- - -It is good practice to tell git_ who you are, for labeling any changes -you make to the code. The simplest way to do this is from the command -line:: - - git config --global user.name "Your Name" - git config --global user.email you@yourdomain.example.com - -This will write the settings into your git configuration file, which -should now contain a user section with your name and email: - -.. code-block:: none - - [user] - name = Your Name - email = you@yourdomain.example.com - -You'll need to replace ``Your Name`` and ``you@yourdomain.example.com`` -with your actual name and email address. - -Aliases -------- - -You might well benefit from some aliases to common commands. - -For example, you might well want to be able to shorten ``git checkout`` -to ``git co``. Or you may want to alias ``git diff --color-words`` -(which gives a nicely formatted output of the diff) to ``git wdiff`` - -The following ``git config --global`` commands:: - - git config --global alias.ci "commit -a" - git config --global alias.co checkout - git config --global alias.st "status -a" - git config --global alias.stat "status -a" - git config --global alias.br branch - git config --global alias.wdiff "diff --color-words" - -will create an ``alias`` section in your ``.gitconfig`` file with contents -like this: - -.. code-block:: none - - [alias] - ci = commit -a - co = checkout - st = status -a - stat = status -a - br = branch - wdiff = diff --color-words - -Editor ------- - -You may also want to make sure that your editor of choice is used :: - - git config --global core.editor vim - -Merging -------- - -To enforce summaries when doing merges (``~/.gitconfig`` file again): - -.. code-block:: none - - [merge] - log = true - -Or from the command line:: - - git config --global merge.log true - -.. _fancy-log: - -Fancy log output ----------------- - -This is a very nice alias to get a fancy log output; it should go in the -``alias`` section of your ``.gitconfig`` file: - -.. code-block:: none - - lg = log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)[%an]%Creset' --abbrev-commit --date=relative - -You use the alias with:: - - git lg - -and it gives graph / text output something like this (but with color!): - -.. code-block:: none - - * 6d8e1ee - (HEAD, origin/my-fancy-feature, my-fancy-feature) NF - a fancy file (45 minutes ago) [Matthew Brett] - * d304a73 - (origin/placeholder, placeholder) Merge pull request #48 from hhuuggoo/master (2 weeks ago) [Jonathan Terhorst] - |\ - | * 4aff2a8 - fixed bug 35, and added a test in test_bugfixes (2 weeks ago) [Hugo] - |/ - * a7ff2e5 - Added notes on discussion/proposal made during Data Array Summit. (2 weeks ago) [Corran Webster] - * 68f6752 - Initial implementation of AxisIndexer - uses 'index_by' which needs to be changed to a call on an Axes object - this is all very sketchy right now. (2 weeks ago) [Corr - * 376adbd - Merge pull request #46 from terhorst/master (2 weeks ago) [Jonathan Terhorst] - |\ - | * b605216 - updated joshu example to current api (3 weeks ago) [Jonathan Terhorst] - | * 2e991e8 - add testing for outer ufunc (3 weeks ago) [Jonathan Terhorst] - | * 7beda5a - prevent axis from throwing an exception if testing equality with non-axis object (3 weeks ago) [Jonathan Terhorst] - | * 65af65e - convert unit testing code to assertions (3 weeks ago) [Jonathan Terhorst] - | * 956fbab - Merge remote-tracking branch 'upstream/master' (3 weeks ago) [Jonathan Terhorst] - | |\ - | |/ - -Thanks to Yury V. Zaytsev for posting it. - -.. include:: links.inc diff --git a/doc/devel/gitwash/development_workflow.rst b/doc/devel/gitwash/development_workflow.rst deleted file mode 100644 index 7d6b6f0e70c3..000000000000 --- a/doc/devel/gitwash/development_workflow.rst +++ /dev/null @@ -1,423 +0,0 @@ -.. highlight:: bash - -.. _development-workflow: - -#################### -Development workflow -#################### - -You already have your own forked copy of the `Matplotlib`_ repository, by -following :ref:`forking`. You have :ref:`set-up-fork`. You have configured -git by following :ref:`configure-git`. Now you are ready for some real work. - -Workflow summary -================ - -In what follows we'll refer to the upstream Matplotlib ``master`` branch, as -"trunk". - -* Don't use your ``master`` branch for anything. Consider deleting it. -* When you are starting a new set of changes, fetch any changes from trunk, - and start a new *feature branch* from that. -* Make a new branch for each separable set of changes |emdash| "one task, one - branch" (`ipython git workflow`_). -* Name your branch for the purpose of the changes - e.g. - ``bugfix-for-issue-14`` or ``refactor-database-code``. -* If you can possibly avoid it, avoid merging trunk or any other branches into - your feature branch while you are working. -* If you do find yourself merging from trunk, consider :ref:`rebase-on-trunk` -* Ask on the `Matplotlib mailing list`_ if you get stuck. -* Ask for code review! - -This way of working helps to keep work well organized, with readable history. -This in turn makes it easier for project maintainers (that might be you) to see -what you've done, and why you did it. - -See `linux git workflow`_ and `ipython git workflow`_ for some explanation. - -Consider deleting your master branch -==================================== - -It may sound strange, but deleting your own ``master`` branch can help reduce -confusion about which branch you are on. See `deleting master on github`_ for -details. - -.. _update-mirror-trunk: - -Update the mirror of trunk -========================== - -First make sure you have done :ref:`linking-to-upstream`. - -From time to time you should fetch the upstream (trunk) changes from github:: - - git fetch upstream - -This will pull down any commits you don't have, and set the remote branches to -point to the right commit. For example, 'trunk' is the branch referred to by -(remote/branchname) ``upstream/master`` - and if there have been commits since -you last checked, ``upstream/master`` will change after you do the fetch. - -.. _make-feature-branch: - -Make a new feature branch -========================= - -When you are ready to make some changes to the code, you should start a new -branch. Branches that are for a collection of related edits are often called -'feature branches'. - -Making an new branch for each set of related changes will make it easier for -someone reviewing your branch to see what you are doing. - -Choose an informative name for the branch to remind yourself and the rest of us -what the changes in the branch are for. For example ``add-ability-to-fly``, or -``buxfix-for-issue-42``. - -:: - - # Update the mirror of trunk - git fetch upstream - # Make new feature branch starting at current trunk - git branch my-new-feature upstream/master - git checkout my-new-feature - -Generally, you will want to keep your feature branches on your public github_ -fork of `Matplotlib`_. To do this, you `git push`_ this new branch up to your -github repo. Generally (if you followed the instructions in these pages, and by -default), git will have a link to your github repo, called ``origin``. You push -up to your own repo on github with:: - - git push origin my-new-feature - -In git >= 1.7 you can ensure that the link is correctly set by using the -``--set-upstream`` option:: - - git push --set-upstream origin my-new-feature - -From now on git will know that ``my-new-feature`` is related to the -``my-new-feature`` branch in the github repo. - -.. _edit-flow: - -The editing workflow -==================== - -Overview --------- - -:: - - # hack hack - git add my_new_file - git commit -am 'NF - some message' - git push - -In more detail --------------- - -#. Make some changes -#. See which files have changed with ``git status`` (see `git status`_). - You'll see a listing like this one: - - .. code-block:: none - - # On branch ny-new-feature - # Changed but not updated: - # (use "git add ..." to update what will be committed) - # (use "git checkout -- ..." to discard changes in working directory) - # - # modified: README - # - # Untracked files: - # (use "git add ..." to include in what will be committed) - # - # INSTALL - no changes added to commit (use "git add" and/or "git commit -a") - -#. Check what the actual changes are with ``git diff`` (`git diff`_). -#. Add any new files to version control ``git add new_file_name`` (see - `git add`_). -#. To commit all modified files into the local copy of your repo,, do - ``git commit -am 'A commit message'``. Note the ``-am`` options to - ``commit``. The ``m`` flag just signals that you're going to type a - message on the command line. The ``a`` flag |emdash| you can just take on - faith |emdash| or see `why the -a flag?`_ |emdash| and the helpful use-case - description in the `tangled working copy problem`_. The `git commit`_ manual - page might also be useful. -#. To push the changes up to your forked repo on github, do a ``git - push`` (see `git push`_). - -Ask for your changes to be reviewed or merged -============================================= - -When you are ready to ask for someone to review your code and consider a merge: - -#. Go to the URL of your forked repo, say - ``https://github.com/your-user-name/matplotlib``. -#. Use the 'Switch Branches' dropdown menu near the top left of the page to - select the branch with your changes: - - .. image:: branch_dropdown.png - -#. Click on the 'Pull request' button: - - .. image:: pull_button.png - - Enter a title for the set of changes, and some explanation of what you've - done. Say if there is anything you'd like particular attention for - like a - complicated change or some code you are not happy with. - - If you don't think your request is ready to be merged, just say so in your - pull request message. This is still a good way of getting some preliminary - code review. - -Some other things you might want to do -====================================== - -Delete a branch on github -------------------------- - -:: - - git checkout master - # delete branch locally - git branch -D my-unwanted-branch - # delete branch on github - git push origin :my-unwanted-branch - -Note the colon ``:`` before ``my-unwanted-branch``. See also: -https://help.github.com/articles/pushing-to-a-remote/#deleting-a-remote-branch-or-tag - -Several people sharing a single repository ------------------------------------------- - -If you want to work on some stuff with other people, where you are all -committing into the same repository, or even the same branch, then just -share it via github. - -First fork Matplotlib into your account, as from :ref:`forking`. - -Then, go to your forked repository github page, say -``https://github.com/your-user-name/matplotlib`` - -Click on the 'Admin' button, and add anyone else to the repo as a -collaborator: - - .. image:: pull_button.png - -Now all those people can do:: - - git clone https://github.com/your-user-name/matplotlib.git - -Remember that links starting with ``https`` or ``git@`` are read-write, and that -``git@`` uses the ssh protocol; links starting with ``git://`` are read-only. - -Your collaborators can then commit directly into that repo with the -usual:: - - git commit -am 'ENH - much better code' - git push origin master # pushes directly into your repo - -Explore your repository ------------------------ - -To see a graphical representation of the repository branches and -commits:: - - gitk --all - -To see a linear list of commits for this branch:: - - git log - -You can also look at the `network graph visualizer`_ for your github -repo. - -Finally the :ref:`fancy-log` ``lg`` alias will give you a reasonable text-based -graph of the repository. - -.. _rebase-on-trunk: - -Rebasing on trunk ------------------ - -Let's say you thought of some work you'd like to do. You -:ref:`update-mirror-trunk` and :ref:`make-feature-branch` called -``cool-feature``. At this stage trunk is at some commit, let's call it E. Now -you make some new commits on your ``cool-feature`` branch, let's call them A, B, -C. Maybe your changes take a while, or you come back to them after a while. In -the meantime, trunk has progressed from commit E to commit (say) G: - -.. code-block:: none - - A---B---C cool-feature - / - D---E---F---G trunk - -At this stage you consider merging trunk into your feature branch, and you -remember that this here page sternly advises you not to do that, because the -history will get messy. Most of the time you can just ask for a review, and not -worry that trunk has got a little ahead. But sometimes, the changes in trunk -might affect your changes, and you need to harmonize them. In this situation -you may prefer to do a rebase. - -rebase takes your changes (A, B, C) and replays them as if they had been made to -the current state of ``trunk``. In other words, in this case, it takes the -changes represented by A, B, C and replays them on top of G. After the rebase, -your history will look like this: - -.. code-block:: none - - A'--B'--C' cool-feature - / - D---E---F---G trunk - -See `rebase without tears`_ for more detail. - -To do a rebase on trunk:: - - # Update the mirror of trunk - git fetch upstream - # go to the feature branch - git checkout cool-feature - # make a backup in case you mess up - git branch tmp cool-feature - # rebase cool-feature onto trunk - git rebase --onto upstream/master upstream/master cool-feature - -In this situation, where you are already on branch ``cool-feature``, the last -command can be written more succinctly as:: - - git rebase upstream/master - -When all looks good you can delete your backup branch:: - - git branch -D tmp - -If it doesn't look good you may need to have a look at -:ref:`recovering-from-mess-up`. - -If you have made changes to files that have also changed in trunk, this may -generate merge conflicts that you need to resolve - see the `git rebase`_ man -page for some instructions at the end of the "Description" section. There is -some related help on merging in the git user manual - see `resolving a merge`_. - -.. _recovering-from-mess-up: - -Recovering from mess-ups ------------------------- - -Sometimes, you mess up merges or rebases. Luckily, in git it is -relatively straightforward to recover from such mistakes. - -If you mess up during a rebase:: - - git rebase --abort - -If you notice you messed up after the rebase:: - - # reset branch back to the saved point - git reset --hard tmp - -If you forgot to make a backup branch:: - - # look at the reflog of the branch - git reflog show cool-feature - - 8630830 cool-feature@{0}: commit: BUG: io: close file handles immediately - 278dd2a cool-feature@{1}: rebase finished: refs/heads/my-feature-branch onto 11ee694744f2552d - 26aa21a cool-feature@{2}: commit: BUG: lib: make seek_gzip_factory not leak gzip obj - ... - - # reset the branch to where it was before the botched rebase - git reset --hard cool-feature@{2} - -.. _rewriting-commit-history: - -Rewriting commit history ------------------------- - -.. note:: - - Do this only for your own feature branches. - -There's an embarrassing typo in a commit you made? Or perhaps the you -made several false starts you would like the posterity not to see. - -This can be done via *interactive rebasing*. - -Suppose that the commit history looks like this:: - - git log --oneline - eadc391 Fix some remaining bugs - a815645 Modify it so that it works - 2dec1ac Fix a few bugs + disable - 13d7934 First implementation - 6ad92e5 * masked is now an instance of a new object, MaskedConstant - 29001ed Add pre-nep for a copule of structured_array_extensions. - ... - -and ``6ad92e5`` is the last commit in the ``cool-feature`` branch. Suppose we -want to make the following changes: - -* Rewrite the commit message for ``13d7934`` to something more sensible. -* Combine the commits ``2dec1ac``, ``a815645``, ``eadc391`` into a single one. - -We do as follows:: - - # make a backup of the current state - git branch tmp HEAD - # interactive rebase - git rebase -i 6ad92e5 - -This will open an editor with the following text in it:: - - pick 13d7934 First implementation - pick 2dec1ac Fix a few bugs + disable - pick a815645 Modify it so that it works - pick eadc391 Fix some remaining bugs - - # Rebase 6ad92e5..eadc391 onto 6ad92e5 - # - # Commands: - # p, pick = use commit - # r, reword = use commit, but edit the commit message - # e, edit = use commit, but stop for amending - # s, squash = use commit, but meld into previous commit - # f, fixup = like "squash", but discard this commit's log message - # - # If you remove a line here THAT COMMIT WILL BE LOST. - # However, if you remove everything, the rebase will be aborted. - # - -To achieve what we want, we will make the following changes to it:: - - r 13d7934 First implementation - pick 2dec1ac Fix a few bugs + disable - f a815645 Modify it so that it works - f eadc391 Fix some remaining bugs - -This means that (i) we want to edit the commit message for -``13d7934``, and (ii) collapse the last three commits into one. Now we -save and quit the editor. - -Git will then immediately bring up an editor for editing the commit -message. After revising it, we get the output:: - - [detached HEAD 721fc64] FOO: First implementation - 2 files changed, 199 insertions(+), 66 deletions(-) - [detached HEAD 0f22701] Fix a few bugs + disable - 1 files changed, 79 insertions(+), 61 deletions(-) - Successfully rebased and updated refs/heads/my-feature-branch. - -and the history looks now like this:: - - 0f22701 Fix a few bugs + disable - 721fc64 ENH: Sophisticated feature - 6ad92e5 * masked is now an instance of a new object, MaskedConstant - -If it went wrong, recovery is again possible as explained :ref:`above -`. - -.. include:: links.inc diff --git a/doc/devel/gitwash/dot2_dot3.rst b/doc/devel/gitwash/dot2_dot3.rst deleted file mode 100644 index 7759e2e60d68..000000000000 --- a/doc/devel/gitwash/dot2_dot3.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. _dot2-dot3: - -======================================== - Two and three dots in difference specs -======================================== - -Thanks to Yarik Halchenko for this explanation. - -Imagine a series of commits A, B, C, D... Imagine that there are two -branches, *topic* and *master*. You branched *topic* off *master* when -*master* was at commit 'E'. The graph of the commits looks like this:: - - - A---B---C topic - / - D---E---F---G master - -Then:: - - git diff master..topic - -will output the difference from G to C (i.e. with effects of F and G), -while:: - - git diff master...topic - -would output just differences in the topic branch (i.e. only A, B, and -C). diff --git a/doc/devel/gitwash/following_latest.rst b/doc/devel/gitwash/following_latest.rst deleted file mode 100644 index 03518ea52f44..000000000000 --- a/doc/devel/gitwash/following_latest.rst +++ /dev/null @@ -1,38 +0,0 @@ -.. highlight:: bash - -.. _following-latest: - -============================= - Following the latest source -============================= - -These are the instructions if you just want to follow the latest -*Matplotlib* source, but you don't need to do any development for now. - -The steps are: - -* :ref:`install-git` -* get local copy of the `Matplotlib github`_ git repository -* update local copy from time to time - -Get the local copy of the code -============================== - -From the command line:: - - git clone git://github.com/matplotlib/matplotlib.git - -You now have a copy of the code tree in the new ``matplotlib`` directory. - -Updating the code -================= - -From time to time you may want to pull down the latest code. Do this with:: - - cd matplotlib - git pull - -The tree in ``matplotlib`` will now have the latest changes from the initial -repository. - -.. include:: links.inc diff --git a/doc/devel/gitwash/forking_button.png b/doc/devel/gitwash/forking_button.png deleted file mode 100644 index d0e04134d4d0..000000000000 Binary files a/doc/devel/gitwash/forking_button.png and /dev/null differ diff --git a/doc/devel/gitwash/forking_hell.rst b/doc/devel/gitwash/forking_hell.rst deleted file mode 100644 index b79e13400a62..000000000000 --- a/doc/devel/gitwash/forking_hell.rst +++ /dev/null @@ -1,34 +0,0 @@ -.. highlight:: bash - -.. _forking: - -====================================================== -Making your own copy (fork) of Matplotlib -====================================================== - -You need to do this only once. The instructions here are very similar -to the instructions at https://help.github.com/forking/ |emdash| please see -that page for more detail. We're repeating some of it here just to give the -specifics for the `Matplotlib`_ project, and to suggest some default names. - -Set up and configure a github account -===================================== - -If you don't have a github account, go to the github page, and make one. - -You then need to configure your account to allow write access |emdash| see -the ``Generating SSH keys`` help on `github help`_. - -Create your own forked copy of `Matplotlib`_ -====================================================== - -#. Log into your github account. -#. Go to the `Matplotlib`_ github home at `Matplotlib github`_. -#. Click on the *fork* button: - - .. image:: forking_button.png - - Now, after a short pause, you should find yourself at the home page for - your own forked copy of `Matplotlib`_. - -.. include:: links.inc diff --git a/doc/devel/gitwash/git_development.rst b/doc/devel/gitwash/git_development.rst deleted file mode 100644 index c5b910d86342..000000000000 --- a/doc/devel/gitwash/git_development.rst +++ /dev/null @@ -1,16 +0,0 @@ -.. _git-development: - -===================== - Git for development -===================== - -Contents: - -.. toctree:: - :maxdepth: 2 - - forking_hell - set_up_fork - configure_git - development_workflow - maintainer_workflow diff --git a/doc/devel/gitwash/git_install.rst b/doc/devel/gitwash/git_install.rst deleted file mode 100644 index 66eca8c29bde..000000000000 --- a/doc/devel/gitwash/git_install.rst +++ /dev/null @@ -1,28 +0,0 @@ -.. highlight:: bash - -.. _install-git: - -============= - Install git -============= - -Overview -======== - -================ ============= -Debian / Ubuntu ``sudo apt-get install git`` -Fedora ``sudo yum install git`` -Windows Download and install msysGit_ -OS X Use the git-osx-installer_ -================ ============= - -In detail -========= - -See the git page for the most recent information. - -Have a look at the github install help pages available from `github help`_ - -There are good instructions here: https://git-scm.com/book/en/v2/Getting-Started-Installing-Git - -.. include:: links.inc diff --git a/doc/devel/gitwash/git_intro.rst b/doc/devel/gitwash/git_intro.rst deleted file mode 100644 index 1f89b7e9fb51..000000000000 --- a/doc/devel/gitwash/git_intro.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. highlight:: bash - -============== - Introduction -============== - -These pages describe a git_ and github_ workflow for the `Matplotlib`_ -project. - -There are several different workflows here, for different ways of -working with *Matplotlib*. - -This is not a comprehensive git reference, it's just a workflow for our -own project. It's tailored to the github hosting service. You may well -find better or quicker ways of getting stuff done with git, but these -should get you started. - -For general resources for learning git, see :ref:`git-resources`. - -.. include:: links.inc diff --git a/doc/devel/gitwash/git_links.inc b/doc/devel/gitwash/git_links.inc deleted file mode 100644 index c26173367c9b..000000000000 --- a/doc/devel/gitwash/git_links.inc +++ /dev/null @@ -1,59 +0,0 @@ -.. This (-*- rst -*-) format file contains commonly used link targets - and name substitutions. It may be included in many files, - therefore it should only contain link targets and name - substitutions. Try grepping for "^\.\. _" to find plausible - candidates for this list. - -.. NOTE: reST targets are - __not_case_sensitive__, so only one target definition is needed for - nipy, NIPY, Nipy, etc... - -.. git stuff -.. _git: https://git-scm.com/ -.. _github: https://github.com -.. _github help: https://help.github.com -.. _msysgit: https://git-scm.com/download/win -.. _git-osx-installer: https://git-scm.com/download/mac -.. _subversion: https://subversion.apache.org/ -.. _git cheat sheet: https://help.github.com/git-cheat-sheets/ -.. _pro git book: https://git-scm.com/book/en/v2 -.. _git svn crash course: https://git-scm.com/course/svn.html -.. _network graph visualizer: https://github.com/blog/39-say-hello-to-the-network-graph-visualizer -.. _git user manual: https://schacon.github.io/git/user-manual.html -.. _git tutorial: https://schacon.github.io/git/gittutorial.html -.. _git community book: https://git-scm.com/book/en/v2 -.. _git ready: http://gitready.com/ -.. _Fernando's git page: http://www.fperez.org/py4science/git.html -.. _git magic: http://www-cs-students.stanford.edu/~blynn/gitmagic/index.html -.. _git concepts: https://www.sbf5.com/~cduan/technical/git/ -.. _git clone: https://schacon.github.io/git/git-clone.html -.. _git checkout: https://schacon.github.io/git/git-checkout.html -.. _git commit: https://schacon.github.io/git/git-commit.html -.. _git push: https://schacon.github.io/git/git-push.html -.. _git pull: https://schacon.github.io/git/git-pull.html -.. _git add: https://schacon.github.io/git/git-add.html -.. _git status: https://schacon.github.io/git/git-status.html -.. _git diff: https://schacon.github.io/git/git-diff.html -.. _git log: https://schacon.github.io/git/git-log.html -.. _git branch: https://schacon.github.io/git/git-branch.html -.. _git remote: https://schacon.github.io/git/git-remote.html -.. _git rebase: https://schacon.github.io/git/git-rebase.html -.. _git config: https://schacon.github.io/git/git-config.html -.. _why the -a flag?: http://gitready.com/beginner/2009/01/18/the-staging-area.html -.. _git staging area: http://gitready.com/beginner/2009/01/18/the-staging-area.html -.. _tangled working copy problem: http://2ndscale.com/rtomayko/2008/the-thing-about-git -.. _git management: https://web.archive.org/web/20090224195437/http://kerneltrap.org/Linux/Git_Management -.. _linux git workflow: https://www.mail-archive.com/dri-devel@lists.sourceforge.net/msg39091.html -.. _git parable: http://tom.preston-werner.com/2009/05/19/the-git-parable.html -.. _git foundation: https://matthew-brett.github.io/pydagogue/foundation.html -.. _deleting master on github: https://matthew-brett.github.io/pydagogue/gh_delete_master.html -.. _rebase without tears: https://matthew-brett.github.io/pydagogue/rebase_without_tears.html -.. _resolving a merge: https://schacon.github.io/git/user-manual.html#resolving-a-merge -.. _ipython git workflow: https://mail.python.org/pipermail/ipython-dev/2010-October/005632.html - -.. other stuff -.. _python: https://www.python.org - -.. |emdash| unicode:: U+02014 - -.. vim: ft=rst diff --git a/doc/devel/gitwash/git_resources.rst b/doc/devel/gitwash/git_resources.rst deleted file mode 100644 index 2787a575cc43..000000000000 --- a/doc/devel/gitwash/git_resources.rst +++ /dev/null @@ -1,59 +0,0 @@ -.. highlight:: bash - -.. _git-resources: - -============= -git resources -============= - -Tutorials and summaries -======================= - -* `github help`_ has an excellent series of how-to guides. -* The `pro git book`_ is a good in-depth book on git. -* A `git cheat sheet`_ is a page giving summaries of common commands. -* The `git user manual`_ -* The `git tutorial`_ -* The `git community book`_ -* `git ready`_ |emdash| a nice series of tutorials -* `git magic`_ |emdash| extended introduction with intermediate detail -* The `git parable`_ is an easy read explaining the concepts behind git. -* `git foundation`_ expands on the `git parable`_. -* Fernando Perez' git page |emdash| `Fernando's git page`_ |emdash| many - links and tips -* A good but technical page on `git concepts`_ -* `git svn crash course`_: git for those of us used to subversion_ - -Advanced git workflow -===================== - -There are many ways of working with git; here are some posts on the -rules of thumb that other projects have come up with: - -* Linus Torvalds on `git management`_ -* Linus Torvalds on `linux git workflow`_ . Summary; use the git tools - to make the history of your edits as clean as possible; merge from - upstream edits as little as possible in branches where you are doing - active development. - -Manual pages online -=================== - -You can get these on your own machine with (e.g) ``git help push`` or -(same thing) ``git push --help``, but, for convenience, here are the -online manual pages for some common commands: - -* `git add`_ -* `git branch`_ -* `git checkout`_ -* `git clone`_ -* `git commit`_ -* `git config`_ -* `git diff`_ -* `git log`_ -* `git pull`_ -* `git push`_ -* `git remote`_ -* `git status`_ - -.. include:: links.inc diff --git a/doc/devel/gitwash/index.rst b/doc/devel/gitwash/index.rst deleted file mode 100644 index 9ee965d626ff..000000000000 --- a/doc/devel/gitwash/index.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. _using-git: - -Working with *Matplotlib* source code -================================================ - -Contents: - -.. toctree:: - :maxdepth: 2 - - git_intro - git_install - following_latest - patching - git_development - git_resources - dot2_dot3 - - diff --git a/doc/devel/gitwash/known_projects.inc b/doc/devel/gitwash/known_projects.inc deleted file mode 100644 index 710abe08e477..000000000000 --- a/doc/devel/gitwash/known_projects.inc +++ /dev/null @@ -1,41 +0,0 @@ -.. Known projects - -.. PROJECTNAME placeholders -.. _PROJECTNAME: http://nipy.org -.. _`PROJECTNAME github`: https://github.com/nipy -.. _`PROJECTNAME mailing list`: https://mail.python.org/mailman/listinfo/neuroimaging - -.. numpy -.. _numpy: http://www.numpy.org -.. _`numpy github`: https://github.com/numpy/numpy -.. _`numpy mailing list`: https://mail.scipy.org/mailman/listinfo/numpy-discussion - -.. scipy -.. _scipy: https://www.scipy.org -.. _`scipy github`: https://github.com/scipy/scipy -.. _`scipy mailing list`: https://mail.scipy.org/mailman/listinfo/scipy-dev - -.. nipy -.. _nipy: http://nipy.org/nipy/ -.. _`nipy github`: https://github.com/nipy/nipy -.. _`nipy mailing list`: https://mail.python.org/mailman/listinfo/neuroimaging - -.. ipython -.. _ipython: https://ipython.org -.. _`ipython github`: https://github.com/ipython/ipython -.. _`ipython mailing list`: https://mail.scipy.org/mailman/listinfo/IPython-dev - -.. dipy -.. _dipy: http://nipy.org/dipy/ -.. _`dipy github`: https://github.com/Garyfallidis/dipy -.. _`dipy mailing list`: https://mail.python.org/mailman/listinfo/neuroimaging - -.. nibabel -.. _nibabel: http://nipy.org/nibabel/ -.. _`nibabel github`: https://github.com/nipy/nibabel -.. _`nibabel mailing list`: https://mail.python.org/mailman/listinfo/neuroimaging - -.. marsbar -.. _marsbar: http://marsbar.sourceforge.net -.. _`marsbar github`: https://github.com/matthew-brett/marsbar -.. _`MarsBaR mailing list`: https://lists.sourceforge.net/lists/listinfo/marsbar-users diff --git a/doc/devel/gitwash/links.inc b/doc/devel/gitwash/links.inc deleted file mode 100644 index 20f4dcfffd4a..000000000000 --- a/doc/devel/gitwash/links.inc +++ /dev/null @@ -1,4 +0,0 @@ -.. compiling links file -.. include:: known_projects.inc -.. include:: this_project.inc -.. include:: git_links.inc diff --git a/doc/devel/gitwash/patching.rst b/doc/devel/gitwash/patching.rst deleted file mode 100644 index e7f852758477..000000000000 --- a/doc/devel/gitwash/patching.rst +++ /dev/null @@ -1,138 +0,0 @@ -.. highlight:: bash - -================ - Making a patch -================ - -You've discovered a bug or something else you want to change -in `Matplotlib`_ .. |emdash| excellent! - -You've worked out a way to fix it |emdash| even better! - -You want to tell us about it |emdash| best of all! - -The easiest way is to make a *patch* or set of patches. Here -we explain how. Making a patch is the simplest and quickest, -but if you're going to be doing anything more than simple -quick things, please consider following the -:ref:`git-development` model instead. - -.. _making-patches: - -Making patches -============== - -Overview --------- - -:: - - # tell git who you are - git config --global user.email you@yourdomain.example.com - git config --global user.name "Your Name Comes Here" - # get the repository if you don't have it - git clone git://github.com/matplotlib/matplotlib.git - # make a branch for your patching - cd matplotlib - git branch the-fix-im-thinking-of - git checkout the-fix-im-thinking-of - # hack, hack, hack - # Tell git about any new files you've made - git add somewhere/tests/test_my_bug.py - # commit work in progress as you go - git commit -am 'BF - added tests for Funny bug' - # hack hack, hack - git commit -am 'BF - added fix for Funny bug' - # make the patch files - git format-patch -M -C master - -Then, send the generated patch files to the `Matplotlib -mailing list`_ |emdash| where we will thank you warmly. - -In detail ---------- - -#. Tell git who you are so it can label the commits you've - made:: - - git config --global user.email you@yourdomain.example.com - git config --global user.name "Your Name Comes Here" - -#. If you don't already have one, clone a copy of the - `Matplotlib`_ repository:: - - git clone git://github.com/matplotlib/matplotlib.git - cd matplotlib - -#. Make a 'feature branch'. This will be where you work on - your bug fix. It's nice and safe and leaves you with - access to an unmodified copy of the code in the main - branch:: - - git branch the-fix-im-thinking-of - git checkout the-fix-im-thinking-of - -#. Do some edits, and commit them as you go:: - - # hack, hack, hack - # Tell git about any new files you've made - git add somewhere/tests/test_my_bug.py - # commit work in progress as you go - git commit -am 'BF - added tests for Funny bug' - # hack hack, hack - git commit -am 'BF - added fix for Funny bug' - - Note the ``-am`` options to ``commit``. The ``m`` flag just - signals that you're going to type a message on the command - line. The ``a`` flag |emdash| you can just take on faith |emdash| - or see `why the -a flag?`_. - -#. When you have finished, check you have committed all your - changes:: - - git status - -#. Finally, make your commits into patches. You want all the - commits since you branched from the ``master`` branch:: - - git format-patch -M -C master - - You will now have several files named for the commits: - - .. code-block:: none - - 0001-BF-added-tests-for-Funny-bug.patch - 0002-BF-added-fix-for-Funny-bug.patch - - Send these files to the `Matplotlib mailing list`_. - -When you are done, to switch back to the main copy of the -code, just return to the ``master`` branch:: - - git checkout master - -Moving from patching to development -=================================== - -If you find you have done some patches, and you have one or -more feature branches, you will probably want to switch to -development mode. You can do this with the repository you -have. - -Fork the `Matplotlib`_ repository on github |emdash| :ref:`forking`. -Then:: - - # checkout and refresh master branch from main repo - git checkout master - git pull origin master - # rename pointer to main repository to 'upstream' - git remote rename origin upstream - # point your repo to default read / write to your fork on github - git remote add origin git@github.com:your-user-name/matplotlib.git - # push up any branches you've made and want to keep - git push origin the-fix-im-thinking-of - -Then you can, if you want, follow the -:ref:`development-workflow`. - -.. include:: links.inc diff --git a/doc/devel/gitwash/pull_button.png b/doc/devel/gitwash/pull_button.png deleted file mode 100644 index e5031681b97b..000000000000 Binary files a/doc/devel/gitwash/pull_button.png and /dev/null differ diff --git a/doc/devel/gitwash/set_up_fork.rst b/doc/devel/gitwash/set_up_fork.rst deleted file mode 100644 index 6b7e0271c45b..000000000000 --- a/doc/devel/gitwash/set_up_fork.rst +++ /dev/null @@ -1,73 +0,0 @@ -.. highlight:: bash - -.. _set-up-fork: - -================== - Set up your fork -================== - -First you follow the instructions for :ref:`forking`. - -Overview -======== - -:: - - git clone https://github.com/your-user-name/matplotlib.git - cd matplotlib - git remote add upstream git://github.com/matplotlib/matplotlib.git - -In detail -========= - -Clone your fork ---------------- - -#. Clone your fork to the local computer with ``git clone - https://github.com/your-user-name/matplotlib.git`` -#. Investigate. Change directory to your new repo: ``cd matplotlib``. Then - ``git branch -a`` to show you all branches. You'll get something - like: - - .. code-block:: none - - * master - remotes/origin/master - - This tells you that you are currently on the ``master`` branch, and - that you also have a ``remote`` connection to ``origin/master``. - What remote repository is ``remote/origin``? Try ``git remote -v`` to - see the URLs for the remote. They will point to your github fork. - - Now you want to connect to the upstream `Matplotlib github`_ repository, so - you can merge in changes from trunk. - -.. _linking-to-upstream: - -Linking your repository to the upstream repo --------------------------------------------- - -:: - - cd matplotlib - git remote add upstream git://github.com/matplotlib/matplotlib.git - -``upstream`` here is just the arbitrary name we're using to refer to the -main `Matplotlib`_ repository at `Matplotlib github`_. - -Note that we've used ``git://`` for the URL rather than ``https://`` or ``git@``. The -``git://`` URL is read only. This means that we can't accidentally -(or deliberately) write to the upstream repo, and we are only going to -use it to merge into our own code. - -Just for your own satisfaction, show yourself that you now have a new -'remote', with ``git remote -v show``, giving you something like: - -.. code-block:: none - - upstream git://github.com/matplotlib/matplotlib.git (fetch) - upstream git://github.com/matplotlib/matplotlib.git (push) - origin https://github.com/your-user-name/matplotlib.git (fetch) - origin https://github.com/your-user-name/matplotlib.git (push) - -.. include:: links.inc diff --git a/doc/devel/gitwash/this_project.inc b/doc/devel/gitwash/this_project.inc deleted file mode 100644 index e8863d5f78f0..000000000000 --- a/doc/devel/gitwash/this_project.inc +++ /dev/null @@ -1,5 +0,0 @@ -.. Matplotlib -.. _`Matplotlib`: http://matplotlib.org -.. _`Matplotlib github`: https://github.com/matplotlib/matplotlib - -.. _`Matplotlib mailing list`: https://mail.python.org/mailman/listinfo/matplotlib-devel diff --git a/doc/devel/index.rst b/doc/devel/index.rst index c2c140173227..e0fc978a6659 100644 --- a/doc/devel/index.rst +++ b/doc/devel/index.rst @@ -1,8 +1,8 @@ .. _developers-guide-index: -################################ -The Matplotlib Developers' Guide -################################ +############ +Contributing +############ Thank you for your interest in helping to improve Matplotlib! There are various ways to contribute to Matplotlib. All of them are super valuable but don't necessarily @@ -19,14 +19,37 @@ process or how to fix something feel free to ask on `gitter `_ for short questions and on `discourse `_ for longer questions. -.. raw:: html +.. rst-class:: sd-d-inline-block - + .. button-ref:: submitting-a-bug-report + :class: sd-fs-6 + :color: primary + + Report a bug + +.. rst-class:: sd-d-inline-block + + .. button-ref:: request-a-new-feature + :class: sd-fs-6 + :color: primary + + Request a feature + +.. rst-class:: sd-d-inline-block + + .. button-ref:: contributing-code + :class: sd-fs-6 + :color: primary + + Contribute code + +.. rst-class:: sd-d-inline-block + + .. button-ref:: documenting-matplotlib + :class: sd-fs-6 + :color: primary + + Write documentation .. toctree:: :maxdepth: 2 @@ -34,14 +57,15 @@ process or how to fix something feel free to ask on `gitter contributing.rst triage.rst development_setup.rst + development_workflow.rst testing.rst documenting_mpl.rst - add_new_projection.rst - gitwash/index.rst + style_guide.rst coding_guide.rst release_guide.rst dependencies.rst min_dep_policy.rst + maintainer_workflow.rst MEP/index .. toctree:: diff --git a/doc/devel/gitwash/maintainer_workflow.rst b/doc/devel/maintainer_workflow.rst similarity index 83% rename from doc/devel/gitwash/maintainer_workflow.rst rename to doc/devel/maintainer_workflow.rst index 302f75241399..7723e6cb8c0c 100644 --- a/doc/devel/gitwash/maintainer_workflow.rst +++ b/doc/devel/maintainer_workflow.rst @@ -6,7 +6,7 @@ Maintainer workflow ################### -This page is for maintainers |emdash| those of us who merge our own or other +This page is for maintainers — those of us who merge our own or other peoples' changes into the upstream repository. Being as how you're a maintainer, you are completely on top of the basic stuff @@ -26,12 +26,12 @@ Integrating changes ******************* Let's say you have some changes that need to go into trunk -(``upstream-rw/master``). +(``upstream-rw/main``). The changes are in some branch that you are currently on. For example, you are looking at someone's changes like this:: - git remote add someone git://github.com/someone/matplotlib.git + git remote add someone https://github.com/someone/matplotlib.git git fetch someone git branch cool-feature --track someone/cool-feature git checkout cool-feature @@ -47,10 +47,10 @@ If there are only a few commits, consider rebasing to upstream:: # Fetch upstream changes git fetch upstream-rw # rebase - git rebase upstream-rw/master + git rebase upstream-rw/main Remember that, if you do a rebase, and push that, you'll have to close any -github pull requests manually, because github will not be able to detect the +GitHub pull requests manually, because GitHub will not be able to detect the changes have already been merged. A long series of commits @@ -59,9 +59,9 @@ A long series of commits If there are a longer series of related commits, consider a merge instead:: git fetch upstream-rw - git merge --no-ff upstream-rw/master + git merge --no-ff upstream-rw/main -The merge will be detected by github, and should close any related pull requests +The merge will be detected by GitHub, and should close any related pull requests automatically. Note the ``--no-ff`` above. This forces git to make a merge commit, rather than @@ -76,11 +76,11 @@ Now, in either case, you should check that the history is sensible and you have the right commits:: git log --oneline --graph - git log -p upstream-rw/master.. + git log -p upstream-rw/main.. The first line above just shows the history in a compact way, with a text representation of the history graph. The second line shows the log of commits -excluding those that can be reached from trunk (``upstream-rw/master``), and +excluding those that can be reached from trunk (``upstream-rw/main``), and including those that can be reached from current HEAD (implied with the ``..`` at the end). So, it shows the commits unique to this branch compared to trunk. The ``-p`` option shows the diff for these commits in patch form. @@ -90,9 +90,7 @@ Push to trunk :: - git push upstream-rw my-new-feature:master + git push upstream-rw my-new-feature:main -This pushes the ``my-new-feature`` branch in this repository to the ``master`` +This pushes the ``my-new-feature`` branch in this repository to the ``main`` branch in the ``upstream-rw`` repository. - -.. include:: links.inc diff --git a/doc/devel/min_dep_policy.rst b/doc/devel/min_dep_policy.rst index 45cee59e34a3..0836315b8dd3 100644 --- a/doc/devel/min_dep_policy.rst +++ b/doc/devel/min_dep_policy.rst @@ -1,7 +1,7 @@ .. _min_deps_policy: ====================================== -Minimum Version of Dependencies Policy +Minimum version of dependencies policy ====================================== For the purpose of this document, 'minor version' is in the sense of @@ -32,7 +32,7 @@ on every major and minor release, but never on a patch release. See also the :ref:`list-of-dependency-min-versions`. -Python Dependencies +Python dependencies =================== For Python dependencies we should support at least: @@ -41,14 +41,14 @@ with compiled extensions minor versions initially released in the 24 months prior to our planned release date or the oldest that support our minimum Python + NumPy -without complied extensions +without compiled extensions minor versions initially released in the 12 months prior to our planned release date or the oldest that supports our minimum Python. We will only bump these dependencies as we need new features or the old versions no longer support our minimum NumPy or Python. -Test and Documentation Dependencies +Test and documentation dependencies =================================== As these packages are only needed for testing or building the docs and @@ -61,7 +61,7 @@ We will support at least minor versions of the development dependencies released in the 12 months prior to our planned release. We will only bump these as needed or versions no longer support our -minimum Python and numpy. +minimum Python and NumPy. System and C-dependencies ========================= @@ -71,6 +71,10 @@ Ghostscript, FFmpeg) support as old as practical. These can be difficult to install for end-users and we want to be usable on as many systems as possible. We will bump these on a case-by-case basis. +In the case of GUI frameworks for which we rely on Python bindings being +available, we will also drop support for bindings so old that they don't +support any Python version that we support. + .. _list-of-dependency-min-versions: List of dependency versions @@ -83,7 +87,10 @@ specification of the dependencies. ========== ======== ====== Matplotlib Python NumPy ========== ======== ====== -3.4 3.7 1.16.0 +`3.7`_ 3.8 1.20.0 +`3.6`_ 3.8 1.19.0 +`3.5`_ 3.7 1.17.0 +`3.4`_ 3.7 1.16.0 `3.3`_ 3.6 1.15.0 `3.2`_ 3.6 1.11.0 `3.1`_ 3.6 1.11.0 @@ -99,6 +106,10 @@ Matplotlib Python NumPy 1.0 2.4 1.1 ========== ======== ====== +.. _`3.7`: https://matplotlib.org/3.7.0/devel/dependencies.html +.. _`3.6`: https://matplotlib.org/3.6.0/devel/dependencies.html +.. _`3.5`: https://matplotlib.org/3.5.0/devel/dependencies.html +.. _`3.4`: https://matplotlib.org/3.4.0/devel/dependencies.html .. _`3.3`: https://matplotlib.org/3.3.0/users/installing.html#dependencies .. _`3.2`: https://matplotlib.org/3.2.0/users/installing.html#dependencies .. _`3.1`: https://matplotlib.org/3.1.0/users/installing.html#dependencies diff --git a/doc/devel/release_guide.rst b/doc/devel/release_guide.rst index 451d482bc819..3f49631d00d8 100644 --- a/doc/devel/release_guide.rst +++ b/doc/devel/release_guide.rst @@ -3,7 +3,7 @@ .. _release-guide: ============= -Release Guide +Release guide ============= @@ -45,8 +45,8 @@ is currently broken:: .. _release_ghstats: -GitHub Stats -============ +GitHub statistics +================= We automatically extract GitHub issue, PRs, and authors from GitHub via the @@ -85,7 +85,7 @@ most common issue is ``*`` which is interpreted as unclosed markup). .. _release_chkdocs: -Update and Validate the Docs +Update and validate the docs ============================ Merge ``*-doc`` branch @@ -101,27 +101,62 @@ When making major or minor releases, update the supported versions in the Security Policy in :file:`SECURITY.md`. Commonly, this may be one or two previous minor releases, but is dependent on release managers. -Update "What's New" and "API changes" -------------------------------------- +Update release notes +-------------------- + +What's new +~~~~~~~~~~ + +*Only needed for major and minor releases. Bugfix releases should not have new +features.* + +Merge the contents of all the files in :file:`doc/users/next_whats_new/` +into a single file :file:`doc/users/prev_whats_new/whats_new_X.Y.0.rst` +and delete the individual files. + +API changes +~~~~~~~~~~~ + +*Primarily needed for major and minor releases. We may sometimes have API +changes in bugfix releases.* + +Merge the contents of all the files in :file:`doc/api/next_api_changes/` +into a single file :file:`doc/api/prev_api_changes/api_changes_X.Y.Z.rst` +and delete the individual files. + +Release notes TOC +~~~~~~~~~~~~~~~~~ -Before tagging major and minor releases, the "what's new" and "API changes" -listings should be updated. This is not needed for micro releases. +Update :file:`doc/users/release_notes.rst`: -For the "what's new", +- For major and minor releases add a new section - 1. copy the current content to a file in :file:`doc/users/prev_whats_new` - 2. merge all of the files in :file:`doc/users/next_whats_new/` into - :file:`doc/users/whats_new.rst` and delete the individual files - 3. comment out the next what's new glob at the top + .. code:: rst -Similarly for the "API changes", + X.Y + === + .. toctree:: + :maxdepth: 1 - 1. copy the current api changes to a file is :file:`doc/api/prev_api_changes` - 2. merge all of the files in the most recent :file:`doc/api/next_api_changes` - into :file:`doc/api/api_changes.rst` - 3. comment out the most recent API changes at the top. + prev_whats_new/whats_new_X.Y.0.rst + ../api/prev_api_changes/api_changes_X.Y.0.rst + prev_whats_new/github_stats_X.Y.0.rst +- For bugfix releases add the GitHub stats and (if present) the API changes to + the existing X.Y section -In both cases step 3 will have to be un-done right after the release. + .. code:: rst + + ../api/prev_api_changes/api_changes_X.Y.Z.rst + prev_whats_new/github_stats_X.Y.Z.rst + +Update version switcher +~~~~~~~~~~~~~~~~~~~~~~~ + +Update ``doc/_static/switcher.json``. If a minor release, ``X.Y.Z``, create +a new entry ``version: X.Y.(Z-1)``, and change the name of stable +``name: stable/X.Y.Z``. If a major release, ``X.Y.0``, change the name +of ``name: devel/X.(Y+1)`` and ``name: stable/X.Y.0`` as well as adding +a new version for the previous stable. Verify that docs build ---------------------- @@ -145,6 +180,19 @@ Python3 yet. You will need to create a Python2 environment with Address any issues which may arise. The internal links are checked on Circle CI, this should only flag failed external links. + +Update supported versions in SECURITY.md +---------------------------------------- + +For minor version release update the table in :file:`SECURITY.md` to specify +that the 2 most recent minor releases in the current major version series are +supported. + +For a major version release update the table in :file:`SECURITY.md` to specify +that the last minor version in the previous major version series is still +supported. Dropping support for the last version of a major version series +will be handled on an ad-hoc basis. + .. _release_tag: Create release commit and tag @@ -172,18 +220,16 @@ with the tag [#]_:: Finally, push the tag to GitHub:: - git push DANGER master v2.0.0 + git push DANGER main v2.0.0 Congratulations, the scariest part is done! -.. [#] The tarball that is provided by GitHub is produced using `git - archive `__. We use - `versioneer `__ - which uses a format string in +.. [#] The tarball that is provided by GitHub is produced using `git archive`_. + We use setuptools_scm_ which uses a format string in :file:`lib/matplotlib/_version.py` to have ``git`` insert a list of references to exported commit (see :file:`.gitattributes` for the configuration). This string is - then used by ``versioneer`` to produce the correct version, + then used by ``setuptools_scm`` to produce the correct version, based on the git tag, when users install from the tarball. However, if there is a branch pointed at the tagged commit, then the branch name will also be included in the tarball. @@ -195,6 +241,8 @@ Congratulations, the scariest part is done! git archive v2.0.0 -o matplotlib-2.0.0.tar.gz --prefix=matplotlib-2.0.0/ +.. _git archive: https://git-scm.com/docs/git-archive +.. _setuptools_scm: https://github.com/pypa/setuptools_scm If this is a final release, also create a 'doc' branch (this is not done for pre-releases):: @@ -216,7 +264,7 @@ On this branch un-comment the globs from :ref:`release_chkdocs`. And then :: .. _release_DOI: -Release Management / DOI +Release management / DOI ======================== Via the `GitHub UI @@ -308,7 +356,7 @@ Congratulations, you have now done the second scariest part! .. _release_docs: -Build and Deploy Documentation +Build and deploy documentation ============================== To build the documentation you must have the tagged version installed, but @@ -318,21 +366,21 @@ build the docs from the ``ver-doc`` branch. An easy way to arrange this is:: pip install -r requirements/doc/doc-requirements.txt git checkout v2.0.0-doc git clean -xfd - make -Cdoc O="-Ainclude_analytics=True -j$(nproc)" html latexpdf LATEXMKOPTS="-silent -f" + make -Cdoc O="-t release -j$(nproc)" html latexpdf LATEXMKOPTS="-silent -f" which will build both the html and pdf version of the documentation. The built documentation exists in the `matplotlib.github.com `__ repository. -Pushing changes to master automatically updates the website. +Pushing changes to main automatically updates the website. The documentation is organized by version. At the root of the tree is always the documentation for the latest stable release. Under that, there are directories containing the documentation for older versions. The documentation -for current master is built on Circle CI and pushed to the `devdocs +for current main is built on Circle CI and pushed to the `devdocs `__ repository. These are available at -`matplotlib.org/devdocs `__. +`matplotlib.org/devdocs `__. Assuming you have this repository checked out in the same directory as matplotlib :: @@ -355,7 +403,7 @@ the newly released version. Now commit and push everything to GitHub :: git add * git commit -a -m 'Updating docs for v2.0.0' - git push DANGER master + git push DANGER main Congratulations you have now done the third scariest part! diff --git a/doc/devel/style_guide.rst b/doc/devel/style_guide.rst new file mode 100644 index 000000000000..9dab7a6d99d2 --- /dev/null +++ b/doc/devel/style_guide.rst @@ -0,0 +1,411 @@ + +========================= +Documentation style guide +========================= + +This guide contains best practices for the language and formatting of Matplotlib +documentation. + +.. seealso:: + + For more information about contributing, see the :ref:`documenting-matplotlib` + section. + +Expository language +=================== + +For explanatory writing, the following guidelines are for clear and concise +language use. + +Terminology +----------- + +There are several key terms in Matplotlib that are standards for +reliability and consistency in documentation. They are not interchangeable. + +.. table:: + :widths: 15, 15, 35, 35 + + +------------------+--------------------------+--------------+--------------+ + | Term | Description | Correct | Incorrect | + +==================+==========================+==============+==============+ + | |Figure| | Matplotlib working space | - *For | - "The figure| + | | for programming. | Matplotlib | is the | + | | | objects*: | working | + | | | Figure, | space for | + | | | "The Figure| visuals." | + | | | is the | - "Methods in| + | | | working | the figure | + | | | space for | provide the| + | | | the visual.| visuals." | + | | | - *Referring | - "The | + | | | to class*: | |Figure| | + | | | |Figure|, | Four | + | | | "Methods | leglock is | + | | | within the | a wrestling| + | | | |Figure| | move." | + | | | provide the| | + | | | visuals." | | + | | | - *General | | + | | | language*: | | + | | | figure, | | + | | | "Michelle | | + | | | Kwan is a | | + | | | famous | | + | | | figure | | + | | | skater." | | + +------------------+--------------------------+--------------+--------------+ + | |Axes| | Subplots within Figure. | - *For | - "The axes | + | | Contains plot elements | Matplotlib | methods | + | | and is responsible for | objects*: | transform | + | | plotting and configuring | Axes, "An | the data." | + | | additional details. | Axes is a | - "Each | + | | | subplot | |Axes| is | + | | | within the | specific to| + | | | Figure." | a Figure." | + | | | - *Referring | - "The | + | | | to class*: | musicians | + | | | |Axes|, | on stage | + | | | "Each | call their | + | | | |Axes| is | guitars | + | | | specific to| Axes." | + | | | one | - "The point | + | | | Figure." | where the | + | | | - *General | Axes meet | + | | | language*: | is the | + | | | axes, "Both| origin of | + | | | loggers and| the | + | | | lumberjacks| coordinate | + | | | use axes to| system." | + | | | chop wood."| | + | | | OR "There | | + | | | are no | | + | | | standard | | + | | | names for | | + | | | the | | + | | | coordinates| | + | | | in the | | + | | | three | | + | | | axes." | | + | | | (Plural of | | + | | | axis) | | + +------------------+--------------------------+--------------+--------------+ + | |Artist| | Broad variety of | - *For | - "Configure | + | | Matplotlib objects that | Matplotlib | the legend | + | | display visuals. | objects*: | artist with| + | | | Artist, | its | + | | | "Artists | respective | + | | | display | method." | + | | | visuals and| - "There is | + | | | are the | an | + | | | visible | |Artist| | + | | | elements | for that | + | | | when | visual in | + | | | rendering a| the graph."| + | | | Figure." | - "Some | + | | | - *Referring | Artists | + | | | to class*: | became | + | | | |Artist| , | famous only| + | | | "Each | by | + | | | |Artist| | accident." | + | | | has | | + | | | respective | | + | | | methods and| | + | | | functions."| | + | | | - *General | | + | | | language*: | | + | | | artist, | | + | | | "The | | + | | | artist in | | + | | | the museum | | + | | | is from | | + | | | France." | | + +------------------+--------------------------+--------------+--------------+ + | |Axis| | Human-readable single | - *For | - "Plot the | + | | dimensional object | Matplotlib | graph onto | + | | of reference marks | objects*: | the axis." | + | | containing ticks, tick | Axis, "The | - "Each Axis | + | | labels, spines, and | Axis for | is usually | + | | edges. | the bar | named after| + | | | chart is a | the | + | | | separate | coordinate | + | | | Artist." | which is | + | | | (plural, | measured | + | | | Axis | along it." | + | | | objects) | - "In some | + | | | - *Referring | computer | + | | | to class*: | graphics | + | | | |Axis|, | contexts, | + | | | "The | the | + | | | |Axis| | ordinate | + | | | contains | |Axis| may | + | | | respective | be oriented| + | | | XAxis and | downwards."| + | | | YAxis | | + | | | objects." | | + | | | - *General | | + | | | language*: | | + | | | axis, | | + | | | "Rotation | | + | | | around a | | + | | | fixed axis | | + | | | is a | | + | | | special | | + | | | case of | | + | | | rotational | | + | | | motion." | | + +------------------+--------------------------+--------------+--------------+ + | Explicit, | Explicit approach of | - Explicit | - object | + | Object Oriented | programming in | - explicit | oriented | + | Programming (OOP)| Matplotlib. | - OOP | - OO-style | + +------------------+--------------------------+--------------+--------------+ + | Implicit, | Implicit approach of | - Implicit | - MATLAB like| + | ``pyplot`` | programming in Matplotlib| - implicit | - Pyplot | + | | with ``pyplot`` module. | - ``pyplot`` | - pyplot | + | | | | interface | + +------------------+--------------------------+--------------+--------------+ + +.. |Figure| replace:: :class:`~matplotlib.figure.Figure` +.. |Axes| replace:: :class:`~matplotlib.axes.Axes` +.. |Artist| replace:: :class:`~matplotlib.artist.Artist` +.. |Axis| replace:: :class:`~matplotlib.axis.Axis` + + +Grammar +------- + +Subject +^^^^^^^ +Use second-person imperative sentences for directed instructions specifying an +action. Second-person pronouns are for individual-specific contexts and +possessive reference. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | Install Matplotlib from the source | You can install Matplotlib from the| + | directory using the Python ``pip`` | source directory. You can find | + | installer program. Depending on | additional support if you are | + | your operating system, you may need| having trouble with your | + | additional support. | installation. | + +------------------------------------+------------------------------------+ + +Tense +^^^^^ +Use present simple tense for explanations. Avoid future tense and other modal +or auxiliary verbs when possible. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | The fundamental ideas behind | Matplotlib will take data and | + | Matplotlib for visualization | transform it through functions and | + | involve taking data and | methods. They can generate many | + | transforming it through functions | kinds of visuals. These will be the| + | and methods. | fundamentals for using Matplotlib. | + +------------------------------------+------------------------------------+ + +Voice +^^^^^ +Write in active sentences. Passive voice is best for situations or conditions +related to warning prompts. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | The function ``plot`` generates the| The graph is generated by the | + | graph. | ``plot`` function. | + +------------------------------------+------------------------------------+ + | An error message is returned by the| You will see an error message from | + | function if there are no arguments.| the function if there are no | + | | arguments. | + +------------------------------------+------------------------------------+ + +Sentence structure +^^^^^^^^^^^^^^^^^^ +Write with short sentences using Subject-Verb-Object order regularly. Limit +coordinating conjunctions in sentences. Avoid pronoun references and +subordinating conjunctive phrases. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | The ``pyplot`` module in Matplotlib| The ``pyplot`` module in Matplotlib| + | is a collection of functions. These| is a collection of functions which | + | functions create, manage, and | create, manage, and manipulate the | + | manipulate the current Figure and | current Figure and plotting area. | + | plotting area. | | + +------------------------------------+------------------------------------+ + | The ``plot`` function plots data | The ``plot`` function plots data | + | to the respective Axes. The Axes | within its respective Axes for its | + | corresponds to the respective | respective Figure. | + | Figure. | | + +------------------------------------+------------------------------------+ + | The implicit approach is a | Users that wish to have convenient | + | convenient shortcut for | shortcuts for generating plots use | + | generating simple plots. | the implicit approach. | + +------------------------------------+------------------------------------+ + + +Formatting +========== + +The following guidelines specify how to incorporate code and use appropriate +formatting for Matplotlib documentation. + +Code +---- + +Matplotlib is a Python library and follows the same standards for +documentation. + +Comments +^^^^^^^^ +Examples of Python code have comments before or on the same line. + +.. table:: + :width: 100% + :widths: 50, 50 + + +---------------------------------------+---------------------------------+ + | Correct | Incorrect | + +=======================================+=================================+ + | :: | :: | + | | | + | # Data | years = [2006, 2007, 2008] | + | years = [2006, 2007, 2008] | # Data | + +---------------------------------------+ | + | :: | | + | | | + | years = [2006, 2007, 2008] # Data | | + +---------------------------------------+---------------------------------+ + +Outputs +^^^^^^^ +When generating visuals with Matplotlib using ``.py`` files in examples, +display the visual with `matplotlib.pyplot.show` to display the visual. +Keep the documentation clear of Python output lines. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | :: | :: | + | | | + | plt.plot([1, 2, 3], [1, 2, 3]) | plt.plot([1, 2, 3], [1, 2, 3]) | + | plt.show() | | + +------------------------------------+------------------------------------+ + | :: | :: | + | | | + | fig, ax = plt.subplots() | fig, ax = plt.subplots() | + | ax.plot([1, 2, 3], [1, 2, 3]) | ax.plot([1, 2, 3], [1, 2, 3]) | + | fig.show() | | + +------------------------------------+------------------------------------+ + +reStructuredText +---------------- + +Matplotlib uses reStructuredText Markup for documentation. Sphinx helps to +transform these documents into appropriate formats for accessibility and +visibility. + +- `reStructuredText Specifications `_ +- `Quick Reference Document `_ + + +Lists +^^^^^ +Bulleted lists are for items that do not require sequencing. Numbered lists are +exclusively for performing actions in a determined order. + +.. table:: + :width: 100% + :widths: 50, 50 + + +------------------------------------+------------------------------------+ + | Correct | Incorrect | + +====================================+====================================+ + | The example uses three graphs. | The example uses three graphs. | + +------------------------------------+------------------------------------+ + | - Bar | 1. Bar | + | - Line | 2. Line | + | - Pie | 3. Pie | + +------------------------------------+------------------------------------+ + | These four steps help to get | The following steps are important | + | started using Matplotlib. | to get started using Matplotlib. | + +------------------------------------+------------------------------------+ + | 1. Import the Matplotlib library. | - Import the Matplotlib library. | + | 2. Import the necessary modules. | - Import the necessary modules. | + | 3. Set and assign data to work on.| - Set and assign data to work on. | + | 4. Transform data with methods and| - Transform data with methods and | + | functions. | functions. | + +------------------------------------+------------------------------------+ + +Tables +^^^^^^ +Use ASCII tables with reStructuredText standards in organizing content. +Markdown tables and the csv-table directive are not accepted. + +.. table:: + :width: 100% + :widths: 50, 50 + + +--------------------------------+----------------------------------------+ + | Correct | Incorrect | + +================================+========================================+ + | +----------+----------+ | :: | + | | Correct | Incorrect| | | + | +==========+==========+ | | Correct | Incorrect | | + | | OK | Not OK | | | ------- | --------- | | + | +----------+----------+ | | OK | Not OK | | + | | | + +--------------------------------+----------------------------------------+ + | :: | :: | + | | | + | +----------+----------+ | .. csv-table:: | + | | Correct | Incorrect| | :header: "correct", "incorrect" | + | +==========+==========+ | :widths: 10, 10 | + | | OK | Not OK | | | + | +----------+----------+ | "OK ", "Not OK" | + | | | + +--------------------------------+ | + | :: | | + | | | + | =========== =========== | | + | Correct Incorrect | | + | =========== =========== | | + | OK Not OK | | + | =========== =========== | | + | | | + +--------------------------------+----------------------------------------+ + + +Additional resources +==================== +This style guide is not a comprehensive standard. For a more thorough +reference of how to contribute to documentation, see the links below. These +resources contain common best practices for writing documentation. + +* `Python Developer's Guide `_ +* `Google Developer Style Guide `_ +* `IBM Style Guide `_ +* `Red Hat Style Guide `_ diff --git a/doc/devel/testing.rst b/doc/devel/testing.rst index b46586104121..06296f5dc701 100644 --- a/doc/devel/testing.rst +++ b/doc/devel/testing.rst @@ -57,14 +57,6 @@ not need to be installed, but Matplotlib should be):: pytest lib/matplotlib/tests/test_simplification.py::test_clipping -An alternative implementation that does not look at command line arguments -and works from within Python is to run the tests from the Matplotlib library -function :func:`matplotlib.test`:: - - import matplotlib - matplotlib.test() - - .. _command-line parameters: http://doc.pytest.org/en/latest/usage.html @@ -95,10 +87,12 @@ Random data in tests Random data is a very convenient way to generate data for examples, however the randomness is problematic for testing (as the tests must be deterministic!). To work around this set the seed in each test. -For numpy use:: +For numpy's default random number generator use:: import numpy as np - np.random.seed(19680801) + rng = np.random.default_rng(19680801) + +and then use ``rng`` when generating the random numbers. The seed is John Hunter's birthday. @@ -115,7 +109,7 @@ tests it:: import matplotlib.pyplot as plt @image_comparison(baseline_images=['line_dashes'], remove_text=True, - extensions=['png']) + extensions=['png'], style='mpl20') def test_line_dashes(): fig, ax = plt.subplots() ax.plot(range(10), linestyle=(0, (3, 3)), lw=5) @@ -136,6 +130,12 @@ images on the figures using two different methods (the tested method and the baseline method). The decorator will arrange for setting up the figures and then collect the drawn results and compare them. +It is preferred that new tests use ``style='mpl20'`` as this leads to smaller +figures and reflects the newer look of default Matplotlib plots. Also, if the +texts (labels, tick labels, etc) are not really part of what is tested, use +``remove_text=True`` as this will lead to smaller figures and reduce possible +issues with font mismatch on different platforms. + See the documentation of `~matplotlib.testing.decorators.image_comparison` and `~matplotlib.testing.decorators.check_figures_equal` for additional information about their use. @@ -165,7 +165,9 @@ workflows GitHub Actions should be automatically enabled for your personal Matplotlib fork once the YAML workflow files are in it. It generally isn't necessary to look at these workflows, since any pull request submitted against the main -Matplotlib repository will be tested. +Matplotlib repository will be tested. The Tests workflow is skipped in forked +repositories but you can trigger a run manually from the `GitHub web interface +`_. You can see the GitHub Actions results at https://github.com/your_GitHub_user_name/matplotlib/actions -- here's `an @@ -177,7 +179,7 @@ Using tox `Tox `_ is a tool for running tests against multiple Python environments, including multiple versions of Python -(e.g., 3.6, 3.7) and even different Python implementations altogether +(e.g., 3.7, 3.8) and even different Python implementations altogether (e.g., CPython, PyPy, Jython, etc.), as long as all these versions are available on your system's $PATH (consider using your system package manager, e.g. apt-get, yum, or Homebrew, to install them). @@ -194,7 +196,7 @@ You can also run tox on a subset of environments: .. code-block:: bash - $ tox -e py37,py38 + $ tox -e py38,py39 Tox processes everything serially so it can take a long time to test several environments. To speed it up, you might try using a new, @@ -252,7 +254,7 @@ The correct target folder can be found using:: python -c "import matplotlib.tests; print(matplotlib.tests.__file__.rsplit('/', 1)[0])" An analogous copying of :file:`lib/mpl_toolkits/tests/baseline_images` -is necessary for testing the :ref:`toolkits`. +is necessary for testing ``mpl_toolkits``. Run the tests ^^^^^^^^^^^^^ @@ -264,4 +266,3 @@ The test discovery scope can be narrowed to single test modules or even single functions:: python -m pytest --pyargs matplotlib.tests.test_simplification.py::test_clipping - diff --git a/doc/devel/triage.rst b/doc/devel/triage.rst old mode 100644 new mode 100755 index b57947b049b7..2106ac99c606 --- a/doc/devel/triage.rst +++ b/doc/devel/triage.rst @@ -16,7 +16,7 @@ internals of Matplotlib, is extremely valuable to the project, and we welcome anyone to participate in issue triage! However, people who are not part of the Matplotlib organization do not have `permissions to change milestones, add labels, or close issue -`_. +`_. If you do not have enough GitHub permissions do something (e.g. add a label, close an issue), please leave a comment tagging ``@matplotlib/triageteam`` with your recommendations! @@ -62,7 +62,7 @@ The following actions are typically useful: explores how to lead online discussions in the context of open source. -Triage Team +Triage team ----------- @@ -87,7 +87,7 @@ Triaging operations for members of the core and triage teams In addition to the above, members of the core team and the triage team can do the following important tasks: -- Update labels for issues and PRs: see the list of `available github +- Update labels for issues and PRs: see the list of `available GitHub labels `_. - Triage issues: @@ -208,7 +208,7 @@ The following workflow [1]_ is a good way to approach issue triaging: .. [1] Adapted from the pandas project `maintainers guide - `_ and + `_ and `the scikit-learn project `_ . @@ -223,5 +223,5 @@ participate to the review process following our :ref:`review guidelines Acknowledgments --------------- -This page is lightly adapted from `the sckit-learn project +This page is lightly adapted from `the scikit-learn project `_ . diff --git a/doc/faq/installing_faq.rst b/doc/faq/installing_faq.rst deleted file mode 100644 index e97a8737ad02..000000000000 --- a/doc/faq/installing_faq.rst +++ /dev/null @@ -1,137 +0,0 @@ -.. _installing-faq: - -************* - Installation -************* - -.. contents:: - :backlinks: none - -Report a compilation problem -============================ - -See :ref:`reporting-problems`. - -Matplotlib compiled fine, but nothing shows up when I use it -============================================================ - -The first thing to try is a :ref:`clean install ` and see if -that helps. If not, the best way to test your install is by running a script, -rather than working interactively from a python shell or an integrated -development environment such as :program:`IDLE` which add additional -complexities. Open up a UNIX shell or a DOS command prompt and run, for -example:: - - python -c "from pylab import *; set_loglevel('debug'); plot(); show()" - -This will give you additional information about which backends Matplotlib is -loading, version information, and more. At this point you might want to make -sure you understand Matplotlib's :doc:`configuration ` -process, governed by the :file:`matplotlibrc` configuration file which contains -instructions within and the concept of the Matplotlib backend. - -If you are still having trouble, see :ref:`reporting-problems`. - -.. _clean-install: - -How to completely remove Matplotlib -=================================== - -Occasionally, problems with Matplotlib can be solved with a clean -installation of the package. In order to fully remove an installed Matplotlib: - -1. Delete the caches from your :ref:`Matplotlib configuration directory - `. - -2. Delete any Matplotlib directories or eggs from your :ref:`installation - directory `. - -OSX Notes -========= - -.. _which-python-for-osx: - -Which python for OSX? ---------------------- - -Apple ships OSX with its own Python, in ``/usr/bin/python``, and its own copy -of Matplotlib. Unfortunately, the way Apple currently installs its own copies -of NumPy, Scipy and Matplotlib means that these packages are difficult to -upgrade (see `system python packages`_). For that reason we strongly suggest -that you install a fresh version of Python and use that as the basis for -installing libraries such as NumPy and Matplotlib. One convenient way to -install Matplotlib with other useful Python software is to use the Anaconda_ -Python scientific software collection, which includes Python itself and a -wide range of libraries; if you need a library that is not available from the -collection, you can install it yourself using standard methods such as *pip*. -See the Ananconda web page for installation support. - -.. _system python packages: - https://github.com/MacPython/wiki/wiki/Which-Python#system-python-and-extra-python-packages -.. _Anaconda: https://www.anaconda.com/ - -Other options for a fresh Python install are the standard installer from -`python.org `_, or installing -Python using a general OSX package management system such as `homebrew -`_ or `macports `_. Power users on -OSX will likely want one of homebrew or macports on their system to install -open source software packages, but it is perfectly possible to use these -systems with another source for your Python binary, such as Anaconda -or Python.org Python. - -.. _install_osx_binaries: - -Installing OSX binary wheels ----------------------------- - -If you are using Python from https://www.python.org, Homebrew, or Macports, -then you can use the standard pip installer to install Matplotlib binaries in -the form of wheels. - -pip is installed by default with python.org and Homebrew Python, but needs to -be manually installed on Macports with :: - - sudo port install py38-pip - -Once pip is installed, you can install Matplotlib and all its dependencies with -from the Terminal.app command line:: - - python3 -mpip install matplotlib - -(``sudo python3.6 ...`` on Macports). - -You might also want to install IPython or the Jupyter notebook (``python3 -mpip -install ipython notebook``). - -Checking your installation --------------------------- - -The new version of Matplotlib should now be on your Python "path". Check this -at the Terminal.app command line:: - - python3 -c 'import matplotlib; print(matplotlib.__version__, matplotlib.__file__)' - -You should see something like :: - - 3.0.0 /Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages/matplotlib/__init__.py - -where ``3.0.0`` is the Matplotlib version you just installed, and the path -following depends on whether you are using Python.org Python, Homebrew or -Macports. If you see another version, or you get an error like :: - - Traceback (most recent call last): - File "", line 1, in - ImportError: No module named matplotlib - -then check that the Python binary is the one you expected by running :: - - which python3 - -If you get a result like ``/usr/bin/python...``, then you are getting the -Python installed with OSX, which is probably not what you want. Try closing -and restarting Terminal.app before running the check again. If that doesn't fix -the problem, depending on which Python you wanted to use, consider reinstalling -Python.org Python, or check your homebrew or macports setup. Remember that -the disk image installer only works for Python.org Python, and will not get -picked up by other Pythons. If all these fail, please :ref:`let us know -`. diff --git a/doc/index.rst b/doc/index.rst index 50526173e3bd..1c608c0d62cb 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -1,223 +1,111 @@ :orphan: -.. title:: Matplotlib: Python plotting +.. title:: Matplotlib documentation .. module:: matplotlib -Matplotlib: Visualization with Python -------------------------------------- + +################################## +Matplotlib |release| documentation +################################## Matplotlib is a comprehensive library for creating static, animated, and interactive visualizations in Python. -.. raw:: html - - - - -Matplotlib makes easy things easy and hard things possible. - -.. container:: bullet-box-container - - .. container:: bullet-box - - Create - - - Develop `publication quality plots`_ with just a few lines of code - - Use `interactive figures`_ that can zoom, pan, update... - - .. _publication quality plots: https://matplotlib.org/gallery/index.html - .. _interactive figures: https://matplotlib.org/gallery/index.html#event-handling - - .. container:: bullet-box - - Customize - - - `Take full control`_ of line styles, font properties, axes properties... - - `Export and embed`_ to a number of file formats and interactive environments - - .. _Take full control: https://matplotlib.org/tutorials/index.html#tutorials - .. _Export and embed: https://matplotlib.org/api/index_backend_api.html - - .. container:: bullet-box - - Extend - - - Explore tailored functionality provided by - :doc:`third party packages ` - - Learn more about Matplotlib through the many - :doc:`external learning resources ` - -Documentation -~~~~~~~~~~~~~ - -To get started, read the :doc:`User's Guide `. - -Trying to learn how to do a particular kind of plot? Check out the -:doc:`examples gallery ` or the :doc:`list of plotting commands -`. - -Join our community! -~~~~~~~~~~~~~~~~~~~ - -Matplotlib is a welcoming, inclusive project, and everyone within the community -is expected to abide by our `code of conduct -`_. - - -.. raw:: html - -

Get help

-
-
- Discourse -

Join our community at discourse.matplotlib.org - to get help, discuss contributing & development, and share your work.

-
-
- Questions -

If you have questions, be sure to check the FAQ, - the API docs. The full text - search is a good way to discover the docs including the many examples.

-
-
- Stackoverflow -

Check out the Matplotlib tag on stackoverflow.

-
-
- Gitter -

Short questions may be posted on the gitter channel.

-
-
-
-

News

-
-
- News -

To keep up to date with what's going on in Matplotlib, see the - what's new page or browse the - source code. Anything that could - require changes to your existing code is logged in the - API changes file.

-
-
- Social media - -
-
-
-

Development

-
-
- Github -

Matplotlib is hosted on GitHub.

- -

It is a good idea to ping us on Discourse as well.

-
-
- Mailing lists -

Mailing lists

- -
-
- - -Toolkits -======== - -Matplotlib ships with several add-on :doc:`toolkits `, -including 3D plotting with `.mplot3d`, axes helpers in `.axes_grid1` and axis -helpers in `.axisartist`. - -Third party packages -==================== - -A large number of :doc:`third party packages ` -extend and build on Matplotlib functionality, including several higher-level -plotting interfaces (seaborn_, HoloViews_, ggplot_, ...), and a projection -and mapping toolkit (Cartopy_). - -.. _seaborn: https://seaborn.pydata.org -.. _HoloViews: https://holoviews.org -.. _ggplot: https://yhat.github.io/ggpy/ -.. _Cartopy: https://scitools.org.uk/cartopy/docs/latest - -Citing Matplotlib -================= - -Matplotlib is the brainchild of John Hunter (1968-2012), who, along with its -many contributors, have put an immeasurable amount of time and effort into -producing a piece of software utilized by thousands of scientists worldwide. - -If Matplotlib contributes to a project that leads to a scientific publication, -please acknowledge this work by citing the project. A :doc:`ready-made citation -entry ` is available. - -Open source -=========== - -.. raw:: html - - - A Fiscally Sponsored Project of NUMFocus - - - -Matplotlib is a Sponsored Project of NumFOCUS, a 501(c)(3) nonprofit -charity in the United States. NumFOCUS provides Matplotlib with -fiscal, legal, and administrative support to help ensure the health -and sustainability of the project. Visit `numfocus.org `_ for more -information. - -Donations to Matplotlib are managed by NumFOCUS. For donors in the -United States, your gift is tax-deductible to the extent provided by -law. As with any donation, you should consult with your tax adviser -about your particular tax situation. - -Please consider `donating to the Matplotlib project `_ through -the NumFOCUS organization or to the `John Hunter Technology Fellowship -`_. - -.. _donating: https://numfocus.org/donate-to-matplotlib -.. _jdh-fellowship: https://numfocus.org/programs/john-hunter-technology-fellowship/ -.. _nf: https://numfocus.org - -The :doc:`Matplotlib license ` is based on the `Python Software -Foundation (PSF) license `_. - -.. _psf-license: https://www.python.org/psf/license - -There is an active developer community and a long list of people who have made -significant :doc:`contributions `. +************ +Installation +************ + +.. grid:: 1 1 2 2 + + .. grid-item:: + + Install using `pip `__: + + .. code-block:: bash + + pip install matplotlib + + .. grid-item:: + + Install using `conda `__: + + .. code-block:: bash + + conda install -c conda-forge matplotlib + +Further details are available in the :doc:`Installation Guide `. + + +****************** +Learning resources +****************** + +.. grid:: 1 1 2 2 + + .. grid-item-card:: + :padding: 2 + + Tutorials + ^^^ + + - :doc:`Quick-start guide ` + - :doc:`Plot types ` + - :ref:`Introductory tutorials ` + - :doc:`External learning resources ` + + .. grid-item-card:: + :padding: 2 + + How-tos + ^^^ + + - :doc:`Example gallery ` + - :doc:`Matplotlib FAQ ` + + .. grid-item-card:: + :padding: 2 + + Understand how Matplotlib works + ^^^ + + - The :ref:`users-guide-explain` in the :doc:`Users guide + ` + - Many of the :ref:`Intermediate ` and + :ref:`Advanced ` tutorials have explanatory + material + + .. grid-item-card:: + :padding: 2 + + Reference + ^^^ + + - :doc:`API Reference ` + - :doc:`Axes API ` for most plotting methods + - :doc:`Figure API ` for figure-level methods + - Top-level interfaces to create: + + - Figures (`.pyplot.figure`) + - Subplots (`.pyplot.subplots`, `.pyplot.subplot_mosaic`) + + +******************** +Third-party packages +******************** + +There are many `Third-party packages +`_ built on top of and extending +Matplotlib. + + +************ +Contributing +************ + +Matplotlib is a community project maintained for and by its users. There are many ways +you can help! + +- Help other users `on discourse `__ +- report a bug or request a feature `on GitHub `__ +- or improve the :ref:`documentation and code ` diff --git a/doc/make.bat b/doc/make.bat index 042c9ef3543b..aa6612f08eae 100644 --- a/doc/make.bat +++ b/doc/make.bat @@ -10,8 +10,9 @@ if "%SPHINXBUILD%" == "" ( set SOURCEDIR=. set BUILDDIR=build set SPHINXPROJ=matplotlib -set SPHINXOPTS=-W -set O= +if defined SPHINXOPTS goto skipopts +set SPHINXOPTS=-W --keep-going +:skipopts %SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( @@ -27,12 +28,32 @@ if errorlevel 9009 ( ) if "%1" == "" goto help +if "%1" == "html-noplot" goto html-noplot +if "%1" == "show" goto show %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +if "%1" == "clean" ( + REM workaround because sphinx does not completely clean up (#11139) + rmdir /s /q "%SOURCEDIR%\build" + rmdir /s /q "%SOURCEDIR%\api\_as_gen" + rmdir /s /q "%SOURCEDIR%\gallery" + rmdir /s /q "%SOURCEDIR%\plot_types" + rmdir /s /q "%SOURCEDIR%\tutorials" + rmdir /s /q "%SOURCEDIR%\savefig" + rmdir /s /q "%SOURCEDIR%\sphinxext\__pycache__" +) goto end :help %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:html-noplot +%SPHINXBUILD% -M html %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% -D plot_gallery=0 +goto end + +:show +python -m webbrowser -t "%~dp0\build\html\index.html" :end popd diff --git a/doc/missing-references.json b/doc/missing-references.json index 054a201acca3..d9a64ae39c2a 100644 --- a/doc/missing-references.json +++ b/doc/missing-references.json @@ -1,389 +1,155 @@ { "py:attr": { - "axis": [ - "lib/matplotlib/category.py:docstring of matplotlib.category.StrCategoryLocator.tick_values:5", - "lib/matplotlib/dates.py:docstring of matplotlib.dates.AutoDateLocator.tick_values:5", - "lib/matplotlib/dates.py:docstring of matplotlib.dates.MicrosecondLocator.tick_values:5", - "lib/matplotlib/dates.py:docstring of matplotlib.dates.RRuleLocator.tick_values:5", - "lib/matplotlib/dates.py:docstring of matplotlib.dates.YearLocator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.AutoMinorLocator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.IndexLocator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.LinearLocator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.Locator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.LogLocator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.LogitLocator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.MaxNLocator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.MultipleLocator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.OldAutoLocator.tick_values:5", - "lib/matplotlib/ticker.py:docstring of matplotlib.ticker.SymmetricalLogLocator.tick_values:5" - ], - "button": [ - "doc/users/prev_whats_new/whats_new_3.1.0.rst:335" - ], "cbar_axes": [ - "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid.__init__:49", - "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:49", - "lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:49" + "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:41", + "lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:41" ], "eventson": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.CheckButtons.set_active:4", - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.RadioButtons.set_active:4" + "lib/matplotlib/widgets.py:docstring of matplotlib.widgets:1" ], "fmt_zdata": [ - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.format_zdata:2" + "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.format_zdata:1" ], "height": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.bounds:2" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" ], "input_dims": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform:8", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_affine:15", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_non_affine:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_affine:15", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_non_affine:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform:8", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_affine:15", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_non_affine:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_non_affine:20" + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes:1", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:10", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:11", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:4" ], "lines": [ - "lib/matplotlib/colorbar.py:docstring of matplotlib.colorbar.ColorbarBase.add_lines:4" - ], - "matplotlib.axes.Axes.dataLim": [ - "doc/api/prev_api_changes/api_changes_0.99.x.rst:23" - ], - "matplotlib.axes.Axes.frame": [ - "doc/api/prev_api_changes/api_changes_0.98.x.rst:89" - ], - "matplotlib.axes.Axes.lines": [ - "doc/tutorials/intermediate/artists.rst:425", - "doc/tutorials/intermediate/artists.rst:91" + "lib/matplotlib/colorbar.py:docstring of matplotlib.colorbar:1" ], "matplotlib.axes.Axes.patch": [ - "doc/api/prev_api_changes/api_changes_0.98.x.rst:89", - "doc/tutorials/intermediate/artists.rst:174", - "doc/tutorials/intermediate/artists.rst:409" + "doc/tutorials/intermediate/artists.rst:184", + "doc/tutorials/intermediate/artists.rst:423" ], "matplotlib.axes.Axes.patches": [ - "doc/tutorials/intermediate/artists.rst:448" + "doc/tutorials/intermediate/artists.rst:461" ], "matplotlib.axes.Axes.transAxes": [ - "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredDirectionArrows.__init__:8", - "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredDirectionArrows:8" + "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredDirectionArrows:4" ], "matplotlib.axes.Axes.transData": [ - "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredAuxTransformBox.__init__:11", - "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredAuxTransformBox:11", - "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredEllipse.__init__:8", - "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredEllipse:8", - "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredSizeBar.__init__:8", - "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredSizeBar:8" - ], - "matplotlib.axes.Axes.viewLim": [ - "doc/api/prev_api_changes/api_changes_0.99.x.rst:23" + "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredAuxTransformBox:7", + "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredEllipse:4", + "lib/mpl_toolkits/axes_grid1/anchored_artists.py:docstring of mpl_toolkits.axes_grid1.anchored_artists.AnchoredSizeBar:4" ], "matplotlib.axes.Axes.xaxis": [ - "doc/tutorials/intermediate/artists.rst:593" + "doc/tutorials/intermediate/artists.rst:607" ], "matplotlib.axes.Axes.yaxis": [ - "doc/tutorials/intermediate/artists.rst:593" + "doc/tutorials/intermediate/artists.rst:607" ], "matplotlib.axis.Axis.label": [ - "doc/tutorials/intermediate/artists.rst:640" - ], - "matplotlib.cm.ScalarMappable.callbacksSM": [ - "doc/api/prev_api_changes/api_changes_0.98.0.rst:10" - ], - "matplotlib.colorbar.ColorbarBase.ax": [ - "doc/api/prev_api_changes/api_changes_1.3.x.rst:100" + "doc/tutorials/intermediate/artists.rst:654" ], "matplotlib.colors.Colormap.name": [ - "lib/matplotlib/cm.py:docstring of matplotlib.cm.register_cmap:14" + "lib/matplotlib/cm.py:docstring of matplotlib.cm:10" ], "matplotlib.figure.Figure.patch": [ - "doc/api/prev_api_changes/api_changes_0.98.x.rst:89", - "doc/tutorials/intermediate/artists.rst:174", - "doc/tutorials/intermediate/artists.rst:307" + "doc/tutorials/intermediate/artists.rst:184", + "doc/tutorials/intermediate/artists.rst:317" ], "matplotlib.figure.Figure.transFigure": [ - "doc/tutorials/intermediate/artists.rst:356" + "doc/tutorials/intermediate/artists.rst:366" ], "max": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.p1:4" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" ], "min": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.p0:4" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" ], "mpl_toolkits.mplot3d.axis3d._axinfo": [ - "doc/api/toolkits/mplot3d.rst:40" + "doc/api/toolkits/mplot3d.rst:66" ], "name": [ - "lib/matplotlib/scale.py:docstring of matplotlib.scale.ScaleBase:8" + "lib/matplotlib/scale.py:docstring of matplotlib.scale:7" ], "output_dims": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.AffineBase.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.CompositeGenericTransform.transform_non_affine:20", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform:14", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_affine:21", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.IdentityTransform.transform_non_affine:20" + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo.AitoffAxes:6", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:10", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:16", + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:17" ], "triangulation": [ - "lib/matplotlib/tri/trirefine.py:docstring of matplotlib.tri.trirefine.UniformTriRefiner.refine_triangulation:2" + "lib/matplotlib/tri/_trirefine.py:docstring of matplotlib.tri._trirefine.UniformTriRefiner:1" ], "use_sticky_edges": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.margins:53" + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.margins:48" ], "width": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.bounds:2" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" ], "xmax": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.x1:4" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" ], "xmin": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.x0:4" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" ], "ymax": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.y1:4" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" ], "ymin": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Bbox.y0:4" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" ] }, "py:class": { - "Cursors": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.NavigationToolbar2.set_cursor:2" - ], - "FigureCanvasQTAgg": [ - "doc/api/prev_api_changes/api_changes_2.2.0.rst:215" - ], - "Patch3DCollection": [ - "doc/api/toolkits/mplot3d.rst:83::1" - ], - "Path3DCollection": [ - "doc/api/toolkits/mplot3d.rst:83::1" - ], - "backend_qt5.FigureCanvasQT": [ - "doc/api/prev_api_changes/api_changes_2.2.0.rst:199" - ], - "backend_qt5.FigureCanvasQTAgg": [ - "doc/api/prev_api_changes/api_changes_2.2.0.rst:210" - ], - "dateutil.rrule.rrulebase": [ - "/rrule.py:docstring of dateutil.rrule.rrule:1" - ], - "matplotlib._mathtext.Box": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.Char": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.ComputerModernFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.DejaVuSansFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.DejaVuSerifFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.FontConstantsBase": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.Fonts": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.Glue": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.Kern": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.Node": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.Parser": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.STIXFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.STIXSansFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.Ship": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.StandardPsFonts": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib._mathtext.TruetypeFonts": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.axes.Subplot": [ - "doc/tutorials/intermediate/artists.rst:35", - "doc/tutorials/intermediate/artists.rst:58" - ], - "matplotlib.axes._axes.Axes": [ - "doc/api/artist_api.rst:189", - "doc/api/axes_api.rst:609", - "lib/matplotlib/projections/polar.py:docstring of matplotlib.projections.polar.PolarAxes:1", - "lib/mpl_toolkits/axes_grid1/mpl_axes.py:docstring of mpl_toolkits.axes_grid1.mpl_axes.Axes:1", - "lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.Axes:1", - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D:1" - ], "matplotlib.axes._base._AxesBase": [ - "doc/api/artist_api.rst:189", - "doc/api/axes_api.rst:609", + "doc/api/artist_api.rst:202", + "doc/api/axes_api.rst:603", "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes:1" ], "matplotlib.backend_bases.FigureCanvas": [ - "doc/tutorials/intermediate/artists.rst:20", - "doc/tutorials/intermediate/artists.rst:22", - "doc/tutorials/intermediate/artists.rst:27" + "doc/tutorials/intermediate/artists.rst:32", + "doc/tutorials/intermediate/artists.rst:34", + "doc/tutorials/intermediate/artists.rst:39" ], "matplotlib.backend_bases.Renderer": [ - "doc/tutorials/intermediate/artists.rst:22", - "doc/tutorials/intermediate/artists.rst:27" + "doc/tutorials/intermediate/artists.rst:34", + "doc/tutorials/intermediate/artists.rst:39" ], "matplotlib.backend_bases._Backend": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ShowBase:1" - ], - "matplotlib.backend_tools._ToolEnableAllNavigation": [ - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ToolEnableAllNavigation:1" - ], - "matplotlib.backend_tools._ToolEnableNavigation": [ - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ToolEnableNavigation:1" + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases:1" ], "matplotlib.backends._backend_pdf_ps.RendererPDFPSBase": [ - "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf:1", - "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS:1" + "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf:1", + "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps:1" ], "matplotlib.backends._backend_tk.FigureCanvasTk": [ - "lib/matplotlib/backends/backend_tkagg.py:docstring of matplotlib.backends.backend_tkagg.FigureCanvasTkAgg:1", - "lib/matplotlib/backends/backend_tkcairo.py:docstring of matplotlib.backends.backend_tkcairo.FigureCanvasTkCairo:1" + "lib/matplotlib/backends/backend_tkagg.py:docstring of matplotlib.backends.backend_tkagg:1", + "lib/matplotlib/backends/backend_tkcairo.py:docstring of matplotlib.backends.backend_tkcairo:1" ], "matplotlib.backends.backend_webagg_core.FigureCanvasWebAggCore": [ - "lib/matplotlib/backends/backend_nbagg.py:docstring of matplotlib.backends.backend_nbagg.FigureCanvasNbAgg:1" + "lib/matplotlib/backends/backend_nbagg.py:docstring of matplotlib.backends.backend_nbagg:1", + "lib/matplotlib/backends/backend_webagg.py:docstring of matplotlib.backends.backend_webagg:1" ], "matplotlib.backends.backend_webagg_core.FigureManagerWebAgg": [ - "lib/matplotlib/backends/backend_nbagg.py:docstring of matplotlib.backends.backend_nbagg.FigureManagerNbAgg:1" + "lib/matplotlib/backends/backend_nbagg.py:docstring of matplotlib.backends.backend_nbagg:1", + "lib/matplotlib/backends/backend_webagg.py:docstring of matplotlib.backends.backend_webagg:1" ], "matplotlib.backends.backend_webagg_core.NavigationToolbar2WebAgg": [ - "lib/matplotlib/backends/backend_nbagg.py:docstring of matplotlib.backends.backend_nbagg.NavigationIPy:1" - ], - "matplotlib.cm.Wistia": [ - "doc/users/prev_whats_new/whats_new_1.4.rst:21" + "lib/matplotlib/backends/backend_nbagg.py:docstring of matplotlib.backends.backend_nbagg:1" ], "matplotlib.collections._CollectionWithSizes": [ - "doc/api/artist_api.rst:189", + "doc/api/artist_api.rst:202", "doc/api/collections_api.rst:13", - "lib/matplotlib/collections.py:docstring of matplotlib.collections.CircleCollection:1", - "lib/matplotlib/collections.py:docstring of matplotlib.collections.PathCollection:1", - "lib/matplotlib/collections.py:docstring of matplotlib.collections.PolyCollection:1", - "lib/matplotlib/collections.py:docstring of matplotlib.collections.RegularPolyCollection:1" - ], - "matplotlib.dates.rrulewrapper": [ - "doc/api/dates_api.rst:11" + "lib/matplotlib/collections.py:docstring of matplotlib.collections:1" ], "matplotlib.image._ImageBase": [ - "doc/api/artist_api.rst:189", - "lib/matplotlib/image.py:docstring of matplotlib.image.AxesImage:1", - "lib/matplotlib/image.py:docstring of matplotlib.image.BboxImage:1", - "lib/matplotlib/image.py:docstring of matplotlib.image.FigureImage:1" - ], - "matplotlib.mathtext.Box": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.Char": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.ComputerModernFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.DejaVuSansFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.DejaVuSerifFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.FontConstantsBase": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.Fonts": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.Glue": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.Kern": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.Node": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.Parser": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.STIXFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.STIXSansFontConstants": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.Ship": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.StandardPsFonts": [ - "doc/api/mathtext_api.rst:12" - ], - "matplotlib.mathtext.TruetypeFonts": [ - "doc/api/mathtext_api.rst:12" + "doc/api/artist_api.rst:202", + "lib/matplotlib/image.py:docstring of matplotlib.image:1" ], "matplotlib.patches.ArrowStyle._Base": [ - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.Fancy:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.Simple:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.Wedge:1" - ], - "matplotlib.patches.ArrowStyle._Bracket": [ - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.BarAB:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.BracketA:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.BracketAB:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.BracketB:1" + "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle:1" ], "matplotlib.patches.ArrowStyle._Curve": [ - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.Curve:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.CurveA:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.CurveAB:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.CurveB:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.CurveFilledA:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.CurveFilledAB:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle.CurveFilledB:1" - ], - "matplotlib.patches.BoxStyle._Base": [ - "lib/matplotlib/patches.py:docstring of matplotlib.patches.BoxStyle.Circle:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.BoxStyle.DArrow:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.BoxStyle.LArrow:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.BoxStyle.Round4:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.BoxStyle.Round:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.BoxStyle.Sawtooth:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.BoxStyle.Square:1" + "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle:1" ], "matplotlib.patches.ConnectionStyle._Base": [ - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ConnectionStyle.Angle3:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ConnectionStyle.Angle:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ConnectionStyle.Arc3:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ConnectionStyle.Arc:1", - "lib/matplotlib/patches.py:docstring of matplotlib.patches.ConnectionStyle.Bar:1" + "lib/matplotlib/patches.py:docstring of matplotlib.patches.ConnectionStyle:1" ], "matplotlib.patches._Style": [ "lib/matplotlib/patches.py:docstring of matplotlib.patches.ArrowStyle:1", @@ -391,45 +157,19 @@ "lib/matplotlib/patches.py:docstring of matplotlib.patches.ConnectionStyle:1", "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" ], - "matplotlib.projections.geo.AitoffAxes": [ - "doc/api/artist_api.rst:189" - ], - "matplotlib.projections.geo.GeoAxes": [ - "doc/api/artist_api.rst:189" - ], - "matplotlib.projections.geo.HammerAxes": [ - "doc/api/artist_api.rst:189" - ], - "matplotlib.projections.geo.LambertAxes": [ - "doc/api/artist_api.rst:189" - ], - "matplotlib.projections.geo.MollweideAxes": [ - "doc/api/artist_api.rst:189" + "matplotlib.projections.geo._GeoTransform": [ + "lib/matplotlib/projections/geo.py:docstring of matplotlib.projections.geo:1" ], "matplotlib.text._AnnotationBase": [ - "doc/api/artist_api.rst:189", - "lib/matplotlib/offsetbox.py:docstring of matplotlib.offsetbox.AnnotationBbox:1", + "doc/api/artist_api.rst:202", + "lib/matplotlib/offsetbox.py:docstring of matplotlib.offsetbox:1", "lib/matplotlib/text.py:docstring of matplotlib.text.Annotation:1" ], "matplotlib.transforms._BlendedMixin": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.BlendedAffine2D:1", - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.BlendedGenericTransform:1" - ], - "matplotlib.tri.trifinder.TriFinder": [ - "lib/matplotlib/tri/trifinder.py:docstring of matplotlib.tri.trifinder.TrapezoidMapTriFinder:1" - ], - "matplotlib.tri.triinterpolate.TriInterpolator": [ - "lib/matplotlib/tri/triinterpolate.py:docstring of matplotlib.tri.triinterpolate.CubicTriInterpolator:1", - "lib/matplotlib/tri/triinterpolate.py:docstring of matplotlib.tri.triinterpolate.LinearTriInterpolator:1" - ], - "matplotlib.tri.trirefine.TriRefiner": [ - "lib/matplotlib/tri/trirefine.py:docstring of matplotlib.tri.trirefine.UniformTriRefiner:1" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:1" ], "matplotlib.widgets._SelectorWidget": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.LassoSelector:1", - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.PolygonSelector:1", - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.RectangleSelector:1", - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.SpanSelector:1" + "lib/matplotlib/widgets.py:docstring of matplotlib.widgets:1" ], "mpl_toolkits.axes_grid1.axes_size._Base": [ "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.Add:1", @@ -444,560 +184,260 @@ "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size.SizeFromFunc:1" ], "mpl_toolkits.axes_grid1.parasite_axes.AxesHostAxes": [ - ":1", - "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:31::1" + "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:30::1", + "lib/mpl_toolkits/axes_grid1/parasite_axes.py:docstring of mpl_toolkits.axes_grid1.parasite_axes.AxesHostAxes:1" ], "mpl_toolkits.axes_grid1.parasite_axes.AxesParasite": [ - ":1", - "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:31::1" - ], - "mpl_toolkits.axes_grid1.parasite_axes.AxesParasiteParasiteAuxTrans": [ - ":1", - "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:31::1" + "doc/api/_as_gen/mpl_toolkits.axes_grid1.parasite_axes.rst:30::1", + "lib/mpl_toolkits/axes_grid1/parasite_axes.py:docstring of mpl_toolkits.axes_grid1.parasite_axes.AxesParasite:1" ], "mpl_toolkits.axisartist.Axes": [ - "doc/api/toolkits/axisartist.rst:5", "doc/api/toolkits/axisartist.rst:6" ], "mpl_toolkits.axisartist.axisline_style.AxislineStyle._Base": [ - "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle.SimpleArrow:1" + "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" ], "mpl_toolkits.axisartist.axisline_style._FancyAxislineStyle.FilledArrow": [ - ":1" + "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" ], "mpl_toolkits.axisartist.axisline_style._FancyAxislineStyle.SimpleArrow": [ - ":1" + "lib/mpl_toolkits/axisartist/axisline_style.py:docstring of mpl_toolkits.axisartist.axisline_style.AxislineStyle:1" ], "mpl_toolkits.axisartist.axislines.AxisArtistHelper._Base": [ - "lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.AxisArtistHelper.Fixed:1", - "lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.AxisArtistHelper.Floating:1" + "lib/mpl_toolkits/axisartist/axislines.py:docstring of mpl_toolkits.axisartist.axislines.AxisArtistHelper:1" ], - "mpl_toolkits.axisartist.floating_axes.Floating AxesHostAxes": [ - ":1", - "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:31::1" + "mpl_toolkits.axisartist.floating_axes.FloatingAxesHostAxes": [ + "doc/api/_as_gen/mpl_toolkits.axisartist.floating_axes.rst:32::1", + "lib/mpl_toolkits/axisartist/floating_axes.py:docstring of mpl_toolkits.axisartist.floating_axes.FloatingAxesHostAxes:1" ], "numpy.uint8": [ - ":1" + "lib/matplotlib/path.py:docstring of matplotlib.path:1" ], "unittest.case.TestCase": [ - "lib/matplotlib/testing/decorators.py:docstring of matplotlib.testing.decorators.CleanupTestCase:1" + "lib/matplotlib/testing/decorators.py:docstring of matplotlib.testing.decorators:1" ] }, "py:data": { "matplotlib.axes.Axes.transAxes": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:219", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:220", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:179", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:220", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:219" - ] - }, - "py:func": { - "matplotlib.Axis.set_ticks_position": [ - "doc/users/prev_whats_new/whats_new_1.4.rst:141" - ], - "matplotlib.InvertedPolarTransform.transform_non_affine": [ - "doc/users/prev_whats_new/whats_new_1.4.rst:236" - ], - "matplotlib.axis.Tick.label1On": [ - "doc/users/prev_whats_new/whats_new_2.1.0.rst:409" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:234", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.add_axes:2", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:91", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:233", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:234" ] }, "py:meth": { "AbstractPathEffect._update_gc": [ - "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.SimpleLineShadow:44", - "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.SimplePatchShadow:43", - "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.TickedStroke:60", - "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.withSimplePatchShadow:52", - "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.withTickedStroke:55" - ], - "FigureCanvasQTAgg.blit": [ - "doc/api/prev_api_changes/api_changes_2.2.0.rst:199" - ], - "FigureCanvasQTAgg.paintEvent": [ - "doc/api/prev_api_changes/api_changes_2.2.0.rst:199" - ], - "FigureCanvasQTAgg.print_figure": [ - "doc/api/prev_api_changes/api_changes_2.2.0.rst:199" + "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:26", + "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:28", + "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:35", + "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:39", + "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:44" ], "IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook": [ - "doc/users/interactive_guide.rst:421" + "doc/users/explain/interactive_guide.rst:420" ], "_find_tails": [ - "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:9" - ], - "_iter_collection": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.RendererBase.draw_path_collection:12", - "lib/matplotlib/backends/backend_agg.py:docstring of matplotlib.backends.backend_agg.RendererAgg.draw_path_collection:12", - "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf.draw_path_collection:12", - "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS.draw_path_collection:12", - "lib/matplotlib/backends/backend_svg.py:docstring of matplotlib.backends.backend_svg.RendererSVG.draw_path_collection:12", - "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.PathEffectRenderer.draw_path_collection:12" - ], - "_iter_collection_raw_paths": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.RendererBase.draw_path_collection:12", - "lib/matplotlib/backends/backend_agg.py:docstring of matplotlib.backends.backend_agg.RendererAgg.draw_path_collection:12", - "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf.draw_path_collection:12", - "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS.draw_path_collection:12", - "lib/matplotlib/backends/backend_svg.py:docstring of matplotlib.backends.backend_svg.RendererSVG.draw_path_collection:12", - "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.PathEffectRenderer.draw_path_collection:12" + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:5" ], "_make_barbs": [ - "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:9" - ], - "autoscale_view": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.margins:32" - ], - "colorbar.Colobar.minorticks_off": [ - "doc/users/prev_whats_new/whats_new_3.0.rst:63" - ], - "colorbar.Colobar.minorticks_on": [ - "doc/users/prev_whats_new/whats_new_3.0.rst:63" - ], - "draw_image": [ - "lib/matplotlib/backends/backend_agg.py:docstring of matplotlib.backends.backend_agg.RendererAgg.option_scale_image:2" + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:5" ], "get_matrix": [ - "lib/matplotlib/transforms.py:docstring of matplotlib.transforms.Affine2DBase:13" - ], - "matplotlib.dates.DateFormatter.__call__": [ - "doc/users/prev_whats_new/whats_new_1.5.rst:497" - ], - "matplotlib.dates.MicrosecondLocator.__call__": [ - "doc/api/prev_api_changes/api_changes_1.5.0.rst:122" - ], - "option_scale_image": [ - "lib/matplotlib/backends/backend_cairo.py:docstring of matplotlib.backends.backend_cairo.RendererCairo.draw_image:22", - "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.RendererPdf.draw_image:22", - "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.RendererPS.draw_image:22", - "lib/matplotlib/backends/backend_template.py:docstring of matplotlib.backends.backend_template.RendererTemplate.draw_image:22" - ] - }, - "py:mod": { - "IPython.terminal.pt_inputhooks": [ - "doc/users/interactive_guide.rst:421" - ], - "dateutil": [ - "lib/matplotlib/dates.py:docstring of matplotlib.dates:1" - ], - "matplotlib": [ - "doc/api/prev_api_changes/api_changes_0.91.2.rst:15" - ], - "matplotlib.ft2font": [ - "doc/api/prev_api_changes/api_changes_0.91.0.rst:34" + "lib/matplotlib/transforms.py:docstring of matplotlib.transforms:12" + ], + "matplotlib.collections._CollectionWithSizes.set_sizes": [ + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.barbs:171", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.broken_barh:81", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_between:113", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.fill_betweenx:113", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.hexbin:201", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.pcolor:173", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.quiver:207", + "lib/matplotlib/collections.py:docstring of matplotlib.collections.AsteriskPolygonCollection:22", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.barbs:171", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.broken_barh:81", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_between:113", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.fill_betweenx:113", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.hexbin:201", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.pcolor:173", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.quiver:207", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:205", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Barbs:38", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:244", + "lib/matplotlib/quiver.py:docstring of matplotlib.quiver.Quiver:38", + "lib/mpl_toolkits/mplot3d/art3d.py:docstring of mpl_toolkits.mplot3d.art3d.Path3DCollection:39", + "lib/mpl_toolkits/mplot3d/art3d.py:docstring of mpl_toolkits.mplot3d.art3d.Poly3DCollection:37" ] }, "py:obj": { "Artist.stale_callback": [ - "doc/users/interactive_guide.rst:323" + "doc/users/explain/interactive_guide.rst:323" ], "Artist.sticky_edges": [ - "doc/api/axes_api.rst:364::1", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes.Axes.use_sticky_edges:2" - ], - "ArtistInspector.aliasd": [ - "doc/api/prev_api_changes/api_changes_3.0.0.rst:332", - "doc/users/prev_whats_new/whats_new_3.1.0.rst:171" - ], - "ArtistInspector.get_aliases": [ - "doc/api/prev_api_changes/api_changes_3.0.0.rst:327" + "doc/api/axes_api.rst:352::1", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes.Axes.use_sticky_edges:1" ], "Axes.dataLim": [ - "doc/api/axes_api.rst:304::1", - "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.update_datalim:2", - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.update_datalim:2" - ], - "Axes.datalim": [ - "doc/api/axes_api.rst:304::1", - "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.update_datalim_bounds:2" - ], - "Axes.fmt_xdata": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:397", - "doc/api/prev_api_changes/api_changes_3.1.0.rst:400" - ], - "Axes.fmt_ydata": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:397", - "doc/api/prev_api_changes/api_changes_3.1.0.rst:400" - ], - "Axes.transData": [ - "doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst:91", - "doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst:93", - "doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst:95", - "doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst:98" + "doc/api/axes_api.rst:291::1", + "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.update_datalim:1" ], "AxesBase": [ - "doc/api/axes_api.rst:456::1", - "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.add_child_axes:2" - ], - "Axis._update_ticks": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:1072" - ], - "Axis.set_tick_params": [ - "doc/users/prev_whats_new/whats_new_2.1.0.rst:394" - ], - "Axis.units": [ - "doc/api/prev_api_changes/api_changes_2.2.0.rst:77" - ], - "FT2Font": [ - "doc/gallery/misc/ftface_props.rst:16", - "lib/matplotlib/font_manager.py:docstring of matplotlib.font_manager.ttfFontProperty:8" + "doc/api/axes_api.rst:444::1", + "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.add_child_axes:1" ], "Figure.stale_callback": [ - "doc/users/interactive_guide.rst:333" - ], - "FigureCanvas": [ - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ToolBase:25" - ], - "Formatter.__call__": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:1122", - "doc/api/prev_api_changes/api_changes_3.1.0.rst:1128" - ], - "GaussianKDE": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.violinplot:46", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.violinplot:46" - ], - "Glue": [ - "lib/matplotlib/mathtext.py:docstring of matplotlib.mathtext.GlueSpec:2" + "doc/users/explain/interactive_guide.rst:333" ], "Glyph": [ - "doc/gallery/misc/ftface_props.rst:16" + "doc/gallery/misc/ftface_props.rst:28" ], "Image": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.gci:4" + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.gci:1" ], "ImageComparisonFailure": [ - "lib/matplotlib/testing/decorators.py:docstring of matplotlib.testing.decorators.image_comparison:2" - ], - "ImageComparisonTest": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:706" + "lib/matplotlib/testing/decorators.py:docstring of matplotlib.testing.decorators:1" ], "Line2D.pick": [ - "doc/users/event_handling.rst:438" - ], - "MicrosecondLocator.__call__": [ - "doc/api/prev_api_changes/api_changes_1.5.0.rst:119" - ], - "MovieWriter.saving": [ - "doc/api/animation_api.rst:221" - ], - "MovieWriterBase": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.AVConvBase:4", - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FFMpegBase:4", - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickBase:4" - ], - "MovieWriterRegistry": [ - "doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst:49", - "doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst:51", - "doc/api/prev_api_changes/api_changes_3.2.0/deprecations.rst:121" + "doc/users/explain/event_handling.rst:468" ], "QuadContourSet.changed()": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contour:131", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contourf:131", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contour:131", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contourf:131" - ], - "Quiver.pivot": [ - "doc/api/prev_api_changes/api_changes_1.5.0.rst:44" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contour:147", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.contourf:147", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contour:147", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.contourf:147" ], "Rectangle.contains": [ - "doc/users/event_handling.rst:150" + "doc/users/explain/event_handling.rst:180" ], "Size.from_any": [ - "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid.__init__:61", - "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:61", - "lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:61" + "lib/mpl_toolkits/axes_grid1/axes_grid.py:docstring of mpl_toolkits.axes_grid1.axes_grid.ImageGrid:53", + "lib/mpl_toolkits/axisartist/axes_grid.py:docstring of mpl_toolkits.axisartist.axes_grid.ImageGrid:53" ], "Timer": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.FigureCanvasBase.new_timer:2", - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.TimerBase:14" + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases:1", + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases:13" ], "ToolContainer": [ - "doc/users/prev_whats_new/whats_new_1.5.rst:615", - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase.remove_toolitem:2", - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase:20" - ], - "ToolContainers": [ - "doc/users/prev_whats_new/whats_new_1.5.rst:615" + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases:1", + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases:19" ], - "Toolbars": [ - "doc/users/prev_whats_new/whats_new_1.5.rst:615" + "_iter_collection": [ + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases:11", + "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.FigureCanvasPdf:1", + "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.FigureCanvasPS:1", + "lib/matplotlib/backends/backend_svg.py:docstring of matplotlib.backends.backend_svg.FigureCanvasSVG:1", + "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:1" ], - "Tools": [ - "doc/users/prev_whats_new/whats_new_1.5.rst:608" + "_iter_collection_raw_paths": [ + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases:11", + "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.FigureCanvasPdf:1", + "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.FigureCanvasPS:1", + "lib/matplotlib/backends/backend_svg.py:docstring of matplotlib.backends.backend_svg.FigureCanvasSVG:1", + "lib/matplotlib/patheffects.py:docstring of matplotlib.patheffects.AbstractPathEffect:1" ], "_read": [ - "lib/matplotlib/dviread.py:docstring of matplotlib.dviread.Vf:20" + "lib/matplotlib/dviread.py:docstring of matplotlib.dviread:19" ], "active": [ - "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.AxesWidget:34" - ], - "add_subplot": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figure:50" - ], - "add_tool": [ - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.add_tools_to_container:11", - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.add_tools_to_manager:11" - ], - "autoscale_view": [ - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.autoscale:19", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.plot:128" + "lib/matplotlib/widgets.py:docstring of matplotlib.widgets.AxesWidget:15" ], "ax.transAxes": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.indicate_inset:19", - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.inset_axes:11" - ], - "axes.Axes.patch": [ - "doc/api/prev_api_changes/api_changes_1.3.x.rst:36" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.indicate_inset:14", + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.inset_axes:6" ], "axes.bbox": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:128", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:129", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:88", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:129", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:128" - ], - "axes3d.Axes3D.xaxis": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:939" - ], - "axes3d.Axes3D.yaxis": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:939" - ], - "axes3d.Axes3D.zaxis": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:939" - ], - "axis.Axis.get_ticks_position": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:826" - ], - "backend_bases.RendererBase": [ - "lib/matplotlib/backends/backend_template.py:docstring of matplotlib.backends.backend_template.RendererTemplate:4" - ], - "backend_bases.ToolContainerBase": [ - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.add_tools_to_container:8" - ], - "blocking_input.BlockingInput.__call__": [ - "doc/api/prev_api_changes/api_changes_3.2.0/behavior.rst:295" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:137", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.Figure:116", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.DraggableLegend.finalize_offset:20", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:136", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:137" ], "can_composite": [ - "lib/matplotlib/image.py:docstring of matplotlib.image.composite_images:9" + "lib/matplotlib/image.py:docstring of matplotlib.image:5" ], "cleanup": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter.setup:18", - "lib/matplotlib/animation.py:docstring of matplotlib.animation.HTMLWriter.setup:18" - ], - "colorbar.ColorbarBase.outline": [ - "doc/api/prev_api_changes/api_changes_1.4.x.rst:83" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter:13", + "lib/matplotlib/animation.py:docstring of matplotlib.animation.HTMLWriter:13" ], "converter": [ - "lib/matplotlib/testing/compare.py:docstring of matplotlib.testing.compare.compare_images:4" + "lib/matplotlib/testing/compare.py:docstring of matplotlib.testing.compare:1" ], - "fc-list": [ - "lib/matplotlib/font_manager.py:docstring of matplotlib.font_manager.get_fontconfig_fonts:2" - ], - "figure.Figure.canvas.set_window_title()": [ - "doc/users/prev_whats_new/whats_new_3.0.rst:84" + "draw_image": [ + "lib/matplotlib/backends/backend_agg.py:docstring of matplotlib.backends.backend_agg:1" ], "figure.bbox": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:128", - "lib/matplotlib/figure.py:docstring of matplotlib.figure.FigureBase.legend:129", - "lib/matplotlib/legend.py:docstring of matplotlib.legend.Legend:88", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:129", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:128" - ], - "floating_axes.FloatingSubplot": [ - "doc/gallery/axisartist/demo_floating_axes.rst:22" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.legend:137", + "lib/matplotlib/figure.py:docstring of matplotlib.figure.Figure:116", + "lib/matplotlib/legend.py:docstring of matplotlib.legend.DraggableLegend.finalize_offset:20", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.figlegend:136", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.legend:137" ], "fmt_xdata": [ - "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.format_xdata:4" + "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.format_xdata:1" ], "fmt_ydata": [ - "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.format_ydata:4" - ], - "font.*": [ - "doc/users/prev_whats_new/whats_new_1.3.rst:302" - ], - "gaussian_kde": [ - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:32" + "lib/matplotlib/axes/_base.py:docstring of matplotlib.axes._base._AxesBase.format_ydata:1" ], "get_size": [ - "doc/users/prev_whats_new/whats_new_1.4.rst:223", "lib/mpl_toolkits/axes_grid1/axes_size.py:docstring of mpl_toolkits.axes_grid1.axes_size:1" ], "get_xbound": [ - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_xlim3d:22" + "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_xlim:17" ], "get_ybound": [ - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_ylim3d:22" - ], - "h_pad": [ - "lib/matplotlib/figure.py:docstring of matplotlib.figure.Figure.set_constrained_layout:5" - ], - "image": [ - "lib/matplotlib/sphinxext/plot_directive.py:docstring of matplotlib.sphinxext.plot_directive:78" - ], - "interactive": [ - "lib/matplotlib/backends/backend_nbagg.py:docstring of matplotlib.backends.backend_nbagg._BackendNbAgg.show:4" + "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_ylim:17" ], "invert_xaxis": [ - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_xlim3d:24" + "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_xlim:19" ], "invert_yaxis": [ - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_ylim3d:24" + "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_ylim:19" ], "ipykernel.pylab.backend_inline": [ - "doc/users/interactive.rst:257" + "doc/users/explain/interactive.rst:255" ], "kde.covariance_factor": [ - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:41" + "lib/matplotlib/mlab.py:docstring of matplotlib.mlab:40" ], "kde.factor": [ - "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.violinplot:46", - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:12", - "lib/matplotlib/mlab.py:docstring of matplotlib.mlab.GaussianKDE:45", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.violinplot:46" - ], - "labelcolor": [ - "doc/users/prev_whats_new/whats_new_2.0.0.rst:120", - "doc/users/prev_whats_new/whats_new_2.0.0.rst:123" - ], - "legend.Legend.set_draggable()": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:499" - ], - "levels": [ - "doc/api/prev_api_changes/api_changes_3.0.0.rst:197" - ], - "load_char": [ - "doc/gallery/misc/ftface_props.rst:16" - ], - "mainloop": [ - "lib/matplotlib/backends/backend_nbagg.py:docstring of matplotlib.backends.backend_nbagg._BackendNbAgg.show:4" + "lib/matplotlib/axes/_axes.py:docstring of matplotlib.axes._axes.Axes.violinplot:41", + "lib/matplotlib/mlab.py:docstring of matplotlib.mlab:11", + "lib/matplotlib/mlab.py:docstring of matplotlib.mlab:44", + "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.violinplot:41" ], "make_image": [ - "lib/matplotlib/image.py:docstring of matplotlib.image.composite_images:9" - ], - "markevery": [ - "doc/users/prev_whats_new/whats_new_1.4.rst:212", - "doc/users/prev_whats_new/whats_new_1.4.rst:214", - "doc/users/prev_whats_new/whats_new_3.0.rst:110" - ], - "matplotlib.animation.AVConvBase.output_args": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.MovieWriter.isAvailable:1::1" - ], - "matplotlib.animation.AVConvFileWriter.args_key": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:39::1" - ], - "matplotlib.animation.AVConvFileWriter.bin_path": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:28::1" - ], - "matplotlib.animation.AVConvFileWriter.cleanup": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:28::1" - ], - "matplotlib.animation.AVConvFileWriter.clear_temp": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:39::1" - ], - "matplotlib.animation.AVConvFileWriter.exec_key": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:39::1" - ], - "matplotlib.animation.AVConvFileWriter.finish": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:28::1" - ], - "matplotlib.animation.AVConvFileWriter.frame_format": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:39::1" - ], - "matplotlib.animation.AVConvFileWriter.frame_size": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:39::1" - ], - "matplotlib.animation.AVConvFileWriter.grab_frame": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:28::1" - ], - "matplotlib.animation.AVConvFileWriter.isAvailable": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:28::1" - ], - "matplotlib.animation.AVConvFileWriter.output_args": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:39::1" - ], - "matplotlib.animation.AVConvFileWriter.saving": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:28::1" - ], - "matplotlib.animation.AVConvFileWriter.setup": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:28::1" - ], - "matplotlib.animation.AVConvFileWriter.supported_formats": [ - "doc/api/_as_gen/matplotlib.animation.AVConvFileWriter.rst:39::1" - ], - "matplotlib.animation.AVConvWriter.args_key": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:37::1" - ], - "matplotlib.animation.AVConvWriter.bin_path": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:28::1" - ], - "matplotlib.animation.AVConvWriter.cleanup": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:28::1" - ], - "matplotlib.animation.AVConvWriter.exec_key": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:37::1" - ], - "matplotlib.animation.AVConvWriter.finish": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:28::1" - ], - "matplotlib.animation.AVConvWriter.frame_size": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:37::1" - ], - "matplotlib.animation.AVConvWriter.grab_frame": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:28::1" - ], - "matplotlib.animation.AVConvWriter.isAvailable": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:28::1" - ], - "matplotlib.animation.AVConvWriter.output_args": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:37::1" - ], - "matplotlib.animation.AVConvWriter.saving": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:28::1" - ], - "matplotlib.animation.AVConvWriter.setup": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:28::1" - ], - "matplotlib.animation.AVConvWriter.supported_formats": [ - "doc/api/_as_gen/matplotlib.animation.AVConvWriter.rst:37::1" + "lib/matplotlib/image.py:docstring of matplotlib.image:5" ], "matplotlib.animation.ArtistAnimation.new_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" ], "matplotlib.animation.ArtistAnimation.new_saved_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" ], "matplotlib.animation.ArtistAnimation.pause": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" + ], + "matplotlib.animation.ArtistAnimation.repeat": [ + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:33::1" ], "matplotlib.animation.ArtistAnimation.resume": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" ], "matplotlib.animation.ArtistAnimation.save": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" ], "matplotlib.animation.ArtistAnimation.to_html5_video": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" ], "matplotlib.animation.ArtistAnimation.to_jshtml": [ - "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:23::1" - ], - "matplotlib.animation.FFMpegFileWriter.args_key": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FFMpegFileWriter.supported_formats:1::1" + "doc/api/_as_gen/matplotlib.animation.ArtistAnimation.rst:28::1" ], "matplotlib.animation.FFMpegFileWriter.bin_path": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:28::1" - ], - "matplotlib.animation.FFMpegFileWriter.cleanup": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:28::1" - ], - "matplotlib.animation.FFMpegFileWriter.clear_temp": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FFMpegFileWriter.supported_formats:1::1" - ], - "matplotlib.animation.FFMpegFileWriter.exec_key": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FFMpegFileWriter.supported_formats:1::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:27::1" ], "matplotlib.animation.FFMpegFileWriter.finish": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:27::1" ], "matplotlib.animation.FFMpegFileWriter.frame_format": [ "lib/matplotlib/animation.py:docstring of matplotlib.animation.FFMpegFileWriter.supported_formats:1::1" @@ -1006,202 +446,157 @@ "lib/matplotlib/animation.py:docstring of matplotlib.animation.FFMpegFileWriter.supported_formats:1::1" ], "matplotlib.animation.FFMpegFileWriter.grab_frame": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:27::1" ], "matplotlib.animation.FFMpegFileWriter.isAvailable": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:27::1" ], "matplotlib.animation.FFMpegFileWriter.output_args": [ "lib/matplotlib/animation.py:docstring of matplotlib.animation.FFMpegFileWriter.supported_formats:1::1" ], "matplotlib.animation.FFMpegFileWriter.saving": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:27::1" ], "matplotlib.animation.FFMpegFileWriter.setup": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:28::1" - ], - "matplotlib.animation.FFMpegWriter.args_key": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:37::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegFileWriter.rst:27::1" ], "matplotlib.animation.FFMpegWriter.bin_path": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:28::1" - ], - "matplotlib.animation.FFMpegWriter.cleanup": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:28::1" - ], - "matplotlib.animation.FFMpegWriter.exec_key": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:37::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:27::1" ], "matplotlib.animation.FFMpegWriter.finish": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:27::1" ], "matplotlib.animation.FFMpegWriter.frame_size": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:37::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:34::1" ], "matplotlib.animation.FFMpegWriter.grab_frame": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:27::1" ], "matplotlib.animation.FFMpegWriter.isAvailable": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:27::1" ], "matplotlib.animation.FFMpegWriter.output_args": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:37::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:34::1" ], "matplotlib.animation.FFMpegWriter.saving": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:27::1" ], "matplotlib.animation.FFMpegWriter.setup": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:27::1" ], "matplotlib.animation.FFMpegWriter.supported_formats": [ - "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:37::1" - ], - "matplotlib.animation.FileMovieWriter.args_key": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter.clear_temp:1::1" + "doc/api/_as_gen/matplotlib.animation.FFMpegWriter.rst:34::1" ], "matplotlib.animation.FileMovieWriter.bin_path": [ - "doc/api/_as_gen/matplotlib.animation.FileMovieWriter.rst:28::1" - ], - "matplotlib.animation.FileMovieWriter.cleanup": [ - "doc/api/_as_gen/matplotlib.animation.FileMovieWriter.rst:28::1" - ], - "matplotlib.animation.FileMovieWriter.exec_key": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter.clear_temp:1::1" + "doc/api/_as_gen/matplotlib.animation.FileMovieWriter.rst:27::1" ], "matplotlib.animation.FileMovieWriter.frame_size": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter.clear_temp:1::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter.finish:1::1" ], "matplotlib.animation.FileMovieWriter.isAvailable": [ - "doc/api/_as_gen/matplotlib.animation.FileMovieWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FileMovieWriter.rst:27::1" ], "matplotlib.animation.FileMovieWriter.saving": [ - "doc/api/_as_gen/matplotlib.animation.FileMovieWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.FileMovieWriter.rst:27::1" ], "matplotlib.animation.FileMovieWriter.supported_formats": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter.clear_temp:1::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.FileMovieWriter.finish:1::1" ], "matplotlib.animation.FuncAnimation.pause": [ + "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" + ], + "matplotlib.animation.FuncAnimation.repeat": [ "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" ], "matplotlib.animation.FuncAnimation.resume": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" + "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" ], "matplotlib.animation.FuncAnimation.save": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" + "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" ], "matplotlib.animation.FuncAnimation.to_html5_video": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" + "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" ], "matplotlib.animation.FuncAnimation.to_jshtml": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.FuncAnimation.new_frame_seq:1::1" + "doc/api/_as_gen/matplotlib.animation.FuncAnimation.rst:28::1" ], "matplotlib.animation.HTMLWriter.bin_path": [ - "doc/api/_as_gen/matplotlib.animation.HTMLWriter.rst:28::1" - ], - "matplotlib.animation.HTMLWriter.cleanup": [ - "doc/api/_as_gen/matplotlib.animation.HTMLWriter.rst:28::1" - ], - "matplotlib.animation.HTMLWriter.clear_temp": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.HTMLWriter.args_key:1::1" - ], - "matplotlib.animation.HTMLWriter.exec_key": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.HTMLWriter.args_key:1::1" + "doc/api/_as_gen/matplotlib.animation.HTMLWriter.rst:27::1" ], "matplotlib.animation.HTMLWriter.frame_format": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.HTMLWriter.args_key:1::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.HTMLWriter.finish:1::1" ], "matplotlib.animation.HTMLWriter.frame_size": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.HTMLWriter.args_key:1::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.HTMLWriter.finish:1::1" ], "matplotlib.animation.HTMLWriter.saving": [ - "doc/api/_as_gen/matplotlib.animation.HTMLWriter.rst:28::1" - ], - "matplotlib.animation.ImageMagickFileWriter.args_key": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.supported_formats:1::1" + "doc/api/_as_gen/matplotlib.animation.HTMLWriter.rst:27::1" ], "matplotlib.animation.ImageMagickFileWriter.bin_path": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:28::1" - ], - "matplotlib.animation.ImageMagickFileWriter.cleanup": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:28::1" - ], - "matplotlib.animation.ImageMagickFileWriter.clear_temp": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.supported_formats:1::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], "matplotlib.animation.ImageMagickFileWriter.delay": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.supported_formats:1::1" - ], - "matplotlib.animation.ImageMagickFileWriter.exec_key": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.supported_formats:1::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.input_names:1::1" ], "matplotlib.animation.ImageMagickFileWriter.finish": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], "matplotlib.animation.ImageMagickFileWriter.frame_format": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.supported_formats:1::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.input_names:1::1" ], "matplotlib.animation.ImageMagickFileWriter.frame_size": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.supported_formats:1::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.input_names:1::1" ], "matplotlib.animation.ImageMagickFileWriter.grab_frame": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], "matplotlib.animation.ImageMagickFileWriter.isAvailable": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], "matplotlib.animation.ImageMagickFileWriter.output_args": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.supported_formats:1::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickFileWriter.input_names:1::1" ], "matplotlib.animation.ImageMagickFileWriter.saving": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], "matplotlib.animation.ImageMagickFileWriter.setup": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:28::1" - ], - "matplotlib.animation.ImageMagickWriter.args_key": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:38::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickFileWriter.rst:27::1" ], "matplotlib.animation.ImageMagickWriter.bin_path": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:28::1" - ], - "matplotlib.animation.ImageMagickWriter.cleanup": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], "matplotlib.animation.ImageMagickWriter.delay": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:38::1" - ], - "matplotlib.animation.ImageMagickWriter.exec_key": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:38::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickWriter.input_names:1::1" ], "matplotlib.animation.ImageMagickWriter.finish": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], "matplotlib.animation.ImageMagickWriter.frame_size": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:38::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickWriter.input_names:1::1" ], "matplotlib.animation.ImageMagickWriter.grab_frame": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], "matplotlib.animation.ImageMagickWriter.isAvailable": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], "matplotlib.animation.ImageMagickWriter.output_args": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:38::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickWriter.input_names:1::1" ], "matplotlib.animation.ImageMagickWriter.saving": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], "matplotlib.animation.ImageMagickWriter.setup": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:27::1" ], "matplotlib.animation.ImageMagickWriter.supported_formats": [ - "doc/api/_as_gen/matplotlib.animation.ImageMagickWriter.rst:38::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.ImageMagickWriter.input_names:1::1" ], "matplotlib.animation.MovieWriter.frame_size": [ - "lib/matplotlib/animation.py:docstring of matplotlib.animation.MovieWriter.args_key:1::1" + "lib/matplotlib/animation.py:docstring of matplotlib.animation.MovieWriter.bin_path:1::1" ], "matplotlib.animation.MovieWriter.saving": [ - "doc/api/_as_gen/matplotlib.animation.MovieWriter.rst:28::1" + "doc/api/_as_gen/matplotlib.animation.MovieWriter.rst:27::1" ], "matplotlib.animation.PillowWriter.frame_size": [ "lib/matplotlib/animation.py:docstring of matplotlib.animation.PillowWriter.finish:1::1" @@ -1210,131 +605,61 @@ "doc/api/_as_gen/matplotlib.animation.PillowWriter.rst:26::1" ], "matplotlib.animation.TimedAnimation.new_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" ], "matplotlib.animation.TimedAnimation.new_saved_frame_seq": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" ], "matplotlib.animation.TimedAnimation.pause": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" ], "matplotlib.animation.TimedAnimation.resume": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" ], "matplotlib.animation.TimedAnimation.save": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" ], "matplotlib.animation.TimedAnimation.to_html5_video": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" ], "matplotlib.animation.TimedAnimation.to_jshtml": [ - "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:23::1" - ], - "matplotlib.colorbar.ColorbarBase.set_clim": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:290" - ], - "matplotlib.colorbar.ColorbarBase.set_cmap": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:290" - ], - "matplotlib.colorbar.ColorbarBase.set_norm": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:290" - ], - "matplotlib.dates.rrulewrapper": [ - "lib/matplotlib/dates.py:docstring of matplotlib.dates:121" - ], - "matplotlib.sphinxext.mathmpl": [ - "doc/users/prev_whats_new/whats_new_3.0.rst:224" - ], - "matplotlib.style.core.STYLE_BLACKLIST": [ - "doc/api/prev_api_changes/api_changes_3.0.0.rst:298", - "lib/matplotlib/__init__.py:docstring of matplotlib.rc_file:4", - "lib/matplotlib/__init__.py:docstring of matplotlib.rc_file_defaults:4", - "lib/matplotlib/__init__.py:docstring of matplotlib.rcdefaults:4", - "lib/matplotlib/pyplot.py:docstring of matplotlib.pyplot.rcdefaults:4" - ], - "matplotlib.testing.conftest.mpl_test_settings": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:947" + "doc/api/_as_gen/matplotlib.animation.TimedAnimation.rst:28::1" ], "mpl_toolkits.axislines.Axes": [ - "lib/mpl_toolkits/axisartist/axis_artist.py:docstring of mpl_toolkits.axisartist.axis_artist:26" + "lib/mpl_toolkits/axisartist/axis_artist.py:docstring of mpl_toolkits.axisartist.axis_artist:7" ], "next_whats_new": [ "doc/users/next_whats_new/README.rst:6" ], - "plot": [ - "lib/matplotlib/sphinxext/plot_directive.py:docstring of matplotlib.sphinxext.plot_directive:4" - ], - "plot_include_source": [ - "lib/matplotlib/sphinxext/plot_directive.py:docstring of matplotlib.sphinxext.plot_directive:52" + "option_scale_image": [ + "lib/matplotlib/backends/backend_cairo.py:docstring of matplotlib.backends.backend_cairo.FigureCanvasCairo:1", + "lib/matplotlib/backends/backend_pdf.py:docstring of matplotlib.backends.backend_pdf.FigureCanvasPdf:1", + "lib/matplotlib/backends/backend_ps.py:docstring of matplotlib.backends.backend_ps.FigureCanvasPS:2", + "lib/matplotlib/backends/backend_template.py:docstring of matplotlib.backends.backend_template:18" ], "print_xyz": [ "lib/matplotlib/backends/backend_template.py:docstring of matplotlib.backends.backend_template:22" ], - "pyplot.set_loglevel": [ - "doc/users/prev_whats_new/whats_new_3.1.0.rst:374" - ], - "rrulewrapper": [ - "lib/matplotlib/dates.py:docstring of matplotlib.dates:121" - ], - "scipy.stats.norm.pdf": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:569", - "doc/api/prev_api_changes/api_changes_3.1.0.rst:655" - ], - "self.codes": [ - "lib/matplotlib/path.py:docstring of matplotlib.path.Path.codes:2" - ], - "self.vertices": [ - "lib/matplotlib/path.py:docstring of matplotlib.path.Path.codes:2" - ], - "set_size": [ - "doc/users/prev_whats_new/whats_new_1.4.rst:223" - ], "set_xbound": [ - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_xlim3d:22" + "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_xlim:17" ], "set_ybound": [ - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_ylim3d:22" - ], - "streamplot.Grid": [ - "doc/api/prev_api_changes/api_changes_3.1.0.rst:479" - ], - "style.available": [ - "lib/matplotlib/style/__init__.py:docstring of matplotlib.style.core.context:12", - "lib/matplotlib/style/__init__.py:docstring of matplotlib.style.core.use:19" - ], - "to_html5_video": [ - "doc/users/prev_whats_new/whats_new_1.5.rst:728" + "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_ylim:17" ], "toggled": [ - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.AxisScaleBase.disable:4", - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.AxisScaleBase.enable:4", - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.AxisScaleBase.trigger:2", - "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", - "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools.ZoomPanBase.trigger:2" + "lib/matplotlib/backend_tools.py:docstring of matplotlib.backend_tools:1" ], "tool_removed_event": [ - "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases.ToolContainerBase.remove_toolitem:6" - ], - "toolbar": [ - "doc/users/prev_whats_new/whats_new_1.5.rst:615" - ], - "trigger": [ - "doc/users/prev_whats_new/whats_new_1.5.rst:615", - "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" - ], - "w_pad": [ - "lib/matplotlib/figure.py:docstring of matplotlib.figure.Figure.set_constrained_layout:5" + "lib/matplotlib/backend_bases.py:docstring of matplotlib.backend_bases:2" ], "whats_new.rst": [ "doc/users/next_whats_new/README.rst:6" ], "xaxis_inverted": [ - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_xlim3d:24" + "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_xlim:19" ], "yaxis_inverted": [ - "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_ylim3d:24" + "lib/mpl_toolkits/mplot3d/axes3d.py:docstring of mpl_toolkits.mplot3d.axes3d.Axes3D.get_ylim:19" ] } -} \ No newline at end of file +} diff --git a/doc/resources/index.rst b/doc/resources/index.rst deleted file mode 100644 index 3dd15348f091..000000000000 --- a/doc/resources/index.rst +++ /dev/null @@ -1,63 +0,0 @@ -.. _resources-index: - -******************* - External Resources -******************* - - -============================= - Books, Chapters and Articles -============================= - -* `Mastering matplotlib - `_ - by Duncan M. McGreggor - -* `Interactive Applications Using Matplotlib - `_ - by Benjamin Root - -* `Matplotlib for Python Developers - `_ - by Sandro Tosi - -* `Matplotlib chapter `_ - by John Hunter and Michael Droettboom in The Architecture of Open Source - Applications - -* `Ten Simple Rules for Better Figures - `_ - by Nicolas P. Rougier, Michael Droettboom and Philip E. Bourne - -* `Learning Scientific Programming with Python chapter 7 - `_ - by Christian Hill - -======= - Videos -======= - -* `Plotting with matplotlib `_ - by Mike Müller - -* `Introduction to NumPy and Matplotlib - `_ by Eric Jones - -* `Anatomy of Matplotlib - `_ - by Benjamin Root - -* `Data Visualization Basics with Python (O'Reilly) - `_ - by Randal S. Olson - -========== - Tutorials -========== - -* `Matplotlib tutorial `_ - by Nicolas P. Rougier - -* `Anatomy of Matplotlib - IPython Notebooks - `_ - by Benjamin Root diff --git a/doc/sphinxext/custom_roles.py b/doc/sphinxext/custom_roles.py index 0734f2b15ac3..3dcecc3df733 100644 --- a/doc/sphinxext/custom_roles.py +++ b/doc/sphinxext/custom_roles.py @@ -1,21 +1,57 @@ +from urllib.parse import urlsplit, urlunsplit + from docutils import nodes -from os.path import sep + from matplotlib import rcParamsDefault -def rcparam_role(name, rawtext, text, lineno, inliner, options={}, content=[]): - rendered = nodes.Text(f'rcParams["{text}"]') +class QueryReference(nodes.Inline, nodes.TextElement): + """ + Wraps a reference or pending reference to add a query string. + + The query string is generated from the attributes added to this node. + + Also equivalent to a `~docutils.nodes.literal` node. + """ + + def to_query_string(self): + """Generate query string from node attributes.""" + return '&'.join(f'{name}={value}' for name, value in self.attlist()) + + +def visit_query_reference_node(self, node): + """ + Resolve *node* into query strings on its ``reference`` children. - source = inliner.document.attributes['source'].replace(sep, '/') - rel_source = source.split('/doc/', 1)[1] + Then act as if this is a `~docutils.nodes.literal`. + """ + query = node.to_query_string() + for refnode in node.findall(nodes.reference): + uri = urlsplit(refnode['refuri'])._replace(query=query) + refnode['refuri'] = urlunsplit(uri) - levels = rel_source.count('/') - refuri = ('../' * levels + - 'tutorials/introductory/customizing.html' + - f"?highlight={text}#a-sample-matplotlibrc-file") + self.visit_literal(node) + + +def depart_query_reference_node(self, node): + """ + Act as if this is a `~docutils.nodes.literal`. + """ + self.depart_literal(node) + + +def rcparam_role(name, rawtext, text, lineno, inliner, options={}, content=[]): + # Generate a pending cross-reference so that Sphinx will ensure this link + # isn't broken at some point in the future. + title = f'rcParams["{text}"]' + target = 'matplotlibrc-sample' + ref_nodes, messages = inliner.interpreted(title, f'{title} <{target}>', + 'ref', lineno) + + qr = QueryReference(rawtext, highlight=text) + qr += ref_nodes + node_list = [qr] - ref = nodes.reference(rawtext, rendered, refuri=refuri) - node_list = [nodes.literal('', '', ref)] # The default backend would be printed as "agg", but that's not correct (as # the default is actually determined by fallback). if text in rcParamsDefault and text != "backend": @@ -24,9 +60,16 @@ def rcparam_role(name, rawtext, text, lineno, inliner, options={}, content=[]): nodes.literal('', repr(rcParamsDefault[text])), nodes.Text(')'), ]) - return node_list, [] + + return node_list, messages def setup(app): app.add_role("rc", rcparam_role) + app.add_node( + QueryReference, + html=(visit_query_reference_node, depart_query_reference_node), + latex=(visit_query_reference_node, depart_query_reference_node), + text=(visit_query_reference_node, depart_query_reference_node), + ) return {"parallel_read_safe": True, "parallel_write_safe": True} diff --git a/doc/sphinxext/gallery_order.py b/doc/sphinxext/gallery_order.py index 1c1034ec5819..82c721f3c220 100644 --- a/doc/sphinxext/gallery_order.py +++ b/doc/sphinxext/gallery_order.py @@ -6,7 +6,7 @@ from sphinx_gallery.sorting import ExplicitOrder # Gallery sections shall be displayed in the following order. -# Non-matching sections are appended. +# Non-matching sections are inserted at UNSORTED explicit_order_folders = [ '../examples/lines_bars_and_markers', '../examples/images_contours_and_fields', @@ -23,7 +23,16 @@ '../examples/showcase', '../tutorials/introductory', '../tutorials/intermediate', - '../tutorials/advanced'] + '../tutorials/advanced', + '../plot_types/basic', + '../plot_types/arrays', + '../plot_types/stats', + '../plot_types/unstructured', + '../plot_types/3D', + 'UNSORTED', + '../examples/userdemo', + '../tutorials/provisional', + ] class MplExplicitOrder(ExplicitOrder): @@ -31,11 +40,9 @@ class MplExplicitOrder(ExplicitOrder): def __call__(self, item): """Return a string determining the sort order.""" if item in self.ordered_list: - return "{:04d}".format(self.ordered_list.index(item)) + return f"{self.ordered_list.index(item):04d}" else: - # ensure not explicitly listed items come last. - return "zzz" + item - + return f"{self.ordered_list.index('UNSORTED'):04d}{item}" # Subsection order: # Subsections are ordered by filename, unless they appear in the following @@ -45,9 +52,9 @@ def __call__(self, item): list_all = [ # **Tutorials** # introductory - "usage", "pyplot", "sample_plots", "images", "lifecycle", "customizing", + "quick_start", "pyplot", "images", "lifecycle", "customizing", # intermediate - "artists", "legend_guide", "color_cycle", "gridspec", + "artists", "legend_guide", "color_cycle", "constrainedlayout_guide", "tight_layout_guide", # advanced # text @@ -60,6 +67,21 @@ def __call__(self, item): "color_demo", # pies "pie_features", "pie_demo2", + + # **Plot Types + # Basic + "plot", "scatter_plot", "bar", "stem", "step", "fill_between", + # Arrays + "imshow", "pcolormesh", "contour", "contourf", + "barbs", "quiver", "streamplot", + # Stats + "hist_plot", "boxplot_plot", "errorbar_plot", "violin", + "eventplot", "hist2d", "hexbin", "pie", + # Unstructured + "tricontour", "tricontourf", "tripcolor", "triplot", + # Spines + "spines", "spine_placement_demo", "spines_dropped", + "multiple_yaxis_with_spines", "centered_spines_with_arrows", ] explicit_subsection_order = [item + ".py" for item in list_all] diff --git a/doc/sphinxext/math_symbol_table.py b/doc/sphinxext/math_symbol_table.py index 3041b15b36a8..6609f91bbd10 100644 --- a/doc/sphinxext/math_symbol_table.py +++ b/doc/sphinxext/math_symbol_table.py @@ -1,6 +1,6 @@ from docutils.parsers.rst import Directive -from matplotlib import mathtext +from matplotlib import _mathtext, _mathtext_data symbols = [ @@ -103,9 +103,9 @@ def run(state_machine): def render_symbol(sym): if sym.startswith("\\"): sym = sym[1:] - if sym not in {*mathtext.Parser._overunder_functions, - *mathtext.Parser._function_names}: - sym = chr(mathtext.tex2uni[sym]) + if sym not in (_mathtext.Parser._overunder_functions | + _mathtext.Parser._function_names): + sym = chr(_mathtext_data.tex2uni[sym]) return f'\\{sym}' if sym in ('\\', '|') else sym lines = [] @@ -149,7 +149,6 @@ def setup(app): if __name__ == "__main__": # Do some verification of the tables - from matplotlib import _mathtext_data print("SYMBOLS NOT IN STIX:") all_symbols = {} diff --git a/doc/sphinxext/missing_references.py b/doc/sphinxext/missing_references.py index 6aa82a4dd17d..12d836f296f1 100644 --- a/doc/sphinxext/missing_references.py +++ b/doc/sphinxext/missing_references.py @@ -90,7 +90,7 @@ def get_location(node, app): Usually, this will be of the form "path/to/file:linenumber". Two special values can be emitted, "" for paths which are not contained in this source tree (e.g. docstrings included from - other modules) or "", inidcating that the sphinx application + other modules) or "", indicating that the sphinx application cannot locate the original source file (usually because an extension has injected text into the sphinx parsing engine). """ diff --git a/doc/sphinxext/redirect_from.py b/doc/sphinxext/redirect_from.py index 95f87aa38dcb..f8aee0a3e52a 100644 --- a/doc/sphinxext/redirect_from.py +++ b/doc/sphinxext/redirect_from.py @@ -3,7 +3,7 @@ ==================================== If an rst file is moved or its content subsumed in a different file, it -is desireable to redirect the old file to the new or existing file. This +is desirable to redirect the old file to the new or existing file. This extension enables this with a simple html refresh. For example suppose ``doc/topic/old-page.rst`` is removed and its content @@ -15,8 +15,10 @@ This creates in the build directory a file ``build/html/topic/old-page.html`` that contains a relative refresh:: + + @@ -38,8 +40,10 @@ logger = logging.getLogger(__name__) -HTML_TEMPLATE = """ +HTML_TEMPLATE = """ + + @@ -66,7 +70,7 @@ class RedirectFromDomain(Domain): @property def redirects(self): - """The mapping of the redirectes.""" + """The mapping of the redirects.""" return self.data.setdefault('redirects', {}) def clear_doc(self, docnames): @@ -79,7 +83,7 @@ def merge_domaindata(self, docnames, otherdata): elif self.redirects[src] != dst: raise ValueError( f"Inconsistent redirections from {src} to " - F"{self.redirects[src]} and {otherdata.redirects[src]}") + f"{self.redirects[src]} and {otherdata['redirects'][src]}") class RedirectFrom(Directive): @@ -88,7 +92,6 @@ class RedirectFrom(Directive): def run(self): redirected_doc, = self.arguments env = self.app.env - builder = self.app.builder domain = env.get_domain('redirect_from') current_doc = env.path2doc(self.state.document.current_source) redirected_reldoc, _ = env.relfn2path(redirected_doc, current_doc) @@ -115,4 +118,4 @@ def _generate_redirects(app, exception): else: logger.info(f'making refresh html file: {k} redirect to {v}') p.parent.mkdir(parents=True, exist_ok=True) - p.write_text(html) + p.write_text(html, encoding='utf-8') diff --git a/doc/thirdpartypackages/index.rst b/doc/thirdpartypackages/index.rst index aa5507b19f94..81dc4d710a52 100644 --- a/doc/thirdpartypackages/index.rst +++ b/doc/thirdpartypackages/index.rst @@ -1,363 +1,5 @@ -.. _thirdparty-index: +:orphan: -******************** -Third party packages -******************** +.. raw:: html -Several external packages that extend or build on Matplotlib functionality are -listed below. You can find more packages at `PyPI `_. -They are maintained and distributed separately from Matplotlib, -and thus need to be installed individually. - -If you have a created a package that extends or builds on Matplotlib -and would like to have your package listed on this page, please submit -an issue or pull request on GitHub. The pull request should include a short -description of the library and an image demonstrating the functionality. -To be included in the PyPI listing, please include ``Framework :: Matplotlib`` -in the classifier list in the ``setup.py`` file for your package. We are also -happy to host third party packages within the `Matplotlib GitHub Organization -`_. - - -Mapping toolkits -**************** - -Basemap -======= -`Basemap `_ plots data on map projections, -with continental and political boundaries. - -.. image:: /_static/basemap_contour1.png - :height: 400px - -Cartopy -======= -`Cartopy `_ builds on top -of Matplotlib to provide object oriented map projection definitions -and close integration with Shapely for powerful yet easy-to-use vector -data processing tools. An example plot from the `Cartopy gallery -`_: - -.. image:: /_static/cartopy_hurricane_katrina_01_00.png - :height: 400px - -Geoplot -======= -`Geoplot `_ builds on top -of Matplotlib and Cartopy to provide a "standard library" of simple, powerful, -and customizable plot types. An example plot from the `Geoplot gallery -`_: - -.. image:: /_static/geoplot_nyc_traffic_tickets.png - :height: 400px - -Ridge Map -========= -`ridge_map `_ uses Matplotlib, -SRTM.py, NumPy, and scikit-image to make ridge plots of your favorite -ridges. - -.. image:: /_static/ridge_map_white_mountains.png - :height: 364px - -Declarative libraries -********************* - -ggplot -====== -`ggplot `_ is a port of the R ggplot2 package -to python based on Matplotlib. - -.. image:: /_static/ggplot.png - :height: 195px - -holoviews -========= -`holoviews `_ makes it easier to visualize data -interactively, especially in a `Jupyter notebook `_, by -providing a set of declarative plotting objects that store your data and -associated metadata. Your data is then immediately visualizable alongside or -overlaid with other data, either statically or with automatically provided -widgets for parameter exploration. - -.. image:: /_static/holoviews.png - :height: 354px - -plotnine -======== - -`plotnine `_ implements a grammar -of graphics, similar to R's `ggplot2 `_. -The grammar allows users to compose plots by explicitly mapping data to the -visual objects that make up the plot. - -.. image:: /_static/plotnine.png - -Specialty plots -*************** - -Broken Axes -=========== -`brokenaxes `_ supplies an axes -class that can have a visual break to indicate a discontinuous range. - -.. image:: /_static/brokenaxes.png - -DeCiDa -====== - -`DeCiDa `_ is a library of functions -and classes for electron device characterization, electronic circuit design and -general data visualization and analysis. - -matplotlib-scalebar -=================== - -`matplotlib-scalebar `_ provides a new artist to display a scale bar, aka micron bar. -It is particularly useful when displaying calibrated images plotted using ``plt.imshow(...)``. - -.. image:: /_static/gold_on_carbon.jpg - -Matplotlib-Venn -=============== -`Matplotlib-Venn `_ provides a -set of functions for plotting 2- and 3-set area-weighted (or unweighted) Venn -diagrams. - -mpl-probscale -============= -`mpl-probscale `_ is a small extension -that allows Matplotlib users to specify probability scales. Simply importing the -``probscale`` module registers the scale with Matplotlib, making it accessible -via e.g., ``ax.set_xscale('prob')`` or ``plt.yscale('prob')``. - -.. image:: /_static/probscale_demo.png - -mpl-scatter-density -=================== - -`mpl-scatter-density `_ is a -small package that makes it easy to make scatter plots of large numbers -of points using a density map. The following example contains around 13 million -points and the plotting (excluding reading in the data) took less than a -second on an average laptop: - -.. image:: /_static/mpl-scatter-density.png - :height: 400px - -When used in interactive mode, the density map is downsampled on-the-fly while -panning/zooming in order to provide a smooth interactive experience. - -mplstereonet -============ -`mplstereonet `_ provides -stereonets for plotting and analyzing orientation data in Matplotlib. - -Natgrid -======= -`mpl_toolkits.natgrid `_ is an interface -to the natgrid C library for gridding irregularly spaced data. - -pyUpSet -======= -`pyUpSet `_ is a -static Python implementation of the `UpSet suite by Lex et al. -`_ to explore complex intersections of -sets and data frames. - -seaborn -======= -`seaborn `_ is a high level interface for drawing -statistical graphics with Matplotlib. It aims to make visualization a central -part of exploring and understanding complex datasets. - -.. image:: /_static/seaborn.png - :height: 157px - -WCSAxes -======= - -The `Astropy `_ core package includes a submodule -called WCSAxes (available at `astropy.visualization.wcsaxes -`_) which -adds Matplotlib projections for Astronomical image data. The following is an -example of a plot made with WCSAxes which includes the original coordinate -system of the image and an overlay of a different coordinate system: - -.. image:: /_static/wcsaxes.jpg - :height: 400px - -Windrose -======== -`Windrose `_ is a Python Matplotlib, -Numpy library to manage wind data, draw windroses (also known as polar rose -plots), draw probability density functions and fit Weibull distributions. - -Yellowbrick -=========== -`Yellowbrick `_ is a suite of visual diagnostic tools for machine learning that enables human steering of the model selection process. Yellowbrick combines scikit-learn with matplotlib using an estimator-based API called the ``Visualizer``, which wraps both sklearn models and matplotlib Axes. ``Visualizer`` objects fit neatly into the machine learning workflow allowing data scientists to integrate visual diagnostic and model interpretation tools into experimentation without extra steps. - -.. image:: /_static/yellowbrick.png - :height: 400px - -Animations -********** - -animatplot -========== -`animatplot `_ is a library for -producing interactive animated plots with the goal of making production of -animated plots almost as easy as static ones. - -.. image:: /_static/animatplot.png - -For an animated version of the above picture and more examples, see the -`animatplot gallery. `_ - -gif -=== -`gif `_ is an ultra lightweight animated gif API. - -.. image:: /_static/gif_attachment_example.png - -numpngw -======= - -`numpngw `_ provides functions for writing -NumPy arrays to PNG and animated PNG files. It also includes the class -``AnimatedPNGWriter`` that can be used to save a Matplotlib animation as an -animated PNG file. See the example on the PyPI page or at the ``numpngw`` -`github repository `_. - -.. image:: /_static/numpngw_animated_example.png - -Interactivity -************* - -mplcursors -========== -`mplcursors `_ provides interactive data -cursors for Matplotlib. - -MplDataCursor -============= -`MplDataCursor `_ is a toolkit -written by Joe Kington to provide interactive "data cursors" (clickable -annotation boxes) for Matplotlib. - -mpl_interactions -================ -`mpl_interactions `_ -makes it easy to create interactive plots controlled by sliders and other -widgets. It also provides several handy capabilities such as manual -image segmentation, comparing cross-sections of arrays, and using the -scroll wheel to zoom. - -.. image:: /_static/mpl-interactions-slider-animated.png - -Rendering backends -****************** - -mplcairo -======== -`mplcairo `_ is a cairo backend for -Matplotlib, with faster and more accurate marker drawing, support for a wider -selection of font formats and complex text layout, and various other features. - -gr -== -`gr `_ is a framework for cross-platform -visualisation applications, which can be used as a high-performance Matplotlib -backend. - -GUI integration -*************** - -wxmplot -======= -`WXMPlot `_ provides advanced wxPython -widgets for plotting and image display of numerical data based on Matplotlib. - -Miscellaneous -************* - -adjustText -========== -`adjustText `_ is a small library for -automatically adjusting text position in Matplotlib plots to minimize overlaps -between them, specified points and other objects. - -.. image:: /_static/adjustText.png - -iTerm2 terminal backend -======================= -`matplotlib_iterm2 `_ is an -external Matplotlib backend using the iTerm2 nightly build inline image display -feature. - -.. image:: /_static/matplotlib_iterm2_demo.png - -mpl-template -============ -`mpl-template `_ provides -a customizable way to add engineering figure elements such as a title block, -border, and logo. - -.. image:: /_static/mpl_template_example.png - :height: 330px - -figpager -======== -`figpager `_ provides customizable figure -elements such as text, lines and images and subplot layout control for single -or multi page output. - - .. image:: /_static/figpager.png - -blume -===== - -`blume `_ provides a replacement for -the Matplotlib ``table`` module. It fixes a number of issues with the -existing table. See the `blume github repository -`_ for more details. - -.. image:: /_static/blume_table_example.png - -highlight-text -============== - -`highlight-text `_ is a small library -that provides an easy way to effectively annotate plots by highlighting -substrings with the font properties of your choice. -See the `highlight-text github repository -`_ for more details and examples. - -.. image:: /_static/highlight_text_examples.png - -DNA Features Viewer -=================== - -`DNA Features Viewer `_ -provides methods to plot annotated DNA sequence maps (possibly along other Matplotlib -plots) for Bioinformatics and Synthetic Biology applications. - -.. image:: /_static/dna_features_viewer_screenshot.png - -GUI applications -**************** - -sviewgui -======== - -`sviewgui `_ is a PyQt-based GUI for -visualisation of data from csv files or `pandas.DataFrame`\s. Main features: - -- Scatter, line, density, histogram, and box plot types -- Settings for the marker size, line width, number of bins of histogram, - colormap (from cmocean) -- Save figure as editable PDF -- Code of the plotted graph is available so that it can be reused and modified - outside of sviewgui - -.. image:: /_static/sviewgui_sample.png + diff --git a/doc/users/explain/api_interfaces.rst b/doc/users/explain/api_interfaces.rst new file mode 100644 index 000000000000..89354cc3a13b --- /dev/null +++ b/doc/users/explain/api_interfaces.rst @@ -0,0 +1,290 @@ +.. redirect-from:: /gallery/misc/pythonic_matplotlib + +.. _api_interfaces: + +======================================== +Matplotlib Application Interfaces (APIs) +======================================== + +Matplotlib has two major application interfaces, or styles of using the library: + +- An explicit "Axes" interface that uses methods on a Figure or Axes object to + create other Artists, and build a visualization step by step. This has also + been called an "object-oriented" interface. +- An implicit "pyplot" interface that keeps track of the last Figure and Axes + created, and adds Artists to the object it thinks the user wants. + +In addition, a number of downstream libraries (like `pandas` and xarray_) offer +a ``plot`` method implemented directly on their data classes so that users can +call ``data.plot()``. + +.. _xarray: https://xarray.pydata.org + +The difference between these interfaces can be a bit confusing, particularly +given snippets on the web that use one or the other, or sometimes multiple +interfaces in the same example. Here we attempt to point out how the "pyplot" +and downstream interfaces relate to the explicit "Axes" interface to help users +better navigate the library. + + +Native Matplotlib interfaces +---------------------------- + +The explicit "Axes" interface +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The "Axes" interface is how Matplotlib is implemented, and many customizations +and fine-tuning end up being done at this level. + +This interface works by instantiating an instance of a +`~.matplotlib.figure.Figure` class (``fig`` below), using a method +`~.Figure.subplots` method (or similar) on that object to create one or more +`~.matplotlib.axes.Axes` objects (``ax`` below), and then calling drawing +methods on the Axes (``plot`` in this example): + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + fig = plt.figure() + ax = fig.subplots() + ax.plot([1, 2, 3, 4], [0, 0.5, 1, 0.2]) + +We call this an "explicit" interface because each object is explicitly +referenced, and used to make the next object. Keeping references to the objects +is very flexible, and allows us to customize the objects after they are created, +but before they are displayed. + + +The implicit "pyplot" interface +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The `~.matplotlib.pyplot` module shadows most of the +`~.matplotlib.axes.Axes` plotting methods to give the equivalent of +the above, where the creation of the Figure and Axes is done for the user: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + plt.plot([1, 2, 3, 4], [0, 0.5, 1, 0.2]) + +This can be convenient, particularly when doing interactive work or simple +scripts. A reference to the current Figure can be retrieved using +`~.pyplot.gcf` and to the current Axes by `~.pyplot.gca`. The `~.pyplot` module +retains a list of Figures, and each Figure retains a list of Axes on the figure +for the user so that the following: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + plt.subplot(1, 2, 1) + plt.plot([1, 2, 3], [0, 0.5, 0.2]) + + plt.subplot(1, 2, 2) + plt.plot([3, 2, 1], [0, 0.5, 0.2]) + +is equivalent to: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + plt.subplot(1, 2, 1) + ax = plt.gca() + ax.plot([1, 2, 3], [0, 0.5, 0.2]) + + plt.subplot(1, 2, 2) + ax = plt.gca() + ax.plot([3, 2, 1], [0, 0.5, 0.2]) + +In the explicit interface, this would be: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + fig, axs = plt.subplots(1, 2) + axs[0].plot([1, 2, 3], [0, 0.5, 0.2]) + axs[1].plot([3, 2, 1], [0, 0.5, 0.2]) + +Why be explicit? +^^^^^^^^^^^^^^^^ + +What happens if you have to backtrack, and operate on an old axes that is not +referenced by ``plt.gca()``? One simple way is to call ``subplot`` again with +the same arguments. However, that quickly becomes inelegant. You can also +inspect the Figure object and get its list of Axes objects, however, that can be +misleading (colorbars are Axes too!). The best solution is probably to save a +handle to every Axes you create, but if you do that, why not simply create the +all the Axes objects at the start? + +The first approach is to call ``plt.subplot`` again: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + plt.subplot(1, 2, 1) + plt.plot([1, 2, 3], [0, 0.5, 0.2]) + + plt.subplot(1, 2, 2) + plt.plot([3, 2, 1], [0, 0.5, 0.2]) + + plt.suptitle('Implicit Interface: re-call subplot') + + for i in range(1, 3): + plt.subplot(1, 2, i) + plt.xlabel('Boo') + +The second is to save a handle: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + axs = [] + ax = plt.subplot(1, 2, 1) + axs += [ax] + plt.plot([1, 2, 3], [0, 0.5, 0.2]) + + ax = plt.subplot(1, 2, 2) + axs += [ax] + plt.plot([3, 2, 1], [0, 0.5, 0.2]) + + plt.suptitle('Implicit Interface: save handles') + + for i in range(2): + plt.sca(axs[i]) + plt.xlabel('Boo') + +However, the recommended way would be to be explicit from the outset: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + fig, axs = plt.subplots(1, 2) + axs[0].plot([1, 2, 3], [0, 0.5, 0.2]) + axs[1].plot([3, 2, 1], [0, 0.5, 0.2]) + fig.suptitle('Explicit Interface') + for i in range(2): + axs[i].set_xlabel('Boo') + + +Third-party library "Data-object" interfaces +-------------------------------------------- + +Some third party libraries have chosen to implement plotting for their data +objects, e.g. ``data.plot()``, is seen in `pandas`, xarray_, and other +third-party libraries. For illustrative purposes, a downstream library may +implement a simple data container that has ``x`` and ``y`` data stored together, +and then implements a ``plot`` method: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + # supplied by downstream library: + class DataContainer: + + def __init__(self, x, y): + """ + Proper docstring here! + """ + self._x = x + self._y = y + + def plot(self, ax=None, **kwargs): + if ax is None: + ax = plt.gca() + ax.plot(self._x, self._y, **kwargs) + ax.set_title('Plotted from DataClass!') + return ax + + + # what the user usually calls: + data = DataContainer([0, 1, 2, 3], [0, 0.2, 0.5, 0.3]) + data.plot() + +So the library can hide all the nitty-gritty from the user, and can make a +visualization appropriate to the data type, often with good labels, choices of +colormaps, and other convenient features. + +In the above, however, we may not have liked the title the library provided. +Thankfully, they pass us back the Axes from the ``plot()`` method, and +understanding the explicit Axes interface, we could call: +``ax.set_title('My preferred title')`` to customize the title. + +Many libraries also allow their ``plot`` methods to accept an optional *ax* +argument. This allows us to place the visualization in an Axes that we have +placed and perhaps customized. + +Summary +------- + +Overall, it is useful to understand the explicit "Axes" interface since it is +the most flexible and underlies the other interfaces. A user can usually +figure out how to drop down to the explicit interface and operate on the +underlying objects. While the explicit interface can be a bit more verbose +to setup, complicated plots will often end up simpler than trying to use +the implicit "pyplot" interface. + +.. note:: + + It is sometimes confusing to people that we import ``pyplot`` for both + interfaces. Currently, the ``pyplot`` module implements the "pyplot" + interface, but it also provides top-level Figure and Axes creation + methods, and ultimately spins up the graphical user interface, if one + is being used. So ``pyplot`` is still needed regardless of the + interface chosen. + +Similarly, the declarative interfaces provided by partner libraries use the +objects accessible by the "Axes" interface, and often accept these as arguments +or pass them back from methods. It is usually essential to use the explicit +"Axes" interface to perform any customization of the default visualization, or +to unpack the data into NumPy arrays and pass directly to Matplotlib. + +Appendix: "Axes" interface with data structures +----------------------------------------------- + +Most `~.axes.Axes` methods allow yet another API addressing by passing a +*data* object to the method and specifying the arguments as strings: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + + data = {'xdat': [0, 1, 2, 3], 'ydat': [0, 0.2, 0.4, 0.1]} + fig, ax = plt.subplots(figsize=(2, 2)) + ax.plot('xdat', 'ydat', data=data) + + +Appendix: "pylab" interface +--------------------------- + +There is one further interface that is highly discouraged, and that is to +basically do ``from matplotlib.pyplot import *``. This allows users to simply +call ``plot(x, y)``. While convenient, this can lead to obvious problems if the +user unwittingly names a variable the same name as a pyplot method. diff --git a/doc/users/explain/backends.rst b/doc/users/explain/backends.rst new file mode 100644 index 000000000000..9b188048de50 --- /dev/null +++ b/doc/users/explain/backends.rst @@ -0,0 +1,242 @@ +.. _backends: + +======== +Backends +======== + +.. _what-is-a-backend: + +What is a backend? +------------------ + +A lot of documentation on the website and in the mailing lists refers +to the "backend" and many new users are confused by this term. +Matplotlib targets many different use cases and output formats. Some +people use Matplotlib interactively from the Python shell and have +plotting windows pop up when they type commands. Some people run +`Jupyter `_ notebooks and draw inline plots for +quick data analysis. Others embed Matplotlib into graphical user +interfaces like PyQt or PyGObject to build rich applications. Some +people use Matplotlib in batch scripts to generate postscript images +from numerical simulations, and still others run web application +servers to dynamically serve up graphs. + +To support all of these use cases, Matplotlib can target different +outputs, and each of these capabilities is called a backend; the +"frontend" is the user facing code, i.e., the plotting code, whereas the +"backend" does all the hard work behind-the-scenes to make the figure. +There are two types of backends: user interface backends (for use in +PyQt/PySide, PyGObject, Tkinter, wxPython, or macOS/Cocoa); also referred to +as "interactive backends") and hardcopy backends to make image files +(PNG, SVG, PDF, PS; also referred to as "non-interactive backends"). + +Selecting a backend +------------------- + +There are three ways to configure your backend: + +- The :rc:`backend` parameter in your :file:`matplotlibrc` file +- The :envvar:`MPLBACKEND` environment variable +- The function :func:`matplotlib.use` + +Below is a more detailed description. + +If there is more than one configuration present, the last one from the +list takes precedence; e.g. calling :func:`matplotlib.use()` will override +the setting in your :file:`matplotlibrc`. + +Without a backend explicitly set, Matplotlib automatically detects a usable +backend based on what is available on your system and on whether a GUI event +loop is already running. The first usable backend in the following list is +selected: MacOSX, QtAgg, GTK4Agg, Gtk3Agg, TkAgg, WxAgg, Agg. The last, Agg, +is a non-interactive backend that can only write to files. It is used on +Linux, if Matplotlib cannot connect to either an X display or a Wayland +display. + +Here is a detailed description of the configuration methods: + +#. Setting :rc:`backend` in your :file:`matplotlibrc` file:: + + backend : qtagg # use pyqt with antigrain (agg) rendering + + See also :doc:`/tutorials/introductory/customizing`. + +#. Setting the :envvar:`MPLBACKEND` environment variable: + + You can set the environment variable either for your current shell or for + a single script. + + On Unix:: + + > export MPLBACKEND=qtagg + > python simple_plot.py + + > MPLBACKEND=qtagg python simple_plot.py + + On Windows, only the former is possible:: + + > set MPLBACKEND=qtagg + > python simple_plot.py + + Setting this environment variable will override the ``backend`` parameter + in *any* :file:`matplotlibrc`, even if there is a :file:`matplotlibrc` in + your current working directory. Therefore, setting :envvar:`MPLBACKEND` + globally, e.g. in your :file:`.bashrc` or :file:`.profile`, is discouraged + as it might lead to counter-intuitive behavior. + +#. If your script depends on a specific backend you can use the function + :func:`matplotlib.use`:: + + import matplotlib + matplotlib.use('qtagg') + + This should be done before any figure is created, otherwise Matplotlib may + fail to switch the backend and raise an ImportError. + + Using `~matplotlib.use` will require changes in your code if users want to + use a different backend. Therefore, you should avoid explicitly calling + `~matplotlib.use` unless absolutely necessary. + +.. _the-builtin-backends: + +The builtin backends +-------------------- + +By default, Matplotlib should automatically select a default backend which +allows both interactive work and plotting from scripts, with output to the +screen and/or to a file, so at least initially, you will not need to worry +about the backend. The most common exception is if your Python distribution +comes without :mod:`tkinter` and you have no other GUI toolkit installed. +This happens with certain Linux distributions, where you need to install a +Linux package named ``python-tk`` (or similar). + +If, however, you want to write graphical user interfaces, or a web +application server +(:doc:`/gallery/user_interfaces/web_application_server_sgskip`), or need a +better understanding of what is going on, read on. To make things easily +more customizable for graphical user interfaces, Matplotlib separates +the concept of the renderer (the thing that actually does the drawing) +from the canvas (the place where the drawing goes). The canonical +renderer for user interfaces is ``Agg`` which uses the `Anti-Grain +Geometry`_ C++ library to make a raster (pixel) image of the figure; it +is used by the ``QtAgg``, ``GTK4Agg``, ``GTK3Agg``, ``wxAgg``, ``TkAgg``, and +``macosx`` backends. An alternative renderer is based on the Cairo library, +used by ``QtCairo``, etc. + +For the rendering engines, users can also distinguish between `vector +`_ or `raster +`_ renderers. Vector +graphics languages issue drawing commands like "draw a line from this +point to this point" and hence are scale free. Raster backends +generate a pixel representation of the line whose accuracy depends on a +DPI setting. + +Here is a summary of the Matplotlib renderers (there is an eponymous +backend for each; these are *non-interactive backends*, capable of +writing to a file): + +======== ========= ======================================================= +Renderer Filetypes Description +======== ========= ======================================================= +AGG png raster_ graphics -- high quality images using the + `Anti-Grain Geometry`_ engine. +PDF pdf vector_ graphics -- `Portable Document Format`_ output. +PS ps, eps vector_ graphics -- PostScript_ output. +SVG svg vector_ graphics -- `Scalable Vector Graphics`_ output. +PGF pgf, pdf vector_ graphics -- using the pgf_ package. +Cairo png, ps, raster_ or vector_ graphics -- using the Cairo_ library + pdf, svg (requires pycairo_ or cairocffi_). +======== ========= ======================================================= + +To save plots using the non-interactive backends, use the +``matplotlib.pyplot.savefig('filename')`` method. + +These are the user interfaces and renderer combinations supported; +these are *interactive backends*, capable of displaying to the screen +and using appropriate renderers from the table above to write to +a file: + +========= ================================================================ +Backend Description +========= ================================================================ +QtAgg Agg rendering in a Qt_ canvas (requires PyQt_ or `Qt for Python`_, + a.k.a. PySide). This backend can be activated in IPython with + ``%matplotlib qt``. The Qt binding can be selected via the + :envvar:`QT_API` environment variable; see :ref:`QT_bindings` for + more details. +ipympl Agg rendering embedded in a Jupyter widget (requires ipympl_). + This backend can be enabled in a Jupyter notebook with + ``%matplotlib ipympl``. +GTK3Agg Agg rendering to a GTK_ 3.x canvas (requires PyGObject_ and + pycairo_). This backend can be activated in IPython with + ``%matplotlib gtk3``. +GTK4Agg Agg rendering to a GTK_ 4.x canvas (requires PyGObject_ and + pycairo_). This backend can be activated in IPython with + ``%matplotlib gtk4``. +macosx Agg rendering into a Cocoa canvas in OSX. This backend can be + activated in IPython with ``%matplotlib osx``. +TkAgg Agg rendering to a Tk_ canvas (requires TkInter_). This + backend can be activated in IPython with ``%matplotlib tk``. +nbAgg Embed an interactive figure in a Jupyter classic notebook. This + backend can be enabled in Jupyter notebooks via + ``%matplotlib notebook``. +WebAgg On ``show()`` will start a tornado server with an interactive + figure. +GTK3Cairo Cairo rendering to a GTK_ 3.x canvas (requires PyGObject_ and + pycairo_). +GTK4Cairo Cairo rendering to a GTK_ 4.x canvas (requires PyGObject_ and + pycairo_). +wxAgg Agg rendering to a wxWidgets_ canvas (requires wxPython_ 4). + This backend can be activated in IPython with ``%matplotlib wx``. +========= ================================================================ + +.. note:: + The names of builtin backends case-insensitive; e.g., 'QtAgg' and + 'qtagg' are equivalent. + +.. _`Anti-Grain Geometry`: http://agg.sourceforge.net/antigrain.com/ +.. _`Portable Document Format`: https://en.wikipedia.org/wiki/Portable_Document_Format +.. _Postscript: https://en.wikipedia.org/wiki/PostScript +.. _`Scalable Vector Graphics`: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics +.. _pgf: https://ctan.org/pkg/pgf +.. _Cairo: https://www.cairographics.org +.. _PyGObject: https://wiki.gnome.org/action/show/Projects/PyGObject +.. _pycairo: https://www.cairographics.org/pycairo/ +.. _cairocffi: https://pythonhosted.org/cairocffi/ +.. _wxPython: https://www.wxpython.org/ +.. _TkInter: https://docs.python.org/3/library/tk.html +.. _PyQt: https://riverbankcomputing.com/software/pyqt/intro +.. _`Qt for Python`: https://doc.qt.io/qtforpython/ +.. _Qt: https://qt.io/ +.. _GTK: https://www.gtk.org/ +.. _Tk: https://www.tcl.tk/ +.. _wxWidgets: https://www.wxwidgets.org/ +.. _ipympl: https://www.matplotlib.org/ipympl + +ipympl +^^^^^^ + +The Jupyter widget ecosystem is moving too fast to support directly in +Matplotlib. To install ipympl: + +.. code-block:: bash + + pip install ipympl + +or + +.. code-block:: bash + + conda install ipympl -c conda-forge + +See `installing ipympl `__ for more details. + +Using non-builtin backends +-------------------------- +More generally, any importable backend can be selected by using any of the +methods above. If ``name.of.the.backend`` is the module containing the +backend, use ``module://name.of.the.backend`` as the backend name, e.g. +``matplotlib.use('module://name.of.the.backend')``. + +Information for backend implementers is available at +:doc:`/users/explain/writing_a_backend_pyplot_interface`. diff --git a/doc/users/event_handling.rst b/doc/users/explain/event_handling.rst similarity index 85% rename from doc/users/event_handling.rst rename to doc/users/explain/event_handling.rst index 3a368d3b5312..c7fe7f5ea38d 100644 --- a/doc/users/event_handling.rst +++ b/doc/users/explain/event_handling.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/event_handling + .. _event-handling-tutorial: ************************** @@ -80,29 +82,127 @@ Event name Class Description you may encounter inconsistencies between the different user interface toolkits that Matplotlib works with. This is due to inconsistencies/limitations of the user interface toolkit. The following table shows some basic examples of - what you may expect to receive as key(s) from the different user interface toolkits, - where a comma separates different keys: - - ============== ============================= ============================== ============================= ============================== ============================== - Key(s) Pressed WxPython Qt WebAgg Gtk Tkinter - ============== ============================= ============================== ============================= ============================== ============================== - Shift+2 shift, shift+2 shift, " shift, " shift, " shift, " - Shift+F1 shift, shift+f1 shift, shift+f1 shift, shift+f1 shift, shift+f1 shift, shift+f1 - Shift shift shift shift shift shift - Control control control control control control - Alt alt alt alt alt alt - AltGr Nothing Nothing alt iso_level3_shift iso_level3_shift - CapsLock caps_lock caps_lock caps_lock caps_lock caps_lock - A a a A A A - a a a a a a - Shift+a shift, A shift, A shift, A shift, A shift, A - Shift+A shift, A shift, A shift, a shift, a shift, a - Ctrl+Shift+Alt control, ctrl+shift, ctrl+alt control, ctrl+shift, ctrl+meta control, ctrl+shit, ctrl+meta control, ctrl+shift, ctrl+meta control, ctrl+shift, ctrl+meta - Ctrl+Shift+a control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shit, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+a - Ctrl+Shift+A control, ctrl+shift, ctrl+A control, ctrl+shift, ctrl+A control, ctrl+shit, ctrl+a control, ctrl+shift, ctrl+a control, ctrl+shift, ctrl+a - F1 f1 f1 f1 f1 f1 - Ctrl+F1 control, ctrl+f1 control, ctrl+f1 control, ctrl+f1 control, ctrl+f1 control, ctrl+f1 - ============== ============================= ============================== ============================= ============================== ============================== + what you may expect to receive as key(s) (using a QWERTY keyboard layout) + from the different user interface toolkits, where a comma separates different keys: + + .. container:: wide-table + + .. list-table:: + :header-rows: 1 + :stub-columns: 1 + + * - Key(s) Pressed + - Tkinter + - Qt + - macosx + - WebAgg + - GTK + - WxPython + * - :kbd:`Shift+2` + - shift, @ + - shift, @ + - shift, @ + - shift, @ + - shift, @ + - shift, shift+2 + * - :kbd:`Shift+F1` + - shift, shift+f1 + - shift, shift+f1 + - shift, shift+f1 + - shift, shift+f1 + - shift, shift+f1 + - shift, shift+f1 + * - :kbd:`Shift` + - shift + - shift + - shift + - shift + - shift + - shift + * - :kbd:`Control` + - control + - control + - control + - control + - control + - control + * - :kbd:`Alt` + - alt + - alt + - alt + - alt + - alt + - alt + * - :kbd:`AltGr` + - iso_level3_shift + - *nothing* + - + - alt + - iso_level3_shift + - *nothing* + * - :kbd:`CapsLock` + - caps_lock + - caps_lock + - caps_lock + - caps_lock + - caps_lock + - caps_lock + * - :kbd:`CapsLock+a` + - caps_lock, A + - caps_lock, a + - caps_lock, a + - caps_lock, A + - caps_lock, A + - caps_lock, a + * - :kbd:`a` + - a + - a + - a + - a + - a + - a + * - :kbd:`Shift+a` + - shift, A + - shift, A + - shift, A + - shift, A + - shift, A + - shift, A + * - :kbd:`CapsLock+Shift+a` + - caps_lock, shift, a + - caps_lock, shift, A + - caps_lock, shift, A + - caps_lock, shift, a + - caps_lock, shift, a + - caps_lock, shift, A + * - :kbd:`Ctrl+Shift+Alt` + - control, ctrl+shift, ctrl+meta + - control, ctrl+shift, ctrl+meta + - control, ctrl+shift, ctrl+alt+shift + - control, ctrl+shift, ctrl+meta + - control, ctrl+shift, ctrl+meta + - control, ctrl+shift, ctrl+alt + * - :kbd:`Ctrl+Shift+a` + - control, ctrl+shift, ctrl+a + - control, ctrl+shift, ctrl+A + - control, ctrl+shift, ctrl+A + - control, ctrl+shift, ctrl+A + - control, ctrl+shift, ctrl+A + - control, ctrl+shift, ctrl+A + * - :kbd:`F1` + - f1 + - f1 + - f1 + - f1 + - f1 + - f1 + * - :kbd:`Ctrl+F1` + - control, ctrl+f1 + - control, ctrl+f1 + - control, *nothing* + - control, ctrl+f1 + - control, ctrl+f1 + - control, ctrl+f1 Matplotlib attaches some keypress callbacks by default for interactivity; they are documented in the :ref:`key-event-handling` section. diff --git a/doc/users/explain/figures.rst b/doc/users/explain/figures.rst new file mode 100644 index 000000000000..617281a60f61 --- /dev/null +++ b/doc/users/explain/figures.rst @@ -0,0 +1,252 @@ + +.. _figure_explanation: + +================================================ +Creating, viewing, and saving Matplotlib Figures +================================================ + +.. plot:: + :include-source: + + fig = plt.figure(figsize=(2, 2), facecolor='lightskyblue', + layout='constrained') + fig.suptitle('Figure') + ax = fig.add_subplot() + ax.set_title('Axes', loc='left', fontstyle='oblique', fontsize='medium') + +When looking at Matplotlib visualization, you are almost always looking at +Artists placed on a `~.Figure`. In the example above, the figure is the +blue region and `~.Figure.add_subplot` has added an `~.axes.Axes` artist to the +`~.Figure` (see :ref:`figure_parts`). A more complicated visualization can add +multiple Axes to the Figure, colorbars, legends, annotations, and the Axes +themselves can have multiple Artists added to them +(e.g. ``ax.plot`` or ``ax.imshow``). + +.. _viewing_figures: + +Viewing Figures +================ + +We will discuss how to create Figures in more detail below, but first it is +helpful to understand how to view a Figure. This varies based on how you are +using Matplotlib, and what :ref:`Backend ` you are using. + +Notebooks and IDEs +------------------ + +.. figure:: /_static/FigureInline.png + :alt: Image of figure generated in Jupyter Notebook with inline backend. + :width: 400 + + Screenshot of a `Jupyter Notebook `_, with a figure + generated via the default `inline + `_ backend. + + +If you are using a Notebook (e.g. `Jupyter `_) or an IDE +that renders Notebooks (PyCharm, VSCode, etc), then they have a backend that +will render the Matplotlib Figure when a code cell is executed. One thing to +be aware of is that the default Jupyter backend (``%matplotlib inline``) will +by default trim or expand the figure size to have a tight box around Artists +added to the Figure (see :ref:`saving_figures`, below). If you use a backend +other than the default "inline" backend, you will likely need to use an ipython +"magic" like ``%matplotlib notebook`` for the Matplotlib :ref:`notebook +` or ``%matplotlib widget`` for the `ipympl +`_ backend. + +.. figure:: /_static/FigureNotebook.png + :alt: Image of figure generated in Jupyter Notebook with notebook + backend, including a toolbar. + :width: 400 + + Screenshot of a Jupyter Notebook with an interactive figure generated via + the ``%matplotlib notebook`` magic. Users should also try the similar + `widget `_ backend if using `JupyterLab + `_. + + +.. seealso:: + :ref:`interactive_figures`. + +Standalone scripts and interactive use +-------------------------------------- + +If the user is on a client with a windowing system, there are a number of +:ref:`Backends ` that can be used to render the Figure to +the screen, usually using a Python Qt, Tk, or Wx toolkit, or the native MacOS +backend. These are typically chosen either in the user's :ref:`matplotlibrc +`, or by calling, for example, +``matplotlib.use('QtAgg')`` at the beginning of a session or script. + +.. figure:: /_static/FigureQtAgg.png + :alt: Image of figure generated from a script via the QtAgg backend. + :width: 370 + + Screenshot of a Figure generated via a python script and shown using the + QtAgg backend. + +When run from a script, or interactively (e.g. from an +`iPython shell `_) the Figure +will not be shown until we call ``plt.show()``. The Figure will appear in +a new GUI window, and usually will have a toolbar with Zoom, Pan, and other tools +for interacting with the Figure. By default, ``plt.show()`` blocks +further interaction from the script or shell until the Figure window is closed, +though that can be toggled off for some purposes. For more details, please see +:ref:`controlling-interactive`. + +Note that if you are on a client that does not have access to a windowing +system, the Figure will fallback to being drawn using the "Agg" backend, and +cannot be viewed, though it can be :ref:`saved `. + +.. seealso:: + :ref:`interactive_figures`. + +.. _creating_figures: + +Creating Figures +================ + +By far the most common way to create a figure is using the +:doc:`pyplot ` interface. As noted in +:ref:`api_interfaces`, the pyplot interface serves two purposes. One is to spin +up the Backend and keep track of GUI windows. The other is a global state for +Axes and Artists that allow a short-form API to plotting methods. In the +example above, we use pyplot for the first purpose, and create the Figure object, +``fig``. As a side effect ``fig`` is also added to pyplot's global state, and +can be accessed via `~.pyplot.gcf`. + +Users typically want an Axes or a grid of Axes when they create a Figure, so in +addition to `~.pyplot.figure`, there are convenience methods that return both +a Figure and some Axes. A simple grid of Axes can be achieved with +`.pyplot.subplots` (which +simply wraps `.Figure.subplots`): + +.. plot:: + :include-source: + + fig, axs = plt.subplots(2, 2, figsize=(4, 3), layout='constrained') + +More complex grids can be achieved with `.pyplot.subplot_mosaic` (which wraps +`.Figure.subplot_mosaic`): + +.. plot:: + :include-source: + + fig, axs = plt.subplot_mosaic([['A', 'right'], ['B', 'right']], + figsize=(4, 3), layout='constrained') + for ax_name in axs: + axs[ax_name].text(0.5, 0.5, ax_name, ha='center', va='center') + +Sometimes we want to have a nested layout in a Figure, with two or more sets of +Axes that do not share the same subplot grid. +We can use `~.Figure.add_subfigure` or `~.Figure.subfigures` to create virtual +figures inside a parent Figure; see +:doc:`/gallery/subplots_axes_and_figures/subfigures` for more details. + +.. plot:: + :include-source: + + fig = plt.figure(layout='constrained', facecolor='lightskyblue') + fig.suptitle('Figure') + figL, figR = fig.subfigures(1, 2) + figL.set_facecolor('thistle') + axL = figL.subplots(2, 1, sharex=True) + axL[1].set_xlabel('x [m]') + figL.suptitle('Left subfigure') + figR.set_facecolor('paleturquoise') + axR = figR.subplots(1, 2, sharey=True) + axR[0].set_title('Axes 1') + figR.suptitle('Right subfigure') + +It is possible to directly instantiate a `.Figure` instance without using the +pyplot interface. This is usually only necessary if you want to create your +own GUI application or service that you do not want carrying the pyplot global +state. See the embedding examples in :ref:`user_interfaces` for examples of +how to do this. + +Figure options +-------------- + +There are a few options available when creating figures. The Figure size on +the screen is set by *figsize* and *dpi*. *figsize* is the ``(width, height)`` +of the Figure in inches (or, if preferred, units of 72 typographic points). *dpi* +are how many pixels per inch the figure will be rendered at. To make your Figures +appear on the screen at the physical size you requested, you should set *dpi* +to the same *dpi* as your graphics system. Note that many graphics systems now use +a "dpi ratio" to specify how many screen pixels are used to represent a graphics +pixel. Matplotlib applies the dpi ratio to the *dpi* passed to the figure to make +it have higher resolution, so you should pass the lower number to the figure. + +The *facecolor*, *edgecolor*, *linewidth*, and *frameon* options all change the appearance of the +figure in expected ways, with *frameon* making the figure transparent if set to *False*. + +Finally, the user can specify a layout engine for the figure with the *layout* +parameter. Currently Matplotlib supplies +:doc:`"constrained" `, +:ref:`"compressed" ` and +:doc:`"tight" ` layout engines. These +rescale axes inside the Figure to prevent overlap of ticklabels, and try and align +axes, and can save significant manual adjustment of artists on a Figure for many +common cases. + +Adding Artists +-------------- + +The `~.FigureBase` class has a number of methods to add artists to a `~.Figure` or +a `~.SubFigure`. By far the most common are to add Axes of various configurations +(`~.FigureBase.add_axes`, `~.FigureBase.add_subplot`, `~.FigureBase.subplots`, +`~.FigureBase.subplot_mosaic`) and subfigures (`~.FigureBase.subfigures`). Colorbars +are added to Axes or group of Axes at the Figure level (`~.FigureBase.colorbar`). +It is also possible to have a Figure-level legend (`~.FigureBase.legend`). +Other Artists include figure-wide labels (`~.FigureBase.suptitle`, +`~.FigureBase.supxlabel`, `~.FigureBase.supylabel`) and text (`~.FigureBase.text`). +Finally, low-level Artists can be added directly using `~.FigureBase.add_artist` +usually with care being taken to use the appropriate transform. Usually these +include ``Figure.transFigure`` which ranges from 0 to 1 in each direction, and +represents the fraction of the current Figure size, or ``Figure.dpi_scale_trans`` +which will be in physical units of inches from the bottom left corner of the Figure +(see :doc:`/tutorials/advanced/transforms_tutorial` for more details). + + +.. _saving_figures: + +Saving Figures +============== + +Finally, Figures can be saved to disk using the `~.Figure.savefig` method. +``fig.savefig('MyFigure.png', dpi=200)`` will save a PNG formatted figure to +the file ``MyFigure.png`` in the current directory on disk with 200 dots-per-inch +resolution. Note that the filename can include a relative or absolute path to +any place on the file system. + +Many types of output are supported, including raster formats like PNG, GIF, JPEG, +TIFF and vector formats like PDF, EPS, and SVG. + +By default, the size of the saved Figure is set by the Figure size (in inches) and, for the raster +formats, the *dpi*. If *dpi* is not set, then the *dpi* of the Figure is used. +Note that *dpi* still has meaning for vector formats like PDF if the Figure includes +Artists that have been :doc:`rasterized
`; the +*dpi* specified will be the resolution of the rasterized objects. + +It is possible to change the size of the Figure using the *bbox_inches* argument +to savefig. This can be specified manually, again in inches. However, by far +the most common use is ``bbox_inches='tight'``. This option "shrink-wraps", trimming +or expanding as needed, the size of the figure so that it is tight around all the artists +in a figure, with a small pad that can be specified by *pad_inches*, which defaults to +0.1 inches. The dashed box in the plot below shows the portion of the figure that +would be saved if ``bbox_inches='tight'`` were used in savefig. + +.. plot:: + + import matplotlib.pyplot as plt + from matplotlib.patches import FancyBboxPatch + + fig, ax = plt.subplots(figsize=(4, 2), facecolor='lightskyblue') + ax.set_position([0.1, 0.2, 0.8, 0.7]) + ax.set_aspect(1) + bb = ax.get_tightbbox() + bb = bb.padded(10) + fancy = FancyBboxPatch(bb.p0, bb.width, bb.height, fc='none', + ec=(0, 0.0, 0, 0.5), lw=2, linestyle='--', + transform=None, clip_on=False) + ax.add_patch(fancy) diff --git a/doc/users/explain/fonts.rst b/doc/users/explain/fonts.rst new file mode 100644 index 000000000000..1f12c5760d25 --- /dev/null +++ b/doc/users/explain/fonts.rst @@ -0,0 +1,193 @@ +.. redirect-from:: /users/fonts + +Fonts in Matplotlib +=================== + +Matplotlib needs fonts to work with its text engine, some of which are shipped +alongside the installation. The default font is `DejaVu Sans +`_ which covers most European writing systems. +However, users can configure the default fonts, and provide their own custom +fonts. See :doc:`Customizing text properties ` for +details and :ref:`font-nonlatin` in particular for glyphs not supported by +DejaVu Sans. + +Matplotlib also provides an option to offload text rendering to a TeX engine +(``usetex=True``), see :doc:`Text rendering with LaTeX +`. + +Fonts in PDF and PostScript +--------------------------- + +Fonts have a long (and sometimes incompatible) history in computing, leading to +different platforms supporting different types of fonts. In practice, +Matplotlib supports three font specifications (in addition to pdf 'core fonts', +which are explained later in the guide): + +.. list-table:: Type of Fonts + :header-rows: 1 + + * - Type 1 (PDF) + - Type 3 (PDF/PS) + - TrueType (PDF) + * - One of the oldest types, introduced by Adobe + - Similar to Type 1 in terms of introduction + - Newer than previous types, used commonly today, introduced by Apple + * - Restricted subset of PostScript, charstrings are in bytecode + - Full PostScript language, allows embedding arbitrary code + (in theory, even render fractals when rasterizing!) + - Include a virtual machine that can execute code! + * - These fonts support font hinting + - Do not support font hinting + - Hinting supported (virtual machine processes the "hints") + * - Non-subsetted through Matplotlib + - Subsetted via external module ttconv + - Subsetted via external module `fontTools `__ + +.. note:: + + Adobe disabled__ support for authoring with Type 1 fonts in January 2023. + + __ https://helpx.adobe.com/fonts/kb/postscript-type-1-fonts-end-of-support.html + +Other font specifications which Matplotlib supports: + +- Type 42 fonts (PS): + + - PostScript wrapper around TrueType fonts + - 42 is the `Answer to Life, the Universe, and Everything! `_ + - Matplotlib uses the external library `fontTools `__ + to subset these types of fonts + +- OpenType fonts: + + - OpenType is a new standard for digital type fonts, developed jointly by + Adobe and Microsoft + - Generally contain a much larger character set! + - Limited support with Matplotlib + +Font subsetting +~~~~~~~~~~~~~~~ + +The PDF and PostScript formats support embedding fonts in files, allowing the +display program to correctly render the text, independent of what fonts are +installed on the viewer's computer and without the need to pre-rasterize the text. +This ensures that if the output is zoomed or resized the text does not become +pixelated. However, embedding full fonts in the file can lead to large output +files, particularly with fonts with many glyphs such as those that support CJK +(Chinese/Japanese/Korean). + +The solution to this problem is to subset the fonts used in the document and +only embed the glyphs actually used. This gets both vector text and small +files sizes. Computing the subset of the font required and writing the new +(reduced) font are both complex problem and thus Matplotlib relies on +`fontTools `__ and a vendored fork +of ttconv. + +Currently Type 3, Type 42, and TrueType fonts are subsetted. Type 1 fonts are not. + +Core Fonts +~~~~~~~~~~ + +In addition to the ability to embed fonts, as part of the `PostScript +`_ and `PDF +specification +`_ +there are 14 Core Fonts that compliant viewers must ensure are available. If +you restrict your document to only these fonts you do not have to embed any +font information in the document but still get vector text. + +This is especially helpful to generate *really lightweight* documents:: + + # trigger core fonts for PDF backend + plt.rcParams["pdf.use14corefonts"] = True + # trigger core fonts for PS backend + plt.rcParams["ps.useafm"] = True + + chars = "AFM ftw!" + fig, ax = plt.subplots() + ax.text(0.5, 0.5, chars) + + fig.savefig("AFM_PDF.pdf", format="pdf") + fig.savefig("AFM_PS.ps", format="ps) + +Fonts in SVG +------------ + +Text can output to SVG in two ways controlled by :rc:`svg.fonttype`: + +- as a path (``'path'``) in the SVG +- as string in the SVG with font styling on the element (``'none'``) + +When saving via ``'path'`` Matplotlib will compute the path of the glyphs used +as vector paths and write those to the output. The advantage of doing so is +that the SVG will look the same on all computers independent of what fonts are +installed. However the text will not be editable after the fact. +In contrast, saving with ``'none'`` will result in smaller files and the +text will appear directly in the markup. However, the appearance may vary +based on the SVG viewer and what fonts are available. + +Fonts in Agg +------------ + +To output text to raster formats via Agg, Matplotlib relies on `FreeType +`_. Because the exact rendering of the glyphs +changes between FreeType versions we pin to a specific version for our image +comparison tests. + +How Matplotlib selects fonts +---------------------------- + +Internally, using a font in Matplotlib is a three step process: + +1. a `.FontProperties` object is created (explicitly or implicitly) +2. based on the `.FontProperties` object the methods on `.FontManager` are used + to select the closest "best" font Matplotlib is aware of (except for + ``'none'`` mode of SVG). +3. the Python proxy for the font object is used by the backend code to render + the text -- the exact details depend on the backend via `.font_manager.get_font`. + +The algorithm to select the "best" font is a modified version of the algorithm +specified by the `CSS1 Specifications +`_ which is used by web browsers. +This algorithm takes into account the font family name (e.g. "Arial", "Noto +Sans CJK", "Hack", ...), the size, style, and weight. In addition to family +names that map directly to fonts there are five "generic font family names" +(serif, monospace, fantasy, cursive, and sans-serif) that will internally be +mapped to any one of a set of fonts. + +Currently the public API for doing step 2 is `.FontManager.findfont` (and that +method on the global `.FontManager` instance is aliased at the module level as +`.font_manager.findfont`), which will only find a single font and return the absolute +path to the font on the filesystem. + +Font fallback +------------- + +There is no font that covers the entire Unicode space thus it is possible for the +users to require a mix of glyphs that can not be satisfied from a single font. +While it has been possible to use multiple fonts within a Figure, on distinct +`.Text` instances, it was not previous possible to use multiple fonts in the +same `.Text` instance (as a web browser does). As of Matplotlib 3.6 the Agg, +SVG, PDF, and PS backends will "fallback" through multiple fonts in a single +`.Text` instance: + +.. plot:: + :include-source: + :caption: The string "There are 几个汉字 in between!" rendered with 2 fonts. + + fig, ax = plt.subplots() + ax.text( + .5, .5, "There are 几个汉字 in between!", + family=['DejaVu Sans', 'WenQuanYi Zen Hei'], + ha='center' + ) + +Internally this is implemented by setting The "font family" on +`.FontProperties` objects to a list of font families. A (currently) +private API extracts a list of paths to all of the fonts found and then +constructs a single `.ft2font.FT2Font` object that is aware of all of the fonts. +Each glyph of the string is rendered using the first font in the list that +contains that glyph. + +A majority of this work was done by Aitik Gupta supported by Google Summer of +Code 2021. diff --git a/doc/users/explain/index.rst b/doc/users/explain/index.rst new file mode 100644 index 000000000000..d70886dc6878 --- /dev/null +++ b/doc/users/explain/index.rst @@ -0,0 +1,19 @@ +.. _users-guide-explain: + +.. redirect-from:: /users/explain + +Explanations +------------ + +.. toctree:: + :maxdepth: 2 + + api_interfaces.rst + figures.rst + backends.rst + writing_a_backend_pyplot_interface.rst + interactive.rst + fonts.rst + event_handling.rst + performance.rst + interactive_guide.rst diff --git a/doc/users/interactive.rst b/doc/users/explain/interactive.rst similarity index 88% rename from doc/users/interactive.rst rename to doc/users/explain/interactive.rst index b4b51ff591ac..942b682fb04c 100644 --- a/doc/users/interactive.rst +++ b/doc/users/explain/interactive.rst @@ -1,23 +1,27 @@ +.. redirect-from:: /users/interactive + .. currentmodule:: matplotlib .. _mpl-shell: +.. _interactive_figures: -===================== - Interactive Figures -===================== - -.. toctree:: - +=================== +Interactive figures +=================== When working with data, interactivity can be invaluable. The pan/zoom and mouse-location tools built into the Matplotlib GUI windows are often sufficient, but you can also use the event system to build customized data exploration tools. +.. seealso:: + :ref:`figure_explanation`. + + Matplotlib ships with :ref:`backends ` binding to several GUI toolkits (Qt, Tk, Wx, GTK, macOS, JavaScript) and third party packages provide bindings to `kivy `__ and `Jupyter Lab -`__. For the figures to be responsive to +`__. For the figures to be responsive to mouse, keyboard, and paint events, the GUI event loop needs to be integrated with an interactive prompt. We recommend using IPython (see :ref:`below `). @@ -26,15 +30,15 @@ that include interactive tools, a toolbar, a tool-tip, and :ref:`key bindings `: `.pyplot.figure` - Creates a new empty `.figure.Figure` or selects an existing figure + Creates a new empty `.Figure` or selects an existing figure `.pyplot.subplots` - Creates a new `.figure.Figure` and fills it with a grid of `.axes.Axes` + Creates a new `.Figure` and fills it with a grid of `~.axes.Axes` `.pyplot` has a notion of "The Current Figure" which can be accessed through `.pyplot.gcf` and a notion of "The Current Axes" accessed through `.pyplot.gca`. Almost all of the functions in `.pyplot` pass -through the current `.Figure` / `.axes.Axes` (or create one) as +through the current `.Figure` / `~.axes.Axes` (or create one) as appropriate. Matplotlib keeps a reference to all of the open figures @@ -43,13 +47,12 @@ collected. `.Figure`\s can be closed and deregistered from `.pyplot` individuall `.pyplot.close`; all open `.Figure`\s can be closed via ``plt.close('all')``. -For more discussion of Matplotlib's event system and integrated event loops, please read: +.. seealso:: -.. toctree:: - :maxdepth: 1 + For more discussion of Matplotlib's event system and integrated event loops: - interactive_guide.rst - event_handling.rst + - :ref:`interactive_figures_and_eventloops` + - :ref:`event-handling-tutorial` .. _ipython-pylab: @@ -63,7 +66,7 @@ it also ensures that the GUI toolkit event loop is properly integrated with the command line (see :ref:`cp_integration`). In this example, we create and modify a figure via an IPython prompt. -The figure displays in a Qt5Agg GUI window. To configure the integration +The figure displays in a QtAgg GUI window. To configure the integration and enable :ref:`interactive mode ` use the ``%matplotlib`` magic: @@ -72,7 +75,7 @@ and enable :ref:`interactive mode ` use the :: In [1]: %matplotlib - Using matplotlib backend: Qt5Agg + Using matplotlib backend: QtAgg In [2]: import matplotlib.pyplot as plt @@ -153,9 +156,11 @@ If in interactive mode: If not in interactive mode: - newly created figures and changes to figures are not displayed until - * `.pyplot.show()` is called - * `.pyplot.pause()` is called - * `.FigureCanvasBase.flush_events()` is called + + * `.pyplot.show()` is called + * `.pyplot.pause()` is called + * `.FigureCanvasBase.flush_events()` is called + - `pyplot.show()` runs the GUI event loop and does not return until all the plot windows are closed If you are in non-interactive mode (or created figures while in @@ -168,7 +173,7 @@ and run the GUI event loop for the specified period of time. The GUI event loop being integrated with your command prompt and the figures being in interactive mode are independent of each other. -If you use `pyplot.ion` but have not arranged for the event loop integration, +If you try to use `pyplot.ion` without arranging for the event-loop integration, your figures will appear but will not be interactive while the prompt is waiting for input. You will not be able to pan/zoom and the figure may not even render (the window might appear black, transparent, or as a snapshot of the @@ -183,7 +188,7 @@ the GUI main loop in some other way. .. warning:: - Using `.figure.Figure.show` it is possible to display a figure on + Using `.Figure.show` it is possible to display a figure on the screen without starting the event loop and without being in interactive mode. This may work (depending on the GUI toolkit) but will likely result in a non-responsive figure. @@ -201,7 +206,7 @@ helpful keybindings are registered by default. .. _key-event-handling: -Navigation Keyboard Shortcuts +Navigation keyboard shortcuts ----------------------------- The following table holds all the default keys, which can be @@ -248,9 +253,10 @@ and your figures may not be responsive. Please consult the documentation of your GUI toolkit for details. +.. _jupyter_notebooks_jupyterlab: -Jupyter Notebooks / Lab ------------------------ +Jupyter Notebooks / JupyterLab +------------------------------ .. note:: @@ -263,7 +269,7 @@ Jupyter Notebooks / Lab cells. To get interactive figures in the 'classic' notebook or Jupyter lab, -use the `ipympl `__ backend +use the `ipympl `__ backend (must be installed separately) which uses the **ipywidget** framework. If ``ipympl`` is installed use the magic: diff --git a/doc/users/interactive_guide.rst b/doc/users/explain/interactive_guide.rst similarity index 93% rename from doc/users/interactive_guide.rst rename to doc/users/explain/interactive_guide.rst index b56e15f6bc7f..9c110967d94a 100644 --- a/doc/users/interactive_guide.rst +++ b/doc/users/explain/interactive_guide.rst @@ -1,11 +1,13 @@ .. _interactive_figures_and_eventloops: +.. redirect-from:: /users/interactive_guide + .. currentmodule:: matplotlib -================================================== - Interactive Figures and Asynchronous Programming -================================================== +================================================ +Interactive figures and asynchronous programming +================================================ Matplotlib supports rich interactive figures by embedding figures into a GUI window. The basic interactions of panning and zooming in an @@ -21,7 +23,7 @@ handling system `, `Interactive Tutorial `Interactive Applications using Matplotlib `__. -Event Loops +Event loops =========== Fundamentally, all user interaction (and networking) is implemented as @@ -71,7 +73,7 @@ interfaces. .. _cp_integration: -Command Prompt Integration +Command prompt integration ========================== So far, so good. We have the REPL (like the IPython terminal) that @@ -92,7 +94,7 @@ responsive we need a method to allow the loops to 'timeshare' : .. _cp_block_the_prompt: -Blocking the Prompt +Blocking the prompt ------------------- .. autosummary:: @@ -138,7 +140,7 @@ between polling for additional data. See :ref:`interactive_scripts` for more details. -Input Hook integration +Input hook integration ---------------------- While running the GUI event loop in a blocking mode or explicitly @@ -164,8 +166,8 @@ takes over again. This time-share technique only allows the event loop to run while python is otherwise idle and waiting for user input. If you want the GUI to be responsive during long running code it is necessary to -periodically flush the GUI event queue as described :ref:`above -`. In this case it is your code, not the REPL, which +periodically flush the GUI event queue as described in :ref:`spin_event_loop`. +In this case it is your code, not the REPL, which is blocking the process so you need to handle the "time-share" manually. Conversely, a very slow figure draw will block the prompt until it finishes drawing. @@ -211,9 +213,7 @@ Blocking functions ------------------ If you only need to collect points in an Axes you can use -`.figure.Figure.ginput` or more generally the tools from -`.blocking_input` the tools will take care of starting and stopping -the event loop for you. However if you have written some custom event +`.Figure.ginput`. However if you have written some custom event handling or are using `.widgets` you will need to manually run the GUI event loop using the methods described :ref:`above `. @@ -236,7 +236,7 @@ which would poll for new data and update the figure at 1Hz. .. _spin_event_loop: -Explicitly spinning the Event Loop +Explicitly spinning the event Loop ---------------------------------- .. autosummary:: @@ -285,7 +285,7 @@ to call `~.FigureCanvasBase.draw_idle` to request that the canvas be re-drawn. This method can be thought of *draw_soon* in analogy to `asyncio.loop.call_soon`. -We can add this our example above as :: +We can add this to our example above as :: def slow_loop(N, ln): for j in range(N): @@ -306,14 +306,14 @@ resources on the visualization and less on your computation. .. _stale_artists: -Stale Artists +Stale artists ============= Artists (as of Matplotlib 1.5) have a **stale** attribute which is `True` if the internal state of the artist has changed since the last time it was rendered. By default the stale state is propagated up to the Artists parents in the draw tree, e.g., if the color of a `.Line2D` -instance is changed, the `.axes.Axes` and `.figure.Figure` that +instance is changed, the `~.axes.Axes` and `.Figure` that contain it will also be marked as "stale". Thus, ``fig.stale`` will report if any artist in the figure has been modified and is out of sync with what is displayed on the screen. This is intended to be used to @@ -330,11 +330,11 @@ which by default is set to a function that forwards the stale state to the artist's parent. If you wish to suppress a given artist from propagating set this attribute to None. -`.figure.Figure` instances do not have a containing artist and their +`.Figure` instances do not have a containing artist and their default callback is `None`. If you call `.pyplot.ion` and are not in ``IPython`` we will install a callback to invoke `~.backend_bases.FigureCanvasBase.draw_idle` whenever the -`.figure.Figure` becomes stale. In ``IPython`` we use the +`.Figure` becomes stale. In ``IPython`` we use the ``'post_execute'`` hook to invoke `~.backend_bases.FigureCanvasBase.draw_idle` on any stale figures after having executed the user's input, but before returning the prompt @@ -422,17 +422,17 @@ based prompt to a ``prompt_toolkit`` based prompt. ``prompt_toolkit`` has the same conceptual input hook, which is fed into ``prompt_toolkit`` via the :meth:`IPython.terminal.interactiveshell.TerminalInteractiveShell.inputhook` method. The source for the ``prompt_toolkit`` input hooks lives at -:mod:`IPython.terminal.pt_inputhooks`. +``IPython.terminal.pt_inputhooks``. .. rubric:: Footnotes .. [#f1] A limitation of this design is that you can only wait for one - input, if there is a need to multiplex between multiple sources - then the loop would look something like :: + input, if there is a need to multiplex between multiple sources + then the loop would look something like :: - fds = [...] + fds = [...] while True: # Loop inp = select(fds).read() # Read eval(inp) # Evaluate / Print @@ -441,6 +441,6 @@ method. The source for the ``prompt_toolkit`` input hooks lives at `__ if you must. .. [#f3] These examples are aggressively dropping many of the - complexities that must be dealt with in the real world such as - keyboard interrupts, timeouts, bad input, resource - allocation and cleanup, etc. + complexities that must be dealt with in the real world such as + keyboard interrupts, timeouts, bad input, resource + allocation and cleanup, etc. diff --git a/doc/users/explain/performance.rst b/doc/users/explain/performance.rst new file mode 100644 index 000000000000..03bff404b7ab --- /dev/null +++ b/doc/users/explain/performance.rst @@ -0,0 +1,146 @@ +.. _performance: + +Performance +=========== + +Whether exploring data in interactive mode or programmatically +saving lots of plots, rendering performance can be a challenging +bottleneck in your pipeline. Matplotlib provides multiple +ways to greatly reduce rendering time at the cost of a slight +change (to a settable tolerance) in your plot's appearance. +The methods available to reduce rendering time depend on the +type of plot that is being created. + +Line segment simplification +--------------------------- + +For plots that have line segments (e.g. typical line plots, outlines +of polygons, etc.), rendering performance can be controlled by +:rc:`path.simplify` and :rc:`path.simplify_threshold`, which +can be defined e.g. in the :file:`matplotlibrc` file (see +:doc:`/tutorials/introductory/customizing` for more information about +the :file:`matplotlibrc` file). :rc:`path.simplify` is a Boolean +indicating whether or not line segments are simplified at all. +:rc:`path.simplify_threshold` controls how much line segments are simplified; +higher thresholds result in quicker rendering. + +The following script will first display the data without any +simplification, and then display the same data with simplification. +Try interacting with both of them:: + + import numpy as np + import matplotlib.pyplot as plt + import matplotlib as mpl + + # Setup, and create the data to plot + y = np.random.rand(100000) + y[50000:] *= 2 + y[np.geomspace(10, 50000, 400).astype(int)] = -1 + mpl.rcParams['path.simplify'] = True + + mpl.rcParams['path.simplify_threshold'] = 0.0 + plt.plot(y) + plt.show() + + mpl.rcParams['path.simplify_threshold'] = 1.0 + plt.plot(y) + plt.show() + +Matplotlib currently defaults to a conservative simplification +threshold of ``1/9``. To change default settings to use a different +value, change the :file:`matplotlibrc` file. Alternatively, users +can create a new style for interactive plotting (with maximal +simplification) and another style for publication quality plotting +(with minimal simplification) and activate them as necessary. See +:doc:`/tutorials/introductory/customizing` for instructions on +how to perform these actions. + +The simplification works by iteratively merging line segments +into a single vector until the next line segment's perpendicular +distance to the vector (measured in display-coordinate space) +is greater than the ``path.simplify_threshold`` parameter. + +.. note:: + Changes related to how line segments are simplified were made + in version 2.1. Rendering time will still be improved by these + parameters prior to 2.1, but rendering time for some kinds of + data will be vastly improved in versions 2.1 and greater. + +Marker subsampling +------------------ + +Markers can also be simplified, albeit less robustly than line +segments. Marker subsampling is only available to `.Line2D` objects +(through the ``markevery`` property). Wherever `.Line2D` construction +parameters are passed through, such as `.pyplot.plot` and `.Axes.plot`, +the ``markevery`` parameter can be used:: + + plt.plot(x, y, markevery=10) + +The ``markevery`` argument allows for naive subsampling, or an +attempt at evenly spaced (along the *x* axis) sampling. See the +:doc:`/gallery/lines_bars_and_markers/markevery_demo` +for more information. + +Splitting lines into smaller chunks +----------------------------------- + +If you are using the Agg backend (see :ref:`what-is-a-backend`), +then you can make use of :rc:`agg.path.chunksize` +This allows users to specify a chunk size, and any lines with +greater than that many vertices will be split into multiple +lines, each of which has no more than ``agg.path.chunksize`` +many vertices. (Unless ``agg.path.chunksize`` is zero, in +which case there is no chunking.) For some kind of data, +chunking the line up into reasonable sizes can greatly +decrease rendering time. + +The following script will first display the data without any +chunk size restriction, and then display the same data with +a chunk size of 10,000. The difference can best be seen when +the figures are large, try maximizing the GUI and then +interacting with them:: + + import numpy as np + import matplotlib.pyplot as plt + import matplotlib as mpl + mpl.rcParams['path.simplify_threshold'] = 1.0 + + # Setup, and create the data to plot + y = np.random.rand(100000) + y[50000:] *= 2 + y[np.geomspace(10, 50000, 400).astype(int)] = -1 + mpl.rcParams['path.simplify'] = True + + mpl.rcParams['agg.path.chunksize'] = 0 + plt.plot(y) + plt.show() + + mpl.rcParams['agg.path.chunksize'] = 10000 + plt.plot(y) + plt.show() + +Legends +------- + +The default legend behavior for axes attempts to find the location +that covers the fewest data points (``loc='best'``). This can be a +very expensive computation if there are lots of data points. In +this case, you may want to provide a specific location. + +Using the *fast* style +---------------------- + +The *fast* style can be used to automatically set +simplification and chunking parameters to reasonable +settings to speed up plotting large amounts of data. +The following code runs it:: + + import matplotlib.style as mplstyle + mplstyle.use('fast') + +It is very lightweight, so it works well with other +styles. Be sure the fast style is applied last +so that other styles do not overwrite the settings:: + + mplstyle.use(['dark_background', 'ggplot', 'fast']) diff --git a/doc/users/explain/writing_a_backend_pyplot_interface.rst b/doc/users/explain/writing_a_backend_pyplot_interface.rst new file mode 100644 index 000000000000..e193135f970d --- /dev/null +++ b/doc/users/explain/writing_a_backend_pyplot_interface.rst @@ -0,0 +1,82 @@ +========================================= +Writing a backend -- the pyplot interface +========================================= + +This page assumes general understanding of the information in the +:doc:`/users/explain/backends` page, and is instead intended as reference for +third-party backend implementers. It also only deals with the interaction +between backends and `.pyplot`, not with the rendering side, which is described +in `.backend_template`. + +There are two APIs for defining backends: a new canvas-based API (introduced in +Matplotlib 3.6), and an older function-based API. The new API is simpler to +implement because many methods can be inherited from "parent backends". It is +recommended if back-compatibility for Matplotlib < 3.6 is not a concern. +However, the old API remains supported. + +Fundamentally, a backend module needs to provide information to `.pyplot`, so +that + +1. `.pyplot.figure()` can create a new `.Figure` instance and associate it with + an instance of a backend-provided canvas class, itself hosted in an instance + of a backend-provided manager class. +2. `.pyplot.show()` can show all figures and start the GUI event loop (if any). + +To do so, the backend module must define a ``backend_module.FigureCanvas`` +subclass of `.FigureCanvasBase`. In the canvas-based API, this is the only +strict requirement for backend modules. The function-based API additionally +requires many module-level functions to be defined. + +Canvas-based API (Matplotlib >= 3.6) +------------------------------------ + +1. **Creating a figure**: `.pyplot.figure()` calls + ``figure = Figure(); FigureCanvas.new_manager(figure, num)`` + (``new_manager`` is a classmethod) to instantiate a canvas and a manager and + set up the ``figure.canvas`` and ``figure.canvas.manager`` attributes. + Figure unpickling uses the same approach, but replaces the newly + instantiated ``Figure()`` by the unpickled figure. + + Interactive backends should customize the effect of ``new_manager`` by + setting the ``FigureCanvas.manager_class`` attribute to the desired manager + class, and additionally (if the canvas cannot be created before the manager, + as in the case of the wx backends) by overriding the + ``FigureManager.create_with_canvas`` classmethod. (Non-interactive backends + can normally use a trivial ``FigureManagerBase`` and can therefore skip this + step.) + + After a new figure is registered with `.pyplot` (either via + `.pyplot.figure()` or via unpickling), if in interactive mode, `.pyplot` + will call its canvas' ``draw_idle()`` method, which can be overridden as + desired. + +2. **Showing figures**: `.pyplot.show()` calls + ``FigureCanvas.manager_class.pyplot_show()`` (a classmethod), forwarding any + arguments, to start the main event loop. + + By default, ``pyplot_show()`` checks whether there are any ``managers`` + registered with `.pyplot` (exiting early if not), calls ``manager.show()`` + on all such managers, and then, if called with ``block=True`` (or with + the default ``block=None`` and out of IPython's pylab mode and not in + interactive mode), calls ``FigureCanvas.manager_class.start_main_loop()`` + (a classmethod) to start the main event loop. Interactive backends should + therefore override the ``FigureCanvas.manager_class.start_main_loop`` + classmethod accordingly (or alternatively, they may also directly override + ``FigureCanvas.manager_class.pyplot_show`` directly). + +Function-based API +------------------ + +1. **Creating a figure**: `.pyplot.figure()` calls + ``new_figure_manager(num, *args, **kwargs)`` (which also takes care of + creating the new figure as ``Figure(*args, **kwargs)``); unpickling calls + ``new_figure_manager_given_figure(num, figure)``. + + Furthermore, in interactive mode, the first draw of the newly registered + figure can be customized by providing a module-level + ``draw_if_interactive()`` function. (In the new canvas-based API, this + function is not taken into account anymore.) + +2. **Showing figures**: `.pyplot.show()` calls a module-level ``show()`` + function, which is typically generated via the ``ShowBase`` class and its + ``mainloop`` method. diff --git a/doc/faq/environment_variables_faq.rst b/doc/users/faq/environment_variables_faq.rst similarity index 83% rename from doc/faq/environment_variables_faq.rst rename to doc/users/faq/environment_variables_faq.rst index 76dd3696a3e6..fb9341db1147 100644 --- a/doc/faq/environment_variables_faq.rst +++ b/doc/users/faq/environment_variables_faq.rst @@ -1,20 +1,14 @@ .. _environment-variables: +.. redirect-from:: /faq/environment_variables_faq + ********************* -Environment Variables +Environment variables ********************* .. contents:: :backlinks: none - -.. envvar:: DISPLAY - - The server and screen on which to place windows. This is interpreted by GUI - toolkits in a backend-specific manner, but generally refers to an `X.org - display name - `_. - .. envvar:: HOME The user's home directory. On Linux, :envvar:`~ ` is shorthand for :envvar:`HOME`. @@ -35,6 +29,13 @@ Environment Variables used to find a base directory in which the :file:`matplotlib` subdirectory is created. +.. envvar:: MPLSETUPCFG + + This optional variable can be set to the full path of a :file:`mplsetup.cfg` + configuration file used to customize the Matplotlib build. By default, a + :file:`mplsetup.cfg` file in the root of the Matplotlib source tree will be + read. Supported build options are listed in :file:`mplsetup.cfg.template`. + .. envvar:: PATH The list of directories searched to find executable programs. @@ -47,7 +48,7 @@ Environment Variables .. envvar:: QT_API The Python Qt wrapper to prefer when using Qt-based backends. See :ref:`the - entry in the usage guide ` for more information. + entry in the usage guide ` for more information. .. _setting-linux-osx-environment-variables: @@ -81,7 +82,7 @@ searched first or last? To append to an existing environment variable:: setenv PATH ${PATH}:~/bin # csh/tcsh To make your changes available in the future, add the commands to your -:file:`~/.bashrc`/:file:`.cshrc` file. +:file:`~/.bashrc` or :file:`~/.cshrc` file. .. _setting-windows-environment-variables: diff --git a/doc/faq/howto_faq.rst b/doc/users/faq/howto_faq.rst similarity index 59% rename from doc/faq/howto_faq.rst rename to doc/users/faq/howto_faq.rst index 562823ab4104..fd4e81f32d00 100644 --- a/doc/faq/howto_faq.rst +++ b/doc/users/faq/howto_faq.rst @@ -1,5 +1,7 @@ .. _howto-faq: +.. redirect-from:: /faq/howto_faq + ****** How-to ****** @@ -7,6 +9,59 @@ How-to .. contents:: :backlinks: none + +.. _how-to-too-many-ticks: + +Why do I have so many ticks, and/or why are they out of order? +-------------------------------------------------------------- + +One common cause for unexpected tick behavior is passing a *list of strings +instead of numbers or datetime objects*. This can easily happen without notice +when reading in a comma-delimited text file. Matplotlib treats lists of strings +as *categorical* variables +(:doc:`/gallery/lines_bars_and_markers/categorical_variables`), and by default +puts one tick per category, and plots them in the order in which they are +supplied. + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots(1, 2, constrained_layout=True, figsize=(6, 2)) + + ax[0].set_title('Ticks seem out of order / misplaced') + x = ['5', '20', '1', '9'] # strings + y = [5, 20, 1, 9] + ax[0].plot(x, y, 'd') + ax[0].tick_params(axis='x', labelcolor='red', labelsize=14) + + ax[1].set_title('Many ticks') + x = [str(xx) for xx in np.arange(100)] # strings + y = np.arange(100) + ax[1].plot(x, y) + ax[1].tick_params(axis='x', labelcolor='red', labelsize=14) + +The solution is to convert the list of strings to numbers or +datetime objects (often ``np.asarray(numeric_strings, dtype='float')`` or +``np.asarray(datetime_strings, dtype='datetime64[s]')``). + +For more information see :doc:`/gallery/ticks/ticks_too_many`. + +.. _howto-determine-artist-extent: + +Determine the extent of Artists in the Figure +--------------------------------------------- + +Sometimes we want to know the extent of an Artist. Matplotlib `.Artist` objects +have a method `.Artist.get_window_extent` that will usually return the extent of +the artist in pixels. However, some artists, in particular text, must be +rendered at least once before their extent is known. Matplotlib supplies +`.Figure.draw_without_rendering`, which should be called before calling +``get_window_extent``. + .. _howto-figure-empty: Check whether a figure is empty @@ -34,8 +89,8 @@ way of defining empty, and checking the above is only rarely necessary. Usually the user or program handling the figure know if they have added something to the figure. -Checking whether a figure would render empty cannot be reliably checked except -by actually rendering the figure and investigating the rendered result. +The only reliable way to check whether a figure would render empty is to +actually perform such a rendering and inspect the result. .. _howto-findobj: @@ -62,7 +117,7 @@ You can also filter on class instances:: for o in fig.findobj(text.Text): o.set_fontstyle('italic') -.. _howto-supress_offset: +.. _howto-suppress_offset: Prevent ticklabels from having an offset ---------------------------------------- @@ -110,133 +165,29 @@ on individual elements, e.g.:: Save multiple plots to one pdf file ----------------------------------- -Many image file formats can only have one image per file, but some -formats support multi-page files. Currently only the pdf backend has -support for this. To make a multi-page pdf file, first initialize the -file:: +Many image file formats can only have one image per file, but some formats +support multi-page files. Currently, Matplotlib only provides multi-page +output to pdf files, using either the pdf or pgf backends, via the +`.backend_pdf.PdfPages` and `.backend_pgf.PdfPages` classes. - from matplotlib.backends.backend_pdf import PdfPages - pp = PdfPages('multipage.pdf') - -You can give the :class:`~matplotlib.backends.backend_pdf.PdfPages` -object to :func:`~matplotlib.pyplot.savefig`, but you have to specify -the format:: - - plt.savefig(pp, format='pdf') - -An easier way is to call -:meth:`PdfPages.savefig `:: - - pp.savefig() - -Finally, the multipage pdf object has to be closed:: - - pp.close() - -The same can be done using the pgf backend:: - - from matplotlib.backends.backend_pgf import PdfPages - -.. _howto-subplots-adjust: - -Move the edge of an axes to make room for tick labels ------------------------------------------------------ +.. _howto-auto-adjust: -For subplots, you can control the default spacing on the left, right, -bottom, and top as well as the horizontal and vertical spacing between -multiple rows and columns using the -:meth:`matplotlib.figure.Figure.subplots_adjust` method (in pyplot it -is :func:`~matplotlib.pyplot.subplots_adjust`). For example, to move -the bottom of the subplots up to make room for some rotated x tick -labels:: +Make room for tick labels +------------------------- - fig = plt.figure() - fig.subplots_adjust(bottom=0.2) - ax = fig.add_subplot(111) +By default, Matplotlib uses fixed percentage margins around subplots. This can +lead to labels overlapping or being cut off at the figure boundary. There are +multiple ways to fix this: -You can control the defaults for these parameters in your -:file:`matplotlibrc` file; see :doc:`/tutorials/introductory/customizing`. For -example, to make the above setting permanent, you would set:: - - figure.subplot.bottom : 0.2 # the bottom of the subplots of the figure - -The other parameters you can configure are, with their defaults - -*left* = 0.125 - the left side of the subplots of the figure -*right* = 0.9 - the right side of the subplots of the figure -*bottom* = 0.1 - the bottom of the subplots of the figure -*top* = 0.9 - the top of the subplots of the figure -*wspace* = 0.2 - the amount of width reserved for space between subplots, - expressed as a fraction of the average axis width -*hspace* = 0.2 - the amount of height reserved for space between subplots, - expressed as a fraction of the average axis height - -If you want additional control, you can create an -:class:`~matplotlib.axes.Axes` using the -:func:`~matplotlib.pyplot.axes` command (or equivalently the figure -:meth:`~matplotlib.figure.Figure.add_axes` method), which allows you to -specify the location explicitly:: - - ax = fig.add_axes([left, bottom, width, height]) - -where all values are in fractional (0 to 1) coordinates. See -:doc:`/gallery/subplots_axes_and_figures/axes_demo` for an example of -placing axes manually. +- Manually adapt the subplot parameters using `.Figure.subplots_adjust` / + `.pyplot.subplots_adjust`. +- Use one of the automatic layout mechanisms: -.. _howto-auto-adjust: + - constrained layout (:doc:`/tutorials/intermediate/constrainedlayout_guide`) + - tight layout (:doc:`/tutorials/intermediate/tight_layout_guide`) -Automatically make room for tick labels ---------------------------------------- - -.. note:: - This is now easier to handle than ever before. - Calling :func:`~matplotlib.pyplot.tight_layout` or alternatively using - ``constrained_layout=True`` argument in :func:`~matplotlib.pyplot.subplots` - can fix many common layout issues. See the - :doc:`/tutorials/intermediate/tight_layout_guide` and - :doc:`/tutorials/intermediate/constrainedlayout_guide` for more details. - - The information below is kept here in case it is useful for other - purposes. - -In most use cases, it is enough to simply change the subplots adjust -parameters as described in :ref:`howto-subplots-adjust`. But in some -cases, you don't know ahead of time what your tick labels will be, or -how large they will be (data and labels outside your control may be -being fed into your graphing application), and you may need to -automatically adjust your subplot parameters based on the size of the -tick labels. Any :class:`~matplotlib.text.Text` instance can report -its extent in window coordinates (a negative x coordinate is outside -the window), but there is a rub. - -The :class:`~matplotlib.backend_bases.RendererBase` instance, which is -used to calculate the text size, is not known until the figure is -drawn (:meth:`~matplotlib.figure.Figure.draw`). After the window is -drawn and the text instance knows its renderer, you can call -:meth:`~matplotlib.text.Text.get_window_extent`. One way to solve -this chicken and egg problem is to wait until the figure is draw by -connecting -(:meth:`~matplotlib.backend_bases.FigureCanvasBase.mpl_connect`) to the -"on_draw" signal (:class:`~matplotlib.backend_bases.DrawEvent`) and -get the window extent there, and then do something with it, e.g., move -the left of the canvas over; see :ref:`event-handling-tutorial`. - -Here is an example that gets a bounding box in relative figure coordinates -(0..1) of each of the labels and uses it to move the left of the subplots -over so that the tick labels fit in the figure: - -.. figure:: ../gallery/pyplots/images/sphx_glr_auto_subplots_adjust_001.png - :target: ../gallery/pyplots/auto_subplots_adjust.html - :align: center - :scale: 50 - - Auto Subplots Adjust +- Calculate good values from the size of the plot elements yourself + (:doc:`/gallery/subplots_axes_and_figures/auto_subplots_adjust`) .. _howto-align-label: @@ -252,13 +203,11 @@ behavior by specifying the coordinates of the label. The example below shows the default behavior in the left subplots, and the manual setting in the right subplots. -.. figure:: ../gallery/pyplots/images/sphx_glr_align_ylabels_001.png - :target: ../gallery/pyplots/align_ylabels.html +.. figure:: ../../gallery/text_labels_and_annotations/images/sphx_glr_align_ylabels_001.png + :target: ../../gallery/text_labels_and_annotations/align_ylabels.html :align: center :scale: 50 - Align Ylabels - .. _howto-set-zorder: Control the draw order of plot elements diff --git a/doc/faq/index.rst b/doc/users/faq/index.rst similarity index 53% rename from doc/faq/index.rst rename to doc/users/faq/index.rst index bf71718e622b..636c90904aba 100644 --- a/doc/faq/index.rst +++ b/doc/users/faq/index.rst @@ -1,20 +1,21 @@ .. _faq-index: -################## -The Matplotlib FAQ -################## +.. redirect-from:: /faq/index + +########################## +How-to and troubleshooting +########################## .. only:: html :Release: |version| :Date: |today| - Frequently asked questions about matplotlib + Frequently asked questions about Matplotlib: .. toctree:: :maxdepth: 2 - installing_faq.rst howto_faq.rst troubleshooting_faq.rst environment_variables_faq.rst diff --git a/doc/faq/troubleshooting_faq.rst b/doc/users/faq/troubleshooting_faq.rst similarity index 91% rename from doc/faq/troubleshooting_faq.rst rename to doc/users/faq/troubleshooting_faq.rst index f0644003201b..f88cb19d1395 100644 --- a/doc/faq/troubleshooting_faq.rst +++ b/doc/users/faq/troubleshooting_faq.rst @@ -1,5 +1,7 @@ .. _troubleshooting-faq: +.. redirect-from:: /faq/troubleshooting_faq + *************** Troubleshooting *************** @@ -46,10 +48,10 @@ locate your :file:`matplotlib/` configuration directory, use >>> mpl.get_configdir() '/home/darren/.config/matplotlib' -On unix-like systems, this directory is generally located in your +On Unix-like systems, this directory is generally located in your :envvar:`HOME` directory under the :file:`.config/` directory. -In addition, users have a cache directory. On unix-like systems, this is +In addition, users have a cache directory. On Unix-like systems, this is separate from the configuration directory by default. To locate your :file:`.cache/` directory, use :func:`matplotlib.get_cachedir`:: @@ -57,7 +59,7 @@ separate from the configuration directory by default. To locate your >>> mpl.get_cachedir() '/home/darren/.cache/matplotlib' -On windows, both the config directory and the cache directory are +On Windows, both the config directory and the cache directory are the same and are in your :file:`Documents and Settings` or :file:`Users` directory by default:: @@ -82,18 +84,19 @@ Getting help There are a number of good resources for getting help with Matplotlib. There is a good chance your question has already been asked: -- The `mailing list archive `_. +- The `mailing list archive + `_. - `GitHub issues `_. - Stackoverflow questions tagged `matplotlib - `_. + `_. If you are unable to find an answer to your question through search, please provide the following information in your e-mail to the `mailing list `_: -* Your operating system (Linux/UNIX users: post the output of ``uname -a``). +* Your operating system (Linux/Unix users: post the output of ``uname -a``). * Matplotlib version:: @@ -154,7 +157,7 @@ the tracker so the issue doesn't get lost. Problems with recent git versions ================================= -First make sure you have a clean build and install (see :ref:`clean-install`), +First, make sure you have a clean build and install (see :ref:`clean-install`), get the latest git update, install it and run a simple test script in debug mode:: diff --git a/doc/users/getting_started/index.rst b/doc/users/getting_started/index.rst new file mode 100644 index 000000000000..ec1f769e5e78 --- /dev/null +++ b/doc/users/getting_started/index.rst @@ -0,0 +1,55 @@ +Getting started +=============== + +Installation quick-start +------------------------ + +.. grid:: 1 1 2 2 + + .. grid-item:: + + Install using `pip `__: + + .. code-block:: bash + + pip install matplotlib + + .. grid-item:: + + Install using `conda `__: + + .. code-block:: bash + + conda install -c conda-forge matplotlib + +Further details are available in the :doc:`Installation Guide
`. + + +Draw a first plot +----------------- + +Here is a minimal example plot: + +.. plot:: + :include-source: + :align: center + + import matplotlib.pyplot as plt + import numpy as np + + x = np.linspace(0, 2 * np.pi, 200) + y = np.sin(x) + + fig, ax = plt.subplots() + ax.plot(x, y) + plt.show() + +If a plot does not show up please check :ref:`troubleshooting-faq`. + +Where to go next +---------------- + +- Check out :doc:`Plot types ` to get an overview of the + types of plots you can create with Matplotlib. +- Learn Matplotlib from the ground up in the + :doc:`Quick-start guide `. diff --git a/doc/users/github_stats.rst b/doc/users/github_stats.rst index e2759a0d06e5..c7324b556272 100644 --- a/doc/users/github_stats.rst +++ b/doc/users/github_stats.rst @@ -1,141 +1,121 @@ .. _github-stats: -GitHub Stats -============ +GitHub statistics for 3.7.1 (Mar 03, 2023) +========================================== -GitHub stats for 2021/05/08 - 2021/08/12 (tag: v3.4.2) +GitHub statistics for 2023/02/13 (tag: v3.7.0) - 2023/03/03 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 22 issues and merged 69 pull requests. -The full list can be seen `on GitHub `__ +We closed 14 issues and merged 62 pull requests. +The full list can be seen `on GitHub `__ -The following 20 authors contributed 95 commits. +The following 16 authors contributed 129 commits. +* Albert Y. Shih * Antony Lee -* David Stansby -* Diego -* Diego Leal Petrola -* Diego Petrola +* devRD * Elliott Sales de Andrade -* Eric Firing -* Frank Sauerburger +* Fabian Joswig * Greg Lucas -* Ian Hunt-Isaak -* Jash Shah +* Hasan Rashid +* hasanrashid * Jody Klymak -* Jouni K. Seppänen -* MichaÅ‚ Górny -* sandipanpanda -* Slava Ostroukh +* Kyle Sunden +* Oscar Gustafsson +* Ratnabali Dutta +* RishabhSpark +* Ruth Comer * Thomas A Caswell * Tim Hoffmann -* Viacheslav Ostroukh -* Xianxiang Li GitHub issues and pull requests: -Pull Requests (69): - -* :ghpull:`20830`: Backport PR #20826 on branch v3.4.x (Fix clear of Axes that are shared.) -* :ghpull:`20826`: Fix clear of Axes that are shared. -* :ghpull:`20823`: Backport PR #20817 on branch v3.4.x (Make test_change_epoch more robust.) -* :ghpull:`20817`: Make test_change_epoch more robust. -* :ghpull:`20820`: Backport PR #20771 on branch v3.4.x (FIX: tickspacing for subfigures) -* :ghpull:`20771`: FIX: tickspacing for subfigures -* :ghpull:`20777`: FIX: dpi and scatter for subfigures now correct -* :ghpull:`20787`: Backport PR #20786 on branch v3.4.x (Fixed typo in _constrained_layout.py (#20782)) -* :ghpull:`20786`: Fixed typo in _constrained_layout.py (#20782) -* :ghpull:`20763`: Backport PR #20761 on branch v3.4.x (Fix suplabel autopos) -* :ghpull:`20761`: Fix suplabel autopos -* :ghpull:`20751`: Backport PR #20748 on branch v3.4.x (Ensure _static directory exists before copying CSS.) -* :ghpull:`20748`: Ensure _static directory exists before copying CSS. -* :ghpull:`20713`: Backport PR #20710 on branch v3.4.x (Fix tests with Inkscape 1.1.) -* :ghpull:`20687`: Enable PyPy wheels for v3.4.x -* :ghpull:`20710`: Fix tests with Inkscape 1.1. -* :ghpull:`20696`: Backport PR #20662 on branch v3.4.x (Don't forget to disable autoscaling after interactive zoom.) -* :ghpull:`20662`: Don't forget to disable autoscaling after interactive zoom. -* :ghpull:`20683`: Backport PR #20645 on branch v3.4.x (Fix leak if affine_transform is passed invalid vertices.) -* :ghpull:`20645`: Fix leak if affine_transform is passed invalid vertices. -* :ghpull:`20642`: Backport PR #20629 on branch v3.4.x (Add protection against out-of-bounds read in ttconv) -* :ghpull:`20643`: Backport PR #20597 on branch v3.4.x -* :ghpull:`20629`: Add protection against out-of-bounds read in ttconv -* :ghpull:`20597`: Fix TTF headers for type 42 stix font -* :ghpull:`20624`: Backport PR #20609 on branch v3.4.x (FIX: fix figbox deprecation) -* :ghpull:`20609`: FIX: fix figbox deprecation -* :ghpull:`20594`: Backport PR #20590 on branch v3.4.x (Fix class docstrings for Norms created from Scales.) -* :ghpull:`20590`: Fix class docstrings for Norms created from Scales. -* :ghpull:`20587`: Backport PR #20584: FIX: do not simplify path in LineCollection.get_s… -* :ghpull:`20584`: FIX: do not simplify path in LineCollection.get_segments -* :ghpull:`20578`: Backport PR #20511 on branch v3.4.x (Fix calls to np.ma.masked_where) -* :ghpull:`20511`: Fix calls to np.ma.masked_where -* :ghpull:`20568`: Backport PR #20565 on branch v3.4.x (FIX: PILLOW asarray bug) -* :ghpull:`20566`: Backout pillow=8.3.0 due to a crash -* :ghpull:`20565`: FIX: PILLOW asarray bug -* :ghpull:`20503`: Backport PR #20488 on branch v3.4.x (FIX: Include 0 when checking lognorm vmin) -* :ghpull:`20488`: FIX: Include 0 when checking lognorm vmin -* :ghpull:`20483`: Backport PR #20480 on branch v3.4.x (Fix str of empty polygon.) -* :ghpull:`20480`: Fix str of empty polygon. -* :ghpull:`20478`: Backport PR #20473 on branch v3.4.x (_GSConverter: handle stray 'GS' in output gracefully) -* :ghpull:`20473`: _GSConverter: handle stray 'GS' in output gracefully -* :ghpull:`20456`: Backport PR #20453 on branch v3.4.x (Remove ``Tick.apply_tickdir`` from 3.4 deprecations.) -* :ghpull:`20441`: Backport PR #20416 on branch v3.4.x (Fix missing Patch3DCollection._z_markers_idx) -* :ghpull:`20416`: Fix missing Patch3DCollection._z_markers_idx -* :ghpull:`20417`: Backport PR #20395 on branch v3.4.x (Pathing issue) -* :ghpull:`20395`: Pathing issue -* :ghpull:`20404`: Backport PR #20403: FIX: if we have already subclassed mixin class ju… -* :ghpull:`20403`: FIX: if we have already subclassed mixin class just return -* :ghpull:`20383`: Backport PR #20381 on branch v3.4.x (Prevent corrections and completions in search field) -* :ghpull:`20307`: Backport PR #20154 on branch v3.4.x (ci: Bump Ubuntu to 18.04 LTS.) -* :ghpull:`20285`: Backport PR #20275 on branch v3.4.x (Fix some examples that are skipped in docs build) -* :ghpull:`20275`: Fix some examples that are skipped in docs build -* :ghpull:`20267`: Backport PR #20265 on branch v3.4.x (Legend edgecolor face) -* :ghpull:`20265`: Legend edgecolor face -* :ghpull:`20260`: Fix legend edgecolor face -* :ghpull:`20259`: Backport PR #20248 on branch v3.4.x (Replace pgf image-streaming warning by error.) -* :ghpull:`20248`: Replace pgf image-streaming warning by error. -* :ghpull:`20241`: Backport PR #20212 on branch v3.4.x (Update span_selector.py) -* :ghpull:`20212`: Update span_selector.py -* :ghpull:`19980`: Tidy up deprecation messages in ``_subplots.py`` -* :ghpull:`20234`: Backport PR #20225 on branch v3.4.x (FIX: correctly handle ax.legend(..., legendcolor='none')) -* :ghpull:`20225`: FIX: correctly handle ax.legend(..., legendcolor='none') -* :ghpull:`20232`: Backport PR #19636 on branch v3.4.x (Correctly check inaxes for multicursor) -* :ghpull:`20228`: Backport PR #19849 on branch v3.4.x (FIX DateFormatter for month names when usetex=True) -* :ghpull:`19849`: FIX DateFormatter for month names when usetex=True -* :ghpull:`20154`: ci: Bump Ubuntu to 18.04 LTS. -* :ghpull:`20186`: Backport PR #19975 on branch v3.4.x (CI: remove workflow to push commits to macpython/matplotlib-wheels) -* :ghpull:`19975`: CI: remove workflow to push commits to macpython/matplotlib-wheels -* :ghpull:`19636`: Correctly check inaxes for multicursor - -Issues (22): - -* :ghissue:`20219`: Regression: undocumented change of behaviour in mpl 3.4.2 with axis ticks direction -* :ghissue:`20721`: ax.clear() adds extra ticks, un-hides shared-axis tick labels -* :ghissue:`20765`: savefig re-scales xticks and labels of some (but not all) subplots -* :ghissue:`20782`: [Bug]: _supylabel get_in_layout() typo? -* :ghissue:`20747`: [Bug]: _copy_css_file assumes that the _static directory already exists -* :ghissue:`20617`: tests fail with new inkscape -* :ghissue:`20519`: Toolbar zoom doesn't change autoscale status for versions 3.2.0 and above -* :ghissue:`20628`: Out-of-bounds read leads to crash or broken TrueType fonts -* :ghissue:`20612`: Broken EPS for Type 42 STIX -* :ghissue:`19982`: regression for 3.4.x - ax.figbox replacement incompatible to all version including 3.3.4 -* :ghissue:`19938`: unuseful deprecation warning figbox -* :ghissue:`16400`: Inconsistent behavior between Normalizers when input is Dataframe -* :ghissue:`20583`: Lost class descriptions since 3.4 docs -* :ghissue:`20551`: set_segments(get_segments()) makes lines coarse -* :ghissue:`20560`: test_png is failing -* :ghissue:`20487`: test_huge_range_log is failing... -* :ghissue:`20472`: test_backend_pgf.py::test_xelatex[pdf] - ValueError: invalid literal for int() with base 10: b'ate missing from Resources. [...] -* :ghissue:`20328`: Path.intersects_path sometimes returns incorrect values -* :ghissue:`20258`: Using edgecolors='face' with stackplot causes value error when using plt.legend() -* :ghissue:`20200`: examples/widgets/span_selector.py is brittle -* :ghissue:`20231`: MultiCursor bug -* :ghissue:`19836`: Month names not set as text when using usetex - - -Previous GitHub Stats ---------------------- - +Pull Requests (62): + +* :ghpull:`25377`: Backport PR #25372 on branch v3.7.x (Clean up Curve ArrowStyle docs) +* :ghpull:`25376`: Backport PR #25371 on branch v3.7.x (Tk: Fix size of spacers when changing display DPI) +* :ghpull:`25375`: Backport PR #25364 on branch v3.7.x (BLD: Pre-download Qhull license to put in wheels) +* :ghpull:`25372`: Clean up Curve ArrowStyle docs +* :ghpull:`25371`: Tk: Fix size of spacers when changing display DPI +* :ghpull:`25364`: BLD: Pre-download Qhull license to put in wheels +* :ghpull:`25370`: Backport PR#25369: Pin sphinx themes more strictly +* :ghpull:`25368`: Backport PR #25339 on branch v3.7.x (Disable discarded animation warning on save) +* :ghpull:`25369`: Pin sphinx themes more strictly +* :ghpull:`25339`: Disable discarded animation warning on save +* :ghpull:`25354`: Backport PR #25353 on branch v3.7.x (link to ipympl docs instead of github) +* :ghpull:`25307`: Pin mpl-sphinx-theme on the v3.7.x branch +* :ghpull:`25350`: Backport PR #25346 on branch v3.7.x (FIX: use wrapped text in Text._get_layout) +* :ghpull:`25348`: Backport PR #25325 on branch v3.7.x (Clean up legend loc parameter documentation) +* :ghpull:`25325`: Clean up legend loc parameter documentation +* :ghpull:`25346`: FIX: use wrapped text in Text._get_layout +* :ghpull:`25343`: Backport PR #25340 on branch v3.7.x (Fix RangeSlider.set_val when outside existing value) +* :ghpull:`25342`: Backport PR #25341 on branch v3.7.x (TST: Increase test_set_line_coll_dash_image tolerance slightly.) +* :ghpull:`25340`: Fix RangeSlider.set_val when outside existing value +* :ghpull:`25341`: TST: Increase test_set_line_coll_dash_image tolerance slightly. +* :ghpull:`25337`: Backport PR #25311 on branch v3.7.x (Make draggable legends picklable.) +* :ghpull:`25311`: Make draggable legends picklable. +* :ghpull:`25331`: Backport PR #25327 on branch v3.7.x (Fix doc-string issues identified by velin) +* :ghpull:`25327`: Fix doc-string issues identified by velin +* :ghpull:`25321`: Backport PR #25320 on branch v3.7.x (DOC: fix typo) +* :ghpull:`25319`: Backport PR #25305 on branch v3.7.x (DOC: add layout='none' option to Figure constructor) +* :ghpull:`25305`: DOC: add layout='none' option to Figure constructor +* :ghpull:`25315`: Backport PR #24878 on branch v3.7.x ( [Doc]: Add alt-text to images in 3.6 release notes #24844 ) +* :ghpull:`24878`: [Doc]: Add alt-text to images in 3.6 release notes #24844 +* :ghpull:`25312`: Backport PR #25308 on branch v3.7.x (DOC: add include source to a 3.7 whats new) +* :ghpull:`25309`: Backport PR #25302 on branch v3.7.x (Cleanup gradient_bar example.) +* :ghpull:`25299`: Backport PR #25238 on branch v3.7.x (Check file path for animation and raise if it does not exist) +* :ghpull:`25297`: Backport PR #25295 on branch v3.7.x (Increase timeout for interactive backend tests) +* :ghpull:`25238`: Check file path for animation and raise if it does not exist +* :ghpull:`25295`: Increase timeout for interactive backend tests +* :ghpull:`25288`: Backport PR #25279 on branch v3.7.x (Fix Lasso line cleanup) +* :ghpull:`25294`: Backport PR #25278 on branch v3.7.x (Revert #23417 (Consistently set label on axis with units)) +* :ghpull:`25293`: Backport PR #25155 on branch v3.7.x (Fix lasso unresponsive issue by adding a lock release event) +* :ghpull:`25289`: Backport PR #25286 on branch v3.7.x (DOC: add cache-busting query to switcher json url) +* :ghpull:`25278`: Revert #23417 (Consistently set label on axis with units) +* :ghpull:`25155`: Fix lasso unresponsive issue by adding a lock release event +* :ghpull:`25285`: Backport PR #25280 on branch v3.7.x (Fix setting CSS with latest GTK4) +* :ghpull:`25279`: Fix Lasso line cleanup +* :ghpull:`25284`: Backport PR #25283 on branch v3.7.x (CI: unpin reviewdog eslint) +* :ghpull:`25280`: Fix setting CSS with latest GTK4 +* :ghpull:`25283`: CI: unpin reviewdog eslint +* :ghpull:`25277`: Backport PR #25268 on branch v3.7.x (Fix import of styles with relative path) +* :ghpull:`25276`: Backport PR #25237 on branch v3.7.x (Fixed a bug where rcParams settings were being ignored for formatting axes labels) +* :ghpull:`25237`: Fixed a bug where rcParams settings were being ignored for formatting axes labels +* :ghpull:`25268`: Fix import of styles with relative path +* :ghpull:`25264`: Backport PR #25262 on branch v3.7.x (CI: Pin reviewdog eslint to use node 18.13) +* :ghpull:`25245`: Backport PR #25236: Re-enable CI buildwheel and cygwin labels +* :ghpull:`25262`: CI: Pin reviewdog eslint to use node 18.13 +* :ghpull:`25260`: Backport PR #25234 on branch v3.7.x (added layout="compressed" for pyplot #25223) +* :ghpull:`25234`: added layout="compressed" for pyplot #25223 +* :ghpull:`25246`: Backport PR #25240 on branch v3.7.x (Avoid calling vars() on arbitrary third-party manager_class.) +* :ghpull:`25240`: Avoid calling vars() on arbitrary third-party manager_class. +* :ghpull:`25236`: Re-enable CI buildwheel and cygwin labels +* :ghpull:`25217`: Backport PR #25213 on branch v3.7.x (DOC: correct default value of pcolormesh shading) +* :ghpull:`25213`: DOC: correct default value of pcolormesh shading +* :ghpull:`25215`: Backport PR #25198 - DOC: remove constrained_layout kwarg from examples +* :ghpull:`25198`: DOC: remove constrained_layout kwarg from examples + +Issues (14): + +* :ghissue:`25361`: [Doc]: matplotlib.patches.ArrowStyle +* :ghissue:`25365`: [Bug]: Inconsistent size/padx for spacers in NavigationToolbar2Tk._rescale and _Spacer +* :ghissue:`25212`: [Bug]: LICENSE_QHULL is included in wheel only the second time +* :ghissue:`25323`: [Doc]: Misleading Figure.legend() options, loc='best' is not valid. +* :ghissue:`25336`: [Bug]: constrained layout with wrapped titles +* :ghissue:`25338`: [Bug]: set_val of rangeslider sets incorrect value +* :ghissue:`25300`: [Bug]: Unable to pickle figure with draggable legend +* :ghissue:`25223`: [Doc]: ``layout="none"`` for figure constructor? +* :ghissue:`25219`: [Bug]: axes.set_xlim with string dates raises when plotting with datetimes +* :ghissue:`21666`: [Doc]: Sidebar not always very helpful +* :ghissue:`25298`: [Bug]: ``axes.labelsize`` is ignored +* :ghissue:`25233`: [MNT]: FFMpegWriter does not check if out path exists when initialized. +* :ghissue:`25242`: [Bug]: Relative paths in ``plt.style.use()`` no longer work in 3.7 +* :ghissue:`25251`: [CI]: eslint failure + + +Previous GitHub statistics +-------------------------- .. toctree:: :maxdepth: 1 diff --git a/doc/users/index.rst b/doc/users/index.rst index 852ca4321e90..0ecff14c9a23 100644 --- a/doc/users/index.rst +++ b/doc/users/index.rst @@ -1,24 +1,41 @@ .. _users-guide-index: -############ -User's Guide -############ +.. redirect-from:: /contents -.. only:: html - :Release: |version| - :Date: |today| +########### +Users guide +########### + +General +####### + +.. toctree:: + :maxdepth: 2 + + getting_started/index.rst + installing/index.rst + explain/index.rst + faq/index.rst + resources/index.rst + +Tutorials and examples +###################### .. toctree:: - :maxdepth: 2 - - installing.rst - ../tutorials/index.rst - interactive.rst - whats_new.rst - history.rst - github_stats.rst - whats_new_old.rst - license.rst - ../citing.rst - credits.rst + :maxdepth: 1 + + ../plot_types/index.rst + ../tutorials/index.rst + ../gallery/index.rst + +Reference +######### + +.. toctree:: + :maxdepth: 2 + + ../api/index.rst + ../devel/index.rst + project/index.rst + release_notes.rst diff --git a/doc/users/installing.rst b/doc/users/installing.rst deleted file mode 100644 index 545ae4fa153e..000000000000 --- a/doc/users/installing.rst +++ /dev/null @@ -1 +0,0 @@ -.. include:: ../../INSTALL.rst diff --git a/doc/users/installing/index.rst b/doc/users/installing/index.rst new file mode 100644 index 000000000000..1b6fdf65055f --- /dev/null +++ b/doc/users/installing/index.rst @@ -0,0 +1,328 @@ +.. redirect-from:: /users/installing + +############ +Installation +############ + +============================== +Installing an official release +============================== + +Matplotlib releases are available as wheel packages for macOS, Windows and +Linux on `PyPI `_. Install it using +``pip``: + +.. code-block:: sh + + python -m pip install -U pip + python -m pip install -U matplotlib + +If this command results in Matplotlib being compiled from source and +there's trouble with the compilation, you can add ``--prefer-binary`` to +select the newest version of Matplotlib for which there is a +precompiled wheel for your OS and Python. + +.. note:: + + The following backends work out of the box: Agg, ps, pdf, svg + + Python is typically shipped with tk bindings which are used by + TkAgg. + + For support of other GUI frameworks, LaTeX rendering, saving + animations and a larger selection of file formats, you can + install :ref:`optional_dependencies`. + +========================= +Third-party distributions +========================= + +Various third-parties provide Matplotlib for their environments. + +Conda packages +============== +Matplotlib is available both via the *anaconda main channel* + +.. code-block:: sh + + conda install matplotlib + +as well as via the *conda-forge community channel* + +.. code-block:: sh + + conda install -c conda-forge matplotlib + +Python distributions +==================== + +Matplotlib is part of major Python distributions: + +- `Anaconda `_ + +- `ActiveState ActivePython + `_ + +- `WinPython `_ + +Linux package manager +===================== + +If you are using the Python version that comes with your Linux distribution, +you can install Matplotlib via your package manager, e.g.: + +* Debian / Ubuntu: ``sudo apt-get install python3-matplotlib`` +* Fedora: ``sudo dnf install python3-matplotlib`` +* Red Hat: ``sudo yum install python3-matplotlib`` +* Arch: ``sudo pacman -S python-matplotlib`` + +.. redirect-from:: /users/installing/installing_source + +.. _install_from_source: + +========================== +Installing a nightly build +========================== + +Matplotlib makes nightly development build wheels available on the +`scipy-wheels-nightly Anaconda Cloud organization +`_. +These wheels can be installed with ``pip`` by specifying scipy-wheels-nightly +as the package index to query: + +.. code-block:: sh + + python -m pip install \ + --upgrade \ + --pre \ + --index-url https://pypi.anaconda.org/scipy-wheels-nightly/simple \ + --extra-index-url https://pypi.org/simple \ + matplotlib + +====================== +Installing from source +====================== + +If you are interested in contributing to Matplotlib development, +running the latest source code, or just like to build everything +yourself, it is not difficult to build Matplotlib from source. + +First you need to install the :ref:`dependencies`. + +A C compiler is required. Typically, on Linux, you will need ``gcc``, which +should be installed using your distribution's package manager; on macOS, you +will need xcode_; on Windows, you will need `Visual Studio`_ 2015 or later. + +For those using Visual Studio, make sure "Desktop development with C++" is +selected, and that the latest MSVC, "C++ CMake tools for Windows," and a +Windows SDK compatible with your version of Windows are selected and installed. +They should be selected by default under the "Optional" subheading, but are +required to build matplotlib from source. + +.. _xcode: https://guide.macports.org/chunked/installing.html#installing.xcode + +.. _Visual Studio: https://visualstudio.microsoft.com/downloads/ + +The easiest way to get the latest development version to start contributing +is to go to the git `repository `_ +and run:: + + git clone https://github.com/matplotlib/matplotlib.git + +or:: + + git clone git@github.com:matplotlib/matplotlib.git + +If you're developing, it's better to do it in editable mode. The reason why +is that pytest's test discovery only works for Matplotlib +if installation is done this way. Also, editable mode allows your code changes +to be instantly propagated to your library code without reinstalling (though +you will have to restart your python process / kernel):: + + cd matplotlib + python -m pip install -e . + +If you're not developing, it can be installed from the source directory with +a simple (just replace the last step):: + + python -m pip install . + +To run the tests you will need to install some additional dependencies:: + + python -m pip install -r requirements/dev/dev-requirements.txt + +Then, if you want to update your Matplotlib at any time, just do:: + + git pull + +When you run ``git pull``, if the output shows that only Python files have +been updated, you are all set. If C files have changed, you need to run ``pip +install -e .`` again to compile them. + +There is more information on :ref:`using git ` in the developer +docs. + +.. warning:: + + The following instructions in this section are for very custom + installations of Matplotlib. Proceed with caution because these instructions + may result in your build producing unexpected behavior and/or causing + local testing to fail. + +If you would like to build from a tarball, grab the latest *tar.gz* release +file from `the PyPI files page `_. + +We provide a `mplsetup.cfg`_ file which you can use to customize the build +process. For example, which default backend to use, whether some of the +optional libraries that Matplotlib ships with are installed, and so on. This +file will be particularly useful to those packaging Matplotlib. + +.. _mplsetup.cfg: https://raw.githubusercontent.com/matplotlib/matplotlib/main/mplsetup.cfg.template + +If you are building your own Matplotlib wheels (or sdists) on Windows, note +that any DLLs that you copy into the source tree will be packaged too. + +========================== +Installing for development +========================== +See :ref:`installing_for_devs`. + +.. redirect-from:: /faq/installing_faq +.. redirect-from:: /users/faq/installing_faq + +.. _installing-faq: + +========================== +Frequently asked questions +========================== + +.. contents:: + :backlinks: none + :local: + +Report a compilation problem +============================ + +See :ref:`reporting-problems`. + +Matplotlib compiled fine, but nothing shows up when I use it +============================================================ + +The first thing to try is a :ref:`clean install ` and see if +that helps. If not, the best way to test your install is by running a script, +rather than working interactively from a python shell or an integrated +development environment such as :program:`IDLE` which add additional +complexities. Open up a UNIX shell or a DOS command prompt and run, for +example:: + + python -c "from pylab import *; set_loglevel('debug'); plot(); show()" + +This will give you additional information about which backends Matplotlib is +loading, version information, and more. At this point you might want to make +sure you understand Matplotlib's :doc:`configuration ` +process, governed by the :file:`matplotlibrc` configuration file which contains +instructions within and the concept of the Matplotlib backend. + +If you are still having trouble, see :ref:`reporting-problems`. + +.. _clean-install: + +How to completely remove Matplotlib +=================================== + +Occasionally, problems with Matplotlib can be solved with a clean +installation of the package. In order to fully remove an installed Matplotlib: + +1. Delete the caches from your :ref:`Matplotlib configuration directory + `. + +2. Delete any Matplotlib directories or eggs from your :ref:`installation + directory `. + +OSX Notes +========= + +.. _which-python-for-osx: + +Which python for OSX? +--------------------- + +Apple ships OSX with its own Python, in ``/usr/bin/python``, and its own copy +of Matplotlib. Unfortunately, the way Apple currently installs its own copies +of NumPy, Scipy and Matplotlib means that these packages are difficult to +upgrade (see `system python packages`_). For that reason we strongly suggest +that you install a fresh version of Python and use that as the basis for +installing libraries such as NumPy and Matplotlib. One convenient way to +install Matplotlib with other useful Python software is to use the Anaconda_ +Python scientific software collection, which includes Python itself and a +wide range of libraries; if you need a library that is not available from the +collection, you can install it yourself using standard methods such as *pip*. +See the Anaconda web page for installation support. + +.. _system python packages: + https://github.com/MacPython/wiki/wiki/Which-Python#system-python-and-extra-python-packages +.. _Anaconda: https://www.anaconda.com/ + +Other options for a fresh Python install are the standard installer from +`python.org `_, or installing +Python using a general OSX package management system such as `homebrew +`_ or `macports `_. Power users on +OSX will likely want one of homebrew or macports on their system to install +open source software packages, but it is perfectly possible to use these +systems with another source for your Python binary, such as Anaconda +or Python.org Python. + +.. _install_osx_binaries: + +Installing OSX binary wheels +---------------------------- + +If you are using Python from https://www.python.org, Homebrew, or Macports, +then you can use the standard pip installer to install Matplotlib binaries in +the form of wheels. + +pip is installed by default with python.org and Homebrew Python, but needs to +be manually installed on Macports with :: + + sudo port install py38-pip + +Once pip is installed, you can install Matplotlib and all its dependencies with +from the Terminal.app command line:: + + python3 -m pip install matplotlib + +You might also want to install IPython or the Jupyter notebook (``python3 -m pip +install ipython notebook``). + +Checking your installation +-------------------------- + +The new version of Matplotlib should now be on your Python "path". Check this +at the Terminal.app command line:: + + python3 -c 'import matplotlib; print(matplotlib.__version__, matplotlib.__file__)' + +You should see something like :: + + 3.6.0 /Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9/site-packages/matplotlib/__init__.py + +where ``3.6.0`` is the Matplotlib version you just installed, and the path +following depends on whether you are using Python.org Python, Homebrew or +Macports. If you see another version, or you get an error like :: + + Traceback (most recent call last): + File "", line 1, in + ImportError: No module named matplotlib + +then check that the Python binary is the one you expected by running :: + + which python3 + +If you get a result like ``/usr/bin/python...``, then you are getting the +Python installed with OSX, which is probably not what you want. Try closing +and restarting Terminal.app before running the check again. If that doesn't fix +the problem, depending on which Python you wanted to use, consider reinstalling +Python.org Python, or check your homebrew or macports setup. Remember that +the disk image installer only works for Python.org Python, and will not get +picked up by other Pythons. If all these fail, please :ref:`let us know +`. diff --git a/doc/users/installing_source.rst b/doc/users/installing_source.rst deleted file mode 100644 index 7d7d927cfd9a..000000000000 --- a/doc/users/installing_source.rst +++ /dev/null @@ -1,76 +0,0 @@ -.. _install_from_source: - -====================== -Installing from source -====================== - -If you are interested in contributing to Matplotlib development, -running the latest source code, or just like to build everything -yourself, it is not difficult to build Matplotlib from source. - -First you need to install the :ref:`dependencies`. - -A C compiler is required. Typically, on Linux, you will need ``gcc``, which -should be installed using your distribution's package manager; on macOS, you -will need xcode_; on Windows, you will need Visual Studio 2015 or later. - -.. _xcode: https://guide.macports.org/chunked/installing.html#installing.xcode - -The easiest way to get the latest development version to start contributing -is to go to the git `repository `_ -and run:: - - git clone https://github.com/matplotlib/matplotlib.git - -or:: - - git clone git@github.com:matplotlib/matplotlib.git - -If you're developing, it's better to do it in editable mode. The reason why -is that pytest's test discovery only works for Matplotlib -if installation is done this way. Also, editable mode allows your code changes -to be instantly propagated to your library code without reinstalling (though -you will have to restart your python process / kernel):: - - cd matplotlib - python -m pip install -e . - -If you're not developing, it can be installed from the source directory with -a simple (just replace the last step):: - - python -m pip install . - -To run the tests you will need to install some additional dependencies:: - - python -m pip install -r requirements/dev/dev-requirements.txt - -Then, if you want to update your Matplotlib at any time, just do:: - - git pull - -When you run ``git pull``, if the output shows that only Python files have -been updated, you are all set. If C files have changed, you need to run ``pip -install -e .`` again to compile them. - -There is more information on :ref:`using git ` in the developer -docs. - -.. warning:: - - The following instructions in this section are for very custom - installations of Matplotlib. Proceed with caution because these instructions - may result in your build producing unexpected behavior and/or causing - local testing to fail. - -If you would like to build from a tarball, grab the latest *tar.gz* release -file from `the PyPI files page `_. - -We provide a `setup.cfg`_ file which you can use to customize the build -process. For example, which default backend to use, whether some of the -optional libraries that Matplotlib ships with are installed, and so on. This -file will be particularly useful to those packaging Matplotlib. - -.. _setup.cfg: https://raw.githubusercontent.com/matplotlib/matplotlib/master/setup.cfg.template - -If you are building your own Matplotlib wheels (or sdists) on Windows, note -that any DLLs that you copy into the source tree will be packaged too. diff --git a/doc/users/license.rst b/doc/users/license.rst deleted file mode 100644 index 4dcb0798712f..000000000000 --- a/doc/users/license.rst +++ /dev/null @@ -1,141 +0,0 @@ -.. _license: - -*********************************************** -License -*********************************************** - - -Matplotlib only uses BSD compatible code, and its license is based on -the `PSF `_ license. See the Open -Source Initiative `licenses page -`_ for details on individual -licenses. Non-BSD compatible licenses (e.g., LGPL) are acceptable in -matplotlib toolkits. For a discussion of the motivations behind the -licencing choice, see :ref:`license-discussion`. - -Copyright Policy -================ - -John Hunter began matplotlib around 2003. Since shortly before his -passing in 2012, Michael Droettboom has been the lead maintainer of -matplotlib, but, as has always been the case, matplotlib is the work -of many. - -Prior to July of 2013, and the 1.3.0 release, the copyright of the -source code was held by John Hunter. As of July 2013, and the 1.3.0 -release, matplotlib has moved to a shared copyright model. - -matplotlib uses a shared copyright model. Each contributor maintains -copyright over their contributions to matplotlib. But, it is important to -note that these contributions are typically only changes to the -repositories. Thus, the matplotlib source code, in its entirety, is not -the copyright of any single person or institution. Instead, it is the -collective copyright of the entire matplotlib Development Team. If -individual contributors want to maintain a record of what -changes/contributions they have specific copyright on, they should -indicate their copyright in the commit message of the change, when -they commit the change to one of the matplotlib repositories. - -The Matplotlib Development Team is the set of all contributors to the -matplotlib project. A full list can be obtained from the git version -control logs. - -License agreement for matplotlib |version| -============================================== - -1. This LICENSE AGREEMENT is between the Matplotlib Development Team -("MDT"), and the Individual or Organization ("Licensee") accessing and -otherwise using matplotlib software in source or binary form and its -associated documentation. - -2. Subject to the terms and conditions of this License Agreement, MDT -hereby grants Licensee a nonexclusive, royalty-free, world-wide license -to reproduce, analyze, test, perform and/or display publicly, prepare -derivative works, distribute, and otherwise use matplotlib |version| -alone or in any derivative version, provided, however, that MDT's -License Agreement and MDT's notice of copyright, i.e., "Copyright (c) -2012-2013 Matplotlib Development Team; All Rights Reserved" are retained in -matplotlib |version| alone or in any derivative version prepared by -Licensee. - -3. In the event Licensee prepares a derivative work that is based on or -incorporates matplotlib |version| or any part thereof, and wants to -make the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to matplotlib |version|. - -4. MDT is making matplotlib |version| available to Licensee on an "AS -IS" basis. MDT MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, MDT MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB |version| -WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. - -5. MDT SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB -|version| FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR -LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING -MATPLOTLIB |version|, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF -THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between MDT and -Licensee. This License Agreement does not grant permission to use MDT -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using matplotlib |version|, -Licensee agrees to be bound by the terms and conditions of this License -Agreement. - -License agreement for matplotlib versions prior to 1.3.0 -======================================================== - -1. This LICENSE AGREEMENT is between John D. Hunter ("JDH"), and the -Individual or Organization ("Licensee") accessing and otherwise using -matplotlib software in source or binary form and its associated -documentation. - -2. Subject to the terms and conditions of this License Agreement, JDH -hereby grants Licensee a nonexclusive, royalty-free, world-wide license -to reproduce, analyze, test, perform and/or display publicly, prepare -derivative works, distribute, and otherwise use matplotlib |version| -alone or in any derivative version, provided, however, that JDH's -License Agreement and JDH's notice of copyright, i.e., "Copyright (c) -2002-2009 John D. Hunter; All Rights Reserved" are retained in -matplotlib |version| alone or in any derivative version prepared by -Licensee. - -3. In the event Licensee prepares a derivative work that is based on or -incorporates matplotlib |version| or any part thereof, and wants to -make the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to matplotlib |version|. - -4. JDH is making matplotlib |version| available to Licensee on an "AS -IS" basis. JDH MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, JDH MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF MATPLOTLIB |version| -WILL NOT INFRINGE ANY THIRD PARTY RIGHTS. - -5. JDH SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF MATPLOTLIB -|version| FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR -LOSS AS A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING -MATPLOTLIB |version|, OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF -THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between JDH and -Licensee. This License Agreement does not grant permission to use JDH -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using matplotlib |version|, -Licensee agrees to be bound by the terms and conditions of this License -Agreement. diff --git a/doc/users/next_whats_new.rst b/doc/users/next_whats_new.rst new file mode 100644 index 000000000000..ddd82faf6731 --- /dev/null +++ b/doc/users/next_whats_new.rst @@ -0,0 +1,13 @@ +.. _whats-new: + +================ +Next what's new? +================ + +.. ifconfig:: releaselevel == 'dev' + + .. toctree:: + :glob: + :maxdepth: 1 + + next_whats_new/* diff --git a/doc/users/next_whats_new/README.rst b/doc/users/next_whats_new/README.rst index 6555d229a1b5..e1b27ef97f1e 100644 --- a/doc/users/next_whats_new/README.rst +++ b/doc/users/next_whats_new/README.rst @@ -15,7 +15,7 @@ Please avoid using references in section titles, as it causes links to be confusing in the table of contents. Instead, ensure that a reference is included in the descriptive text. Include contents of the form: :: - Section Title for Feature + Section title for feature ------------------------- A bunch of text about how awesome the new feature is and examples of how diff --git a/doc/users/prev_whats_new/changelog.rst b/doc/users/prev_whats_new/changelog.rst index e35878e9935e..f1f3cca3225e 100644 --- a/doc/users/prev_whats_new/changelog.rst +++ b/doc/users/prev_whats_new/changelog.rst @@ -4,4032 +4,4745 @@ List of changes to Matplotlib prior to 2015 =========================================== This is a list of the changes made to Matplotlib from 2003 to 2015. For more -recent changes, please refer to the `what's new <../whats_new.html>`_ or -the `API changes <../../api/api_changes.html>`_. +recent changes, please refer to the :doc:`/users/release_notes`. -2015-11-16 Levels passed to contour(f) and tricontour(f) must be in increasing - order. +2015-11-16 + Levels passed to contour(f) and tricontour(f) must be in increasing order. -2015-10-21 Added TextBox widget +2015-10-21 + Added TextBox widget + +2015-10-21 + Added get_ticks_direction() +2015-02-27 + Added the rcParam 'image.composite_image' to permit users to decide whether + they want the vector graphics backends to combine all images within a set + of axes into a single composite image. (If images do not get combined, + users can open vector graphics files in Adobe Illustrator or Inkscape and + edit each image individually.) -2015-10-21 Added get_ticks_direction() +2015-02-19 + Rewrite of C++ code that calculates contours to add support for corner + masking. This is controlled by the 'corner_mask' keyword in plotting + commands 'contour' and 'contourf'. - IMT -2015-02-27 Added the rcParam 'image.composite_image' to permit users - to decide whether they want the vector graphics backends to combine - all images within a set of axes into a single composite image. - (If images do not get combined, users can open vector graphics files - in Adobe Illustrator or Inkscape and edit each image individually.) +2015-01-23 + Text bounding boxes are now computed with advance width rather than ink + area. This may result in slightly different placement of text. + +2014-10-27 + Allowed selection of the backend using the :envvar:`MPLBACKEND` environment + variable. Added documentation on backend selection methods. + +2014-09-27 + Overhauled `.colors.LightSource`. Added `.LightSource.hillshade` to allow + the independent generation of illumination maps. Added new types of + blending for creating more visually appealing shaded relief plots (e.g. + ``blend_mode="overlay"``, etc, in addition to the legacy "hsv" mode). + +2014-06-10 + Added Colorbar.remove() + +2014-06-07 + Fixed bug so radial plots can be saved as ps in py3k. -2015-02-19 Rewrite of C++ code that calculates contours to add support for - corner masking. This is controlled by the 'corner_mask' keyword - in plotting commands 'contour' and 'contourf'. - IMT +2014-06-01 + Changed the fmt kwarg of errorbar to support the mpl convention that + "none" means "don't draw it", and to default to the empty string, so that + plotting of data points is done with the plot() function defaults. + Deprecated use of the None object in place "none". + +2014-05-22 + Allow the linscale keyword parameter of symlog scale to be smaller than + one. + +2014-05-20 + Added logic to in FontManager to invalidate font-cache if if font-family + rcparams have changed. + +2014-05-16 + Fixed the positioning of multi-line text in the PGF backend. + +2014-05-14 + Added Axes.add_image() as the standard way to add AxesImage instances to + Axes. This improves the consistency with add_artist(), add_collection(), + add_container(), add_line(), add_patch(), and add_table(). + +2014-05-02 + Added colorblind-friendly colormap, named 'Wistia'. + +2014-04-27 + Improved input clean up in Axes.{h|v}lines + Coerce input into a 1D ndarrays (after dealing with units). + +2014-04-27 + removed un-needed cast to float in stem + +2014-04-23 + Updated references to "ipython -pylab" The preferred method for invoking + pylab is now using the "%pylab" magic. + -Chris G. + +2014-04-22 + Added (re-)generate a simple automatic legend to "Figure Options" dialog of + the Qt4Agg backend. + +2014-04-22 + Added an example showing the difference between interpolation = 'none' and + interpolation = 'nearest' in `~.Axes.imshow` when saving vector graphics + files. + +2014-04-22 + Added violin plotting functions. See `.Axes.violinplot`, `.Axes.violin`, + `.cbook.violin_stats` and `.mlab.GaussianKDE` for details. + +2014-04-10 + Fixed the triangular marker rendering error. The "Up" triangle was rendered + instead of "Right" triangle and vice-versa. + +2014-04-08 + Fixed a bug in parasite_axes.py by making a list out of a generator at line + 263. + +2014-04-02 + Added ``clipon=False`` to patch creation of wedges and shadows in + `~.Axes.pie`. + +2014-02-25 + In backend_qt4agg changed from using update -> repaint under windows. See + comment in source near ``self._priv_update`` for longer explanation. + +2014-03-27 + Added tests for pie ccw parameter. Removed pdf and svg images from tests + for pie linewidth parameter. + +2014-03-24 + Changed the behaviour of axes to not ignore leading or trailing patches of + height 0 (or width 0) while calculating the x and y axis limits. Patches + having both height == 0 and width == 0 are ignored. + +2014-03-24 + Added bool kwarg (manage_xticks) to boxplot to enable/disable the + management of the xlimits and ticks when making a boxplot. Default in True + which maintains current behavior by default. + +2014-03-23 + Fixed a bug in projections/polar.py by making sure that the theta value + being calculated when given the mouse coordinates stays within the range of + 0 and 2 * pi. + +2014-03-22 + Added the keyword arguments wedgeprops and textprops to pie. Users can + control the wedge and text properties of the pie in more detail, if they + choose. + +2014-03-17 + Bug was fixed in append_axes from the AxesDivider class would not append + axes in the right location with respect to the reference locator axes + +2014-03-13 + Add parameter 'clockwise' to function pie, True by default. + +2014-02-28 + Added 'origin' kwarg to `~.Axes.spy` + +2014-02-27 + Implemented separate horizontal/vertical axes padding to the ImageGrid in + the AxesGrid toolkit + +2014-02-27 + Allowed markevery property of matplotlib.lines.Line2D to be, an int numpy + fancy index, slice object, or float. The float behaviour turns on markers + at approximately equal display-coordinate-distances along the line. + +2014-02-25 + In backend_qt4agg changed from using update -> repaint under windows. See + comment in source near ``self._priv_update`` for longer explanation. + +2014-01-02 + `~.Axes.triplot` now returns the artist it adds and support of line and + marker kwargs has been improved. GBY + +2013-12-30 + Made streamplot grid size consistent for different types of density + argument. A 30x30 grid is now used for both density=1 and density=(1, 1). + +2013-12-03 + Added a pure boxplot-drawing method that allow a more complete + customization of boxplots. It takes a list of dicts contains stats. Also + created a function (`.cbook.boxplot_stats`) that generates the stats + needed. + +2013-11-28 + Added qhull extension module to perform Delaunay triangulation more + robustly than before. It is used by tri.Triangulation (and hence all + pyplot.tri* methods) and mlab.griddata. Deprecated matplotlib.delaunay + module. - IMT + +2013-11-05 + Add power-law normalization method. This is useful for, e.g., showing small + populations in a "hist2d" histogram. + +2013-10-27 + Added get_rlabel_position and set_rlabel_position methods to PolarAxes to + control angular position of radial tick labels. + +2013-10-06 + Add stride-based functions to mlab for easy creation of 2D arrays with less + memory. + +2013-10-06 + Improve window and detrend functions in mlab, particular support for 2D + arrays. + +2013-10-06 + Improve performance of all spectrum-related mlab functions and plots. + +2013-10-06 + Added support for magnitude, phase, and angle spectrums to axes.specgram, + and support for magnitude, phase, angle, and complex spectrums to + mlab-specgram. + +2013-10-06 + Added magnitude_spectrum, angle_spectrum, and phase_spectrum plots, as well + as magnitude_spectrum, angle_spectrum, phase_spectrum, and complex_spectrum + functions to mlab + +2013-07-12 + Added support for datetime axes to 2d plots. Axis values are passed through + Axes.convert_xunits/Axes.convert_yunits before being used by + contour/contourf, pcolormesh and pcolor. + +2013-07-12 + Allowed matplotlib.dates.date2num, matplotlib.dates.num2date, and + matplotlib.dates.datestr2num to accept n-d inputs. Also factored in support + for n-d arrays to matplotlib.dates.DateConverter and + matplotlib.units.Registry. + +2013-06-26 + Refactored the axes module: the axes module is now a folder, containing the + following submodule: + + - _subplots.py, containing all the subplots helper methods + - _base.py, containing several private methods and a new _AxesBase class. + This _AxesBase class contains all the methods that are not directly + linked to plots of the "old" Axes + - _axes.py contains the Axes class. This class now inherits from _AxesBase: + it contains all "plotting" methods and labelling methods. + + This refactoring should not affect the API. Only private methods are not + importable from the axes module anymore. + +2013-05-18 + Added support for arbitrary rasterization resolutions to the SVG backend. + Previously the resolution was hard coded to 72 dpi. Now the backend class + takes a image_dpi argument for its constructor, adjusts the image bounding + box accordingly and forwards a magnification factor to the image renderer. + The code and results now resemble those of the PDF backend. + - MW + +2013-05-08 + Changed behavior of hist when given stacked=True and normed=True. + Histograms are now stacked first, then the sum is normalized. Previously, + each histogram was normalized, then they were stacked. + +2013-04-25 + Changed all instances of:: + + from matplotlib import MatplotlibDeprecationWarning as mplDeprecation + + to:: + + from cbook import mplDeprecation + + and removed the import into the matplotlib namespace in __init__.py + - Thomas Caswell + +2013-04-15 + Added 'axes.xmargin' and 'axes.ymargin' to rpParams to set default margins + on auto-scaling. - TAC + +2013-04-16 + Added patheffect support for Line2D objects. -JJL + +2013-03-31 + Added support for arbitrary unstructured user-specified triangulations to + Axes3D.tricontour[f] - Damon McDougall + +2013-03-19 + Added support for passing *linestyle* kwarg to `~.Axes.step` so all + `~.Axes.plot` kwargs are passed to the underlying `~.Axes.plot` call. -TAC + +2013-02-25 + Added classes CubicTriInterpolator, UniformTriRefiner, TriAnalyzer to + matplotlib.tri module. - GBy + +2013-01-23 + Add 'savefig.directory' to rcParams to remember and fill in the last + directory saved to for figure save dialogs - Martin Spacek + +2013-01-13 + Add eventplot method to axes and pyplot and EventCollection class to + collections. + +2013-01-08 + Added two extra titles to axes which are flush with the left and right + edges of the plot respectively. Andrew Dawson + +2013-01-07 + Add framealpha keyword argument to legend - PO + +2013-01-16 + Till Stensitzki added a baseline feature to stackplot + +2012-12-22 + Added classes for interpolation within triangular grids + (LinearTriInterpolator) and to find the triangles in which points lie + (TrapezoidMapTriFinder) to matplotlib.tri module. - IMT + +2012-12-05 + Added MatplotlibDeprecationWarning class for signaling deprecation. + Matplotlib developers can use this class as follows:: + + from matplotlib import MatplotlibDeprecationWarning as mplDeprecation + + In light of the fact that Python builtin DeprecationWarnings are ignored by + default as of Python 2.7, this class was put in to allow for the signaling + of deprecation, but via UserWarnings which are not ignored by default. - PI + +2012-11-27 + Added the *mtext* parameter for supplying matplotlib.text.Text instances to + RendererBase.draw_tex and RendererBase.draw_text. This allows backends to + utilize additional text attributes, like the alignment of text elements. - + pwuertz + +2012-11-26 + deprecate matplotlib/mpl.py, which was used only in pylab.py and is now + replaced by the more suitable ``import matplotlib as mpl``. - PI + +2012-11-25 + Make rc_context available via pyplot interface - PI + +2012-11-16 + plt.set_cmap no longer throws errors if there is not already an active + colorable artist, such as an image, and just sets up the colormap to use + from that point forward. - PI + +2012-11-16 + Added the function _get_rbga_face, which is identical to _get_rbg_face + except it return a (r,g,b,a) tuble, to line2D. Modified Line2D.draw to use + _get_rbga_face to get the markerface color so that any alpha set by + markerfacecolor will respected. - Thomas Caswell + +2012-11-13 + Add a symmetric log normalization class to colors.py. Also added some + tests for the normalization class. Till Stensitzki -2015-01-23 Text bounding boxes are now computed with advance width rather than - ink area. This may result in slightly different placement of text. +2012-11-12 + Make axes.stem take at least one argument. Uses a default range(n) when + the first arg not provided. Damon McDougall -2014-10-27 Allowed selection of the backend using the :envvar:`MPLBACKEND` environment - variable. Added documentation on backend selection methods. +2012-11-09 + Make plt.subplot() without arguments act as subplot(111) - PI -2014-09-27 Overhauled `.colors.LightSource`. Added `.LightSource.hillshade` to - allow the independent generation of illumination maps. Added new - types of blending for creating more visually appealing shaded relief - plots (e.g. ``blend_mode="overlay"``, etc, in addition to the legacy - "hsv" mode). +2012-11-08 + Replaced plt.figure and plt.subplot calls by the newer, more convenient + single call to plt.subplots() in the documentation examples - PI -2014-06-10 Added Colorbar.remove() +2012-10-05 + Add support for saving animations as animated GIFs. - JVDP + +2012-08-11 + Fix path-closing bug in patches.Polygon, so that regardless of whether the + path is the initial one or was subsequently set by set_xy(), get_xy() will + return a closed path if and only if get_closed() is True. Thanks to Jacob + Vanderplas. - EF + +2012-08-05 + When a norm is passed to contourf, either or both of the vmin, vmax + attributes of that norm are now respected. Formerly they were respected + only if both were specified. In addition, vmin and/or vmax can now be + passed to contourf directly as kwargs. - EF + +2012-07-24 + Contourf handles the extend kwarg by mapping the extended ranges outside + the normed 0-1 range so that they are handled by colormap colors determined + by the set_under and set_over methods. Previously the extended ranges were + mapped to 0 or 1 so that the "under" and "over" colormap colors were + ignored. This change also increases slightly the color contrast for a given + set of contour levels. - EF + +2012-06-24 + Make use of mathtext in tick labels configurable - DSD + +2012-06-05 + Images loaded through PIL are now ordered correctly - CG + +2012-06-02 + Add new Axes method and pyplot function, hist2d. - PO + +2012-05-31 + Remove support for 'cairo.' style of backend specification. + Deprecate 'cairo.format' and 'savefig.extension' rcParams and replace with + 'savefig.format'. - Martin Spacek + +2012-05-29 + pcolormesh now obeys the passed in "edgecolor" kwarg. To support this, the + "shading" argument to pcolormesh now only takes "flat" or "gouraud". To + achieve the old "faceted" behavior, pass "edgecolors='k'". - MGD + +2012-05-22 + Added radius kwarg to pie charts. - HH + +2012-05-22 + Collections now have a setting "offset_position" to select whether the + offsets are given in "screen" coordinates (default, following the old + behavior) or "data" coordinates. This is currently used internally to + improve the performance of hexbin. + + As a result, the "draw_path_collection" backend methods have grown a new + argument "offset_position". - MGD + +2012-05-04 + Add a new argument to pie charts - startingangle - that allows one to + specify the angle offset for the first wedge of the chart. - EP + +2012-05-03 + symlog scale now obeys the logarithmic base. Previously, it was completely + ignored and always treated as base e. - MGD + +2012-05-03 + Allow linscalex/y keyword to symlog scale that allows the size of the + linear portion relative to the logarithmic portion to be adjusted. - MGD + +2012-04-14 + Added new plot style: stackplot. This new feature supports stacked area + plots. - Damon McDougall + +2012-04-06 + When path clipping changes a LINETO to a MOVETO, it also changes any + CLOSEPOLY command to a LINETO to the initial point. This fixes a problem + with pdf and svg where the CLOSEPOLY would then draw a line to the latest + MOVETO position instead of the intended initial position. - JKS + +2012-03-27 + Add support to ImageGrid for placing colorbars only at one edge of each + column/row. - RMM + +2012-03-07 + Refactor movie writing into useful classes that make use of pipes to write + image data to ffmpeg or mencoder. Also improve settings for these and the + ability to pass custom options. - RMM + +2012-02-29 + errorevery keyword added to errorbar to enable errorbar subsampling. fixes + issue #600. + +2012-02-28 + Added plot_trisurf to the mplot3d toolkit. This supports plotting three + dimensional surfaces on an irregular grid. - Damon McDougall + +2012-01-23 + The radius labels in polar plots no longer use a fixed padding, but use a + different alignment depending on the quadrant they are in. This fixes + numerical problems when (rmax - rmin) gets too small. - MGD + +2012-01-08 + Add axes.streamplot to plot streamlines of a velocity field. Adapted from + Tom Flannaghan streamplot implementation. -TSY + +2011-12-29 + ps and pdf markers are now stroked only if the line width is nonzero for + consistency with agg, fixes issue #621. - JKS + +2011-12-27 + Work around an EINTR bug in some versions of subprocess. - JKS + +2011-10-25 + added support for \operatorname to mathtext, including the ability to + insert spaces, such as $\operatorname{arg\,max}$ - PI + +2011-08-18 + Change api of Axes.get_tightbbox and add an optional keyword parameter + *call_axes_locator*. - JJL + +2011-07-29 + A new rcParam "axes.formatter.use_locale" was added, that, when True, will + use the current locale to format tick labels. This means that, for + example, in the fr_FR locale, ',' will be used as a decimal separator. - + MGD -2014-06-07 Fixed bug so radial plots can be saved as ps in py3k. +2011-07-15 + The set of markers available in the plot() and scatter() commands has been + unified. In general, this gives more options to both than were previously + available, however, there is one backward-incompatible change to the + markers in scatter: + + "d" used to mean "diamond", it now means "narrow diamond". "D" can be + used for a "diamond". + + -MGD + +2011-07-13 + Fix numerical problems in symlog scale, particularly when linthresh <= 1.0. + Symlog plots may look different if one was depending on the old broken + behavior - MGD + +2011-07-10 + Fixed argument handling error in tripcolor/triplot/tricontour, issue #203. + - IMT + +2011-07-08 + Many functions added to mplot3d.axes3d to bring Axes3D objects more + feature-parity with regular Axes objects. Significant revisions to the + documentation as well. - BVR + +2011-07-07 + Added compatibility with IPython strategy for picking a version of Qt4 + support, and an rcParam for making the choice explicitly: backend.qt4. - EF + +2011-07-07 + Modified AutoMinorLocator to improve automatic choice of the number of + minor intervals per major interval, and to allow one to specify this number + via a kwarg. - EF -2014-06-01 Changed the fmt kwarg of errorbar to support the - the mpl convention that "none" means "don't draw it", - and to default to the empty string, so that plotting - of data points is done with the plot() function - defaults. Deprecated use of the None object in place - "none". +2011-06-28 + 3D versions of scatter, plot, plot_wireframe, plot_surface, bar3d, and some + other functions now support empty inputs. - BVR -2014-05-22 Allow the linscale keyword parameter of symlog scale to be - smaller than one. +2011-06-22 + Add set_theta_offset, set_theta_direction and set_theta_zero_location to + polar axes to control the location of 0 and directionality of theta. - MGD -2014-05-20 Added logic to in FontManager to invalidate font-cache if - if font-family rcparams have changed. +2011-06-22 + Add axes.labelweight parameter to set font weight to axis labels - MGD. -2014-05-16 Fixed the positioning of multi-line text in the PGF backend. +2011-06-20 + Add pause function to pyplot. - EF -2014-05-14 Added Axes.add_image() as the standard way to add AxesImage - instances to Axes. This improves the consistency with - add_artist(), add_collection(), add_container(), add_line(), - add_patch(), and add_table(). +2011-06-16 + Added *bottom* keyword parameter for the stem command. Also, implemented a + legend handler for the stem plot. - JJL -2014-05-02 Added colorblind-friendly colormap, named 'Wistia'. +2011-06-16 + Added legend.frameon rcParams. - Mike Kaufman -2014-04-27 Improved input clean up in Axes.{h|v}lines - Coerce input into a 1D ndarrays (after dealing with units). +2011-05-31 + Made backend_qt4 compatible with PySide . - Gerald Storer -2014-04-27 removed un-needed cast to float in stem +2011-04-17 + Disable keyboard auto-repeat in qt4 backend by ignoring key events + resulting from auto-repeat. This makes constrained zoom/pan work. - EF -2014-04-23 Updated references to "ipython -pylab" - The preferred method for invoking pylab is now using the - "%pylab" magic. - -Chris G. +2011-04-14 + interpolation="nearest" always interpolate images. A new mode "none" is + introduced for no interpolation - JJL -2014-04-22 Added (re-)generate a simple automatic legend to "Figure Options" - dialog of the Qt4Agg backend. +2011-04-03 + Fixed broken pick interface to AsteriskCollection objects used by scatter. + - EF -2014-04-22 Added an example showing the difference between - interpolation = 'none' and interpolation = 'nearest' in - `~.Axes.imshow` when saving vector graphics files. +2011-04-01 + The plot directive Sphinx extension now supports all of the features in the + Numpy fork of that extension. These include doctest formatting, an + 'include-source' option, and a number of new configuration options. - MGD -2014-04-22 Added violin plotting functions. See `.Axes.violinplot`, - `.Axes.violin`, `.cbook.violin_stats` and `.mlab.GaussianKDE` for - details. +2011-03-29 + Wrapped ViewVCCachedServer definition in a factory function. This class + now inherits from urllib2.HTTPSHandler in order to fetch data from github, + but HTTPSHandler is not defined if python was built without SSL support. - + DSD -2014-04-10 Fixed the triangular marker rendering error. The "Up" triangle was - rendered instead of "Right" triangle and vice-versa. +2011-03-10 + Update pytz version to 2011c, thanks to Simon Cross. - JKS -2014-04-08 Fixed a bug in parasite_axes.py by making a list out - of a generator at line 263. +2011-03-06 + Add standalone tests.py test runner script. - JKS + +2011-03-06 + Set edgecolor to 'face' for scatter asterisk-type symbols; this fixes a bug + in which these symbols were not responding to the c kwarg. The symbols + have no face area, so only the edgecolor is visible. - EF -2014-04-02 Added ``clipon=False`` to patch creation of wedges and shadows - in `~.Axes.pie`. +2011-02-27 + Support libpng version 1.5.x; suggestion by Michael Albert. Changed + installation specification to a minimum of libpng version 1.2. - EF -2014-02-25 In backend_qt4agg changed from using update -> repaint under - windows. See comment in source near ``self._priv_update`` for - longer explanation. +2011-02-20 + clabel accepts a callable as an fmt kwarg; modified patch by Daniel Hyams. + - EF -2014-03-27 Added tests for pie ccw parameter. Removed pdf and svg images - from tests for pie linewidth parameter. +2011-02-18 + scatter([], []) is now valid. Also fixed issues with empty collections - + BVR -2014-03-24 Changed the behaviour of axes to not ignore leading or trailing - patches of height 0 (or width 0) while calculating the x and y - axis limits. Patches having both height == 0 and width == 0 are - ignored. +2011-02-07 + Quick workaround for dviread bug #3175113 - JKS -2014-03-24 Added bool kwarg (manage_xticks) to boxplot to enable/disable - the managemnet of the xlimits and ticks when making a boxplot. - Default in True which maintains current behavior by default. +2011-02-05 + Add cbook memory monitoring for Windows, using tasklist. - EF -2014-03-23 Fixed a bug in projections/polar.py by making sure that the theta - value being calculated when given the mouse coordinates stays within - the range of 0 and 2 * pi. +2011-02-05 + Speed up Normalize and LogNorm by using in-place operations and by using + float32 for float32 inputs and for ints of 2 bytes or shorter; based on + patch by Christoph Gohlke. - EF -2014-03-22 Added the keyword arguments wedgeprops and textprops to pie. - Users can control the wedge and text properties of the pie - in more detail, if they choose. +2011-02-04 + Changed imshow to use rgba as uint8 from start to finish, instead of going + through an intermediate step as double precision; thanks to Christoph + Gohlke. - EF -2014-03-17 Bug was fixed in append_axes from the AxesDivider class would not - append axes in the right location with respect to the reference - locator axes +2011-01-13 + Added zdir and offset arguments to contourf3d to bring contourf3d in + feature parity with contour3d. - BVR -2014-03-13 Add parameter 'clockwise' to function pie, True by default. +2011-01-04 + Tag 1.0.1 for release at r8896 -2014-02-28 Added 'origin' kwarg to `~.Axes.spy` +2011-01-03 + Added display of ticker offset to 3d plots. - BVR + +2011-01-03 + Turn off tick labeling on interior subplots for pyplots.subplots when + sharex/sharey is True. - JDH -2014-02-27 Implemented separate horizontal/vertical axes padding to the - ImageGrid in the AxesGrid toolkit +2010-12-29 + Implement axes_divider.HBox and VBox. -JJL -2014-02-27 Allowed markevery property of matplotlib.lines.Line2D to be, an int - numpy fancy index, slice object, or float. The float behaviour - turns on markers at approximately equal display-coordinate-distances - along the line. +2010-11-22 + Fixed error with Hammer projection. - BVR + +2010-11-12 + Fixed the placement and angle of axis labels in 3D plots. - BVR + +2010-11-07 + New rc parameters examples.download and examples.directory allow bypassing + the download mechanism in get_sample_data. - JKS + +2010-10-04 + Fix JPEG saving bug: only accept the kwargs documented by PIL for JPEG + files. - JKS + +2010-09-15 + Remove unused _wxagg extension and numerix.h. - EF + +2010-08-25 + Add new framework for doing animations with examples.- RM + +2010-08-21 + Remove unused and inappropriate methods from Tick classes: + set_view_interval, get_minpos, and get_data_interval are properly found in + the Axis class and don't need to be duplicated in XTick and YTick. - EF + +2010-08-21 + Change Axis.set_view_interval() so that when updating an existing interval, + it respects the orientation of that interval, and can enlarge but not + reduce the interval. This fixes a bug in which Axis.set_ticks would change + the view limits of an inverted axis. Whether set_ticks should be affecting + the viewLim at all remains an open question. - EF + +2010-08-16 + Handle NaN's correctly in path analysis routines. Fixes a bug where the + best location for a legend was not calculated correctly when the line + contains NaNs. - MGD -2014-02-25 In backend_qt4agg changed from using update -> repaint under - windows. See comment in source near ``self._priv_update`` for - longer explanation. +2010-08-14 + Fix bug in patch alpha handling, and in bar color kwarg - EF + +2010-08-12 + Removed all traces of numerix module after 17 months of deprecation + warnings. - EF -2014-01-02 `~.Axes.triplot` now returns the artist it adds and support of line and - marker kwargs has been improved. GBY +2010-08-05 + Added keyword arguments 'thetaunits' and 'runits' for polar plots. Fixed + PolarAxes so that when it set default Formatters, it marked them as such. + Fixed semilogx and semilogy to no longer blindly reset the ticker + information on the non-log axis. Axes.arrow can now accept unitized data. + - JRE -2013-12-30 Made streamplot grid size consistent for different types of density - argument. A 30x30 grid is now used for both density=1 and - density=(1, 1). +2010-08-03 + Add support for MPLSETUPCFG variable for custom setup.cfg filename. Used + by sage buildbot to build an mpl w/ no gui support - JDH -2013-12-03 Added a pure boxplot-drawing method that allow a more complete - customization of boxplots. It takes a list of dicts contains stats. - Also created a function (`.cbook.boxplot_stats`) that generates the - stats needed. +2010-08-01 + Create directory specified by MPLCONFIGDIR if it does not exist. - ADS -2013-11-28 Added qhull extension module to perform Delaunay triangulation more - robustly than before. It is used by tri.Triangulation (and hence - all pyplot.tri* methods) and mlab.griddata. Deprecated - matplotlib.delaunay module. - IMT +2010-07-20 + Return Qt4's default cursor when leaving the canvas - DSD -2013-11-05 Add power-law normalization method. This is useful for, - e.g., showing small populations in a "hist2d" histogram. +2010-07-06 + Tagging for mpl 1.0 at r8502 -2013-10-27 Added get_rlabel_position and set_rlabel_position methods to - PolarAxes to control angular position of radial tick labels. +2010-07-05 + Added Ben Root's patch to put 3D plots in arbitrary axes, allowing you to + mix 3d and 2d in different axes/subplots or to have multiple 3D plots in + one figure. See examples/mplot3d/subplot3d_demo.py - JDH -2013-10-06 Add stride-based functions to mlab for easy creation of 2D arrays - with less memory. +2010-07-05 + Preferred kwarg names in set_xlim are now 'left' and 'right'; in set_ylim, + 'bottom' and 'top'; original kwargs are still accepted without complaint. - + EF -2013-10-06 Improve window and detrend functions in mlab, particulart support for - 2D arrays. +2010-07-05 + TkAgg and FltkAgg backends are now consistent with other interactive + backends: when used in scripts from the command line (not from ipython + -pylab), show blocks, and can be called more than once. - EF -2013-10-06 Improve performance of all spectrum-related mlab functions and plots. +2010-07-02 + Modified CXX/WrapPython.h to fix "swab bug" on solaris so mpl can compile + on Solaris with CXX6 in the trunk. Closes tracker bug 3022815 - JDH -2013-10-06 Added support for magnitude, phase, and angle spectrums to - axes.specgram, and support for magnitude, phase, angle, and complex - spectrums to mlab-specgram. +2010-06-30 + Added autoscale convenience method and corresponding pyplot function for + simplified control of autoscaling; and changed axis, set_xlim, and set_ylim + so that by default, they turn off the autoscaling on the relevant axis or + axes. Therefore one can call set_xlim before plotting a line, for example, + and the limits will be retained. - EF -2013-10-06 Added magnitude_spectrum, angle_spectrum, and phase_spectrum plots, - as well as magnitude_spectrum, angle_spectrum, phase_spectrum, - and complex_spectrum functions to mlab +2010-06-20 + Added Axes.tick_params and corresponding pyplot function to control tick + and tick label appearance after an Axes has been created. - EF -2013-07-12 Added support for datetime axes to 2d plots. Axis values are passed - through Axes.convert_xunits/Axes.convert_yunits before being used by - contour/contourf, pcolormesh and pcolor. +2010-06-09 + Allow Axes.grid to control minor gridlines; allow Axes.grid and Axis.grid + to control major and minor gridlines in the same method call. - EF -2013-07-12 Allowed matplotlib.dates.date2num, matplotlib.dates.num2date, - and matplotlib.dates.datestr2num to accept n-d inputs. Also - factored in support for n-d arrays to matplotlib.dates.DateConverter - and matplotlib.units.Registry. +2010-06-06 + Change the way we do split/dividend adjustments in finance.py to handle + dividends and fix the zero division bug reported in sf bug 2949906 and + 2123566. Note that volume is not adjusted because the Yahoo CSV does not + distinguish between share split and dividend adjustments making it near + impossible to get volume adjustment right (unless we want to guess based on + the size of the adjustment or scrape the html tables, which we don't) - JDH -2013-06-26 Refactored the axes module: the axes module is now a folder, - containing the following submodule: - - _subplots.py, containing all the subplots helper methods - - _base.py, containing several private methods and a new - _AxesBase class. This _AxesBase class contains all the methods - that are not directly linked to plots of the "old" Axes - - _axes.py contains the Axes class. This class now inherits from - _AxesBase: it contains all "plotting" methods and labelling - methods. +2010-06-06 + Updated dateutil to 1.5 and pytz to 2010h. - This refactoring should not affect the API. Only private methods - are not importable from the axes module anymore. +2010-06-02 + Add error_kw kwarg to Axes.bar(). - EF -2013-05-18 Added support for arbitrary rasterization resolutions to the - SVG backend. Previously the resolution was hard coded to 72 - dpi. Now the backend class takes a image_dpi argument for - its constructor, adjusts the image bounding box accordingly - and forwards a magnification factor to the image renderer. - The code and results now resemble those of the PDF backend. - - MW +2010-06-01 + Fix pcolormesh() and QuadMesh to pass on kwargs as appropriate. - RM -2013-05-08 Changed behavior of hist when given stacked=True and normed=True. - Histograms are now stacked first, then the sum is normalized. - Previously, each histogram was normalized, then they were stacked. +2010-05-18 + Merge mpl_toolkits.gridspec into the main tree. - JJL + +2010-05-04 + Improve backend_qt4 so it displays figures with the correct size - DSD + +2010-04-20 + Added generic support for connecting to a timer for events. This adds + TimerBase, TimerGTK, TimerQT, TimerWx, and TimerTk to the backends and a + new_timer() method to each backend's canvas to allow ease of creating a new + timer. - RM + +2010-04-20 + Added margins() Axes method and pyplot function. - EF -2013-04-25 Changed all instances of: +2010-04-18 + update the axes_grid documentation. -JJL - from matplotlib import MatplotlibDeprecationWarning as mplDeprecation - to: +2010-04-18 + Control MaxNLocator parameters after instantiation, and via + Axes.locator_params method, with corresponding pyplot function. -EF - from cbook import mplDeprecation +2010-04-18 + Control ScalarFormatter offsets directly and via the + Axes.ticklabel_format() method, and add that to pyplot. -EF + +2010-04-16 + Add a close_event to the backends. -RM - and removed the import into the matplotlib namespace in __init__.py - Thomas Caswell +2010-04-06 + modify axes_grid examples to use axes_grid1 and axisartist. -JJL -2013-04-15 Added 'axes.xmargin' and 'axes.ymargin' to rpParams to set default - margins on auto-scaleing. - TAC +2010-04-06 + rebase axes_grid using axes_grid1 and axisartist modules. -JJL -2013-04-16 Added patheffect support for Line2D objects. -JJL +2010-04-06 + axes_grid toolkit is split into two separate modules, axes_grid1 and + axisartist. -JJL -2013-03-31 Added support for arbitrary unstructured user-specified - triangulations to Axes3D.tricontour[f] - Damon McDougall +2010-04-05 + Speed up import: import pytz only if and when it is needed. It is not + needed if the rc timezone is UTC. - EF -2013-03-19 Added support for passing *linestyle* kwarg to `~.Axes.step` so all `~.Axes.plot` - kwargs are passed to the underlying `~.Axes.plot` call. -TAC +2010-04-03 + Added color kwarg to Axes.hist(), based on work by Jeff Klukas. - EF -2013-02-25 Added classes CubicTriInterpolator, UniformTriRefiner, TriAnalyzer - to matplotlib.tri module. - GBy +2010-03-24 + refactor colorbar code so that no cla() is necessary when mappable is + changed. -JJL + +2010-03-22 + fix incorrect rubber band during the zoom mode when mouse leaves the axes. + -JJL -2013-01-23 Add 'savefig.directory' to rcParams to remember and fill in the last - directory saved to for figure save dialogs - Martin Spacek +2010-03-21 + x/y key during the zoom mode only changes the x/y limits. -JJL -2013-01-13 Add eventplot method to axes and pyplot and EventCollection class - to collections. +2010-03-20 + Added pyplot.sca() function suggested by JJL. - EF -2013-01-08 Added two extra titles to axes which are flush with the left and - right edges of the plot respectively. - Andrew Dawson +2010-03-20 + Added conditional support for new Tooltip API in gtk backend. - EF -2013-01-07 Add framealpha keyword argument to legend - PO +2010-03-20 + Changed plt.fig_subplot() to plt.subplots() after discussion on list, and + changed its API to return axes as a numpy object array (with control of + dimensions via squeeze keyword). FP. -2013-01-16 Till Stensitzki added a baseline feature to stackplot +2010-03-13 + Manually brought in commits from branch:: -2012-12-22 Added classes for interpolation within triangular grids - (LinearTriInterpolator) and to find the triangles in which points - lie (TrapezoidMapTriFinder) to matplotlib.tri module. - IMT + ------------------------------------------------------------------------ + r8191 | leejjoon | 2010-03-13 + 17:27:57 -0500 (Sat, 13 Mar 2010) | 1 line -2012-12-05 Added MatplotlibDeprecationWarning class for signaling deprecation. - Matplotlib developers can use this class as follows: + fix the bug that handles for scatter are incorrectly set when dpi!=72. + Thanks to Ray Speth for the bug report. - from matplotlib import MatplotlibDeprecationWarning as mplDeprecation +2010-03-03 + Manually brought in commits from branch via diff/patch (svnmerge is broken):: - In light of the fact that Python builtin DeprecationWarnings are - ignored by default as of Python 2.7, this class was put in to allow - for the signaling of deprecation, but via UserWarnings which are - not ignored by default. - PI + ------------------------------------------------------------------------ + r8175 | leejjoon | 2010-03-03 + 10:03:30 -0800 (Wed, 03 Mar 2010) | 1 line -2012-11-27 Added the *mtext* parameter for supplying matplotlib.text.Text - instances to RendererBase.draw_tex and RendererBase.draw_text. - This allows backends to utilize additional text attributes, like - the alignment of text elements. - pwuertz + fix arguments of allow_rasterization.draw_wrapper + ------------------------------------------------------------------------ + r8174 | jdh2358 | 2010-03-03 + 09:15:58 -0800 (Wed, 03 Mar 2010) | 1 line -2012-11-26 deprecate matplotlib/mpl.py, which was used only in pylab.py and is - now replaced by the more suitable ``import matplotlib as mpl``. - PI + added support for favicon in docs build + ------------------------------------------------------------------------ + r8173 | jdh2358 | 2010-03-03 + 08:56:16 -0800 (Wed, 03 Mar 2010) | 1 line -2012-11-25 Make rc_context available via pyplot interface - PI + applied Mattias get_bounds patch + ------------------------------------------------------------------------ + r8172 | jdh2358 | 2010-03-03 + 08:31:42 -0800 (Wed, 03 Mar 2010) | 1 line -2012-11-16 plt.set_cmap no longer throws errors if there is not already - an active colorable artist, such as an image, and just sets - up the colormap to use from that point forward. - PI + fix svnmerge download instructions + ------------------------------------------------------------------------ + r8171 | jdh2358 | 2010-03-03 + 07:47:48 -0800 (Wed, 03 Mar 2010) | 1 line -2012-11-16 Added the funcction _get_rbga_face, which is identical to - _get_rbg_face except it return a (r,g,b,a) tuble, to line2D. - Modified Line2D.draw to use _get_rbga_face to get the markerface - color so that any alpha set by markerfacecolor will respected. - - Thomas Caswell +2010-02-25 + add annotation_demo3.py that demonstrates new functionality. -JJL -2012-11-13 Add a symmetric log normalization class to colors.py. - Also added some tests for the normalization class. - Till Stensitzki +2010-02-25 + refactor Annotation to support arbitrary Transform as xycoords or + textcoords. Also, if a tuple of two coordinates is provided, they are + interpreted as coordinates for each x and y position. -JJL -2012-11-12 Make axes.stem take at least one argument. - Uses a default range(n) when the first arg not provided. - Damon McDougall +2010-02-24 + Added pyplot.fig_subplot(), to create a figure and a group of subplots in a + single call. This offers an easier pattern than manually making figures + and calling add_subplot() multiple times. FP -2012-11-09 Make plt.subplot() without arguments act as subplot(111) - PI +2010-02-17 + Added Gokhan's and Mattias' customizable keybindings patch for the toolbar. + You can now set the keymap.* properties in the matplotlibrc file. + Newbindings were added for toggling log scaling on the x-axis. JDH -2012-11-08 Replaced plt.figure and plt.subplot calls by the newer, more - convenient single call to plt.subplots() in the documentation - examples - PI +2010-02-16 + Committed TJ's filled marker patch for left|right|bottom|top|full filled + markers. See examples/pylab_examples/filledmarker_demo.py. JDH -2012-10-05 Add support for saving animations as animated GIFs. - JVDP +2010-02-11 + Added 'bootstrap' option to boxplot. This allows bootstrap estimates of + median confidence intervals. Based on an initial patch by Paul Hobson. - + ADS -2012-08-11 Fix path-closing bug in patches.Polygon, so that regardless - of whether the path is the initial one or was subsequently - set by set_xy(), get_xy() will return a closed path if and - only if get_closed() is True. Thanks to Jacob Vanderplas. - EF +2010-02-06 + Added setup.cfg "basedirlist" option to override setting in setupext.py + "basedir" dictionary; added "gnu0" platform requested by Benjamin Drung. - + EF -2012-08-05 When a norm is passed to contourf, either or both of the - vmin, vmax attributes of that norm are now respected. - Formerly they were respected only if both were - specified. In addition, vmin and/or vmax can now - be passed to contourf directly as kwargs. - EF +2010-02-06 + Added 'xy' scaling option to EllipseCollection. - EF -2012-07-24 Contourf handles the extend kwarg by mapping the extended - ranges outside the normed 0-1 range so that they are - handled by colormap colors determined by the set_under - and set_over methods. Previously the extended ranges - were mapped to 0 or 1 so that the "under" and "over" - colormap colors were ignored. This change also increases - slightly the color contrast for a given set of contour - levels. - EF +2010-02-03 + Made plot_directive use a custom PlotWarning category, so that warnings can + be turned into fatal errors easily if desired. - FP -2012-06-24 Make use of mathtext in tick labels configurable - DSD +2010-01-29 + Added draggable method to Legend to allow mouse drag placement. Thanks + Adam Fraser. JDH -2012-06-05 Images loaded through PIL are now ordered correctly - CG +2010-01-25 + Fixed a bug reported by Olle Engdegard, when using histograms with + stepfilled and log=True - MM -2012-06-02 Add new Axes method and pyplot function, hist2d. - PO +2010-01-16 + Upgraded CXX to 6.1.1 - JDH -2012-05-31 Remove support for 'cairo.' style of backend specification. - Deprecate 'cairo.format' and 'savefig.extension' rcParams and - replace with 'savefig.format'. - Martin Spacek +2009-01-16 + Don't create minor ticks on top of existing major ticks. Patch by Neil + Crighton. -ADS -2012-05-29 pcolormesh now obeys the passed in "edgecolor" kwarg. - To support this, the "shading" argument to pcolormesh now only - takes "flat" or "gouraud". To achieve the old "faceted" behavior, - pass "edgecolors='k'". - MGD +2009-01-16 + Ensure three minor ticks always drawn (SF# 2924245). Patch by Neil + Crighton. -ADS -2012-05-22 Added radius kwarg to pie charts. - HH +2010-01-16 + Applied patch by Ian Thomas to fix two contouring problems: now contourf + handles interior masked regions, and the boundaries of line and filled + contours coincide. - EF -2012-05-22 Collections now have a setting "offset_position" to select whether - the offsets are given in "screen" coordinates (default, - following the old behavior) or "data" coordinates. This is currently - used internally to improve the performance of hexbin. +2009-01-11 + The color of legend patch follows the rc parameters axes.facecolor and + axes.edgecolor. -JJL - As a result, the "draw_path_collection" backend methods have grown - a new argument "offset_position". - MGD +2009-01-11 + adjustable of Axes can be "box-forced" which allow sharing axes. -JJL -2012-05-04 Add a new argument to pie charts - startingangle - that - allows one to specify the angle offset for the first wedge - of the chart. - EP +2009-01-11 + Add add_click and pop_click methods in BlockingContourLabeler. -JJL -2012-05-03 symlog scale now obeys the logarithmic base. Previously, it was - completely ignored and always treated as base e. - MGD +2010-01-03 + Added rcParams['axes.color_cycle'] - EF -2012-05-03 Allow linscalex/y keyword to symlog scale that allows the size of - the linear portion relative to the logarithmic portion to be - adjusted. - MGD +2010-01-03 + Added Pierre's qt4 formlayout editor and toolbar button - JDH -2012-04-14 Added new plot style: stackplot. This new feature supports stacked - area plots. - Damon McDougall +2009-12-31 + Add support for using math text as marker symbols (Thanks to tcb) - MGD + +2009-12-31 + Commit a workaround for a regression in PyQt4-4.6.{0,1} - DSD -2012-04-06 When path clipping changes a LINETO to a MOVETO, it also - changes any CLOSEPOLY command to a LINETO to the initial - point. This fixes a problem with pdf and svg where the - CLOSEPOLY would then draw a line to the latest MOVETO - position instead of the intended initial position. - JKS +2009-12-22 + Fix cmap data for gist_earth_r, etc. -JJL -2012-03-27 Add support to ImageGrid for placing colorbars only at - one edge of each column/row. - RMM +2009-12-20 + spines: put spines in data coordinates, add set_bounds() call. -ADS -2012-03-07 Refactor movie writing into useful classes that make use - of pipes to write image data to ffmpeg or mencoder. Also - improve settings for these and the ability to pass custom - options. - RMM +2009-12-18 + Don't limit notch size in boxplot to q1-q3 range, as this is effectively + making the data look better than it is. - ADS -2012-02-29 errorevery keyword added to errorbar to enable errorbar - subsampling. fixes issue #600. +2009-12-18 + mlab.prctile handles even-length data, such that the median is the mean of + the two middle values. - ADS -2012-02-28 Added plot_trisurf to the mplot3d toolkit. This supports plotting - three dimensional surfaces on an irregular grid. - Damon McDougall +2009-12-15 + Add raw-image (unsampled) support for the ps backend. - JJL -2012-01-23 The radius labels in polar plots no longer use a fixed - padding, but use a different alignment depending on the - quadrant they are in. This fixes numerical problems when - (rmax - rmin) gets too small. - MGD +2009-12-14 + Add patch_artist kwarg to boxplot, but keep old default. Convert + boxplot_demo2.py to use the new patch_artist. - ADS -2012-01-08 Add axes.streamplot to plot streamlines of a velocity field. - Adapted from Tom Flannaghan streamplot implementation. -TSY +2009-12-06 + axes_grid: reimplemented AxisArtist with FloatingAxes support. Added new + examples. - JJL -2011-12-29 ps and pdf markers are now stroked only if the line width - is nonzero for consistency with agg, fixes issue #621. - JKS +2009-12-01 + Applied Laurent Dufrechou's patch to improve blitting with the qt4 backend + - DSD -2011-12-27 Work around an EINTR bug in some versions of subprocess. - JKS +2009-11-13 + The pdf backend now allows changing the contents of a pdf file's + information dictionary via PdfPages.infodict. - JKS -2011-10-25 added support for \operatorname to mathtext, - including the ability to insert spaces, such as - $\operatorname{arg\,max}$ - PI +2009-11-12 + font_manager.py should no longer cause EINTR on Python 2.6 (but will on the + 2.5 version of subprocess). Also the fc-list command in that file was fixed + so now it should actually find the list of fontconfig fonts. - JKS -2011-08-18 Change api of Axes.get_tightbbox and add an optional - keyword parameter *call_axes_locator*. - JJL +2009-11-10 + Single images, and all images in renderers with option_image_nocomposite + (i.e. agg, macosx and the svg backend when rcParams['svg.image_noscale'] is + True), are now drawn respecting the zorder relative to other artists. (Note + that there may now be inconsistencies across backends when more than one + image is drawn at varying zorders, but this change introduces correct + behavior for the backends in which it's easy to do so.) -2011-07-29 A new rcParam "axes.formatter.use_locale" was added, that, - when True, will use the current locale to format tick - labels. This means that, for example, in the fr_FR locale, - ',' will be used as a decimal separator. - MGD +2009-10-21 + Make AutoDateLocator more configurable by adding options to control the + maximum and minimum number of ticks. Also add control of the intervals to + be used for ticking. This does not change behavior but opens previously + hard-coded behavior to runtime modification`. - RMM -2011-07-15 The set of markers available in the plot() and scatter() - commands has been unified. In general, this gives more - options to both than were previously available, however, - there is one backward-incompatible change to the markers in - scatter: +2009-10-19 + Add "path_effects" support for Text and Patch. See + examples/pylab_examples/patheffect_demo.py -JJL - "d" used to mean "diamond", it now means "narrow - diamond". "D" can be used for a "diamond". +2009-10-19 + Add "use_clabeltext" option to clabel. If True, clabels will be created + with ClabelText class, which recalculates rotation angle of the label + during the drawing time. -JJL - -MGD +2009-10-16 + Make AutoDateFormatter actually use any specified timezone setting.This was + only working correctly when no timezone was specified. - RMM -2011-07-13 Fix numerical problems in symlog scale, particularly when - linthresh <= 1.0. Symlog plots may look different if one - was depending on the old broken behavior - MGD +2009-09-27 + Beginnings of a capability to test the pdf backend. - JKS -2011-07-10 Fixed argument handling error in tripcolor/triplot/tricontour, - issue #203. - IMT - -2011-07-08 Many functions added to mplot3d.axes3d to bring Axes3D - objects more feature-parity with regular Axes objects. - Significant revisions to the documentation as well. - - BVR - -2011-07-07 Added compatibility with IPython strategy for picking - a version of Qt4 support, and an rcParam for making - the choice explicitly: backend.qt4. - EF - -2011-07-07 Modified AutoMinorLocator to improve automatic choice of - the number of minor intervals per major interval, and - to allow one to specify this number via a kwarg. - EF - -2011-06-28 3D versions of scatter, plot, plot_wireframe, plot_surface, - bar3d, and some other functions now support empty inputs. - BVR - -2011-06-22 Add set_theta_offset, set_theta_direction and - set_theta_zero_location to polar axes to control the - location of 0 and directionality of theta. - MGD - -2011-06-22 Add axes.labelweight parameter to set font weight to axis - labels - MGD. - -2011-06-20 Add pause function to pyplot. - EF - -2011-06-16 Added *bottom* keyword parameter for the stem command. - Also, implemented a legend handler for the stem plot. - - JJL - -2011-06-16 Added legend.frameon rcParams. - Mike Kaufman - -2011-05-31 Made backend_qt4 compatible with PySide . - Gerald Storer - -2011-04-17 Disable keyboard auto-repeat in qt4 backend by ignoring - key events resulting from auto-repeat. This makes - constrained zoom/pan work. - EF - -2011-04-14 interpolation="nearest" always interpolate images. A new - mode "none" is introduced for no interpolation - JJL - -2011-04-03 Fixed broken pick interface to AsteriskCollection objects - used by scatter. - EF - -2011-04-01 The plot directive Sphinx extension now supports all of the - features in the Numpy fork of that extension. These - include doctest formatting, an 'include-source' option, and - a number of new configuration options. - MGD - -2011-03-29 Wrapped ViewVCCachedServer definition in a factory function. - This class now inherits from urllib2.HTTPSHandler in order - to fetch data from github, but HTTPSHandler is not defined - if python was built without SSL support. - DSD - -2011-03-10 Update pytz version to 2011c, thanks to Simon Cross. - JKS - -2011-03-06 Add standalone tests.py test runner script. - JKS - -2011-03-06 Set edgecolor to 'face' for scatter asterisk-type - symbols; this fixes a bug in which these symbols were - not responding to the c kwarg. The symbols have no - face area, so only the edgecolor is visible. - EF - -2011-02-27 Support libpng version 1.5.x; suggestion by Michael - Albert. Changed installation specification to a - minimum of libpng version 1.2. - EF - -2011-02-20 clabel accepts a callable as an fmt kwarg; modified - patch by Daniel Hyams. - EF - -2011-02-18 scatter([], []) is now valid. Also fixed issues - with empty collections - BVR - -2011-02-07 Quick workaround for dviread bug #3175113 - JKS - -2011-02-05 Add cbook memory monitoring for Windows, using - tasklist. - EF - -2011-02-05 Speed up Normalize and LogNorm by using in-place - operations and by using float32 for float32 inputs - and for ints of 2 bytes or shorter; based on - patch by Christoph Gohlke. - EF - -2011-02-04 Changed imshow to use rgba as uint8 from start to - finish, instead of going through an intermediate - step as double precision; thanks to Christoph Gohlke. - EF - -2011-01-13 Added zdir and offset arguments to contourf3d to - bring contourf3d in feature parity with contour3d. - BVR - -2011-01-04 Tag 1.0.1 for release at r8896 - -2011-01-03 Added display of ticker offset to 3d plots. - BVR - -2011-01-03 Turn off tick labeling on interior subplots for - pyplots.subplots when sharex/sharey is True. - JDH - -2010-12-29 Implement axes_divider.HBox and VBox. -JJL - - -2010-11-22 Fixed error with Hammer projection. - BVR - -2010-11-12 Fixed the placement and angle of axis labels in 3D plots. - BVR - -2010-11-07 New rc parameters examples.download and examples.directory - allow bypassing the download mechanism in get_sample_data. - - JKS - -2010-10-04 Fix JPEG saving bug: only accept the kwargs documented - by PIL for JPEG files. - JKS - -2010-09-15 Remove unused _wxagg extension and numerix.h. - EF - -2010-08-25 Add new framework for doing animations with examples.- RM - -2010-08-21 Remove unused and inappropriate methods from Tick classes: - set_view_interval, get_minpos, and get_data_interval are - properly found in the Axis class and don't need to be - duplicated in XTick and YTick. - EF - -2010-08-21 Change Axis.set_view_interval() so that when updating an - existing interval, it respects the orientation of that - interval, and can enlarge but not reduce the interval. - This fixes a bug in which Axis.set_ticks would - change the view limits of an inverted axis. Whether - set_ticks should be affecting the viewLim at all remains - an open question. - EF - -2010-08-16 Handle NaN's correctly in path analysis routines. Fixes a - bug where the best location for a legend was not calculated - correctly when the line contains NaNs. - MGD - -2010-08-14 Fix bug in patch alpha handling, and in bar color kwarg - EF - -2010-08-12 Removed all traces of numerix module after 17 months of - deprecation warnings. - EF - -2010-08-05 Added keyword arguments 'thetaunits' and 'runits' for polar - plots. Fixed PolarAxes so that when it set default - Formatters, it marked them as such. Fixed semilogx and - semilogy to no longer blindly reset the ticker information - on the non-log axis. Axes.arrow can now accept unitized - data. - JRE - -2010-08-03 Add support for MPLSETUPCFG variable for custom setup.cfg - filename. Used by sage buildbot to build an mpl w/ no gui - support - JDH - -2010-08-01 Create directory specified by MPLCONFIGDIR if it does - not exist. - ADS - -2010-07-20 Return Qt4's default cursor when leaving the canvas - DSD - -2010-07-06 Tagging for mpl 1.0 at r8502 - - -2010-07-05 Added Ben Root's patch to put 3D plots in arbitrary axes, - allowing you to mix 3d and 2d in different axes/subplots or - to have multiple 3D plots in one figure. See - examples/mplot3d/subplot3d_demo.py - JDH - -2010-07-05 Preferred kwarg names in set_xlim are now 'left' and - 'right'; in set_ylim, 'bottom' and 'top'; original - kwargs are still accepted without complaint. - EF - -2010-07-05 TkAgg and FltkAgg backends are now consistent with other - interactive backends: when used in scripts from the - command line (not from ipython -pylab), show blocks, - and can be called more than once. - EF - -2010-07-02 Modified CXX/WrapPython.h to fix "swab bug" on solaris so - mpl can compile on Solaris with CXX6 in the trunk. Closes - tracker bug 3022815 - JDH - -2010-06-30 Added autoscale convenience method and corresponding - pyplot function for simplified control of autoscaling; - and changed axis, set_xlim, and set_ylim so that by - default, they turn off the autoscaling on the relevant - axis or axes. Therefore one can call set_xlim before - plotting a line, for example, and the limits will be - retained. - EF - -2010-06-20 Added Axes.tick_params and corresponding pyplot function - to control tick and tick label appearance after an Axes - has been created. - EF - -2010-06-09 Allow Axes.grid to control minor gridlines; allow - Axes.grid and Axis.grid to control major and minor - gridlines in the same method call. - EF - -2010-06-06 Change the way we do split/dividend adjustments in - finance.py to handle dividends and fix the zero division bug reported - in sf bug 2949906 and 2123566. Note that volume is not adjusted - because the Yahoo CSV does not distinguish between share - split and dividend adjustments making it near impossible to - get volume adjustment right (unless we want to guess based - on the size of the adjustment or scrape the html tables, - which we don't) - JDH - -2010-06-06 Updated dateutil to 1.5 and pytz to 2010h. - -2010-06-02 Add error_kw kwarg to Axes.bar(). - EF - -2010-06-01 Fix pcolormesh() and QuadMesh to pass on kwargs as - appropriate. - RM - -2010-05-18 Merge mpl_toolkits.gridspec into the main tree. - JJL - -2010-05-04 Improve backend_qt4 so it displays figures with the - correct size - DSD - -2010-04-20 Added generic support for connecting to a timer for events. This - adds TimerBase, TimerGTK, TimerQT, TimerWx, and TimerTk to - the backends and a new_timer() method to each backend's - canvas to allow ease of creating a new timer. - RM - -2010-04-20 Added margins() Axes method and pyplot function. - EF - -2010-04-18 update the axes_grid documentation. -JJL - -2010-04-18 Control MaxNLocator parameters after instantiation, - and via Axes.locator_params method, with corresponding - pyplot function. -EF - -2010-04-18 Control ScalarFormatter offsets directly and via the - Axes.ticklabel_format() method, and add that to pyplot. -EF - -2010-04-16 Add a close_event to the backends. -RM - -2010-04-06 modify axes_grid examples to use axes_grid1 and axisartist. -JJL - -2010-04-06 rebase axes_grid using axes_grid1 and axisartist modules. -JJL - -2010-04-06 axes_grid toolkit is split into two separate modules, - axes_grid1 and axisartist. -JJL - -2010-04-05 Speed up import: import pytz only if and when it is - needed. It is not needed if the rc timezone is UTC. - EF - -2010-04-03 Added color kwarg to Axes.hist(), based on work by - Jeff Klukas. - EF - -2010-03-24 refactor colorbar code so that no cla() is necessary when - mappable is changed. -JJL - -2010-03-22 fix incorrect rubber band during the zoom mode when mouse - leaves the axes. -JJL - -2010-03-21 x/y key during the zoom mode only changes the x/y limits. -JJL - -2010-03-20 Added pyplot.sca() function suggested by JJL. - EF - -2010-03-20 Added conditional support for new Tooltip API in gtk backend. - EF - -2010-03-20 Changed plt.fig_subplot() to plt.subplots() after discussion on - list, and changed its API to return axes as a numpy object array - (with control of dimensions via squeeze keyword). FP. - -2010-03-13 Manually brought in commits from branch:: - - ------------------------------------------------------------------------ - r8191 | leejjoon | 2010-03-13 17:27:57 -0500 (Sat, 13 Mar 2010) | 1 line - - fix the bug that handles for scatter are incorrectly set when dpi!=72. - Thanks to Ray Speth for the bug report. - - -2010-03-03 Manually brought in commits from branch via diff/patch (svnmerge is broken):: - - ------------------------------------------------------------------------ - r8175 | leejjoon | 2010-03-03 10:03:30 -0800 (Wed, 03 Mar 2010) | 1 line - - fix arguments of allow_rasterization.draw_wrapper - ------------------------------------------------------------------------ - r8174 | jdh2358 | 2010-03-03 09:15:58 -0800 (Wed, 03 Mar 2010) | 1 line - - added support for favicon in docs build - ------------------------------------------------------------------------ - r8173 | jdh2358 | 2010-03-03 08:56:16 -0800 (Wed, 03 Mar 2010) | 1 line - - applied Mattias get_bounds patch - ------------------------------------------------------------------------ - r8172 | jdh2358 | 2010-03-03 08:31:42 -0800 (Wed, 03 Mar 2010) | 1 line - - fix svnmerge download instructions - ------------------------------------------------------------------------ - r8171 | jdh2358 | 2010-03-03 07:47:48 -0800 (Wed, 03 Mar 2010) | 1 line - - - -2010-02-25 add annotation_demo3.py that demonstrates new functionality. -JJL - -2010-02-25 refactor Annotation to support arbitrary Transform as xycoords - or textcoords. Also, if a tuple of two coordinates is provided, - they are interpreted as coordinates for each x and y position. - -JJL - -2010-02-24 Added pyplot.fig_subplot(), to create a figure and a group of - subplots in a single call. This offers an easier pattern than - manually making figures and calling add_subplot() multiple times. FP - -2010-02-17 Added Gokhan's and Mattias' customizable keybindings patch - for the toolbar. You can now set the keymap.* properties - in the matplotlibrc file. Newbindings were added for - toggling log scaling on the x-axis. JDH - -2010-02-16 Committed TJ's filled marker patch for - left|right|bottom|top|full filled markers. See - examples/pylab_examples/filledmarker_demo.py. JDH - -2010-02-11 Added 'bootstrap' option to boxplot. This allows bootstrap - estimates of median confidence intervals. Based on an - initial patch by Paul Hobson. - ADS - -2010-02-06 Added setup.cfg "basedirlist" option to override setting - in setupext.py "basedir" dictionary; added "gnu0" - platform requested by Benjamin Drung. - EF - -2010-02-06 Added 'xy' scaling option to EllipseCollection. - EF - -2010-02-03 Made plot_directive use a custom PlotWarning category, so that - warnings can be turned into fatal errors easily if desired. - FP - -2010-01-29 Added draggable method to Legend to allow mouse drag - placement. Thanks Adam Fraser. JDH - -2010-01-25 Fixed a bug reported by Olle Engdegard, when using - histograms with stepfilled and log=True - MM - -2010-01-16 Upgraded CXX to 6.1.1 - JDH - -2009-01-16 Don't create minor ticks on top of existing major - ticks. Patch by Neil Crighton. -ADS - -2009-01-16 Ensure three minor ticks always drawn (SF# 2924245). Patch - by Neil Crighton. -ADS - -2010-01-16 Applied patch by Ian Thomas to fix two contouring - problems: now contourf handles interior masked regions, - and the boundaries of line and filled contours coincide. - EF - -2009-01-11 The color of legend patch follows the rc parameters - axes.facecolor and axes.edgecolor. -JJL - -2009-01-11 adjustable of Axes can be "box-forced" which allow - sharing axes. -JJL - -2009-01-11 Add add_click and pop_click methods in - BlockingContourLabeler. -JJL - - -2010-01-03 Added rcParams['axes.color_cycle'] - EF - -2010-01-03 Added Pierre's qt4 formlayout editor and toolbar button - JDH - -2009-12-31 Add support for using math text as marker symbols (Thanks to tcb) - - MGD - -2009-12-31 Commit a workaround for a regression in PyQt4-4.6.{0,1} - DSD - -2009-12-22 Fix cmap data for gist_earth_r, etc. -JJL - -2009-12-20 spines: put spines in data coordinates, add set_bounds() - call. -ADS - -2009-12-18 Don't limit notch size in boxplot to q1-q3 range, as this - is effectively making the data look better than it is. - ADS - -2009-12-18 mlab.prctile handles even-length data, such that the median - is the mean of the two middle values. - ADS - -2009-12-15 Add raw-image (unsampled) support for the ps backend. - JJL - -2009-12-14 Add patch_artist kwarg to boxplot, but keep old default. - Convert boxplot_demo2.py to use the new patch_artist. - ADS - -2009-12-06 axes_grid: reimplemented AxisArtist with FloatingAxes support. - Added new examples. - JJL - -2009-12-01 Applied Laurent Dufrechou's patch to improve blitting with - the qt4 backend - DSD - -2009-11-13 The pdf backend now allows changing the contents of - a pdf file's information dictionary via PdfPages.infodict. - JKS - -2009-11-12 font_manager.py should no longer cause EINTR on Python 2.6 - (but will on the 2.5 version of subprocess). Also the - fc-list command in that file was fixed so now it should - actually find the list of fontconfig fonts. - JKS - -2009-11-10 Single images, and all images in renderers with - option_image_nocomposite (i.e. agg, macosx and the svg - backend when rcParams['svg.image_noscale'] is True), are - now drawn respecting the zorder relative to other - artists. (Note that there may now be inconsistencies across - backends when more than one image is drawn at varying - zorders, but this change introduces correct behavior for - the backends in which it's easy to do so.) - -2009-10-21 Make AutoDateLocator more configurable by adding options - to control the maximum and minimum number of ticks. Also - add control of the intervals to be used for ticking. This - does not change behavior but opens previously hard-coded - behavior to runtime modification`. - RMM - -2009-10-19 Add "path_effects" support for Text and Patch. See - examples/pylab_examples/patheffect_demo.py -JJL - -2009-10-19 Add "use_clabeltext" option to clabel. If True, clabels - will be created with ClabelText class, which recalculates - rotation angle of the label during the drawing time. -JJL - -2009-10-16 Make AutoDateFormatter actually use any specified - timezone setting.This was only working correctly - when no timezone was specified. - RMM - -2009-09-27 Beginnings of a capability to test the pdf backend. - JKS - -2009-09-27 Add a savefig.extension rcparam to control the default - filename extension used by savefig. - JKS +2009-09-27 + Add a savefig.extension rcparam to control the default filename extension + used by savefig. - JKS =============================================== -2009-09-21 Tagged for release 0.99.1 - -2009-09-20 Fix usetex spacing errors in pdf backend. - JKS +2009-09-21 + Tagged for release 0.99.1 -2009-09-20 Add Sphinx extension to highlight IPython console sessions, - originally authored (I think) by Michael Droetboom. - FP +2009-09-20 + Fix usetex spacing errors in pdf backend. - JKS -2009-09-20 Fix off-by-one error in dviread.Tfm, and additionally protect - against exceptions in case a dvi font is missing some metrics. - JKS +2009-09-20 + Add Sphinx extension to highlight IPython console sessions, originally + authored (I think) by Michael Droetboom. - FP -2009-09-15 Implement draw_text and draw_tex method of backend_base using - the textpath module. Implement draw_tex method of the svg - backend. - JJL +2009-09-20 + Fix off-by-one error in dviread.Tfm, and additionally protect against + exceptions in case a dvi font is missing some metrics. - JKS -2009-09-15 Don't fail on AFM files containing floating-point bounding boxes - JKS +2009-09-15 + Implement draw_text and draw_tex method of backend_base using the textpath + module. Implement draw_tex method of the svg backend. - JJL -2009-09-13 AxesGrid : add modified version of colorbar. Add colorbar - location howto. - JJL +2009-09-15 + Don't fail on AFM files containing floating-point bounding boxes - JKS -2009-09-07 AxesGrid : implemented axisline style. - Added a demo examples/axes_grid/demo_axisline_style.py- JJL +2009-09-13 + AxesGrid : add modified version of colorbar. Add colorbar location howto. - + JJL -2009-09-04 Make the textpath class as a separate module - (textpath.py). Add support for mathtext and tex.- JJL +2009-09-07 + AxesGrid : implemented axisline style. Added a demo + examples/axes_grid/demo_axisline_style.py- JJL -2009-09-01 Added support for Gouraud interpolated triangles. - pcolormesh now accepts shading='gouraud' as an option. - MGD +2009-09-04 + Make the textpath class as a separate module (textpath.py). Add support for + mathtext and tex.- JJL -2009-08-29 Added matplotlib.testing package, which contains a Nose - plugin and a decorator that lets tests be marked as - KnownFailures - ADS +2009-09-01 + Added support for Gouraud interpolated triangles. pcolormesh now accepts + shading='gouraud' as an option. - MGD -2009-08-20 Added scaled dict to AutoDateFormatter for customized - scales - JDH +2009-08-29 + Added matplotlib.testing package, which contains a Nose plugin and a + decorator that lets tests be marked as KnownFailures - ADS -2009-08-15 Pyplot interface: the current image is now tracked at the - figure and axes level, addressing tracker item 1656374. - EF +2009-08-20 + Added scaled dict to AutoDateFormatter for customized scales - JDH -2009-08-15 Docstrings are now manipulated with decorators defined - in a new module, docstring.py, thanks to Jason Coombs. - EF +2009-08-15 + Pyplot interface: the current image is now tracked at the figure and axes + level, addressing tracker item 1656374. - EF -2009-08-14 Add support for image filtering for agg back end. See the example - demo_agg_filter.py. -JJL +2009-08-15 + Docstrings are now manipulated with decorators defined in a new module, + docstring.py, thanks to Jason Coombs. - EF -2009-08-09 AnnotationBbox added. Similar to Annotation, but works with - OffsetBox instead of Text. See the example - demo_annotation_box.py. -JJL +2009-08-14 + Add support for image filtering for agg back end. See the example + demo_agg_filter.py. -JJL -2009-08-07 BboxImage implemented. Two examples, demo_bboximage.py and - demo_ribbon_box.py added. - JJL +2009-08-09 + AnnotationBbox added. Similar to Annotation, but works with OffsetBox + instead of Text. See the example demo_annotation_box.py. -JJL -2009-08-07 In an effort to simplify the backend API, all clipping rectangles - and paths are now passed in using GraphicsContext objects, even - on collections and images. Therefore: +2009-08-07 + BboxImage implemented. Two examples, demo_bboximage.py and + demo_ribbon_box.py added. - JJL - draw_path_collection(self, master_transform, cliprect, clippath, - clippath_trans, paths, all_transforms, offsets, - offsetTrans, facecolors, edgecolors, linewidths, - linestyles, antialiaseds, urls) +2009-08-07 + In an effort to simplify the backend API, all clipping rectangles and paths + are now passed in using GraphicsContext objects, even on collections and + images. Therefore:: - becomes: + draw_path_collection(self, master_transform, cliprect, clippath, + clippath_trans, paths, all_transforms, offsets, + offsetTrans, facecolors, edgecolors, linewidths, + linestyles, antialiaseds, urls) - draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offsetTrans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls) + becomes:: + draw_path_collection(self, gc, master_transform, paths, all_transforms, + offsets, offsetTrans, facecolors, edgecolors, + linewidths, linestyles, antialiaseds, urls) + :: - draw_quad_mesh(self, master_transform, cliprect, clippath, - clippath_trans, meshWidth, meshHeight, coordinates, - offsets, offsetTrans, facecolors, antialiased, - showedges) + draw_quad_mesh(self, master_transform, cliprect, clippath, + clippath_trans, meshWidth, meshHeight, coordinates, + offsets, offsetTrans, facecolors, antialiased, + showedges) - becomes: + becomes:: - draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, - coordinates, offsets, offsetTrans, facecolors, - antialiased, showedges) + draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, + coordinates, offsets, offsetTrans, facecolors, + antialiased, showedges) + :: + draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None) - draw_image(self, x, y, im, bbox, clippath=None, clippath_trans=None) + becomes:: - becomes: + draw_image(self, gc, x, y, im) - draw_image(self, gc, x, y, im) + - MGD - - MGD +2009-08-06 + Tagging the 0.99.0 release at svn r7397 - JDH -2009-08-06 Tagging the 0.99.0 release at svn r7397 - JDH + * fixed an alpha colormapping bug posted on sf 2832575 - * fixed an alpha colormapping bug posted on sf 2832575 + * fix typo in axes_divider.py. use nanmin, nanmax in angle_helper.py (patch + by Christoph Gohlke) - * fix typo in axes_divider.py. use nanmin, nanmax in angle_helper.py - (patch by Christoph Gohlke) + * remove dup gui event in enter/leave events in gtk - * remove dup gui event in enter/leave events in gtk + * lots of fixes for os x binaries (Thanks Russell Owen) - * lots of fixes for os x binaries (Thanks Russell Owen) + * attach gtk events to mpl events -- fixes sf bug 2816580 - * attach gtk events to mpl events -- fixes sf bug 2816580 + * applied sf patch 2815064 (middle button events for wx) and patch 2818092 + (resize events for wx) - * applied sf patch 2815064 (middle button events for wx) and - patch 2818092 (resize events for wx) + * fixed boilerplate.py so it doesn't break the ReST docs. - * fixed boilerplate.py so it doesn't break the ReST docs. + * removed a couple of cases of mlab.load - * removed a couple of cases of mlab.load + * fixed rec2csv win32 file handle bug from sf patch 2831018 - * fixed rec2csv win32 file handle bug from sf patch 2831018 + * added two examples from Josh Hemann: + examples/pylab_examples/barchart_demo2.py and + examples/pylab_examples/boxplot_demo2.py - * added two examples from Josh Hemann: examples/pylab_examples/barchart_demo2.py - and examples/pylab_examples/boxplot_demo2.py + * handled sf bugs 2831556 and 2830525; better bar error messages and + backend driver configs - * handled sf bugs 2831556 and 2830525; better bar error messages and - backend driver configs + * added miktex win32 patch from sf patch 2820194 - * added miktex win32 patch from sf patch 2820194 + * apply sf patches 2830233 and 2823885 for osx setup and 64 bit; thanks + Michiel - * apply sf patches 2830233 and 2823885 for osx setup and 64 bit; thanks Michiel +2009-08-04 + Made cbook.get_sample_data make use of the ETag and Last-Modified headers + of mod_dav_svn. - JKS -2009-08-04 Made cbook.get_sample_data make use of the ETag and Last-Modified - headers of mod_dav_svn. - JKS +2009-08-03 + Add PathCollection; modify contourf to use complex paths instead of simple + paths with cuts. - EF -2009-08-03 Add PathCollection; modify contourf to use complex - paths instead of simple paths with cuts. - EF +2009-08-03 + Fixed boilerplate.py so it doesn't break the ReST docs. - JKS +2009-08-03 + pylab no longer provides a load and save function. These are available in + matplotlib.mlab, or you can use numpy.loadtxt and numpy.savetxt for text + files, or np.save and np.load for binary numpy arrays. - JDH -2009-08-03 Fixed boilerplate.py so it doesn't break the ReST docs. - JKS +2009-07-31 + Added cbook.get_sample_data for urllib enabled fetching and caching of data + needed for examples. See examples/misc/sample_data_demo.py - JDH -2009-08-03 pylab no longer provides a load and save function. These - are available in matplotlib.mlab, or you can use - numpy.loadtxt and numpy.savetxt for text files, or np.save - and np.load for binary numpy arrays. - JDH +2009-07-31 + Tagging 0.99.0.rc1 at 7314 - MGD -2009-07-31 Added cbook.get_sample_data for urllib enabled fetching and - caching of data needed for examples. See - examples/misc/sample_data_demo.py - JDH +2009-07-30 + Add set_cmap and register_cmap, and improve get_cmap, to provide convenient + handling of user-generated colormaps. Reorganized _cm and cm modules. - EF -2009-07-31 Tagging 0.99.0.rc1 at 7314 - MGD +2009-07-28 + Quiver speed improved, thanks to tip by Ray Speth. -EF -2009-07-30 Add set_cmap and register_cmap, and improve get_cmap, - to provide convenient handling of user-generated - colormaps. Reorganized _cm and cm modules. - EF +2009-07-27 + Simplify argument handling code for plot method. -EF -2009-07-28 Quiver speed improved, thanks to tip by Ray Speth. -EF +2009-07-25 + Allow "plot(1, 2, 'r*')" to work. - EF -2009-07-27 Simplify argument handling code for plot method. -EF +2009-07-22 + Added an 'interp' keyword to griddata so the faster linear interpolation + method can be chosen. Default is 'nn', so default behavior (using natural + neighbor method) is unchanged (JSW) -2009-07-25 Allow "plot(1, 2, 'r*')" to work. - EF +2009-07-22 + Improved boilerplate.py so that it generates the correct signatures for + pyplot functions. - JKS -2009-07-22 Added an 'interp' keyword to griddata so the faster linear - interpolation method can be chosen. Default is 'nn', so - default behavior (using natural neighbor method) is unchanged (JSW) +2009-07-19 + Fixed the docstring of Axes.step to reflect the correct meaning of the + kwargs "pre" and "post" - See SF bug + \https://sourceforge.net/tracker/index.php?func=detail&aid=2823304&group_id=80706&atid=560720 + - JDH -2009-07-22 Improved boilerplate.py so that it generates the correct - signatures for pyplot functions. - JKS +2009-07-18 + Fix support for hatches without color fills to pdf and svg backends. Add an + example of that to hatch_demo.py. - JKS -2009-07-19 Fixed the docstring of Axes.step to reflect the correct - meaning of the kwargs "pre" and "post" - See SF bug - \https://sourceforge.net/tracker/index.php?func=detail&aid=2823304&group_id=80706&atid=560720 - - JDH +2009-07-17 + Removed fossils from swig version of agg backend. - EF -2009-07-18 Fix support for hatches without color fills to pdf and svg - backends. Add an example of that to hatch_demo.py. - JKS +2009-07-14 + initial submission of the annotation guide. -JJL -2009-07-17 Removed fossils from swig version of agg backend. - EF +2009-07-14 + axes_grid : minor improvements in anchored_artists and inset_locator. -JJL -2009-07-14 initial submission of the annotation guide. -JJL +2009-07-14 + Fix a few bugs in ConnectionStyle algorithms. Add ConnectionPatch class. + -JJL -2009-07-14 axes_grid : minor improvements in anchored_artists and - inset_locator. -JJL +2009-07-11 + Added a fillstyle Line2D property for half filled markers -- see + examples/pylab_examples/fillstyle_demo.py JDH -2009-07-14 Fix a few bugs in ConnectionStyle algorithms. Add - ConnectionPatch class. -JJL +2009-07-08 + Attempt to improve performance of qt4 backend, do not call + qApp.processEvents while processing an event. Thanks Ole Streicher for + tracking this down - DSD -2009-07-11 Added a fillstyle Line2D property for half filled markers - -- see examples/pylab_examples/fillstyle_demo.py JDH +2009-06-24 + Add withheader option to mlab.rec2csv and changed use_mrecords default to + False in mlab.csv2rec since this is partially broken - JDH -2009-07-08 Attempt to improve performance of qt4 backend, do not call - qApp.processEvents while processing an event. Thanks Ole - Streicher for tracking this down - DSD +2009-06-24 + backend_agg.draw_marker quantizes the main path (as in the draw_path). - + JJL -2009-06-24 Add withheader option to mlab.rec2csv and changed - use_mrecords default to False in mlab.csv2rec since this is - partially broken - JDH +2009-06-24 + axes_grid: floating axis support added. - JJL -2009-06-24 backend_agg.draw_marker quantizes the main path (as in the - draw_path). - JJL +2009-06-14 + Add new command line options to backend_driver.py to support running only + some directories of tests - JKS -2009-06-24 axes_grid: floating axis support added. - JJL +2009-06-13 + partial cleanup of mlab and its importation in pylab - EF -2009-06-14 Add new command line options to backend_driver.py to support - running only some directories of tests - JKS +2009-06-13 + Introduce a rotation_mode property for the Text artist. See + examples/pylab_examples/demo_text_rotation_mode.py -JJL -2009-06-13 partial cleanup of mlab and its importation in pylab - EF +2009-06-07 + add support for bz2 files per sf support request 2794556 - JDH -2009-06-13 Introduce a rotation_mode property for the Text artist. See - examples/pylab_examples/demo_text_rotation_mode.py -JJL +2009-06-06 + added a properties method to the artist and inspector to return a dict + mapping property name -> value; see sf feature request 2792183 - JDH -2009-06-07 add support for bz2 files per sf support request 2794556 - - JDH +2009-06-06 + added Neil's auto minor tick patch; sf patch #2789713 - JDH -2009-06-06 added a properties method to the artist and inspector to - return a dict mapping property name -> value; see sf - feature request 2792183 - JDH +2009-06-06 + do not apply alpha to rgba color conversion if input is already rgba - JDH -2009-06-06 added Neil's auto minor tick patch; sf patch #2789713 - JDH +2009-06-03 + axes_grid : Initial check-in of curvelinear grid support. See + examples/axes_grid/demo_curvelinear_grid.py - JJL -2009-06-06 do not apply alpha to rgba color conversion if input is - already rgba - JDH +2009-06-01 + Add set_color method to Patch - EF -2009-06-03 axes_grid : Initial check-in of curvelinear grid support. See - examples/axes_grid/demo_curvelinear_grid.py - JJL +2009-06-01 + Spine is now derived from Patch - ADS -2009-06-01 Add set_color method to Patch - EF +2009-06-01 + use cbook.is_string_like() instead of isinstance() for spines - ADS -2009-06-01 Spine is now derived from Patch - ADS +2009-06-01 + cla() support for spines - ADS -2009-06-01 use cbook.is_string_like() instead of isinstance() for spines - ADS +2009-06-01 + Removed support for gtk < 2.4. - EF -2009-06-01 cla() support for spines - ADS +2009-05-29 + Improved the animation_blit_qt4 example, which was a mix of the + object-oriented and pylab interfaces. It is now strictly object-oriented - + DSD -2009-06-01 Removed support for gtk < 2.4. - EF +2009-05-28 + Fix axes_grid toolkit to work with spine patch by ADS. - JJL -2009-05-29 Improved the animation_blit_qt4 example, which was a mix - of the object-oriented and pylab interfaces. It is now - strictly object-oriented - DSD +2009-05-28 + Applied fbianco's patch to handle scroll wheel events in the qt4 backend - + DSD -2009-05-28 Fix axes_grid toolkit to work with spine patch by ADS. - JJL +2009-05-26 + Add support for "axis spines" to have arbitrary location. -ADS -2009-05-28 Applied fbianco's patch to handle scroll wheel events in - the qt4 backend - DSD +2009-05-20 + Add an empty matplotlibrc to the tests/ directory so that running tests + will use the default set of rcparams rather than the user's config. - RMM -2009-05-26 Add support for "axis spines" to have arbitrary location. -ADS +2009-05-19 + Axis.grid(): allow use of which='major,minor' to have grid on major and + minor ticks. -ADS -2009-05-20 Add an empty matplotlibrc to the tests/ directory so that running - tests will use the default set of rcparams rather than the user's - config. - RMM +2009-05-18 + Make psd(), csd(), and cohere() wrap properly for complex/two-sided + versions, like specgram() (SF #2791686) - RMM -2009-05-19 Axis.grid(): allow use of which='major,minor' to have grid - on major and minor ticks. -ADS +2009-05-18 + Fix the linespacing bug of multiline text (#1239682). See + examples/pylab_examples/multiline.py -JJL -2009-05-18 Make psd(), csd(), and cohere() wrap properly for complex/two-sided - versions, like specgram() (SF #2791686) - RMM +2009-05-18 + Add *annotation_clip* attr. for text.Annotation class. If True, annotation + is only drawn when the annotated point is inside the axes area. -JJL -2009-05-18 Fix the linespacing bug of multiline text (#1239682). See - examples/pylab_examples/multiline.py -JJL +2009-05-17 + Fix bug(#2749174) that some properties of minor ticks are not conserved + -JJL -2009-05-18 Add *annotation_clip* attr. for text.Annotation class. - If True, annotation is only drawn when the annotated point is - inside the axes area. -JJL +2009-05-17 + applied Michiel's sf patch 2790638 to turn off gtk event loop in setupext + for pygtk>=2.15.10 - JDH -2009-05-17 Fix bug(#2749174) that some properties of minor ticks are - not conserved -JJL - -2009-05-17 applied Michiel's sf patch 2790638 to turn off gtk event - loop in setupext for pygtk>=2.15.10 - JDH - -2009-05-17 applied Michiel's sf patch 2792742 to speed up Cairo and - macosx collections; speedups can be 20x. Also fixes some - bugs in which gc got into inconsistent state +2009-05-17 + applied Michiel's sf patch 2792742 to speed up Cairo and macosx + collections; speedups can be 20x. Also fixes some bugs in which gc got + into inconsistent state ----------------------- -2008-05-17 Release 0.98.5.3 at r7107 from the branch - JDH - -2009-05-13 An optional offset and bbox support in restore_bbox. - Add animation_blit_gtk2.py. -JJL +2008-05-17 + Release 0.98.5.3 at r7107 from the branch - JDH -2009-05-13 psfrag in backend_ps now uses baseline-alignment - when preview.sty is used ((default is - bottom-alignment). Also, a small API improvement - in OffsetBox-JJL +2009-05-13 + An optional offset and bbox support in restore_bbox. Add + animation_blit_gtk2.py. -JJL -2009-05-13 When the x-coordinate of a line is monotonically - increasing, it is now automatically clipped at - the stage of generating the transformed path in - the draw method; this greatly speeds up zooming and - panning when one is looking at a short segment of - a long time series, for example. - EF +2009-05-13 + psfrag in backend_ps now uses baseline-alignment when preview.sty is used + ((default is bottom-alignment). Also, a small API improvement in + OffsetBox-JJL -2009-05-11 aspect=1 in log-log plot gives square decades. -JJL +2009-05-13 + When the x-coordinate of a line is monotonically increasing, it is now + automatically clipped at the stage of generating the transformed path in + the draw method; this greatly speeds up zooming and panning when one is + looking at a short segment of a long time series, for example. - EF -2009-05-08 clabel takes new kwarg, rightside_up; if False, labels - will not be flipped to keep them rightside-up. This - allows the use of clabel to make streamfunction arrows, - as requested by Evan Mason. - EF +2009-05-11 + aspect=1 in log-log plot gives square decades. -JJL -2009-05-07 'labelpad' can now be passed when setting x/y labels. This - allows controlling the spacing between the label and its - axis. - RMM +2009-05-08 + clabel takes new kwarg, rightside_up; if False, labels will not be flipped + to keep them rightside-up. This allows the use of clabel to make + streamfunction arrows, as requested by Evan Mason. - EF -2009-05-06 print_ps now uses mixed-mode renderer. Axes.draw rasterize - artists whose zorder smaller than rasterization_zorder. - -JJL +2009-05-07 + 'labelpad' can now be passed when setting x/y labels. This allows + controlling the spacing between the label and its axis. - RMM -2009-05-06 Per-artist Rasterization, originally by Eric Bruning. -JJ +2009-05-06 + print_ps now uses mixed-mode renderer. Axes.draw rasterize artists whose + zorder smaller than rasterization_zorder. -JJL -2009-05-05 Add an example that shows how to make a plot that updates - using data from another process. Thanks to Robert - Cimrman - RMM +2009-05-06 + Per-artist Rasterization, originally by Eric Bruning. -JJ -2009-05-05 Add Axes.get_legend_handles_labels method. - JJL +2009-05-05 + Add an example that shows how to make a plot that updates using data from + another process. Thanks to Robert Cimrman - RMM -2009-05-04 Fix bug that Text.Annotation is still drawn while set to - not visible. - JJL +2009-05-05 + Add Axes.get_legend_handles_labels method. - JJL -2009-05-04 Added TJ's fill_betweenx patch - JDH +2009-05-04 + Fix bug that Text.Annotation is still drawn while set to not visible. - JJL -2009-05-02 Added options to plotfile based on question from - Joseph Smidt and patch by Matthias Michler. - EF +2009-05-04 + Added TJ's fill_betweenx patch - JDH +2009-05-02 + Added options to plotfile based on question from Joseph Smidt and patch by + Matthias Michler. - EF -2009-05-01 Changed add_artist and similar Axes methods to - return their argument. - EF +2009-05-01 + Changed add_artist and similar Axes methods to return their argument. - EF -2009-04-30 Incorrect eps bbox for landscape mode fixed - JJL +2009-04-30 + Incorrect eps bbox for landscape mode fixed - JJL -2009-04-28 Fixed incorrect bbox of eps output when usetex=True. - JJL +2009-04-28 + Fixed incorrect bbox of eps output when usetex=True. - JJL -2009-04-24 Changed use of os.open* to instead use subprocess.Popen. - os.popen* are deprecated in 2.6 and are removed in 3.0. - RMM +2009-04-24 + Changed use of os.open* to instead use subprocess.Popen. os.popen* are + deprecated in 2.6 and are removed in 3.0. - RMM -2009-04-20 Worked on axes_grid documentation. Added - axes_grid.inset_locator. - JJL +2009-04-20 + Worked on axes_grid documentation. Added axes_grid.inset_locator. - JJL -2009-04-17 Initial check-in of the axes_grid toolkit. - JJL +2009-04-17 + Initial check-in of the axes_grid toolkit. - JJL -2009-04-17 Added a support for bbox_to_anchor in - offsetbox.AnchoredOffsetbox. Improved a documentation. - - JJL +2009-04-17 + Added a support for bbox_to_anchor in offsetbox.AnchoredOffsetbox. Improved + a documentation. - JJL -2009-04-16 Fixed a offsetbox bug that multiline texts are not - correctly aligned. - JJL +2009-04-16 + Fixed a offsetbox bug that multiline texts are not correctly aligned. - + JJL -2009-04-16 Fixed a bug in mixed mode renderer that images produced by - an rasterizing backend are placed with incorrect size. - - JJL +2009-04-16 + Fixed a bug in mixed mode renderer that images produced by an rasterizing + backend are placed with incorrect size. - JJL -2009-04-14 Added Jonathan Taylor's Reinier Heeres' port of John - Porters' mplot3d to svn trunk. Package in - mpl_toolkits.mplot3d and demo is examples/mplot3d/demo.py. - Thanks Reiner +2009-04-14 + Added Jonathan Taylor's Reinier Heeres' port of John Porters' mplot3d to + svn trunk. Package in mpl_toolkits.mplot3d and demo is + examples/mplot3d/demo.py. Thanks Reiner -2009-04-06 The pdf backend now escapes newlines and linefeeds in strings. - Fixes sf bug #2708559; thanks to Tiago Pereira for the report. +2009-04-06 + The pdf backend now escapes newlines and linefeeds in strings. Fixes sf + bug #2708559; thanks to Tiago Pereira for the report. -2009-04-06 texmanager.make_dvi now raises an error if LaTeX failed to - create an output file. Thanks to Joao Luis Silva for reporting - this. - JKS +2009-04-06 + texmanager.make_dvi now raises an error if LaTeX failed to create an output + file. Thanks to Joao Luis Silva for reporting this. - JKS -2009-04-05 _png.read_png() reads 12 bit PNGs (patch from - Tobias Wood) - ADS +2009-04-05 + _png.read_png() reads 12 bit PNGs (patch from Tobias Wood) - ADS -2009-04-04 Allow log axis scale to clip non-positive values to - small positive value; this is useful for errorbars. - EF +2009-04-04 + Allow log axis scale to clip non-positive values to small positive value; + this is useful for errorbars. - EF -2009-03-28 Make images handle nan in their array argument. - A helper, cbook.safe_masked_invalid() was added. - EF +2009-03-28 + Make images handle nan in their array argument. A helper, + cbook.safe_masked_invalid() was added. - EF -2009-03-25 Make contour and contourf handle nan in their Z argument. - EF +2009-03-25 + Make contour and contourf handle nan in their Z argument. - EF -2009-03-20 Add AuxTransformBox in offsetbox.py to support some transformation. - anchored_text.py example is enhanced and renamed - (anchored_artists.py). - JJL +2009-03-20 + Add AuxTransformBox in offsetbox.py to support some transformation. + anchored_text.py example is enhanced and renamed (anchored_artists.py). - + JJL -2009-03-20 Add "bar" connection style for annotation - JJL +2009-03-20 + Add "bar" connection style for annotation - JJL -2009-03-17 Fix bugs in edge color handling by contourf, found - by Jae-Joon Lee. - EF +2009-03-17 + Fix bugs in edge color handling by contourf, found by Jae-Joon Lee. - EF -2009-03-14 Added 'LightSource' class to colors module for - creating shaded relief maps. shading_example.py - added to illustrate usage. - JSW +2009-03-14 + Added 'LightSource' class to colors module for creating shaded relief maps. + shading_example.py added to illustrate usage. - JSW -2009-03-11 Ensure wx version >= 2.8; thanks to Sandro Tosi and - Chris Barker. - EF +2009-03-11 + Ensure wx version >= 2.8; thanks to Sandro Tosi and Chris Barker. - EF -2009-03-10 Fix join style bug in pdf. - JKS +2009-03-10 + Fix join style bug in pdf. - JKS -2009-03-07 Add pyplot access to figure number list - EF +2009-03-07 + Add pyplot access to figure number list - EF -2009-02-28 hashing of FontProperties accounts current rcParams - JJL +2009-02-28 + hashing of FontProperties accounts current rcParams - JJL -2009-02-28 Prevent double-rendering of shared axis in twinx, twiny - EF +2009-02-28 + Prevent double-rendering of shared axis in twinx, twiny - EF -2009-02-26 Add optional bbox_to_anchor argument for legend class - JJL +2009-02-26 + Add optional bbox_to_anchor argument for legend class - JJL -2009-02-26 Support image clipping in pdf backend. - JKS +2009-02-26 + Support image clipping in pdf backend. - JKS -2009-02-25 Improve tick location subset choice in FixedLocator. - EF +2009-02-25 + Improve tick location subset choice in FixedLocator. - EF -2009-02-24 Deprecate numerix, and strip out all but the numpy - part of the code. - EF +2009-02-24 + Deprecate numerix, and strip out all but the numpy part of the code. - EF -2009-02-21 Improve scatter argument handling; add an early error - message, allow inputs to have more than one dimension. - EF +2009-02-21 + Improve scatter argument handling; add an early error message, allow inputs + to have more than one dimension. - EF -2009-02-16 Move plot_directive.py to the installed source tree. Add - support for inline code content - MGD +2009-02-16 + Move plot_directive.py to the installed source tree. Add support for + inline code content - MGD -2009-02-16 Move mathmpl.py to the installed source tree so it is - available to other projects. - MGD +2009-02-16 + Move mathmpl.py to the installed source tree so it is available to other + projects. - MGD -2009-02-14 Added the legend title support - JJL +2009-02-14 + Added the legend title support - JJL -2009-02-10 Fixed a bug in backend_pdf so it doesn't break when the setting - pdf.use14corefonts=True is used. Added test case in - unit/test_pdf_use14corefonts.py. - NGR +2009-02-10 + Fixed a bug in backend_pdf so it doesn't break when the setting + pdf.use14corefonts=True is used. Added test case in + unit/test_pdf_use14corefonts.py. - NGR -2009-02-08 Added a new imsave function to image.py and exposed it in - the pyplot interface - GR +2009-02-08 + Added a new imsave function to image.py and exposed it in the pyplot + interface - GR -2009-02-04 Some reorgnization of the legend code. anchored_text.py - added as an example. - JJL +2009-02-04 + Some reorganization of the legend code. anchored_text.py added as an + example. - JJL -2009-02-04 Add extent keyword arg to hexbin - ADS +2009-02-04 + Add extent keyword arg to hexbin - ADS -2009-02-04 Fix bug in mathtext related to \dots and \ldots - MGD +2009-02-04 + Fix bug in mathtext related to \dots and \ldots - MGD -2009-02-03 Change default joinstyle to round - MGD +2009-02-03 + Change default joinstyle to round - MGD -2009-02-02 Reduce number of marker XObjects in pdf output - JKS +2009-02-02 + Reduce number of marker XObjects in pdf output - JKS -2009-02-02 Change default resolution on polar plot to 1 - MGD +2009-02-02 + Change default resolution on polar plot to 1 - MGD -2009-02-02 Avoid malloc errors in ttconv for fonts that don't have - e.g., PostName (a version of Tahoma triggered this) - JKS +2009-02-02 + Avoid malloc errors in ttconv for fonts that don't have e.g., PostName (a + version of Tahoma triggered this) - JKS -2009-01-30 Remove support for pyExcelerator in exceltools -- use xlwt - instead - JDH +2009-01-30 + Remove support for pyExcelerator in exceltools -- use xlwt instead - JDH -2009-01-29 Document 'resolution' kwarg for polar plots. Support it - when using pyplot.polar, not just Figure.add_axes. - MGD +2009-01-29 + Document 'resolution' kwarg for polar plots. Support it when using + pyplot.polar, not just Figure.add_axes. - MGD -2009-01-29 Rework the nan-handling/clipping/quantizing/simplification - framework so each is an independent part of a pipeline. - Expose the C++-implementation of all of this so it can be - used from all Python backends. Add rcParam - "path.simplify_threshold" to control the threshold of - similarity below which vertices will be removed. +2009-01-29 + Rework the nan-handling/clipping/quantizing/simplification framework so + each is an independent part of a pipeline. Expose the C++-implementation + of all of this so it can be used from all Python backends. Add rcParam + "path.simplify_threshold" to control the threshold of similarity below + which vertices will be removed. -2009-01-26 Improved tight bbox option of the savefig. - JJL +2009-01-26 + Improved tight bbox option of the savefig. - JJL -2009-01-26 Make curves and NaNs play nice together - MGD +2009-01-26 + Make curves and NaNs play nice together - MGD -2009-01-21 Changed the defaults of acorr and xcorr to use - usevlines=True, maxlags=10 and normed=True since these are - the best defaults +2009-01-21 + Changed the defaults of acorr and xcorr to use usevlines=True, maxlags=10 + and normed=True since these are the best defaults -2009-01-19 Fix bug in quiver argument handling. - EF +2009-01-19 + Fix bug in quiver argument handling. - EF -2009-01-19 Fix bug in backend_gtk: don't delete nonexistent toolbar. - EF +2009-01-19 + Fix bug in backend_gtk: don't delete nonexistent toolbar. - EF -2009-01-16 Implement bbox_inches option for savefig. If bbox_inches is - "tight", try to determine the tight bounding box. - JJL +2009-01-16 + Implement bbox_inches option for savefig. If bbox_inches is "tight", try to + determine the tight bounding box. - JJL -2009-01-16 Fix bug in is_string_like so it doesn't raise an - unnecessary exception. - EF +2009-01-16 + Fix bug in is_string_like so it doesn't raise an unnecessary exception. - + EF -2009-01-16 Fix an infinite recursion in the unit registry when searching - for a converter for a sequence of strings. Add a corresponding - test. - RM +2009-01-16 + Fix an infinite recursion in the unit registry when searching for a + converter for a sequence of strings. Add a corresponding test. - RM -2009-01-16 Bugfix of C typedef of MPL_Int64 that was failing on - Windows XP 64 bit, as reported by George Goussard on numpy - mailing list. - ADS +2009-01-16 + Bugfix of C typedef of MPL_Int64 that was failing on Windows XP 64 bit, as + reported by George Goussard on numpy mailing list. - ADS -2009-01-16 Added helper function LinearSegmentedColormap.from_list to - facilitate building simple custom colomaps. See - examples/pylab_examples/custom_cmap_fromlist.py - JDH +2009-01-16 + Added helper function LinearSegmentedColormap.from_list to facilitate + building simple custom colomaps. See + examples/pylab_examples/custom_cmap_fromlist.py - JDH -2009-01-16 Applied Michiel's patch for macosx backend to fix rounding - bug. Closed sf bug 2508440 - JSW +2009-01-16 + Applied Michiel's patch for macosx backend to fix rounding bug. Closed sf + bug 2508440 - JSW -2009-01-10 Applied Michiel's hatch patch for macosx backend and - draw_idle patch for qt. Closes sf patched 2497785 and - 2468809 - JDH +2009-01-10 + Applied Michiel's hatch patch for macosx backend and draw_idle patch for + qt. Closes sf patched 2497785 and 2468809 - JDH -2009-01-10 Fix bug in pan/zoom with log coordinates. - EF +2009-01-10 + Fix bug in pan/zoom with log coordinates. - EF -2009-01-06 Fix bug in setting of dashed negative contours. - EF +2009-01-06 + Fix bug in setting of dashed negative contours. - EF -2009-01-06 Be fault tolerant when len(linestyles)>NLev in contour. - MM +2009-01-06 + Be fault tolerant when len(linestyles)>NLev in contour. - MM -2009-01-06 Added marginals kwarg to hexbin to plot marginal densities - JDH +2009-01-06 + Added marginals kwarg to hexbin to plot marginal densities JDH -2009-01-06 Change user-visible multipage pdf object to PdfPages to - avoid accidents with the file-like PdfFile. - JKS +2009-01-06 + Change user-visible multipage pdf object to PdfPages to avoid accidents + with the file-like PdfFile. - JKS -2009-01-05 Fix a bug in pdf usetex: allow using non-embedded fonts. - JKS +2009-01-05 + Fix a bug in pdf usetex: allow using non-embedded fonts. - JKS -2009-01-05 optional use of preview.sty in usetex mode. - JJL +2009-01-05 + optional use of preview.sty in usetex mode. - JJL -2009-01-02 Allow multipage pdf files. - JKS +2009-01-02 + Allow multipage pdf files. - JKS -2008-12-31 Improve pdf usetex by adding support for font effects - (slanting and extending). - JKS +2008-12-31 + Improve pdf usetex by adding support for font effects (slanting and + extending). - JKS -2008-12-29 Fix a bug in pdf usetex support, which occurred if the same - Type-1 font was used with different encodings, e.g., with - Minion Pro and MnSymbol. - JKS +2008-12-29 + Fix a bug in pdf usetex support, which occurred if the same Type-1 font was + used with different encodings, e.g., with Minion Pro and MnSymbol. - JKS -2008-12-20 fix the dpi-dependent offset of Shadow. - JJL +2008-12-20 + fix the dpi-dependent offset of Shadow. - JJL -2008-12-20 fix the hatch bug in the pdf backend. minor update - in docs and example - JJL +2008-12-20 + fix the hatch bug in the pdf backend. minor update in docs and example - + JJL -2008-12-19 Add axes_locator attribute in Axes. Two examples are added. - - JJL +2008-12-19 + Add axes_locator attribute in Axes. Two examples are added. - JJL -2008-12-19 Update Axes.legend documentation. /api/api_changes.rst is also - updated to describe changes in keyword parameters. - Issue a warning if old keyword parameters are used. - JJL +2008-12-19 + Update Axes.legend documentation. /api/api_changes.rst is also updated to + describe changes in keyword parameters. Issue a warning if old keyword + parameters are used. - JJL -2008-12-18 add new arrow style, a line + filled triangles. -JJL +2008-12-18 + add new arrow style, a line + filled triangles. -JJL ---------------- -2008-12-18 Re-Released 0.98.5.2 from v0_98_5_maint at r6679 - Released 0.98.5.2 from v0_98_5_maint at r6667 - -2008-12-18 Removed configobj, experimental traits and doc/mpl_data link - JDH - -2008-12-18 Fix bug where a line with NULL data limits prevents - subsequent data limits from calculating correctly - MGD +2008-12-18 + Re-Released 0.98.5.2 from v0_98_5_maint at r6679 Released 0.98.5.2 from + v0_98_5_maint at r6667 -2008-12-17 Major documentation generator changes - MGD +2008-12-18 + Removed configobj, experimental traits and doc/mpl_data link - JDH -2008-12-17 Applied macosx backend patch with support for path - collections, quadmesh, etc... - JDH +2008-12-18 + Fix bug where a line with NULL data limits prevents subsequent data limits + from calculating correctly - MGD -2008-12-17 fix dpi-dependent behavior of text bbox and arrow in annotate - -JJL +2008-12-17 + Major documentation generator changes - MGD -2008-12-17 Add group id support in artist. Two examples which - demonstrate svg filter are added. -JJL +2008-12-17 + Applied macosx backend patch with support for path collections, quadmesh, + etc... - JDH -2008-12-16 Another attempt to fix dpi-dependent behavior of Legend. -JJL +2008-12-17 + fix dpi-dependent behavior of text bbox and arrow in annotate -JJL -2008-12-16 Fixed dpi-dependent behavior of Legend and fancybox in Text. +2008-12-17 + Add group id support in artist. Two examples which demonstrate svg filter + are added. -JJL -2008-12-16 Added markevery property to Line2D to support subsampling - of markers - JDH -2008-12-15 Removed mpl_data symlink in docs. On platforms that do not - support symlinks, these become copies, and the font files - are large, so the distro becomes unnecessarily bloated. - Keeping the mpl_examples dir because relative links are - harder for the plot directive and the \*.py files are not so - large. - JDH +2008-12-16 + Another attempt to fix dpi-dependent behavior of Legend. -JJL -2008-12-15 Fix \$ in non-math text with usetex off. Document - differences between usetex on/off - MGD +2008-12-16 + Fixed dpi-dependent behavior of Legend and fancybox in Text. -2008-12-15 Fix anti-aliasing when auto-snapping - MGD +2008-12-16 + Added markevery property to Line2D to support subsampling of markers - JDH -2008-12-15 Fix grid lines not moving correctly during pan and zoom - MGD +2008-12-15 + Removed mpl_data symlink in docs. On platforms that do not support + symlinks, these become copies, and the font files are large, so the distro + becomes unnecessarily bloated. Keeping the mpl_examples dir because + relative links are harder for the plot directive and the \*.py files are + not so large. - JDH -2008-12-12 Preparations to eliminate maskedarray rcParams key: its - use will now generate a warning. Similarly, importing - the obsolote numerix.npyma will generate a warning. - EF +2008-12-15 + Fix \$ in non-math text with usetex off. Document differences between + usetex on/off - MGD -2008-12-12 Added support for the numpy.histogram() weights parameter - to the axes hist() method. Docs taken from numpy - MM +2008-12-15 + Fix anti-aliasing when auto-snapping - MGD -2008-12-12 Fixed warning in hist() with numpy 1.2 - MM +2008-12-15 + Fix grid lines not moving correctly during pan and zoom - MGD -2008-12-12 Removed external packages: configobj and enthought.traits - which are only required by the experimental traited config - and are somewhat out of date. If needed, install them - independently, see: +2008-12-12 + Preparations to eliminate maskedarray rcParams key: its use will now + generate a warning. Similarly, importing the obsolete numerix.npyma will + generate a warning. - EF - http://code.enthought.com/pages/traits.html +2008-12-12 + Added support for the numpy.histogram() weights parameter to the axes + hist() method. Docs taken from numpy - MM - and: +2008-12-12 + Fixed warning in hist() with numpy 1.2 - MM - http://www.voidspace.org.uk/python/configobj.html +2008-12-12 + Removed external packages: configobj and enthought.traits which are only + required by the experimental traited config and are somewhat out of date. + If needed, install them independently, see + http://code.enthought.com/pages/traits.html and + http://www.voidspace.org.uk/python/configobj.html -2008-12-12 Added support to assign labels to histograms of multiple - data. - MM +2008-12-12 + Added support to assign labels to histograms of multiple data. - MM ------------------------- -2008-12-11 Released 0.98.5 at svn r6573 +2008-12-11 + Released 0.98.5 at svn r6573 -2008-12-11 Use subprocess.Popen instead of os.popen in dviread - (Windows problem reported by Jorgen Stenarson) - JKS +2008-12-11 + Use subprocess.Popen instead of os.popen in dviread (Windows problem + reported by Jorgen Stenarson) - JKS -2008-12-10 Added Michael's font_manager fix and Jae-Joon's - figure/subplot fix. Bumped version number to 0.98.5 - JDH +2008-12-10 + Added Michael's font_manager fix and Jae-Joon's figure/subplot fix. Bumped + version number to 0.98.5 - JDH ---------------------------- -2008-12-09 Released 0.98.4 at svn r6536 - -2008-12-08 Added mdehoon's native macosx backend from sf patch 2179017 - JDH - -2008-12-08 Removed the prints in the set_*style commands. Return the - list of pprinted strings instead - JDH - -2008-12-08 Some of the changes Michael made to improve the output of - the property tables in the rest docs broke of made - difficult to use some of the interactive doc helpers, e.g., - setp and getp. Having all the rest markup in the ipython - shell also confused the docstrings. I added a new rc param - docstring.hardcopy, to format the docstrings differently for - hard copy and other use. The ArtistInspector could use a - little refactoring now since there is duplication of effort - between the rest out put and the non-rest output - JDH - -2008-12-08 Updated spectral methods (psd, csd, etc.) to scale one-sided - densities by a factor of 2 and, optionally, scale all densities - by the sampling frequency. This gives better MatLab - compatibility. -RM - -2008-12-08 Fixed alignment of ticks in colorbars. -MGD - -2008-12-07 drop the deprecated "new" keyword of np.histogram() for - numpy 1.2 or later. -JJL - -2008-12-06 Fixed a bug in svg backend that new_figure_manager() - ignores keywords arguments such as figsize, etc. -JJL - -2008-12-05 Fixed a bug that the handlelength of the new legend class - set too short when numpoints=1 -JJL - -2008-12-04 Added support for data with units (e.g., dates) to - Axes.fill_between. -RM - -2008-12-04 Added fancybox keyword to legend. Also applied some changes - for better look, including baseline adjustment of the - multiline texts so that it is center aligned. -JJL +2008-12-09 + Released 0.98.4 at svn r6536 + +2008-12-08 + Added mdehoon's native macosx backend from sf patch 2179017 - JDH + +2008-12-08 + Removed the prints in the set_*style commands. Return the list of pprinted + strings instead - JDH -2008-12-02 The transmuter classes in the patches.py are reorganized as - subclasses of the Style classes. A few more box and arrow - styles are added. -JJL +2008-12-08 + Some of the changes Michael made to improve the output of the property + tables in the rest docs broke of made difficult to use some of the + interactive doc helpers, e.g., setp and getp. Having all the rest markup + in the ipython shell also confused the docstrings. I added a new rc param + docstring.hardcopy, to format the docstrings differently for hard copy and + other use. The ArtistInspector could use a little refactoring now since + there is duplication of effort between the rest out put and the non-rest + output - JDH -2008-12-02 Fixed a bug in the new legend class that didn't allowed - a tuple of coordinate values as loc. -JJL +2008-12-08 + Updated spectral methods (psd, csd, etc.) to scale one-sided densities by a + factor of 2 and, optionally, scale all densities by the sampling frequency. + This gives better MatLab compatibility. -RM -2008-12-02 Improve checks for external dependencies, using subprocess - (instead of deprecated popen*) and distutils (for version - checking) - DSD +2008-12-08 + Fixed alignment of ticks in colorbars. -MGD -2008-11-30 Reimplementation of the legend which supports baseline alignment, - multi-column, and expand mode. - JJL +2008-12-07 + drop the deprecated "new" keyword of np.histogram() for numpy 1.2 or later. + -JJL -2008-12-01 Fixed histogram autoscaling bug when bins or range are given - explicitly (fixes Debian bug 503148) - MM +2008-12-06 + Fixed a bug in svg backend that new_figure_manager() ignores keywords + arguments such as figsize, etc. -JJL -2008-11-25 Added rcParam axes.unicode_minus which allows plain hyphen - for minus when False - JDH +2008-12-05 + Fixed a bug that the handlelength of the new legend class set too short + when numpoints=1 -JJL -2008-11-25 Added scatterpoints support in Legend. patch by Erik - Tollerud - JJL +2008-12-04 + Added support for data with units (e.g., dates) to Axes.fill_between. -RM -2008-11-24 Fix crash in log ticking. - MGD +2008-12-04 + Added fancybox keyword to legend. Also applied some changes for better + look, including baseline adjustment of the multiline texts so that it is + center aligned. -JJL -2008-11-20 Added static helper method BrokenHBarCollection.span_where - and Axes/pyplot method fill_between. See - examples/pylab/fill_between.py - JDH +2008-12-02 + The transmuter classes in the patches.py are reorganized as subclasses of + the Style classes. A few more box and arrow styles are added. -JJL -2008-11-12 Add x_isdata and y_isdata attributes to Artist instances, - and use them to determine whether either or both - coordinates are used when updating dataLim. This is - used to fix autoscaling problems that had been triggered - by axhline, axhspan, axvline, axvspan. - EF +2008-12-02 + Fixed a bug in the new legend class that didn't allowed a tuple of + coordinate values as loc. -JJL -2008-11-11 Update the psd(), csd(), cohere(), and specgram() methods - of Axes and the csd() cohere(), and specgram() functions - in mlab to be in sync with the changes to psd(). - In fact, under the hood, these all call the same core - to do computations. - RM +2008-12-02 + Improve checks for external dependencies, using subprocess (instead of + deprecated popen*) and distutils (for version checking) - DSD + +2008-11-30 + Reimplementation of the legend which supports baseline alignment, + multi-column, and expand mode. - JJL + +2008-12-01 + Fixed histogram autoscaling bug when bins or range are given explicitly + (fixes Debian bug 503148) - MM -2008-11-11 Add 'pad_to' and 'sides' parameters to mlab.psd() to - allow controlling of zero padding and returning of - negative frequency components, respecitively. These are - added in a way that does not change the API. - RM +2008-11-25 + Added rcParam axes.unicode_minus which allows plain hyphen for minus when + False - JDH + +2008-11-25 + Added scatterpoints support in Legend. patch by Erik Tollerud - JJL + +2008-11-24 + Fix crash in log ticking. - MGD -2008-11-10 Fix handling of c kwarg by scatter; generalize - is_string_like to accept numpy and numpy.ma string - array scalars. - RM and EF +2008-11-20 + Added static helper method BrokenHBarCollection.span_where and Axes/pyplot + method fill_between. See examples/pylab/fill_between.py - JDH + +2008-11-12 + Add x_isdata and y_isdata attributes to Artist instances, and use them to + determine whether either or both coordinates are used when updating + dataLim. This is used to fix autoscaling problems that had been triggered + by axhline, axhspan, axvline, axvspan. - EF + +2008-11-11 + Update the psd(), csd(), cohere(), and specgram() methods of Axes and the + csd() cohere(), and specgram() functions in mlab to be in sync with the + changes to psd(). In fact, under the hood, these all call the same core to + do computations. - RM + +2008-11-11 + Add 'pad_to' and 'sides' parameters to mlab.psd() to allow controlling of + zero padding and returning of negative frequency components, respectively. + These are added in a way that does not change the API. - RM + +2008-11-10 + Fix handling of c kwarg by scatter; generalize is_string_like to accept + numpy and numpy.ma string array scalars. - RM and EF + +2008-11-09 + Fix a possible EINTR problem in dviread, which might help when saving pdf + files from the qt backend. - JKS + +2008-11-05 + Fix bug with zoom to rectangle and twin axes - MGD + +2008-10-24 + Added Jae Joon's fancy arrow, box and annotation enhancements -- see + examples/pylab_examples/annotation_demo2.py + +2008-10-23 + Autoscaling is now supported with shared axes - EF -2008-11-09 Fix a possible EINTR problem in dviread, which might help - when saving pdf files from the qt backend. - JKS +2008-10-23 + Fixed exception in dviread that happened with Minion - JKS -2008-11-05 Fix bug with zoom to rectangle and twin axes - MGD +2008-10-21 + set_xlim, ylim now return a copy of the viewlim array to avoid modify + inplace surprises -2008-10-24 Added Jae Joon's fancy arrow, box and annotation - enhancements -- see - examples/pylab_examples/annotation_demo2.py +2008-10-20 + Added image thumbnail generating function matplotlib.image.thumbnail. See + examples/misc/image_thumbnail.py - JDH -2008-10-23 Autoscaling is now supported with shared axes - EF +2008-10-20 + Applied scatleg patch based on ideas and work by Erik Tollerud and Jae-Joon + Lee. - MM -2008-10-23 Fixed exception in dviread that happened with Minion - JKS +2008-10-11 + Fixed bug in pdf backend: if you pass a file object for output instead of a + filename, e.g., in a wep app, we now flush the object at the end. - JKS -2008-10-21 set_xlim, ylim now return a copy of the viewlim array to - avoid modify inplace surprises +2008-10-08 + Add path simplification support to paths with gaps. - EF -2008-10-20 Added image thumbnail generating function - matplotlib.image.thumbnail. See - examples/misc/image_thumbnail.py - JDH +2008-10-05 + Fix problem with AFM files that don't specify the font's full name or + family name. - JKS -2008-10-20 Applied scatleg patch based on ideas and work by Erik - Tollerud and Jae-Joon Lee. - MM +2008-10-04 + Added 'scilimits' kwarg to Axes.ticklabel_format() method, for easy access + to the set_powerlimits method of the major ScalarFormatter. - EF -2008-10-11 Fixed bug in pdf backend: if you pass a file object for - output instead of a filename, e.g., in a wep app, we now - flush the object at the end. - JKS +2008-10-04 + Experimental new kwarg borderpad to replace pad in legend, based on + suggestion by Jae-Joon Lee. - EF -2008-10-08 Add path simplification support to paths with gaps. - EF +2008-09-27 + Allow spy to ignore zero values in sparse arrays, based on patch by Tony + Yu. Also fixed plot to handle empty data arrays, and fixed handling of + markers in figlegend. - EF -2008-10-05 Fix problem with AFM files that don't specify the font's - full name or family name. - JKS +2008-09-24 + Introduce drawstyles for lines. Transparently split linestyles like + 'steps--' into drawstyle 'steps' and linestyle '--'. Legends always use + drawstyle 'default'. - MM -2008-10-04 Added 'scilimits' kwarg to Axes.ticklabel_format() method, - for easy access to the set_powerlimits method of the - major ScalarFormatter. - EF +2008-09-18 + Fixed quiver and quiverkey bugs (failure to scale properly when resizing) + and added additional methods for determining the arrow angles - EF -2008-10-04 Experimental new kwarg borderpad to replace pad in legend, - based on suggestion by Jae-Joon Lee. - EF +2008-09-18 + Fix polar interpolation to handle negative values of theta - MGD -2008-09-27 Allow spy to ignore zero values in sparse arrays, based - on patch by Tony Yu. Also fixed plot to handle empty - data arrays, and fixed handling of markers in figlegend. - EF +2008-09-14 + Reorganized cbook and mlab methods related to numerical calculations that + have little to do with the goals of those two modules into a separate + module numerical_methods.py Also, added ability to select points and stop + point selection with keyboard in ginput and manual contour labeling code. + Finally, fixed contour labeling bug. - DMK -2008-09-24 Introduce drawstyles for lines. Transparently split linestyles - like 'steps--' into drawstyle 'steps' and linestyle '--'. - Legends always use drawstyle 'default'. - MM +2008-09-11 + Fix backtick in Postscript output. - MGD -2008-09-18 Fixed quiver and quiverkey bugs (failure to scale properly - when resizing) and added additional methods for determining - the arrow angles - EF +2008-09-10 + [ 2089958 ] Path simplification for vector output backends Leverage the + simplification code exposed through path_to_polygons to simplify certain + well-behaved paths in the vector backends (PDF, PS and SVG). + "path.simplify" must be set to True in matplotlibrc for this to work. + - MGD -2008-09-18 Fix polar interpolation to handle negative values of theta - MGD +2008-09-10 + Add "filled" kwarg to Path.intersects_path and Path.intersects_bbox. - MGD -2008-09-14 Reorganized cbook and mlab methods related to numerical - calculations that have little to do with the goals of those two - modules into a separate module numerical_methods.py - Also, added ability to select points and stop point selection - with keyboard in ginput and manual contour labeling code. - Finally, fixed contour labeling bug. - DMK +2008-09-07 + Changed full arrows slightly to avoid an xpdf rendering problem reported by + Friedrich Hagedorn. - JKS -2008-09-11 Fix backtick in Postscript output. - MGD +2008-09-07 + Fix conversion of quadratic to cubic Bezier curves in PDF and PS backends. + Patch by Jae-Joon Lee. - JKS -2008-09-10 [ 2089958 ] Path simplification for vector output backends - Leverage the simplification code exposed through - path_to_polygons to simplify certain well-behaved paths in - the vector backends (PDF, PS and SVG). "path.simplify" - must be set to True in matplotlibrc for this to work. - - MGD +2008-09-06 + Added 5-point star marker to plot command - EF -2008-09-10 Add "filled" kwarg to Path.intersects_path and - Path.intersects_bbox. - MGD +2008-09-05 + Fix hatching in PS backend - MGD -2008-09-07 Changed full arrows slightly to avoid an xpdf rendering - problem reported by Friedrich Hagedorn. - JKS +2008-09-03 + Fix log with base 2 - MGD -2008-09-07 Fix conversion of quadratic to cubic Bezier curves in PDF - and PS backends. Patch by Jae-Joon Lee. - JKS +2008-09-01 + Added support for bilinear interpolation in NonUniformImage; patch by + Gregory Lielens. - EF -2008-09-06 Added 5-point star marker to plot command - EF +2008-08-28 + Added support for multiple histograms with data of different length - MM -2008-09-05 Fix hatching in PS backend - MGD +2008-08-28 + Fix step plots with log scale - MGD -2008-09-03 Fix log with base 2 - MGD +2008-08-28 + Fix masked arrays with markers in non-Agg backends - MGD -2008-09-01 Added support for bilinear interpolation in - NonUniformImage; patch by Gregory Lielens. - EF +2008-08-28 + Fix clip_on kwarg so it actually works correctly - MGD -2008-08-28 Added support for multiple histograms with data of - different length - MM +2008-08-25 + Fix locale problems in SVG backend - MGD -2008-08-28 Fix step plots with log scale - MGD +2008-08-22 + fix quiver so masked values are not plotted - JSW -2008-08-28 Fix masked arrays with markers in non-Agg backends - MGD +2008-08-18 + improve interactive pan/zoom in qt4 backend on windows - DSD -2008-08-28 Fix clip_on kwarg so it actually works correctly - MGD - -2008-08-25 Fix locale problems in SVG backend - MGD - -2008-08-22 fix quiver so masked values are not plotted - JSW - -2008-08-18 improve interactive pan/zoom in qt4 backend on windows - DSD - -2008-08-11 Fix more bugs in NaN/inf handling. In particular, path simplification - (which does not handle NaNs or infs) will be turned off automatically - when infs or NaNs are present. Also masked arrays are now converted - to arrays with NaNs for consistent handling of masks and NaNs - - MGD and EF +2008-08-11 + Fix more bugs in NaN/inf handling. In particular, path simplification + (which does not handle NaNs or infs) will be turned off automatically when + infs or NaNs are present. Also masked arrays are now converted to arrays + with NaNs for consistent handling of masks and NaNs - MGD and EF ------------------------ -2008-08-03 Released 0.98.3 at svn r5947 +2008-08-03 + Released 0.98.3 at svn r5947 -2008-08-01 Backported memory leak fixes in _ttconv.cpp - MGD +2008-08-01 + Backported memory leak fixes in _ttconv.cpp - MGD -2008-07-31 Added masked array support to griddata. - JSW +2008-07-31 + Added masked array support to griddata. - JSW -2008-07-26 Added optional C and reduce_C_function arguments to - axes.hexbin(). This allows hexbin to accumulate the values - of C based on the x,y coordinates and display in hexagonal - bins. - ADS +2008-07-26 + Added optional C and reduce_C_function arguments to axes.hexbin(). This + allows hexbin to accumulate the values of C based on the x,y coordinates + and display in hexagonal bins. - ADS -2008-07-24 Deprecated (raise NotImplementedError) all the mlab2 - functions from matplotlib.mlab out of concern that some of - them were not clean room implementations. JDH +2008-07-24 + Deprecated (raise NotImplementedError) all the mlab2 functions from + matplotlib.mlab out of concern that some of them were not clean room + implementations. JDH -2008-07-24 Rewrite of a significant portion of the clabel code (class - ContourLabeler) to improve inlining. - DMK +2008-07-24 + Rewrite of a significant portion of the clabel code (class ContourLabeler) + to improve inlining. - DMK -2008-07-22 Added Barbs polygon collection (similar to Quiver) for plotting - wind barbs. Added corresponding helpers to Axes and pyplot as - well. (examples/pylab_examples/barb_demo.py shows it off.) - RMM +2008-07-22 + Added Barbs polygon collection (similar to Quiver) for plotting wind barbs. + Added corresponding helpers to Axes and pyplot as well. + (examples/pylab_examples/barb_demo.py shows it off.) - RMM -2008-07-21 Added scikits.delaunay as matplotlib.delaunay. Added griddata - function in matplotlib.mlab, with example (griddata_demo.py) in - pylab_examples. griddata function will use mpl_toolkits._natgrid - if installed. - JSW +2008-07-21 + Added scikits.delaunay as matplotlib.delaunay. Added griddata function in + matplotlib.mlab, with example (griddata_demo.py) in pylab_examples. + griddata function will use mpl_toolkits._natgrid if installed. - JSW -2008-07-21 Re-introduced offset_copy that works in the context of the - new transforms. - MGD +2008-07-21 + Re-introduced offset_copy that works in the context of the new transforms. + - MGD -2008-07-21 Committed patch by Ryan May to add get_offsets and - set_offsets to Collections base class - EF +2008-07-21 + Committed patch by Ryan May to add get_offsets and set_offsets to + Collections base class - EF -2008-07-21 Changed the "asarray" strategy in image.py so that - colormapping of masked input should work for all - image types (thanks Klaus Zimmerman) - EF +2008-07-21 + Changed the "asarray" strategy in image.py so that colormapping of masked + input should work for all image types (thanks Klaus Zimmerman) - EF -2008-07-20 Rewrote cbook.delete_masked_points and corresponding - unit test to support rgb color array inputs, datetime - inputs, etc. - EF +2008-07-20 + Rewrote cbook.delete_masked_points and corresponding unit test to support + rgb color array inputs, datetime inputs, etc. - EF -2008-07-20 Renamed unit/axes_unit.py to cbook_unit.py and modified - in accord with Ryan's move of delete_masked_points from - axes to cbook. - EF +2008-07-20 + Renamed unit/axes_unit.py to cbook_unit.py and modified in accord with + Ryan's move of delete_masked_points from axes to cbook. - EF -2008-07-18 Check for nan and inf in axes.delete_masked_points(). - This should help hexbin and scatter deal with nans. - ADS +2008-07-18 + Check for nan and inf in axes.delete_masked_points(). This should help + hexbin and scatter deal with nans. - ADS -2008-07-17 Added ability to manually select contour label locations. - Also added a waitforbuttonpress function. - DMK +2008-07-17 + Added ability to manually select contour label locations. Also added a + waitforbuttonpress function. - DMK -2008-07-17 Fix bug with NaNs at end of path (thanks, Andrew Straw for - the report) - MGD +2008-07-17 + Fix bug with NaNs at end of path (thanks, Andrew Straw for the report) - + MGD -2008-07-16 Improve error handling in texmanager, thanks to Ian Henry - for reporting - DSD +2008-07-16 + Improve error handling in texmanager, thanks to Ian Henry for reporting - + DSD -2008-07-12 Added support for external backends with the - "module://my_backend" syntax - JDH +2008-07-12 + Added support for external backends with the "module://my_backend" syntax - + JDH -2008-07-11 Fix memory leak related to shared axes. Grouper should - store weak references. - MGD +2008-07-11 + Fix memory leak related to shared axes. Grouper should store weak + references. - MGD -2008-07-10 Bugfix: crash displaying fontconfig pattern - MGD +2008-07-10 + Bugfix: crash displaying fontconfig pattern - MGD -2008-07-10 Bugfix: [ 2013963 ] update_datalim_bounds in Axes not works - MGD +2008-07-10 + Bugfix: [ 2013963 ] update_datalim_bounds in Axes not works - MGD -2008-07-10 Bugfix: [ 2014183 ] multiple imshow() causes gray edges - MGD +2008-07-10 + Bugfix: [ 2014183 ] multiple imshow() causes gray edges - MGD -2008-07-09 Fix rectangular axes patch on polar plots bug - MGD +2008-07-09 + Fix rectangular axes patch on polar plots bug - MGD -2008-07-09 Improve mathtext radical rendering - MGD +2008-07-09 + Improve mathtext radical rendering - MGD -2008-07-08 Improve mathtext superscript placement - MGD +2008-07-08 + Improve mathtext superscript placement - MGD -2008-07-07 Fix custom scales in pcolormesh (thanks Matthew Turk) - MGD +2008-07-07 + Fix custom scales in pcolormesh (thanks Matthew Turk) - MGD -2008-07-03 Implemented findobj method for artist and pyplot - see - examples/pylab_examples/findobj_demo.py - JDH +2008-07-03 + Implemented findobj method for artist and pyplot - see + examples/pylab_examples/findobj_demo.py - JDH -2008-06-30 Another attempt to fix TextWithDash - DSD +2008-06-30 + Another attempt to fix TextWithDash - DSD -2008-06-30 Removed Qt4 NavigationToolbar2.destroy -- it appears to - have been unnecessary and caused a bug reported by P. - Raybaut - DSD +2008-06-30 + Removed Qt4 NavigationToolbar2.destroy -- it appears to have been + unnecessary and caused a bug reported by P. Raybaut - DSD -2008-06-27 Fixed tick positioning bug - MM +2008-06-27 + Fixed tick positioning bug - MM -2008-06-27 Fix dashed text bug where text was at the wrong end of the - dash - MGD +2008-06-27 + Fix dashed text bug where text was at the wrong end of the dash - MGD -2008-06-26 Fix mathtext bug for expressions like $x_{\leftarrow}$ - MGD +2008-06-26 + Fix mathtext bug for expressions like $x_{\leftarrow}$ - MGD -2008-06-26 Fix direction of horizontal/vertical hatches - MGD +2008-06-26 + Fix direction of horizontal/vertical hatches - MGD -2008-06-25 Figure.figurePatch renamed Figure.patch, Axes.axesPatch - renamed Axes.patch, Axes.axesFrame renamed Axes.frame, - Axes.get_frame, which returns Axes.patch, is deprecated. - Examples and users guide updated - JDH +2008-06-25 + Figure.figurePatch renamed Figure.patch, Axes.axesPatch renamed Axes.patch, + Axes.axesFrame renamed Axes.frame, Axes.get_frame, which returns + Axes.patch, is deprecated. Examples and users guide updated - JDH -2008-06-25 Fix rendering quality of pcolor - MGD +2008-06-25 + Fix rendering quality of pcolor - MGD ---------------------------- -2008-06-24 Released 0.98.2 at svn r5667 - (source only for debian) JDH +2008-06-24 + Released 0.98.2 at svn r5667 - (source only for debian) JDH -2008-06-24 Added "transparent" kwarg to savefig. - MGD +2008-06-24 + Added "transparent" kwarg to savefig. - MGD -2008-06-24 Applied Stefan's patch to draw a single centered marker over - a line with numpoints==1 - JDH +2008-06-24 + Applied Stefan's patch to draw a single centered marker over a line with + numpoints==1 - JDH -2008-06-23 Use splines to render circles in scatter plots - MGD +2008-06-23 + Use splines to render circles in scatter plots - MGD ---------------------------- -2008-06-22 Released 0.98.1 at revision 5637 +2008-06-22 + Released 0.98.1 at revision 5637 -2008-06-22 Removed axes3d support and replaced it with a - NotImplementedError for one release cycle +2008-06-22 + Removed axes3d support and replaced it with a NotImplementedError for one + release cycle -2008-06-21 fix marker placement bug in backend_ps - DSD +2008-06-21 + fix marker placement bug in backend_ps - DSD -2008-06-20 [ 1978629 ] scale documentation missing/incorrect for log - MGD +2008-06-20 + [ 1978629 ] scale documentation missing/incorrect for log - MGD -2008-06-20 Added closed kwarg to PolyCollection. Fixes bug [ 1994535 - ] still missing lines on graph with svn (r 5548). - MGD +2008-06-20 + Added closed kwarg to PolyCollection. Fixes bug [ 1994535 ] still missing + lines on graph with svn (r 5548). - MGD -2008-06-20 Added set/get_closed method to Polygon; fixes error - in hist - MM +2008-06-20 + Added set/get_closed method to Polygon; fixes error in hist - MM -2008-06-19 Use relative font sizes (e.g., 'medium' and 'large') in - rcsetup.py and matplotlibrc.template so that text will - be scaled by default when changing rcParams['font.size'] - - EF +2008-06-19 + Use relative font sizes (e.g., 'medium' and 'large') in rcsetup.py and + matplotlibrc.template so that text will be scaled by default when changing + rcParams['font.size'] - EF -2008-06-17 Add a generic PatchCollection class that can contain any - kind of patch. - MGD +2008-06-17 + Add a generic PatchCollection class that can contain any kind of patch. - + MGD -2008-06-13 Change pie chart label alignment to avoid having labels - overwrite the pie - MGD +2008-06-13 + Change pie chart label alignment to avoid having labels overwrite the pie - + MGD -2008-06-12 Added some helper functions to the mathtext parser to - return bitmap arrays or write pngs to make it easier to use - mathtext outside the context of an mpl figure. modified - the mathpng sphinxext to use the mathtext png save - functionality - see examples/api/mathtext_asarray.py - JDH +2008-06-12 + Added some helper functions to the mathtext parser to return bitmap arrays + or write pngs to make it easier to use mathtext outside the context of an + mpl figure. modified the mathpng sphinxext to use the mathtext png save + functionality - see examples/api/mathtext_asarray.py - JDH -2008-06-11 Use matplotlib.mathtext to render math expressions in - online docs - MGD +2008-06-11 + Use matplotlib.mathtext to render math expressions in online docs - MGD -2008-06-11 Move PNG loading/saving to its own extension module, and - remove duplicate code in _backend_agg.cpp and _image.cpp - that does the same thing - MGD +2008-06-11 + Move PNG loading/saving to its own extension module, and remove duplicate + code in _backend_agg.cpp and _image.cpp that does the same thing - MGD -2008-06-11 Numerous mathtext bugfixes, primarily related to - dpi-independence - MGD +2008-06-11 + Numerous mathtext bugfixes, primarily related to dpi-independence - MGD -2008-06-10 Bar now applies the label only to the first patch only, and - sets '_nolegend_' for the other patch labels. This lets - autolegend work as expected for hist and bar - see - \https://sourceforge.net/tracker/index.php?func=detail&aid=1986597&group_id=80706&atid=560720 - JDH +2008-06-10 + Bar now applies the label only to the first patch only, and sets + '_nolegend_' for the other patch labels. This lets autolegend work as + expected for hist and bar - see + \https://sourceforge.net/tracker/index.php?func=detail&aid=1986597&group_id=80706&atid=560720 + JDH -2008-06-10 Fix text baseline alignment bug. [ 1985420 ] Repair of - baseline alignment in Text._get_layout. Thanks Stan West - - MGD +2008-06-10 + Fix text baseline alignment bug. [ 1985420 ] Repair of baseline alignment + in Text._get_layout. Thanks Stan West - MGD -2008-06-09 Committed Gregor's image resample patch to downsampling - images with new rcparam image.resample - JDH +2008-06-09 + Committed Gregor's image resample patch to downsampling images with new + rcparam image.resample - JDH -2008-06-09 Don't install Enthought.Traits along with matplotlib. For - matplotlib developers convenience, it can still be - installed by setting an option in setup.cfg while we figure - decide if there is a future for the traited config - DSD +2008-06-09 + Don't install Enthought.Traits along with matplotlib. For matplotlib + developers convenience, it can still be installed by setting an option in + setup.cfg while we figure decide if there is a future for the traited + config - DSD -2008-06-09 Added range keyword arg to hist() - MM +2008-06-09 + Added range keyword arg to hist() - MM -2008-06-07 Moved list of backends to rcsetup.py; made use of lower - case for backend names consistent; use validate_backend - when importing backends subpackage - EF +2008-06-07 + Moved list of backends to rcsetup.py; made use of lower case for backend + names consistent; use validate_backend when importing backends subpackage - + EF -2008-06-06 hist() revision, applied ideas proposed by Erik Tollerud and - Olle Engdegard: make histtype='step' unfilled by default - and introduce histtype='stepfilled'; use default color - cycle; introduce reverse cumulative histogram; new align - keyword - MM +2008-06-06 + hist() revision, applied ideas proposed by Erik Tollerud and Olle + Engdegard: make histtype='step' unfilled by default and introduce + histtype='stepfilled'; use default color cycle; introduce reverse + cumulative histogram; new align keyword - MM -2008-06-06 Fix closed polygon patch and also provide the option to - not close the polygon - MGD +2008-06-06 + Fix closed polygon patch and also provide the option to not close the + polygon - MGD -2008-06-05 Fix some dpi-changing-related problems with PolyCollection, - as called by Axes.scatter() - MGD +2008-06-05 + Fix some dpi-changing-related problems with PolyCollection, as called by + Axes.scatter() - MGD -2008-06-05 Fix image drawing so there is no extra space to the right - or bottom - MGD +2008-06-05 + Fix image drawing so there is no extra space to the right or bottom - MGD -2006-06-04 Added a figure title command suptitle as a Figure method - and pyplot command -- see examples/figure_title.py - JDH +2006-06-04 + Added a figure title command suptitle as a Figure method and pyplot command + -- see examples/figure_title.py - JDH -2008-06-02 Added support for log to hist with histtype='step' and fixed - a bug for log-scale stacked histograms - MM +2008-06-02 + Added support for log to hist with histtype='step' and fixed a bug for + log-scale stacked histograms - MM ----------------------------- -2008-05-29 Released 0.98.0 at revision 5314 +2008-05-29 + Released 0.98.0 at revision 5314 -2008-05-29 matplotlib.image.imread now no longer always returns RGBA - -- if the image is luminance or RGB, it will return a MxN - or MxNx3 array if possible. Also uint8 is no longer always - forced to float. +2008-05-29 + matplotlib.image.imread now no longer always returns RGBA -- if the image + is luminance or RGB, it will return a MxN or MxNx3 array if possible. Also + uint8 is no longer always forced to float. -2008-05-29 Implement path clipping in PS backend - JDH +2008-05-29 + Implement path clipping in PS backend - JDH -2008-05-29 Fixed two bugs in texmanager.py: - improved comparison of dvipng versions - fixed a bug introduced when get_grey method was added - - DSD +2008-05-29 + Fixed two bugs in texmanager.py: improved comparison of dvipng versions + fixed a bug introduced when get_grey method was added - DSD -2008-05-28 Fix crashing of PDFs in xpdf and ghostscript when two-byte - characters are used with Type 3 fonts - MGD +2008-05-28 + Fix crashing of PDFs in xpdf and ghostscript when two-byte characters are + used with Type 3 fonts - MGD -2008-05-28 Allow keyword args to configure widget properties as - requested in - \http://sourceforge.net/tracker/index.php?func=detail&aid=1866207&group_id=80706&atid=560722 - - JDH +2008-05-28 + Allow keyword args to configure widget properties as requested in + \http://sourceforge.net/tracker/index.php?func=detail&aid=1866207&group_id=80706&atid=560722 + - JDH -2008-05-28 Replaced '-' with u'\u2212' for minus sign as requested in - \http://sourceforge.net/tracker/index.php?func=detail&aid=1962574&group_id=80706&atid=560720 +2008-05-28 + Replaced '-' with u'\\u2212' for minus sign as requested in + \http://sourceforge.net/tracker/index.php?func=detail&aid=1962574&group_id=80706&atid=560720 -2008-05-28 zero width/height Rectangles no longer influence the - autoscaler. Useful for log histograms with empty bins - - JDH +2008-05-28 + zero width/height Rectangles no longer influence the autoscaler. Useful + for log histograms with empty bins - JDH -2008-05-28 Fix rendering of composite glyphs in Type 3 conversion - (particularly as evidenced in the Eunjin.ttf Korean font) - Thanks Jae-Joon Lee for finding this! +2008-05-28 + Fix rendering of composite glyphs in Type 3 conversion (particularly as + evidenced in the Eunjin.ttf Korean font) Thanks Jae-Joon Lee for finding + this! -2008-05-27 Rewrote the cm.ScalarMappable callback infrastructure to - use cbook.CallbackRegistry rather than custom callback - handling. Amy users of add_observer/notify of the - cm.ScalarMappable should uae the - cm.ScalarMappable.callbacksSM CallbackRegistry instead. JDH +2008-05-27 + Rewrote the cm.ScalarMappable callback infrastructure to use + cbook.CallbackRegistry rather than custom callback handling. Any users of + add_observer/notify of the cm.ScalarMappable should use the + cm.ScalarMappable.callbacksSM CallbackRegistry instead. JDH -2008-05-27 Fix TkAgg build on Ubuntu 8.04 (and hopefully a more - general solution for other platforms, too.) +2008-05-27 + Fix TkAgg build on Ubuntu 8.04 (and hopefully a more general solution for + other platforms, too.) -2008-05-24 Added PIL support for loading images to imread (if PIL is - available) - JDH +2008-05-24 + Added PIL support for loading images to imread (if PIL is available) - JDH -2008-05-23 Provided a function and a method for controlling the - plot color cycle. - EF +2008-05-23 + Provided a function and a method for controlling the plot color cycle. - EF -2008-05-23 Major revision of hist(). Can handle 2D arrays and create - stacked histogram plots; keyword 'width' deprecated and - rwidth (relative width) introduced; align='edge' changed - to center of bin - MM +2008-05-23 + Major revision of hist(). Can handle 2D arrays and create stacked histogram + plots; keyword 'width' deprecated and rwidth (relative width) introduced; + align='edge' changed to center of bin - MM -2008-05-22 Added support for ReST-based doumentation using Sphinx. - Documents are located in doc/, and are broken up into - a users guide and an API reference. To build, run the - make.py files. Sphinx-0.4 is needed to build generate xml, - which will be useful for rendering equations with mathml, - use sphinx from svn until 0.4 is released - DSD +2008-05-22 + Added support for ReST-based documentation using Sphinx. Documents are + located in doc/, and are broken up into a users guide and an API reference. + To build, run the make.py files. Sphinx-0.4 is needed to build generate + xml, which will be useful for rendering equations with mathml, use sphinx + from svn until 0.4 is released - DSD -2008-05-21 Fix segfault in TkAgg backend - MGD +2008-05-21 + Fix segfault in TkAgg backend - MGD -2008-05-21 Fix a "local variable unreferenced" bug in plotfile - MM +2008-05-21 + Fix a "local variable unreferenced" bug in plotfile - MM -2008-05-19 Fix crash when Windows can not access the registry to - determine font path [Bug 1966974, thanks Patrik Simons] - MGD +2008-05-19 + Fix crash when Windows can not access the registry to determine font path + [Bug 1966974, thanks Patrik Simons] - MGD -2008-05-16 removed some unneeded code w/ the python 2.4 requirement. - cbook no longer provides compatibility for reversed, - enumerate, set or izip. removed lib/subprocess, mpl1, - sandbox/units, and the swig code. This stuff should remain - on the maintenance branch for archival purposes. JDH +2008-05-16 + removed some unneeded code w/ the python 2.4 requirement. cbook no longer + provides compatibility for reversed, enumerate, set or izip. removed + lib/subprocess, mpl1, sandbox/units, and the swig code. This stuff should + remain on the maintenance branch for archival purposes. JDH -2008-05-16 Reorganized examples dir - JDH +2008-05-16 + Reorganized examples dir - JDH -2008-05-16 Added 'elinewidth' keyword arg to errorbar, based on patch - by Christopher Brown - MM +2008-05-16 + Added 'elinewidth' keyword arg to errorbar, based on patch by Christopher + Brown - MM -2008-05-16 Added 'cumulative' keyword arg to hist to plot cumulative - histograms. For normed hists, this is normalized to one - MM +2008-05-16 + Added 'cumulative' keyword arg to hist to plot cumulative histograms. For + normed hists, this is normalized to one - MM -2008-05-15 Fix Tk backend segfault on some machines - MGD +2008-05-15 + Fix Tk backend segfault on some machines - MGD -2008-05-14 Don't use stat on Windows (fixes font embedding problem) - MGD +2008-05-14 + Don't use stat on Windows (fixes font embedding problem) - MGD -2008-05-09 Fix /singlequote (') in Postscript backend - MGD +2008-05-09 + Fix /singlequote (') in Postscript backend - MGD -2008-05-08 Fix kerning in SVG when embedding character outlines - MGD +2008-05-08 + Fix kerning in SVG when embedding character outlines - MGD -2008-05-07 Switched to future numpy histogram semantic in hist - MM +2008-05-07 + Switched to future numpy histogram semantic in hist - MM -2008-05-06 Fix strange colors when blitting in QtAgg and Qt4Agg - MGD +2008-05-06 + Fix strange colors when blitting in QtAgg and Qt4Agg - MGD -2008-05-05 pass notify_axes_change to the figure's add_axobserver - in the qt backends, like we do for the other backends. - Thanks Glenn Jones for the report - DSD +2008-05-05 + pass notify_axes_change to the figure's add_axobserver in the qt backends, + like we do for the other backends. Thanks Glenn Jones for the report - DSD -2008-05-02 Added step histograms, based on patch by Erik Tollerud. - MM +2008-05-02 + Added step histograms, based on patch by Erik Tollerud. - MM -2008-05-02 On PyQt <= 3.14 there is no way to determine the underlying - Qt version. [1851364] - MGD +2008-05-02 + On PyQt <= 3.14 there is no way to determine the underlying Qt version. + [1851364] - MGD -2008-05-02 Don't call sys.exit() when pyemf is not found [1924199] - - MGD +2008-05-02 + Don't call sys.exit() when pyemf is not found [1924199] - MGD -2008-05-02 Update _subprocess.c from upstream Python 2.5.2 to get a - few memory and reference-counting-related bugfixes. See - bug 1949978. - MGD +2008-05-02 + Update _subprocess.c from upstream Python 2.5.2 to get a few memory and + reference-counting-related bugfixes. See bug 1949978. - MGD -2008-04-30 Added some record array editing widgets for gtk -- see - examples/rec_edit*.py - JDH +2008-04-30 + Added some record array editing widgets for gtk -- see + examples/rec_edit*.py - JDH -2008-04-29 Fix bug in mlab.sqrtm - MM +2008-04-29 + Fix bug in mlab.sqrtm - MM -2008-04-28 Fix bug in SVG text with Mozilla-based viewers (the symbol - tag is not supported) - MGD +2008-04-28 + Fix bug in SVG text with Mozilla-based viewers (the symbol tag is not + supported) - MGD -2008-04-27 Applied patch by Michiel de Hoon to add hexbin - axes method and pyplot function - EF +2008-04-27 + Applied patch by Michiel de Hoon to add hexbin axes method and pyplot + function - EF -2008-04-25 Enforce python >= 2.4; remove subprocess build - EF +2008-04-25 + Enforce python >= 2.4; remove subprocess build - EF -2008-04-25 Enforce the numpy requirement at build time - JDH +2008-04-25 + Enforce the numpy requirement at build time - JDH -2008-04-24 Make numpy 1.1 and python 2.3 required when importing - matplotlib - EF +2008-04-24 + Make numpy 1.1 and python 2.3 required when importing matplotlib - EF -2008-04-24 Fix compilation issues on VS2003 (Thanks Martin Spacek for - all the help) - MGD +2008-04-24 + Fix compilation issues on VS2003 (Thanks Martin Spacek for all the help) - + MGD -2008-04-24 Fix sub/superscripts when the size of the font has been - changed - MGD +2008-04-24 + Fix sub/superscripts when the size of the font has been changed - MGD -2008-04-22 Use "svg.embed_char_paths" consistently everywhere - MGD +2008-04-22 + Use "svg.embed_char_paths" consistently everywhere - MGD -2008-04-20 Add support to MaxNLocator for symmetric axis autoscaling. - EF +2008-04-20 + Add support to MaxNLocator for symmetric axis autoscaling. - EF -2008-04-20 Fix double-zoom bug. - MM +2008-04-20 + Fix double-zoom bug. - MM -2008-04-15 Speed up colormapping. - EF +2008-04-15 + Speed up colormapping. - EF -2008-04-12 Speed up zooming and panning of dense images. - EF +2008-04-12 + Speed up zooming and panning of dense images. - EF -2008-04-11 Fix global font rcParam setting after initialization - time. - MGD +2008-04-11 + Fix global font rcParam setting after initialization time. - MGD -2008-04-11 Revert commits 5002 and 5031, which were intended to - avoid an unnecessary call to draw(). 5002 broke saving - figures before show(). 5031 fixed the problem created in - 5002, but broke interactive plotting. Unnecessary call to - draw still needs resolution - DSD +2008-04-11 + Revert commits 5002 and 5031, which were intended to avoid an unnecessary + call to draw(). 5002 broke saving figures before show(). 5031 fixed the + problem created in 5002, but broke interactive plotting. Unnecessary call + to draw still needs resolution - DSD -2008-04-07 Improve color validation in rc handling, suggested - by Lev Givon - EF +2008-04-07 + Improve color validation in rc handling, suggested by Lev Givon - EF -2008-04-02 Allow to use both linestyle definition arguments, '-' and - 'solid' etc. in plots/collections - MM +2008-04-02 + Allow to use both linestyle definition arguments, '-' and 'solid' etc. in + plots/collections - MM -2008-03-27 Fix saving to Unicode filenames with Agg backend - (other backends appear to already work...) - (Thanks, Christopher Barker) - MGD +2008-03-27 + Fix saving to Unicode filenames with Agg backend (other backends appear to + already work...) (Thanks, Christopher Barker) - MGD -2008-03-26 Fix SVG backend bug that prevents copying and pasting in - Inkscape (thanks Kaushik Ghose) - MGD +2008-03-26 + Fix SVG backend bug that prevents copying and pasting in Inkscape (thanks + Kaushik Ghose) - MGD -2008-03-24 Removed an unnecessary call to draw() in the backend_qt* - mouseReleaseEvent. Thanks to Ted Drain - DSD +2008-03-24 + Removed an unnecessary call to draw() in the backend_qt* mouseReleaseEvent. + Thanks to Ted Drain - DSD -2008-03-23 Fix a pdf backend bug which sometimes caused the outermost - gsave to not be balanced with a grestore. - JKS +2008-03-23 + Fix a pdf backend bug which sometimes caused the outermost gsave to not be + balanced with a grestore. - JKS -2008-03-20 Fixed a minor bug in ContourSet._process_linestyles when - len(linestyles)==Nlev - MM +2008-03-20 + Fixed a minor bug in ContourSet._process_linestyles when + len(linestyles)==Nlev - MM -2008-03-19 Changed ma import statements to "from numpy import ma"; - this should work with past and future versions of - numpy, whereas "import numpy.ma as ma" will work only - with numpy >= 1.05, and "import numerix.npyma as ma" - is obsolete now that maskedarray is replacing the - earlier implementation, as of numpy 1.05. +2008-03-19 + Changed ma import statements to "from numpy import ma"; this should work + with past and future versions of numpy, whereas "import numpy.ma as ma" + will work only with numpy >= 1.05, and "import numerix.npyma as ma" is + obsolete now that maskedarray is replacing the earlier implementation, as + of numpy 1.05. -2008-03-14 Removed an apparently unnecessary call to - FigureCanvasAgg.draw in backend_qt*agg. Thanks to Ted - Drain - DSD +2008-03-14 + Removed an apparently unnecessary call to FigureCanvasAgg.draw in + backend_qt*agg. Thanks to Ted Drain - DSD -2008-03-10 Workaround a bug in backend_qt4agg's blitting due to a - buffer width/bbox width mismatch in _backend_agg's - copy_from_bbox - DSD +2008-03-10 + Workaround a bug in backend_qt4agg's blitting due to a buffer width/bbox + width mismatch in _backend_agg's copy_from_bbox - DSD -2008-02-29 Fix class Wx toolbar pan and zoom functions (Thanks Jeff - Peery) - MGD +2008-02-29 + Fix class Wx toolbar pan and zoom functions (Thanks Jeff Peery) - MGD -2008-02-16 Added some new rec array functionality to mlab - (rec_summarize, rec2txt and rec_groupby). See - examples/rec_groupby_demo.py. Thanks to Tim M for rec2txt. +2008-02-16 + Added some new rec array functionality to mlab (rec_summarize, rec2txt and + rec_groupby). See examples/rec_groupby_demo.py. Thanks to Tim M for + rec2txt. -2008-02-12 Applied Erik Tollerud's span selector patch - JDH +2008-02-12 + Applied Erik Tollerud's span selector patch - JDH -2008-02-11 Update plotting() doc string to refer to getp/setp. - JKS +2008-02-11 + Update plotting() doc string to refer to getp/setp. - JKS -2008-02-10 Fixed a problem with square roots in the pdf backend with - usetex. - JKS +2008-02-10 + Fixed a problem with square roots in the pdf backend with usetex. - JKS -2008-02-08 Fixed minor __str__ bugs so getp(gca()) works. - JKS +2008-02-08 + Fixed minor __str__ bugs so getp(gca()) works. - JKS -2008-02-05 Added getters for title, xlabel, ylabel, as requested - by Brandon Kieth - EF +2008-02-05 + Added getters for title, xlabel, ylabel, as requested by Brandon Kieth - EF -2008-02-05 Applied Gael's ginput patch and created - examples/ginput_demo.py - JDH +2008-02-05 + Applied Gael's ginput patch and created examples/ginput_demo.py - JDH -2008-02-03 Expose interpnames, a list of valid interpolation - methods, as an AxesImage class attribute. - EF +2008-02-03 + Expose interpnames, a list of valid interpolation methods, as an AxesImage + class attribute. - EF -2008-02-03 Added BoundaryNorm, with examples in colorbar_only.py - and image_masked.py. - EF +2008-02-03 + Added BoundaryNorm, with examples in colorbar_only.py and image_masked.py. + - EF -2008-02-03 Force dpi=72 in pdf backend to fix picture size bug. - JKS +2008-02-03 + Force dpi=72 in pdf backend to fix picture size bug. - JKS -2008-02-01 Fix doubly-included font problem in Postscript backend - MGD +2008-02-01 + Fix doubly-included font problem in Postscript backend - MGD -2008-02-01 Fix reference leak in ft2font Glyph objects. - MGD +2008-02-01 + Fix reference leak in ft2font Glyph objects. - MGD -2008-01-31 Don't use unicode strings with usetex by default - DSD +2008-01-31 + Don't use unicode strings with usetex by default - DSD -2008-01-31 Fix text spacing problems in PDF backend with *some* fonts, - such as STIXGeneral. +2008-01-31 + Fix text spacing problems in PDF backend with *some* fonts, such as + STIXGeneral. -2008-01-31 Fix \sqrt with radical number (broken by making [ and ] - work below) - MGD +2008-01-31 + Fix \sqrt with radical number (broken by making [ and ] work below) - MGD -2008-01-27 Applied Martin Teichmann's patch to improve the Qt4 - backend. Uses Qt's builtin toolbars and statusbars. - See bug 1828848 - DSD +2008-01-27 + Applied Martin Teichmann's patch to improve the Qt4 backend. Uses Qt's + builtin toolbars and statusbars. See bug 1828848 - DSD -2008-01-10 Moved toolkits to mpl_toolkits, made mpl_toolkits - a namespace package - JSWHIT +2008-01-10 + Moved toolkits to mpl_toolkits, made mpl_toolkits a namespace package - + JSWHIT -2008-01-10 Use setup.cfg to set the default parameters (tkagg, - numpy) when building windows installers - DSD +2008-01-10 + Use setup.cfg to set the default parameters (tkagg, numpy) when building + windows installers - DSD -2008-01-10 Fix bug displaying [ and ] in mathtext - MGD +2008-01-10 + Fix bug displaying [ and ] in mathtext - MGD -2008-01-10 Fix bug when displaying a tick value offset with scientific - notation. (Manifests itself as a warning that the \times - symbol can not be found). - MGD +2008-01-10 + Fix bug when displaying a tick value offset with scientific notation. + (Manifests itself as a warning that the \times symbol can not be found). - + MGD -2008-01-10 Use setup.cfg to set the default parameters (tkagg, - numpy) when building windows installers - DSD +2008-01-10 + Use setup.cfg to set the default parameters (tkagg, numpy) when building + windows installers - DSD -------------------- -2008-01-06 Released 0.91.2 at revision 4802 +2008-01-06 + Released 0.91.2 at revision 4802 -2007-12-26 Reduce too-late use of matplotlib.use() to a warning - instead of an exception, for backwards compatibility - EF +2007-12-26 + Reduce too-late use of matplotlib.use() to a warning instead of an + exception, for backwards compatibility - EF -2007-12-25 Fix bug in errorbar, identified by Noriko Minakawa - EF +2007-12-25 + Fix bug in errorbar, identified by Noriko Minakawa - EF -2007-12-25 Changed masked array importing to work with the upcoming - numpy 1.05 (now the maskedarray branch) as well as with - earlier versions. - EF +2007-12-25 + Changed masked array importing to work with the upcoming numpy 1.05 (now + the maskedarray branch) as well as with earlier versions. - EF -2007-12-16 rec2csv saves doubles without losing precision. Also, it - does not close filehandles passed in open. - JDH,ADS +2007-12-16 + rec2csv saves doubles without losing precision. Also, it does not close + filehandles passed in open. - JDH,ADS -2007-12-13 Moved rec2gtk to matplotlib.toolkits.gtktools and rec2excel - to matplotlib.toolkits.exceltools - JDH +2007-12-13 + Moved rec2gtk to matplotlib.toolkits.gtktools and rec2excel to + matplotlib.toolkits.exceltools - JDH -2007-12-12 Support alpha-blended text in the Agg and Svg backends - - MGD +2007-12-12 + Support alpha-blended text in the Agg and Svg backends - MGD -2007-12-10 Fix SVG text rendering bug. - MGD +2007-12-10 + Fix SVG text rendering bug. - MGD -2007-12-10 Increase accuracy of circle and ellipse drawing by using an - 8-piece bezier approximation, rather than a 4-piece one. - Fix PDF, SVG and Cairo backends so they can draw paths - (meaning ellipses as well). - MGD +2007-12-10 + Increase accuracy of circle and ellipse drawing by using an 8-piece bezier + approximation, rather than a 4-piece one. Fix PDF, SVG and Cairo backends + so they can draw paths (meaning ellipses as well). - MGD -2007-12-07 Issue a warning when drawing an image on a non-linear axis. - MGD +2007-12-07 + Issue a warning when drawing an image on a non-linear axis. - MGD -2007-12-06 let widgets.Cursor initialize to the lower x and y bounds - rather than 0,0, which can cause havoc for dates and other - transforms - DSD +2007-12-06 + let widgets.Cursor initialize to the lower x and y bounds rather than 0,0, + which can cause havoc for dates and other transforms - DSD -2007-12-06 updated references to mpl data directories for py2exe - DSD +2007-12-06 + updated references to mpl data directories for py2exe - DSD -2007-12-06 fixed a bug in rcsetup, see bug 1845057 - DSD +2007-12-06 + fixed a bug in rcsetup, see bug 1845057 - DSD -2007-12-05 Fix how fonts are cached to avoid loading the same one multiple times. - (This was a regression since 0.90 caused by the refactoring of - font_manager.py) - MGD +2007-12-05 + Fix how fonts are cached to avoid loading the same one multiple times. + (This was a regression since 0.90 caused by the refactoring of + font_manager.py) - MGD -2007-12-05 Support arbitrary rotation of usetex text in Agg backend. - MGD +2007-12-05 + Support arbitrary rotation of usetex text in Agg backend. - MGD -2007-12-04 Support '|' as a character in mathtext - MGD +2007-12-04 + Support '|' as a character in mathtext - MGD ----------------------------------------------------- -2007-11-27 Released 0.91.1 at revision 4517 +2007-11-27 + Released 0.91.1 at revision 4517 ----------------------------------------------------- -2007-11-27 Released 0.91.0 at revision 4478 - -2007-11-13 All backends now support writing to a file-like object, not - just a regular file. savefig() can be passed a file-like - object in place of a file path. - MGD - -2007-11-13 Improved the default backend selection at build time: - SVG -> Agg -> TkAgg -> WXAgg -> GTK -> GTKAgg. The last usable - backend in this progression will be chosen in the default - config file. If a backend is defined in setup.cfg, that will - be the default backend - DSD - -2007-11-13 Improved creation of default config files at build time for - traited config package - DSD - -2007-11-12 Exposed all the build options in setup.cfg. These options are - read into a dict called "options" by setupext.py. Also, added - "-mpl" tags to the version strings for packages provided by - matplotlib. Versions provided by mpl will be identified and - updated on subsequent installs - DSD - -2007-11-12 Added support for STIX fonts. A new rcParam, - mathtext.fontset, can be used to choose between: - - 'cm': - The TeX/LaTeX Computer Modern fonts - - 'stix': - The STIX fonts (see stixfonts.org) - - 'stixsans': - The STIX fonts, using sans-serif glyphs by default - - 'custom': - A generic Unicode font, in which case the mathtext font - must be specified using mathtext.bf, mathtext.it, - mathtext.sf etc. - - Added a new example, stix_fonts_demo.py to show how to access - different fonts and unusual symbols. - - - MGD - -2007-11-12 Options to disable building backend extension modules moved - from setup.py to setup.cfg - DSD - -2007-11-09 Applied Martin Teichmann's patch 1828813: a QPainter is used in - paintEvent, which has to be destroyed using the method end(). If - matplotlib raises an exception before the call to end - and it - does if you feed it with bad data - this method end() is never - called and Qt4 will start spitting error messages - -2007-11-09 Moved pyparsing back into matplotlib namespace. Don't use - system pyparsing, API is too variable from one release - to the next - DSD - -2007-11-08 Made pylab use straight numpy instead of oldnumeric - by default - EF - -2007-11-08 Added additional record array utilities to mlab (rec2excel, - rec2gtk, rec_join, rec_append_field, rec_drop_field) - JDH +2007-11-27 + Released 0.91.0 at revision 4478 + +2007-11-13 + All backends now support writing to a file-like object, not just a regular + file. savefig() can be passed a file-like object in place of a file path. + - MGD + +2007-11-13 + Improved the default backend selection at build time: SVG -> Agg -> TkAgg + -> WXAgg -> GTK -> GTKAgg. The last usable backend in this progression will + be chosen in the default config file. If a backend is defined in setup.cfg, + that will be the default backend - DSD + +2007-11-13 + Improved creation of default config files at build time for traited config + package - DSD + +2007-11-12 + Exposed all the build options in setup.cfg. These options are read into a + dict called "options" by setupext.py. Also, added "-mpl" tags to the + version strings for packages provided by matplotlib. Versions provided by + mpl will be identified and updated on subsequent installs - DSD + +2007-11-12 + Added support for STIX fonts. A new rcParam, mathtext.fontset, can be used + to choose between: + + 'cm' + The TeX/LaTeX Computer Modern fonts + 'stix' + The STIX fonts (see stixfonts.org) + 'stixsans' + The STIX fonts, using sans-serif glyphs by default + 'custom' + A generic Unicode font, in which case the mathtext font must be + specified using mathtext.bf, mathtext.it, mathtext.sf etc. + + Added a new example, stix_fonts_demo.py to show how to access different + fonts and unusual symbols. - MGD + +2007-11-12 + Options to disable building backend extension modules moved from setup.py + to setup.cfg - DSD + +2007-11-09 + Applied Martin Teichmann's patch 1828813: a QPainter is used in paintEvent, + which has to be destroyed using the method end(). If matplotlib raises an + exception before the call to end - and it does if you feed it with bad data + - this method end() is never called and Qt4 will start spitting error + messages + +2007-11-09 + Moved pyparsing back into matplotlib namespace. Don't use system pyparsing, + API is too variable from one release to the next - DSD + +2007-11-08 + Made pylab use straight numpy instead of oldnumeric by default - EF + +2007-11-08 + Added additional record array utilities to mlab (rec2excel, rec2gtk, + rec_join, rec_append_field, rec_drop_field) - JDH + +2007-11-08 + Updated pytz to version 2007g - DSD -2007-11-08 Updated pytz to version 2007g - DSD +2007-11-08 + Updated pyparsing to version 1.4.8 - DSD + +2007-11-08 + Moved csv2rec to recutils and added other record array utilities - JDH + +2007-11-08 + If available, use existing pyparsing installation - DSD + +2007-11-07 + Removed old enthought.traits from lib/matplotlib, added Gael Varoquaux's + enthought.traits-2.6b1, which is stripped of setuptools. The package is + installed to site-packages if not already available - DSD + +2007-11-05 + Added easy access to minor tick properties; slight mod of patch by Pierre + G-M - EF + +2007-11-02 + Committed Phil Thompson's patch 1599876, fixes to Qt4Agg backend and qt4 + blitting demo - DSD + +2007-11-02 + Committed Phil Thompson's patch 1599876, fixes to Qt4Agg backend and qt4 + blitting demo - DSD + +2007-10-31 + Made log color scale easier to use with contourf; automatic level + generation now works. - EF + +2007-10-29 + TRANSFORMS REFACTORING + + The primary goal of this refactoring was to make it easier to extend + matplotlib to support new kinds of projections. This is primarily an + internal improvement, and the possible user-visible changes it allows are + yet to come. + + The transformation framework was completely rewritten in Python (with + Numpy). This will make it easier to add news kinds of transformations + without writing C/C++ code. + + Transforms are composed into a 'transform tree', made of transforms whose + value depends on other transforms (their children). When the contents of + children change, their parents are automatically updated to reflect those + changes. To do this an "invalidation" method is used: when children + change, all of their ancestors are marked as "invalid". When the value of + a transform is accessed at a later time, its value is recomputed only if it + is invalid, otherwise a cached value may be used. This prevents + unnecessary recomputations of transforms, and contributes to better + interactive performance. + + The framework can be used for both affine and non-affine transformations. + However, for speed, we want use the backend renderers to perform affine + transformations whenever possible. Therefore, it is possible to perform + just the affine or non-affine part of a transformation on a set of data. + The affine is always assumed to occur after the non-affine. For any + transform:: + + full transform == non-affine + affine + + Much of the drawing has been refactored in terms of compound paths. + Therefore, many methods have been removed from the backend interface and + replaced with a handful to draw compound paths. This will make updating + the backends easier, since there is less to update. It also should make + the backends more consistent in terms of functionality. + + User visible changes: + + - POLAR PLOTS: Polar plots are now interactively zoomable, and the r-axis + labels can be interactively rotated. Straight line segments are now + interpolated to follow the curve of the r-axis. + + - Non-rectangular clipping works in more backends and with more types of + objects. + + - Sharing an axis across figures is now done in exactly the same way as + sharing an axis between two axes in the same figure:: + + fig1 = figure() + fig2 = figure() + + ax1 = fig1.add_subplot(111) + ax2 = fig2.add_subplot(111, sharex=ax1, sharey=ax1) -2007-11-08 Updated pyparsing to version 1.4.8 - DSD + - linestyles now include steps-pre, steps-post and steps-mid. The old step + still works and is equivalent to step-pre. -2007-11-08 Moved csv2rec to recutils and added other record array - utilities - JDH + - Multiple line styles may be provided to a collection. -2007-11-08 If available, use existing pyparsing installation - DSD + See API_CHANGES for more low-level information about this refactoring. -2007-11-07 Removed old enthought.traits from lib/matplotlib, added - Gael Varoquaux's enthought.traits-2.6b1, which is stripped - of setuptools. The package is installed to site-packages - if not already available - DSD +2007-10-24 + Added ax kwarg to Figure.colorbar and pyplot.colorbar - EF -2007-11-05 Added easy access to minor tick properties; slight mod - of patch by Pierre G-M - EF +2007-10-19 + Removed a gsave/grestore pair surrounding _draw_ps, which was causing a + loss graphics state info (see "EPS output problem - scatter & edgecolors" + on mpl-dev, 2007-10-29) - DSD -2007-11-02 Committed Phil Thompson's patch 1599876, fixes to Qt4Agg - backend and qt4 blitting demo - DSD +2007-10-15 + Fixed a bug in patches.Ellipse that was broken for aspect='auto'. Scale + free ellipses now work properly for equal and auto on Agg and PS, and they + fall back on a polygonal approximation for nonlinear transformations until + we convince ourselves that the spline approximation holds for nonlinear + transformations. Added unit/ellipse_compare.py to compare spline with + vertex approx for both aspects. JDH + +2007-10-05 + remove generator expressions from texmanager and mpltraits. generator + expressions are not supported by python-2.3 - DSD + +2007-10-01 + Made matplotlib.use() raise an exception if called after backends has been + imported. - EF + +2007-09-30 + Modified update* methods of Bbox and Interval so they work with reversed + axes. Prior to this, trying to set the ticks on a reversed axis failed + with an uninformative error message. - EF + +2007-09-30 + Applied patches to axes3d to fix index error problem - EF + +2007-09-24 + Applied Eike Welk's patch reported on mpl-dev on 2007-09-22 Fixes a bug + with multiple plot windows in the qt backend, ported the changes to + backend_qt4 as well - DSD + +2007-09-21 + Changed cbook.reversed to yield the same result as the python reversed + builtin - DSD + +2007-09-13 + The usetex support in the pdf backend is more usable now, so I am enabling + it. - JKS + +2007-09-12 + Fixed a Axes.bar unit bug - JDH + +2007-09-10 + Made skiprows=1 the default on csv2rec - JDH + +2007-09-09 + Split out the plotting part of pylab and put it in pyplot.py; removed + numerix from the remaining pylab.py, which imports everything from + pyplot.py. The intention is that apart from cleanups, the result of + importing from pylab is nearly unchanged, but there is the new alternative + of importing from pyplot to get the state-engine graphics without all the + numeric functions. Numpified examples; deleted two that were obsolete; + modified some to use pyplot. - EF -2007-11-02 Committed Phil Thompson's patch 1599876, fixes to Qt4Agg - backend and qt4 blitting demo - DSD +2007-09-08 + Eliminated gd and paint backends - EF -2007-10-31 Made log color scale easier to use with contourf; - automatic level generation now works. - EF +2007-09-06 + .bmp file format is now longer an alias for .raw -2007-10-29 TRANSFORMS REFACTORING +2007-09-07 + Added clip path support to pdf backend. - JKS - The primary goal of this refactoring was to make it easier - to extend matplotlib to support new kinds of projections. - This is primarily an internal improvement, and the possible - user-visible changes it allows are yet to come. +2007-09-06 + Fixed a bug in the embedding of Type 1 fonts in PDF. Now it doesn't crash + Preview.app. - JKS - The transformation framework was completely rewritten in - Python (with Numpy). This will make it easier to add news - kinds of transformations without writing C/C++ code. +2007-09-06 + Refactored image saving code so that all GUI backends can save most image + types. See FILETYPES for a matrix of backends and their supported file + types. Backend canvases should no longer write their own print_figure() + method -- instead they should write a print_xxx method for each filetype + they can output and add an entry to their class-scoped filetypes + dictionary. - MGD - Transforms are composed into a 'transform tree', made of - transforms whose value depends on other transforms (their - children). When the contents of children change, their - parents are automatically updated to reflect those changes. - To do this an "invalidation" method is used: when children - change, all of their ancestors are marked as "invalid". - When the value of a transform is accessed at a later time, - its value is recomputed only if it is invalid, otherwise a - cached value may be used. This prevents unnecessary - recomputations of transforms, and contributes to better - interactive performance. +2007-09-05 + Fixed Qt version reporting in setupext.py - DSD - The framework can be used for both affine and non-affine - transformations. However, for speed, we want use the - backend renderers to perform affine transformations - whenever possible. Therefore, it is possible to perform - just the affine or non-affine part of a transformation on a - set of data. The affine is always assumed to occur after - the non-affine. For any transform: +2007-09-04 + Embedding Type 1 fonts in PDF, and thus usetex support via dviread, sort of + works. To test, enable it by renaming _draw_tex to draw_tex. - JKS - full transform == non-affine + affine +2007-09-03 + Added ability of errorbar show limits via caret or arrowhead ends on the + bars; patch by Manual Metz. - EF - Much of the drawing has been refactored in terms of - compound paths. Therefore, many methods have been removed - from the backend interface and replaced with a a handful to - draw compound paths. This will make updating the backends - easier, since there is less to update. It also should make - the backends more consistent in terms of functionality. +2007-09-03 + Created type1font.py, added features to AFM and FT2Font (see API_CHANGES), + started work on embedding Type 1 fonts in pdf files. - JKS - User visible changes: +2007-09-02 + Continued work on dviread.py. - JKS - - POLAR PLOTS: Polar plots are now interactively zoomable, - and the r-axis labels can be interactively rotated. - Straight line segments are now interpolated to follow the - curve of the r-axis. +2007-08-16 + Added a set_extent method to AxesImage, allow data extent to be modified + after initial call to imshow - DSD - - Non-rectangular clipping works in more backends and with - more types of objects. +2007-08-14 + Fixed a bug in pyqt4 subplots-adjust. Thanks to Xavier Gnata for the report + and suggested fix - DSD - - Sharing an axis across figures is now done in exactly - the same way as sharing an axis between two axes in the - same figure:: +2007-08-13 + Use pickle to cache entire fontManager; change to using font_manager + module-level function findfont wrapper for the fontManager.findfont method + - EF - fig1 = figure() - fig2 = figure() +2007-08-11 + Numpification and cleanup of mlab.py and some examples - EF - ax1 = fig1.add_subplot(111) - ax2 = fig2.add_subplot(111, sharex=ax1, sharey=ax1) +2007-08-06 + Removed mathtext2 - - linestyles now include steps-pre, steps-post and - steps-mid. The old step still works and is equivalent to - step-pre. +2007-07-31 + Refactoring of distutils scripts. - - Multiple line styles may be provided to a collection. + - Will not fail on the entire build if an optional Python package (e.g., + Tkinter) is installed but its development headers are not (e.g., + tk-devel). Instead, it will continue to build all other extensions. + - Provide an overview at the top of the output to display what dependencies + and their versions were found, and (by extension) what will be built. + - Use pkg-config, when available, to find freetype2, since this was broken + on Mac OS-X when using MacPorts in a non- standard location. - See API_CHANGES for more low-level information about this - refactoring. +2007-07-30 + Reorganized configuration code to work with traited config objects. The new + config system is located in the matplotlib.config package, but it is + disabled by default. To enable it, set NEWCONFIG=True in + matplotlib.__init__.py. The new configuration system will still use the + old matplotlibrc files by default. To switch to the experimental, traited + configuration, set USE_TRAITED_CONFIG=True in config.__init__.py. -2007-10-24 Added ax kwarg to Figure.colorbar and pyplot.colorbar - EF +2007-07-29 + Changed default pcolor shading to flat; added aliases to make collection + kwargs agree with setter names, so updating works; related minor cleanups. + Removed quiver_classic, scatter_classic, pcolor_classic. - EF -2007-10-19 Removed a gsave/grestore pair surrounding _draw_ps, which - was causing a loss graphics state info (see "EPS output - problem - scatter & edgecolors" on mpl-dev, 2007-10-29) - - DSD +2007-07-26 + Major rewrite of mathtext.py, using the TeX box layout model. -2007-10-15 Fixed a bug in patches.Ellipse that was broken for - aspect='auto'. Scale free ellipses now work properly for - equal and auto on Agg and PS, and they fall back on a - polygonal approximation for nonlinear transformations until - we convince oursleves that the spline approximation holds - for nonlinear transformations. Added - unit/ellipse_compare.py to compare spline with vertex - approx for both aspects. JDH + There is one (known) backward incompatible change. The font commands + (\cal, \rm, \it, \tt) now behave as TeX does: they are in effect until the + next font change command or the end of the grouping. Therefore uses of + $\cal{R}$ should be changed to ${\cal R}$. Alternatively, you may use the + new LaTeX-style font commands (\mathcal, \mathrm, \mathit, \mathtt) which + do affect the following group, e.g., $\mathcal{R}$. -2007-10-05 remove generator expressions from texmanager and mpltraits. - generator expressions are not supported by python-2.3 - DSD + Other new features include: -2007-10-01 Made matplotlib.use() raise an exception if called after - backends has been imported. - EF + - Math may be interspersed with non-math text. Any text with an even + number of $'s (non-escaped) will be sent to the mathtext parser for + layout. -2007-09-30 Modified update* methods of Bbox and Interval so they - work with reversed axes. Prior to this, trying to - set the ticks on a reversed axis failed with an - uninformative error message. - EF - -2007-09-30 Applied patches to axes3d to fix index error problem - EF - -2007-09-24 Applied Eike Welk's patch reported on mpl-dev on 2007-09-22 - Fixes a bug with multiple plot windows in the qt backend, - ported the changes to backend_qt4 as well - DSD - -2007-09-21 Changed cbook.reversed to yield the same result as the - python reversed builtin - DSD - -2007-09-13 The usetex support in the pdf backend is more usable now, - so I am enabling it. - JKS - -2007-09-12 Fixed a Axes.bar unit bug - JDH - -2007-09-10 Made skiprows=1 the default on csv2rec - JDH - -2007-09-09 Split out the plotting part of pylab and put it in - pyplot.py; removed numerix from the remaining pylab.py, - which imports everything from pyplot.py. The intention - is that apart from cleanups, the result of importing - from pylab is nearly unchanged, but there is the - new alternative of importing from pyplot to get - the state-engine graphics without all the numeric - functions. - Numpified examples; deleted two that were obsolete; - modified some to use pyplot. - EF + - Sub/superscripts are less likely to accidentally overlap. -2007-09-08 Eliminated gd and paint backends - EF + - Support for sub/superscripts in either order, e.g., $x^i_j$ and $x_j^i$ + are equivalent. -2007-09-06 .bmp file format is now longer an alias for .raw + - Double sub/superscripts (e.g., $x_i_j$) are considered ambiguous and + raise an exception. Use braces to disambiguate. -2007-09-07 Added clip path support to pdf backend. - JKS + - $\frac{x}{y}$ can be used for displaying fractions. -2007-09-06 Fixed a bug in the embedding of Type 1 fonts in PDF. - Now it doesn't crash Preview.app. - JKS + - $\sqrt[3]{x}$ can be used to display the radical symbol with a root + number and body. -2007-09-06 Refactored image saving code so that all GUI backends can - save most image types. See FILETYPES for a matrix of - backends and their supported file types. - Backend canvases should no longer write their own print_figure() - method -- instead they should write a print_xxx method for - each filetype they can output and add an entry to their - class-scoped filetypes dictionary. - MGD + - $\left(\frac{x}{y}\right)$ may be used to create parentheses and other + delimiters that automatically resize to the height of their contents. -2007-09-05 Fixed Qt version reporting in setupext.py - DSD + - Spacing around operators etc. is now generally more like TeX. -2007-09-04 Embedding Type 1 fonts in PDF, and thus usetex support - via dviread, sort of works. To test, enable it by - renaming _draw_tex to draw_tex. - JKS + - Added support (and fonts) for boldface (\bf) and sans-serif (\sf) + symbols. -2007-09-03 Added ability of errorbar show limits via caret or - arrowhead ends on the bars; patch by Manual Metz. - EF + - Log-like function name shortcuts are supported. For example, $\sin(x)$ + may be used instead of ${\rm sin}(x)$ -2007-09-03 Created type1font.py, added features to AFM and FT2Font - (see API_CHANGES), started work on embedding Type 1 fonts - in pdf files. - JKS + - Limited use of kerning for the easy case (same font) -2007-09-02 Continued work on dviread.py. - JKS + Behind the scenes, the pyparsing.py module used for doing the math parsing + was updated to the latest stable version (1.4.6). A lot of duplicate code + was refactored out of the Font classes. -2007-08-16 Added a set_extent method to AxesImage, allow data extent - to be modified after initial call to imshow - DSD + - MGD -2007-08-14 Fixed a bug in pyqt4 subplots-adjust. Thanks to - Xavier Gnata for the report and suggested fix - DSD +2007-07-19 + completed numpification of most trivial cases - NN -2007-08-13 Use pickle to cache entire fontManager; change to using - font_manager module-level function findfont wrapper for - the fontManager.findfont method - EF +2007-07-19 + converted non-numpy relicts throughout the code - NN -2007-08-11 Numpification and cleanup of mlab.py and some examples - EF +2007-07-19 + replaced the Python code in numerix/ by a minimal wrapper around numpy that + explicitly mentions all symbols that need to be addressed for further + numpification - NN -2007-08-06 Removed mathtext2 +2007-07-18 + make usetex respect changes to rcParams. texmanager used to only configure + itself when it was created, now it reconfigures when rcParams are changed. + Thank you Alexander Schmolck for contributing a patch - DSD -2007-07-31 Refactoring of distutils scripts. - - Will not fail on the entire build if an optional Python - package (e.g., Tkinter) is installed but its development - headers are not (e.g., tk-devel). Instead, it will - continue to build all other extensions. - - Provide an overview at the top of the output to display - what dependencies and their versions were found, and (by - extension) what will be built. - - Use pkg-config, when available, to find freetype2, since - this was broken on Mac OS-X when using MacPorts in a non- - standard location. +2007-07-17 + added validation to setting and changing rcParams - DSD -2007-07-30 Reorganized configuration code to work with traited config - objects. The new config system is located in the - matplotlib.config package, but it is disabled by default. - To enable it, set NEWCONFIG=True in matplotlib.__init__.py. - The new configuration system will still use the old - matplotlibrc files by default. To switch to the experimental, - traited configuration, set USE_TRAITED_CONFIG=True in - config.__init__.py. +2007-07-17 + bugfix segfault in transforms module. Thanks Ben North for the patch. - ADS -2007-07-29 Changed default pcolor shading to flat; added aliases - to make collection kwargs agree with setter names, so - updating works; related minor cleanups. - Removed quiver_classic, scatter_classic, pcolor_classic. - EF +2007-07-16 + clean up some code in ticker.ScalarFormatter, use unicode to render + multiplication sign in offset ticklabel - DSD -2007-07-26 Major rewrite of mathtext.py, using the TeX box layout model. +2007-07-16 + fixed a formatting bug in ticker.ScalarFormatter's scientific notation + (10^0 was being rendered as 10 in some cases) - DSD - There is one (known) backward incompatible change. The - font commands (\cal, \rm, \it, \tt) now behave as TeX does: - they are in effect until the next font change command or - the end of the grouping. Therefore uses of $\cal{R}$ - should be changed to ${\cal R}$. Alternatively, you may - use the new LaTeX-style font commands (\mathcal, \mathrm, - \mathit, \mathtt) which do affect the following group, - e.g., $\mathcal{R}$. +2007-07-13 + Add MPL_isfinite64() and MPL_isinf64() for testing doubles in (the now + misnamed) MPL_isnan.h. - ADS - Other new features include: +2007-07-13 + The matplotlib._isnan module removed (use numpy.isnan) - ADS - - Math may be interspersed with non-math text. Any text - with an even number of $'s (non-escaped) will be sent to - the mathtext parser for layout. +2007-07-13 + Some minor cleanups in _transforms.cpp - ADS - - Sub/superscripts are less likely to accidentally overlap. +2007-07-13 + Removed the rest of the numerix extension code detritus, numpified axes.py, + and cleaned up the imports in axes.py - JDH - - Support for sub/superscripts in either order, e.g., $x^i_j$ - and $x_j^i$ are equivalent. +2007-07-13 + Added legend.loc as configurable option that could in future default to + 'best'. - NN - - Double sub/superscripts (e.g., $x_i_j$) are considered - ambiguous and raise an exception. Use braces to disambiguate. +2007-07-12 + Bugfixes in mlab.py to coerce inputs into numpy arrays. -ADS - - $\frac{x}{y}$ can be used for displaying fractions. +2007-07-11 + Added linespacing kwarg to text.Text - EF - - $\sqrt[3]{x}$ can be used to display the radical symbol - with a root number and body. +2007-07-11 + Added code to store font paths in SVG files. - MGD - - $\left(\frac{x}{y}\right)$ may be used to create - parentheses and other delimiters that automatically - resize to the height of their contents. +2007-07-10 + Store subset of TTF font as a Type 3 font in PDF files. - MGD - - Spacing around operators etc. is now generally more like - TeX. +2007-07-09 + Store subset of TTF font as a Type 3 font in PS files. - MGD - - Added support (and fonts) for boldface (\bf) and - sans-serif (\sf) symbols. +2007-07-09 + Applied Paul's pick restructure pick and add pickers, sourceforge patch + 1749829 - JDH - - Log-like function name shortcuts are supported. For - example, $\sin(x)$ may be used instead of ${\rm sin}(x)$ +2007-07-09 + Applied Allan's draw_lines agg optimization. JDH - - Limited use of kerning for the easy case (same font) +2007-07-08 + Applied Carl Worth's patch to fix cairo draw_arc - SC - Behind the scenes, the pyparsing.py module used for doing - the math parsing was updated to the latest stable version - (1.4.6). A lot of duplicate code was refactored out of the - Font classes. +2007-07-07 + fixed bug 1712099: xpdf distiller on windows - DSD - - MGD +2007-06-30 + Applied patches to tkagg, gtk, and wx backends to reduce memory leakage. + Patches supplied by Mike Droettboom; see tracker numbers 1745400, 1745406, + 1745408. Also made unit/memleak_gui.py more flexible with command-line + options. - EF -2007-07-19 completed numpification of most trivial cases - NN +2007-06-30 + Split defaultParams into separate file rcdefaults (together with validation + code). Some heavy refactoring was necessary to do so, but the overall + behavior should be the same as before. - NN -2007-07-19 converted non-numpy relicts throughout the code - NN +2007-06-27 + Added MPLCONFIGDIR for the default location for mpl data and configuration. + useful for some apache installs where HOME is not writable. Tried to clean + up the logic in _get_config_dir to support non-writable HOME where are + writable HOME/.matplotlib already exists - JDH -2007-07-19 replaced the Python code in numerix/ by a minimal wrapper around - numpy that explicitly mentions all symbols that need to be - addressed for further numpification - NN +2007-06-27 + Fixed locale bug reported at + \http://sourceforge.net/tracker/index.php?func=detail&aid=1744154&group_id=80706&atid=560720 + by adding a cbook.unicode_safe function - JDH -2007-07-18 make usetex respect changes to rcParams. texmanager used to - only configure itself when it was created, now it - reconfigures when rcParams are changed. Thank you Alexander - Schmolck for contributing a patch - DSD +2007-06-27 + Applied Micheal's tk savefig bugfix described at + \http://sourceforge.net/tracker/index.php?func=detail&aid=1716732&group_id=80706&atid=560720 + Thanks Michael! -2007-07-17 added validation to setting and changing rcParams - DSD +2007-06-27 + Patch for get_py2exe_datafiles() to work with new directory layout. (Thanks + Tocer and also Werner Bruhin.) -ADS -2007-07-17 bugfix segfault in transforms module. Thanks Ben North for - the patch. - ADS +2007-06-27 + Added a scroll event to the mpl event handling system and implemented it + for backends GTK* -- other backend users/developers/maintainers, please add + support for your backend. - JDH -2007-07-16 clean up some code in ticker.ScalarFormatter, use unicode to - render multiplication sign in offset ticklabel - DSD +2007-06-25 + Changed default to clip=False in colors.Normalize; modified ColorbarBase + for easier colormap display - EF -2007-07-16 fixed a formatting bug in ticker.ScalarFormatter's scientific - notation (10^0 was being rendered as 10 in some cases) - DSD +2007-06-13 + Added maskedarray option to rc, numerix - EF -2007-07-13 Add MPL_isfinite64() and MPL_isinf64() for testing - doubles in (the now misnamed) MPL_isnan.h. - ADS +2007-06-11 + Python 2.5 compatibility fix for mlab.py - EF -2007-07-13 The matplotlib._isnan module removed (use numpy.isnan) - ADS +2007-06-10 + In matplotlibrc file, use 'dashed' | 'solid' instead of a pair of floats + for contour.negative_linestyle - EF -2007-07-13 Some minor cleanups in _transforms.cpp - ADS +2007-06-08 + Allow plot and fill fmt string to be any mpl string colorspec - EF -2007-07-13 Removed the rest of the numerix extension code detritus, - numpified axes.py, and cleaned up the imports in axes.py - - JDH +2007-06-08 + Added gnuplot file plotfile function to pylab -- see + examples/plotfile_demo.py - JDH -2007-07-13 Added legend.loc as configurable option that could in - future default to 'best'. - NN +2007-06-07 + Disable build of numarray and Numeric extensions for internal MPL use and + the numerix layer. - ADS -2007-07-12 Bugfixes in mlab.py to coerce inputs into numpy arrays. -ADS +2007-06-07 + Added csv2rec to matplotlib.mlab to support automatically converting csv + files to record arrays using type introspection, and turned on native + datetime support using the new units support in matplotlib.dates. See + examples/loadrec.py ! JDH -2007-07-11 Added linespacing kwarg to text.Text - EF +2007-06-07 + Simplified internal code of _auto_legend_data - NN -2007-07-11 Added code to store font paths in SVG files. - MGD +2007-06-04 + Added labeldistance arg to Axes.pie to control the raidal distance of the + wedge labels - JDH -2007-07-10 Store subset of TTF font as a Type 3 font in PDF files. - MGD - -2007-07-09 Store subset of TTF font as a Type 3 font in PS files. - MGD - -2007-07-09 Applied Paul's pick restructure pick and add pickers, - sourceforge patch 1749829 - JDH - - -2007-07-09 Applied Allan's draw_lines agg optimization. JDH - - -2007-07-08 Applied Carl Worth's patch to fix cairo draw_arc - SC - -2007-07-07 fixed bug 1712099: xpdf distiller on windows - DSD - -2007-06-30 Applied patches to tkagg, gtk, and wx backends to reduce - memory leakage. Patches supplied by Mike Droettboom; - see tracker numbers 1745400, 1745406, 1745408. - Also made unit/memleak_gui.py more flexible with - command-line options. - EF - -2007-06-30 Split defaultParams into separate file rcdefaults (together with - validation code). Some heavy refactoring was necessary to do so, - but the overall behavior should be the same as before. - NN - -2007-06-27 Added MPLCONFIGDIR for the default location for mpl data - and configuration. useful for some apache installs where - HOME is not writable. Tried to clean up the logic in - _get_config_dir to support non-writable HOME where are - writable HOME/.matplotlib already exists - JDH - -2007-06-27 Fixed locale bug reported at - \http://sourceforge.net/tracker/index.php?func=detail&aid=1744154&group_id=80706&atid=560720 - by adding a cbook.unicode_safe function - JDH - -2007-06-27 Applied Micheal's tk savefig bugfix described at - \http://sourceforge.net/tracker/index.php?func=detail&aid=1716732&group_id=80706&atid=560720 - Thanks Michael! - - -2007-06-27 Patch for get_py2exe_datafiles() to work with new directory - layout. (Thanks Tocer and also Werner Bruhin.) -ADS - - -2007-06-27 Added a scroll event to the mpl event handling system and - implemented it for backends GTK* -- other backend - users/developers/maintainers, please add support for your - backend. - JDH - -2007-06-25 Changed default to clip=False in colors.Normalize; - modified ColorbarBase for easier colormap display - EF - -2007-06-13 Added maskedarray option to rc, numerix - EF - -2007-06-11 Python 2.5 compatibility fix for mlab.py - EF - -2007-06-10 In matplotlibrc file, use 'dashed' | 'solid' instead - of a pair of floats for contour.negative_linestyle - EF - -2007-06-08 Allow plot and fill fmt string to be any mpl string - colorspec - EF - -2007-06-08 Added gnuplot file plotfile function to pylab -- see - examples/plotfile_demo.py - JDH - -2007-06-07 Disable build of numarray and Numeric extensions for - internal MPL use and the numerix layer. - ADS - -2007-06-07 Added csv2rec to matplotlib.mlab to support automatically - converting csv files to record arrays using type - introspection, and turned on native datetime support using - the new units support in matplotlib.dates. See - examples/loadrec.py ! JDH - -2007-06-07 Simplified internal code of _auto_legend_data - NN - -2007-06-04 Added labeldistance arg to Axes.pie to control the raidal - distance of the wedge labels - JDH - -2007-06-03 Turned mathtext in SVG into single with multiple - objects (easier to edit in inkscape). - NN +2007-06-03 + Turned mathtext in SVG into single with multiple objects + (easier to edit in inkscape). - NN ---------------------------- -2007-06-02 Released 0.90.1 at revision 3352 - -2007-06-02 Display only meaningful labels when calling legend() - without args. - NN - -2007-06-02 Have errorbar follow the color cycle even if line is not plotted. - Suppress plotting of errorbar caps for capsize=0. - NN - -2007-06-02 Set markers to same alpha value as line. - NN - -2007-06-02 Fix mathtext position in svg backend. - NN - -2007-06-01 Deprecate Numeric and numarray for use as numerix. Props to - Travis -- job well done. - ADS +2007-06-02 + Released 0.90.1 at revision 3352 -2007-05-18 Added LaTeX unicode support. Enable with the - 'text.latex.unicode' rcParam. This requires the ucs and - inputenc LaTeX packages. - ADS +2007-06-02 + Display only meaningful labels when calling legend() without args. - NN -2007-04-23 Fixed some problems with polar -- added general polygon - clipping to clip the lines and grids to the polar axes. - Added support for set_rmax to easily change the maximum - radial grid. Added support for polar legend - JDH +2007-06-02 + Have errorbar follow the color cycle even if line is not plotted. Suppress + plotting of errorbar caps for capsize=0. - NN -2007-04-16 Added Figure.autofmt_xdate to handle adjusting the bottom - and rotating the tick labels for date plots when the ticks - often overlap - JDH +2007-06-02 + Set markers to same alpha value as line. - NN -2007-04-09 Beginnings of usetex support for pdf backend. -JKS +2007-06-02 + Fix mathtext position in svg backend. - NN -2007-04-07 Fixed legend/LineCollection bug. Added label support - to collections. - EF +2007-06-01 + Deprecate Numeric and numarray for use as numerix. Props to Travis -- job + well done. - ADS -2007-04-06 Removed deprecated support for a float value as a gray-scale; - now it must be a string, like '0.5'. Added alpha kwarg to - ColorConverter.to_rgba_list. - EF +2007-05-18 + Added LaTeX unicode support. Enable with the 'text.latex.unicode' rcParam. + This requires the ucs and inputenc LaTeX packages. - ADS -2007-04-06 Fixed rotation of ellipses in pdf backend - (sf bug #1690559) -JKS +2007-04-23 + Fixed some problems with polar -- added general polygon clipping to clip + the lines and grids to the polar axes. Added support for set_rmax to + easily change the maximum radial grid. Added support for polar legend - + JDH -2007-04-04 More matshow tweaks; documentation updates; new method - set_bounds() for formatters and locators. - EF +2007-04-16 + Added Figure.autofmt_xdate to handle adjusting the bottom and rotating the + tick labels for date plots when the ticks often overlap - JDH -2007-04-02 Fixed problem with imshow and matshow of integer arrays; - fixed problems with changes to color autoscaling. - EF +2007-04-09 + Beginnings of usetex support for pdf backend. -JKS -2007-04-01 Made image color autoscaling work correctly with - a tracking colorbar; norm.autoscale now scales - unconditionally, while norm.autoscale_None changes - only None-valued vmin, vmax. - EF +2007-04-07 + Fixed legend/LineCollection bug. Added label support to collections. - EF -2007-03-31 Added a qt-based subplot-adjustment dialog - DSD +2007-04-06 + Removed deprecated support for a float value as a gray-scale; now it must + be a string, like '0.5'. Added alpha kwarg to ColorConverter.to_rgba_list. + - EF -2007-03-30 Fixed a bug in backend_qt4, reported on mpl-dev - DSD +2007-04-06 + Fixed rotation of ellipses in pdf backend (sf bug #1690559) -JKS -2007-03-26 Removed colorbar_classic from figure.py; fixed bug in - Figure.clf() in which _axobservers was not getting - cleared. Modernization and cleanups. - EF +2007-04-04 + More matshow tweaks; documentation updates; new method set_bounds() for + formatters and locators. - EF -2007-03-26 Refactored some of the units support -- units now live in - the respective x and y Axis instances. See also - API_CHANGES for some alterations to the conversion - interface. JDH +2007-04-02 + Fixed problem with imshow and matshow of integer arrays; fixed problems + with changes to color autoscaling. - EF -2007-03-25 Fix masked array handling in quiver.py for numpy. (Numeric - and numarray support for masked arrays is broken in other - ways when using quiver. I didn't pursue that.) - ADS +2007-04-01 + Made image color autoscaling work correctly with a tracking colorbar; + norm.autoscale now scales unconditionally, while norm.autoscale_None + changes only None-valued vmin, vmax. - EF -2007-03-23 Made font_manager.py close opened files. - JKS +2007-03-31 + Added a qt-based subplot-adjustment dialog - DSD -2007-03-22 Made imshow default extent match matshow - EF +2007-03-30 + Fixed a bug in backend_qt4, reported on mpl-dev - DSD -2007-03-22 Some more niceties for xcorr -- a maxlags option, normed - now works for xcorr as well as axorr, usevlines is - supported, and a zero correlation hline is added. See - examples/xcorr_demo.py. Thanks Sameer for the patch. - - JDH +2007-03-26 + Removed colorbar_classic from figure.py; fixed bug in Figure.clear() in which + _axobservers was not getting cleared. Modernization and cleanups. - EF -2007-03-21 Axes.vlines and Axes.hlines now create and returns a - LineCollection, not a list of lines. This is much faster. - The kwarg signature has changed, so consult the docs. - Modified Axes.errorbar which uses vlines and hlines. See - API_CHANGES; the return signature for these three functions - is now different +2007-03-26 + Refactored some of the units support -- units now live in the respective x + and y Axis instances. See also API_CHANGES for some alterations to the + conversion interface. JDH -2007-03-20 Refactored units support and added new examples - JDH +2007-03-25 + Fix masked array handling in quiver.py for numpy. (Numeric and numarray + support for masked arrays is broken in other ways when using quiver. I + didn't pursue that.) - ADS -2007-03-19 Added Mike's units patch - JDH +2007-03-23 + Made font_manager.py close opened files. - JKS -2007-03-18 Matshow as an Axes method; test version matshow1() in - pylab; added 'integer' Boolean kwarg to MaxNLocator - initializer to force ticks at integer locations. - EF +2007-03-22 + Made imshow default extent match matshow - EF -2007-03-17 Preliminary support for clipping to paths agg - JDH +2007-03-22 + Some more niceties for xcorr -- a maxlags option, normed now works for + xcorr as well as axorr, usevlines is supported, and a zero correlation + hline is added. See examples/xcorr_demo.py. Thanks Sameer for the patch. + - JDH -2007-03-17 Text.set_text() accepts anything convertible with '%s' - EF +2007-03-21 + Axes.vlines and Axes.hlines now create and returns a LineCollection, not a + list of lines. This is much faster. The kwarg signature has changed, so + consult the docs. Modified Axes.errorbar which uses vlines and hlines. + See API_CHANGES; the return signature for these three functions is now + different -2007-03-14 Add masked-array support to hist. - EF +2007-03-20 + Refactored units support and added new examples - JDH -2007-03-03 Change barh to take a kwargs dict and pass it to bar. - Fixes sf bug #1669506. +2007-03-19 + Added Mike's units patch - JDH -2007-03-02 Add rc parameter pdf.inheritcolor, which disables all - color-setting operations in the pdf backend. The idea is - that you include the resulting file in another program and - set the colors (both stroke and fill color) there, so you - can use the same pdf file for e.g., a paper and a - presentation and have them in the surrounding color. You - will probably not want to draw figure and axis frames in - that case, since they would be filled in the same color. - JKS +2007-03-18 + Matshow as an Axes method; test version matshow1() in pylab; added + 'integer' Boolean kwarg to MaxNLocator initializer to force ticks at + integer locations. - EF -2007-02-26 Prevent building _wxagg.so with broken Mac OS X wxPython. - ADS +2007-03-17 + Preliminary support for clipping to paths agg - JDH -2007-02-23 Require setuptools for Python 2.3 - ADS +2007-03-17 + Text.set_text() accepts anything convertible with '%s' - EF -2007-02-22 WXAgg accelerator updates - KM - WXAgg's C++ accelerator has been fixed to use the correct wxBitmap - constructor. +2007-03-14 + Add masked-array support to hist. - EF - The backend has been updated to use new wxPython functionality to - provide fast blit() animation without the C++ accelerator. This - requires wxPython 2.8 or later. Previous versions of wxPython can - use the C++ acclerator or the old pure Python routines. +2007-03-03 + Change barh to take a kwargs dict and pass it to bar. Fixes sf bug + #1669506. - setup.py no longer builds the C++ accelerator when wxPython >= 2.8 - is present. +2007-03-02 + Add rc parameter pdf.inheritcolor, which disables all color-setting + operations in the pdf backend. The idea is that you include the resulting + file in another program and set the colors (both stroke and fill color) + there, so you can use the same pdf file for e.g., a paper and a + presentation and have them in the surrounding color. You will probably not + want to draw figure and axis frames in that case, since they would be + filled in the same color. - JKS - The blit() method is now faster regardless of which agg/wxPython - conversion routines are used. +2007-02-26 + Prevent building _wxagg.so with broken Mac OS X wxPython. - ADS -2007-02-21 Applied the PDF backend patch by Nicolas Grilly. - This impacts several files and directories in matplotlib: +2007-02-23 + Require setuptools for Python 2.3 - ADS - - Created the directory lib/matplotlib/mpl-data/fonts/pdfcorefonts, - holding AFM files for the 14 PDF core fonts. These fonts are - embedded in every PDF viewing application. +2007-02-22 + WXAgg accelerator updates - KM - - setup.py: Added the directory pdfcorefonts to package_data. + WXAgg's C++ accelerator has been fixed to use the correct wxBitmap + constructor. - - lib/matplotlib/__init__.py: Added the default parameter - 'pdf.use14corefonts'. When True, the PDF backend uses - only the 14 PDF core fonts. + The backend has been updated to use new wxPython functionality to provide + fast blit() animation without the C++ accelerator. This requires wxPython + 2.8 or later. Previous versions of wxPython can use the C++ accelerator or + the old pure Python routines. - - lib/matplotlib/afm.py: Added some keywords found in - recent AFM files. Added a little workaround to handle - Euro symbol. + setup.py no longer builds the C++ accelerator when wxPython >= 2.8 is + present. - - lib/matplotlib/fontmanager.py: Added support for the 14 - PDF core fonts. These fonts have a dedicated cache (file - pdfcorefont.cache), not the same as for other AFM files - (file .afmfont.cache). Also cleaned comments to conform - to CODING_GUIDE. - - - lib/matplotlib/backends/backend_pdf.py: - Added support for 14 PDF core fonts. - Fixed some issues with incorrect character widths and - encodings (works only for the most common encoding, - WinAnsiEncoding, defined by the official PDF Reference). - Removed parameter 'dpi' because it causes alignment issues. - - -JKS (patch by Nicolas Grilly) - -2007-02-17 Changed ft2font.get_charmap, and updated all the files where - get_charmap is mentioned - ES - -2007-02-13 Added barcode demo- JDH - -2007-02-13 Added binary colormap to cm - JDH - -2007-02-13 Added twiny to pylab - JDH - -2007-02-12 Moved data files into lib/matplotlib so that setuptools' - develop mode works. Re-organized the mpl-data layout so - that this source structure is maintained in the - installation. (i.e., the 'fonts' and 'images' - sub-directories are maintained in site-packages.) Suggest - removing site-packages/matplotlib/mpl-data and - ~/.matplotlib/ttffont.cache before installing - ADS - -2007-02-07 Committed Rob Hetland's patch for qt4: remove - references to text()/latin1(), plus some improvements - to the toolbar layout - DSD - ---------------------------- + The blit() method is now faster regardless of which agg/wxPython conversion + routines are used. -2007-02-06 Released 0.90.0 at revision 3003 +2007-02-21 + Applied the PDF backend patch by Nicolas Grilly. This impacts several + files and directories in matplotlib: -2007-01-22 Extended the new picker API to text, patches and patch - collections. Added support for user customizable pick hit - testing and attribute tagging of the PickEvent - Details - and examples in examples/pick_event_demo.py - JDH + - Created the directory lib/matplotlib/mpl-data/fonts/pdfcorefonts, holding + AFM files for the 14 PDF core fonts. These fonts are embedded in every + PDF viewing application. -2007-01-16 Begun work on a new pick API using the mpl event handling - frameowrk. Artists will define their own pick method with - a configurable epsilon tolerance and return pick attrs. - All artists that meet the tolerance threshold will fire a - PickEvent with artist dependent attrs; e.g., a Line2D can set - the indices attribute that shows the indices into the line - that are within epsilon of the pick point. See - examples/pick_event_demo.py. The implementation of pick - for the remaining Artists remains to be done, but the core - infrastructure at the level of event handling is in place - with a proof-of-concept implementation for Line2D - JDH + - setup.py: Added the directory pdfcorefonts to package_data. -2007-01-16 src/_image.cpp: update to use Py_ssize_t (for 64-bit systems). - Use return value of fread() to prevent warning messages - SC. + - lib/matplotlib/__init__.py: Added the default parameter + 'pdf.use14corefonts'. When True, the PDF backend uses only the 14 PDF + core fonts. -2007-01-15 src/_image.cpp: combine buffer_argb32() and buffer_bgra32() into - a new method color_conv(format) - SC + - lib/matplotlib/afm.py: Added some keywords found in recent AFM files. + Added a little workaround to handle Euro symbol. -2007-01-14 backend_cairo.py: update draw_arc() so that - examples/arctest.py looks correct - SC + - lib/matplotlib/fontmanager.py: Added support for the 14 PDF core fonts. + These fonts have a dedicated cache (file pdfcorefont.cache), not the same + as for other AFM files (file .afmfont.cache). Also cleaned comments to + conform to CODING_GUIDE. -2007-01-12 backend_cairo.py: enable clipping. Update draw_image() so that - examples/contour_demo.py looks correct - SC + - lib/matplotlib/backends/backend_pdf.py: Added support for 14 PDF core + fonts. Fixed some issues with incorrect character widths and encodings + (works only for the most common encoding, WinAnsiEncoding, defined by the + official PDF Reference). Removed parameter 'dpi' because it causes + alignment issues. -2007-01-12 backend_cairo.py: fix draw_image() so that examples/image_demo.py - now looks correct - SC + -JKS (patch by Nicolas Grilly) -2007-01-11 Added Axes.xcorr and Axes.acorr to plot the cross - correlation of x vs. y or the autocorrelation of x. pylab - wrappers also provided. See examples/xcorr_demo.py - JDH +2007-02-17 + Changed ft2font.get_charmap, and updated all the files where get_charmap is + mentioned - ES -2007-01-10 Added "Subplot.label_outer" method. It will set the - visibility of the ticklabels so that yticklabels are only - visible in the first column and xticklabels are only - visible in the last row - JDH +2007-02-13 + Added barcode demo- JDH -2007-01-02 Added additional kwarg documentation - JDH +2007-02-13 + Added binary colormap to cm - JDH -2006-12-28 Improved error message for nonpositive input to log - transform; added log kwarg to bar, barh, and hist, - and modified bar method to behave sensibly by default - when the ordinate has a log scale. (This only works - if the log scale is set before or by the call to bar, - hence the utility of the log kwarg.) - EF +2007-02-13 + Added twiny to pylab - JDH -2006-12-27 backend_cairo.py: update draw_image() and _draw_mathtext() to work - with numpy - SC +2007-02-12 + Moved data files into lib/matplotlib so that setuptools' develop mode + works. Re-organized the mpl-data layout so that this source structure is + maintained in the installation. (i.e., the 'fonts' and 'images' + sub-directories are maintained in site-packages.) Suggest removing + site-packages/matplotlib/mpl-data and ~/.matplotlib/ttffont.cache before + installing - ADS -2006-12-20 Fixed xpdf dependency check, which was failing on windows. - Removed ps2eps dependency check. - DSD +2007-02-07 + Committed Rob Hetland's patch for qt4: remove references to + text()/latin1(), plus some improvements to the toolbar layout - DSD -2006-12-19 Added Tim Leslie's spectral patch - JDH - -2006-12-17 Added rc param 'axes.formatter.limits' to control - the default threshold for switching to scientific - notation. Added convenience method - Axes.ticklabel_format() for turning scientific notation - on or off on either or both axes. - EF - -2006-12-16 Added ability to turn control scientific notation - in ScalarFormatter - EF - -2006-12-16 Enhanced boxplot to handle more flexible inputs - EF - -2006-12-13 Replaced calls to where() in colors.py with much faster - clip() and putmask() calls; removed inappropriate - uses of getmaskorNone (which should be needed only - very rarely); all in response to profiling by - David Cournapeau. Also fixed bugs in my 2-D - array support from 12-09. - EF - -2006-12-09 Replaced spy and spy2 with the new spy that combines - marker and image capabilities - EF - -2006-12-09 Added support for plotting 2-D arrays with plot: - columns are plotted as in Matlab - EF - -2006-12-09 Added linewidth kwarg to bar and barh; fixed arg - checking bugs - EF - -2006-12-07 Made pcolormesh argument handling match pcolor; - fixed kwarg handling problem noted by Pierre GM - EF - -2006-12-06 Made pcolor support vector X and/or Y instead of - requiring 2-D arrays - EF - -2006-12-05 Made the default Artist._transform None (rather than - invoking identity_transform for each artist only to have it - overridden later). Use artist.get_transform() rather than - artist._transform, even in derived classes, so that the - default transform will be created lazily as needed - JDH - -2006-12-03 Added LogNorm to colors.py as illustrated by - examples/pcolor_log.py, based on suggestion by - Jim McDonald. Colorbar modified to handle LogNorm. - Norms have additional "inverse" method. - EF - -2006-12-02 Changed class names in colors.py to match convention: - normalize -> Normalize, no_norm -> NoNorm. Old names - are still available. - Changed __init__.py rc defaults to match those in - matplotlibrc - EF - -2006-11-22 Fixed bug in set_*lim that I had introduced on 11-15 - EF - -2006-11-22 Added examples/clippedline.py, which shows how to clip line - data based on view limits -- it also changes the marker - style when zoomed in - JDH - -2006-11-21 Some spy bug-fixes and added precision arg per Robert C's - suggestion - JDH - -2006-11-19 Added semi-automatic docstring generation detailing all the - kwargs that functions take using the artist introspection - tools; e.g., 'help text now details the scatter kwargs - that control the Text properties - JDH - -2006-11-17 Removed obsolete scatter_classic, leaving a stub to - raise NotImplementedError; same for pcolor_classic - EF - -2006-11-15 Removed obsolete pcolor_classic - EF - -2006-11-15 Fixed 1588908 reported by Russel Owen; factored - nonsingular method out of ticker.py, put it into - transforms.py as a function, and used it in - set_xlim and set_ylim. - EF - -2006-11-14 Applied patch 1591716 by Ulf Larssen to fix a bug in - apply_aspect. Modified and applied patch - 1594894 by mdehoon to fix bugs and improve - formatting in lines.py. Applied patch 1573008 - by Greg Willden to make psd etc. plot full frequency - range for complex inputs. - EF - -2006-11-14 Improved the ability of the colorbar to track - changes in corresponding image, pcolor, or - contourf. - EF - -2006-11-11 Fixed bug that broke Numeric compatibility; - added support for alpha to colorbar. The - alpha information is taken from the mappable - object, not specified as a kwarg. - EF - -2006-11-05 Added broken_barh function for makring a sequence of - horizontal bars broken by gaps -- see examples/broken_barh.py - -2006-11-05 Removed lineprops and markerprops from the Annotation code - and replaced them with an arrow configurable with kwarg - arrowprops. See examples/annotation_demo.py - JDH - -2006-11-02 Fixed a pylab subplot bug that was causing axes to be - deleted with hspace or wspace equals zero in - subplots_adjust - JDH +--------------------------- -2006-10-31 Applied axes3d patch 1587359 - \http://sourceforge.net/tracker/index.php?func=detail&aid=1587359&group_id=80706&atid=560722 - JDH +2007-02-06 + Released 0.90.0 at revision 3003 + +2007-01-22 + Extended the new picker API to text, patches and patch collections. Added + support for user customizable pick hit testing and attribute tagging of the + PickEvent - Details and examples in examples/pick_event_demo.py - JDH + +2007-01-16 + Begun work on a new pick API using the mpl event handling framework. + Artists will define their own pick method with a configurable epsilon + tolerance and return pick attrs. All artists that meet the tolerance + threshold will fire a PickEvent with artist dependent attrs; e.g., a Line2D + can set the indices attribute that shows the indices into the line that are + within epsilon of the pick point. See examples/pick_event_demo.py. The + implementation of pick for the remaining Artists remains to be done, but + the core infrastructure at the level of event handling is in place with a + proof-of-concept implementation for Line2D - JDH + +2007-01-16 + src/_image.cpp: update to use Py_ssize_t (for 64-bit systems). Use return + value of fread() to prevent warning messages - SC. + +2007-01-15 + src/_image.cpp: combine buffer_argb32() and buffer_bgra32() into a new + method color_conv(format) - SC + +2007-01-14 + backend_cairo.py: update draw_arc() so that examples/arctest.py looks + correct - SC + +2007-01-12 + backend_cairo.py: enable clipping. Update draw_image() so that + examples/contour_demo.py looks correct - SC + +2007-01-12 + backend_cairo.py: fix draw_image() so that examples/image_demo.py now looks + correct - SC + +2007-01-11 + Added Axes.xcorr and Axes.acorr to plot the cross correlation of x vs. y or + the autocorrelation of x. pylab wrappers also provided. See + examples/xcorr_demo.py - JDH + +2007-01-10 + Added "Subplot.label_outer" method. It will set the visibility of the + ticklabels so that yticklabels are only visible in the first column and + xticklabels are only visible in the last row - JDH + +2007-01-02 + Added additional kwarg documentation - JDH + +2006-12-28 + Improved error message for nonpositive input to log transform; added log + kwarg to bar, barh, and hist, and modified bar method to behave sensibly by + default when the ordinate has a log scale. (This only works if the log + scale is set before or by the call to bar, hence the utility of the log + kwarg.) - EF + +2006-12-27 + backend_cairo.py: update draw_image() and _draw_mathtext() to work with + numpy - SC + +2006-12-20 + Fixed xpdf dependency check, which was failing on windows. Removed ps2eps + dependency check. - DSD + +2006-12-19 + Added Tim Leslie's spectral patch - JDH + +2006-12-17 + Added rc param 'axes.formatter.limits' to control the default threshold for + switching to scientific notation. Added convenience method + Axes.ticklabel_format() for turning scientific notation on or off on either + or both axes. - EF + +2006-12-16 + Added ability to turn control scientific notation in ScalarFormatter - EF + +2006-12-16 + Enhanced boxplot to handle more flexible inputs - EF + +2006-12-13 + Replaced calls to where() in colors.py with much faster clip() and + putmask() calls; removed inappropriate uses of getmaskorNone (which should + be needed only very rarely); all in response to profiling by David + Cournapeau. Also fixed bugs in my 2-D array support from 12-09. - EF + +2006-12-09 + Replaced spy and spy2 with the new spy that combines marker and image + capabilities - EF + +2006-12-09 + Added support for plotting 2-D arrays with plot: columns are plotted as in + Matlab - EF + +2006-12-09 + Added linewidth kwarg to bar and barh; fixed arg checking bugs - EF + +2006-12-07 + Made pcolormesh argument handling match pcolor; fixed kwarg handling + problem noted by Pierre GM - EF + +2006-12-06 + Made pcolor support vector X and/or Y instead of requiring 2-D arrays - EF + +2006-12-05 + Made the default Artist._transform None (rather than invoking + identity_transform for each artist only to have it overridden later). Use + artist.get_transform() rather than artist._transform, even in derived + classes, so that the default transform will be created lazily as needed - + JDH + +2006-12-03 + Added LogNorm to colors.py as illustrated by examples/pcolor_log.py, based + on suggestion by Jim McDonald. Colorbar modified to handle LogNorm. Norms + have additional "inverse" method. - EF + +2006-12-02 + Changed class names in colors.py to match convention: normalize -> + Normalize, no_norm -> NoNorm. Old names are still available. Changed + __init__.py rc defaults to match those in matplotlibrc - EF + +2006-11-22 + Fixed bug in set_*lim that I had introduced on 11-15 - EF + +2006-11-22 + Added examples/clippedline.py, which shows how to clip line data based on + view limits -- it also changes the marker style when zoomed in - JDH + +2006-11-21 + Some spy bug-fixes and added precision arg per Robert C's suggestion - JDH + +2006-11-19 + Added semi-automatic docstring generation detailing all the kwargs that + functions take using the artist introspection tools; e.g., 'help text now + details the scatter kwargs that control the Text properties - JDH + +2006-11-17 + Removed obsolete scatter_classic, leaving a stub to raise + NotImplementedError; same for pcolor_classic - EF + +2006-11-15 + Removed obsolete pcolor_classic - EF + +2006-11-15 + Fixed 1588908 reported by Russel Owen; factored nonsingular method out of + ticker.py, put it into transforms.py as a function, and used it in set_xlim + and set_ylim. - EF + +2006-11-14 + Applied patch 1591716 by Ulf Larssen to fix a bug in apply_aspect. + Modified and applied patch 1594894 by mdehoon to fix bugs and improve + formatting in lines.py. Applied patch 1573008 by Greg Willden to make psd + etc. plot full frequency range for complex inputs. - EF + +2006-11-14 + Improved the ability of the colorbar to track changes in corresponding + image, pcolor, or contourf. - EF + +2006-11-11 + Fixed bug that broke Numeric compatibility; added support for alpha to + colorbar. The alpha information is taken from the mappable object, not + specified as a kwarg. - EF + +2006-11-05 + Added broken_barh function for making a sequence of horizontal bars broken + by gaps -- see examples/broken_barh.py + +2006-11-05 + Removed lineprops and markerprops from the Annotation code and replaced + them with an arrow configurable with kwarg arrowprops. See + examples/annotation_demo.py - JDH + +2006-11-02 + Fixed a pylab subplot bug that was causing axes to be deleted with hspace + or wspace equals zero in subplots_adjust - JDH + +2006-10-31 + Applied axes3d patch 1587359 + \http://sourceforge.net/tracker/index.php?func=detail&aid=1587359&group_id=80706&atid=560722 + JDH ------------------------- -2006-10-26 Released 0.87.7 at revision 2835 +2006-10-26 + Released 0.87.7 at revision 2835 -2006-10-25 Made "tiny" kwarg in Locator.nonsingular much smaller - EF +2006-10-25 + Made "tiny" kwarg in Locator.nonsingular much smaller - EF -2006-10-17 Closed sf bug 1562496 update line props dash/solid/cap/join - styles - JDH +2006-10-17 + Closed sf bug 1562496 update line props dash/solid/cap/join styles - JDH -2006-10-17 Complete overhaul of the annotations API and example code - - See matplotlib.text.Annotation and - examples/annotation_demo.py JDH +2006-10-17 + Complete overhaul of the annotations API and example code - See + matplotlib.text.Annotation and examples/annotation_demo.py JDH -2006-10-12 Committed Manuel Metz's StarPolygon code and - examples/scatter_star_poly.py - JDH +2006-10-12 + Committed Manuel Metz's StarPolygon code and examples/scatter_star_poly.py + - JDH +2006-10-11 + commented out all default values in matplotlibrc.template Default values + should generally be taken from defaultParam in __init__.py - the file + matplotlib should only contain those values that the user wants to + explicitly change from the default. (see thread "marker color handling" on + matplotlib-devel) -2006-10-11 commented out all default values in matplotlibrc.template - Default values should generally be taken from defaultParam in - __init__.py - the file matplotlib should only contain those values - that the user wants to explicitly change from the default. - (see thread "marker color handling" on matplotlib-devel) +2006-10-10 + Changed default comment character for load to '#' - JDH -2006-10-10 Changed default comment character for load to '#' - JDH +2006-10-10 + deactivated rcfile-configurability of markerfacecolor and markeredgecolor. + Both are now hardcoded to the special value 'auto' to follow the line + color. Configurability at run-time (using function arguments) remains + functional. - NN -2006-10-10 deactivated rcfile-configurability of markerfacecolor - and markeredgecolor. Both are now hardcoded to the special value - 'auto' to follow the line color. Configurability at run-time - (using function arguments) remains functional. - NN +2006-10-07 + introduced dummy argument magnification=1.0 to FigImage.make_image to + satisfy unit test figimage_demo.py The argument is not yet handled + correctly, which should only show up when using non-standard DPI settings + in PS backend, introduced by patch #1562394. - NN -2006-10-07 introduced dummy argument magnification=1.0 to - FigImage.make_image to satisfy unit test figimage_demo.py - The argument is not yet handled correctly, which should only - show up when using non-standard DPI settings in PS backend, - introduced by patch #1562394. - NN +2006-10-06 + add backend-agnostic example: simple3d.py - NN -2006-10-06 add backend-agnostic example: simple3d.py - NN +2006-09-29 + fix line-breaking for SVG-inline images (purely cosmetic) - NN -2006-09-29 fix line-breaking for SVG-inline images (purely cosmetic) - NN +2006-09-29 + reworked set_linestyle and set_marker markeredgecolor and markerfacecolor + now default to a special value "auto" that keeps the color in sync with the + line color further, the intelligence of axes.plot is cleaned up, improved + and simplified. Complete compatibility cannot be guaranteed, but the new + behavior should be much more predictable (see patch #1104615 for details) - + NN -2006-09-29 reworked set_linestyle and set_marker - markeredgecolor and markerfacecolor now default to - a special value "auto" that keeps the color in sync with - the line color - further, the intelligence of axes.plot is cleaned up, - improved and simplified. Complete compatibility cannot be - guaranteed, but the new behavior should be much more predictable - (see patch #1104615 for details) - NN +2006-09-29 + changed implementation of clip-path in SVG to work around a limitation in + inkscape - NN -2006-09-29 changed implementation of clip-path in SVG to work around a - limitation in inkscape - NN +2006-09-29 + added two options to matplotlibrc: -2006-09-29 added two options to matplotlibrc: - svg.image_inline - svg.image_noscale - see patch #1533010 for details - NN + - svg.image_inline + - svg.image_noscale -2006-09-29 axes.py: cleaned up kwargs checking - NN + see patch #1533010 for details - NN -2006-09-29 setup.py: cleaned up setup logic - NN +2006-09-29 + axes.py: cleaned up kwargs checking - NN -2006-09-29 setup.py: check for required pygtk versions, fixes bug #1460783 - SC +2006-09-29 + setup.py: cleaned up setup logic - NN + +2006-09-29 + setup.py: check for required pygtk versions, fixes bug #1460783 - SC --------------------------------- -2006-09-27 Released 0.87.6 at revision 2783 +2006-09-27 + Released 0.87.6 at revision 2783 -2006-09-24 Added line pointers to the Annotation code, and a pylab - interface. See matplotlib.text.Annotation, - examples/annotation_demo.py and - examples/annotation_demo_pylab.py - JDH +2006-09-24 + Added line pointers to the Annotation code, and a pylab interface. See + matplotlib.text.Annotation, examples/annotation_demo.py and + examples/annotation_demo_pylab.py - JDH -2006-09-18 mathtext2.py: The SVG backend now supports the same things that - the AGG backend does. Fixed some bugs with rendering, and out of - bounds errors in the AGG backend - ES. Changed the return values - of math_parse_s_ft2font_svg to support lines (fractions etc.) +2006-09-18 + mathtext2.py: The SVG backend now supports the same things that the AGG + backend does. Fixed some bugs with rendering, and out of bounds errors in + the AGG backend - ES. Changed the return values of math_parse_s_ft2font_svg + to support lines (fractions etc.) -2006-09-17 Added an Annotation class to facilitate annotating objects - and an examples file examples/annotation_demo.py. I want - to add dash support as in TextWithDash, but haven't decided - yet whether inheriting from TextWithDash is the right base - class or if another approach is needed - JDH +2006-09-17 + Added an Annotation class to facilitate annotating objects and an examples + file examples/annotation_demo.py. I want to add dash support as in + TextWithDash, but haven't decided yet whether inheriting from TextWithDash + is the right base class or if another approach is needed - JDH ------------------------------ -2006-09-05 Released 0.87.5 at revision 2761 +2006-09-05 + Released 0.87.5 at revision 2761 -2006-09-04 Added nxutils for some numeric add-on extension code -- - specifically a better/more efficient inside polygon tester (see - unit/inside_poly_*.py) - JDH +2006-09-04 + Added nxutils for some numeric add-on extension code -- specifically a + better/more efficient inside polygon tester (see unit/inside_poly_*.py) - + JDH -2006-09-04 Made bitstream fonts the rc default - JDH +2006-09-04 + Made bitstream fonts the rc default - JDH -2006-08-31 Fixed alpha-handling bug in ColorConverter, affecting - collections in general and contour/contourf in - particular. - EF +2006-08-31 + Fixed alpha-handling bug in ColorConverter, affecting collections in + general and contour/contourf in particular. - EF -2006-08-30 ft2font.cpp: Added draw_rect_filled method (now used by mathtext2 - to draw the fraction bar) to FT2Font - ES +2006-08-30 + ft2font.cpp: Added draw_rect_filled method (now used by mathtext2 to draw + the fraction bar) to FT2Font - ES -2006-08-29 setupext.py: wrap calls to tk.getvar() with str(). On some - systems, getvar returns a Tcl_Obj instead of a string - DSD +2006-08-29 + setupext.py: wrap calls to tk.getvar() with str(). On some systems, getvar + returns a Tcl_Obj instead of a string - DSD -2006-08-28 mathtext2.py: Sub/superscripts can now be complex (i.e. - fractions etc.). The demo is also updated - ES +2006-08-28 + mathtext2.py: Sub/superscripts can now be complex (i.e. fractions etc.). + The demo is also updated - ES -2006-08-28 font_manager.py: Added /usr/local/share/fonts to list of - X11 font directories - DSD +2006-08-28 + font_manager.py: Added /usr/local/share/fonts to list of X11 font + directories - DSD -2006-08-28 mahtext2.py: Initial support for complex fractions. Also, - rendering is now completely separated from parsing. The - sub/superscripts now work better. - Updated the mathtext2_demo.py - ES +2006-08-28 + mathtext2.py: Initial support for complex fractions. Also, rendering is now + completely separated from parsing. The sub/superscripts now work better. + Updated the mathtext2_demo.py - ES -2006-08-27 qt backends: don't create a QApplication when backend is - imported, do it when the FigureCanvasQt is created. Simplifies - applications where mpl is embedded in qt. Updated - embedding_in_qt* examples - DSD +2006-08-27 + qt backends: don't create a QApplication when backend is imported, do it + when the FigureCanvasQt is created. Simplifies applications where mpl is + embedded in qt. Updated embedding_in_qt* examples - DSD -2006-08-27 mahtext2.py: Now the fonts are searched in the OS font dir and - in the mpl-data dir. Also env is not a dict anymore. - ES +2006-08-27 + mathtext2.py: Now the fonts are searched in the OS font dir and in the + mpl-data dir. Also env is not a dict anymore. - ES -2006-08-26 minor changes to __init__.py, mathtex2_demo.py. Added matplotlibrc - key "mathtext.mathtext2" (removed the key "mathtext2") - ES +2006-08-26 + minor changes to __init__.py, mathtex2_demo.py. Added matplotlibrc key + "mathtext.mathtext2" (removed the key "mathtext2") - ES -2006-08-21 mathtext2.py: Initial support for fractions - Updated the mathtext2_demo.py - _mathtext_data.py: removed "\" from the unicode dicts - mathtext.py: Minor modification (because of _mathtext_data.py)- ES +2006-08-21 + mathtext2.py: Initial support for fractions Updated the mathtext2_demo.py + _mathtext_data.py: removed "\" from the unicode dicts mathtext.py: Minor + modification (because of _mathtext_data.py)- ES -2006-08-20 Added mathtext2.py: Replacement for mathtext.py. Supports _ ^, - \rm, \cal etc., \sin, \cos etc., unicode, recursive nestings, - inline math mode. The only backend currently supported is Agg - __init__.py: added new rc params for mathtext2 - added mathtext2_demo.py example - ES +2006-08-20 + Added mathtext2.py: Replacement for mathtext.py. Supports _ ^, \rm, \cal + etc., \sin, \cos etc., unicode, recursive nestings, inline math mode. The + only backend currently supported is Agg __init__.py: added new rc params + for mathtext2 added mathtext2_demo.py example - ES -2006-08-19 Added embedding_in_qt4.py example - DSD +2006-08-19 + Added embedding_in_qt4.py example - DSD -2006-08-11 Added scale free Ellipse patch for Agg - CM +2006-08-11 + Added scale free Ellipse patch for Agg - CM -2006-08-10 Added converters to and from julian dates to matplotlib.dates - (num2julian and julian2num) - JDH +2006-08-10 + Added converters to and from julian dates to matplotlib.dates (num2julian + and julian2num) - JDH -2006-08-08 Fixed widget locking so multiple widgets could share the - event handling - JDH +2006-08-08 + Fixed widget locking so multiple widgets could share the event handling - + JDH -2006-08-07 Added scale free Ellipse patch to SVG and PS - CM +2006-08-07 + Added scale free Ellipse patch to SVG and PS - CM -2006-08-05 Re-organized imports in numerix for numpy 1.0b2 -- TEO +2006-08-05 + Re-organized imports in numerix for numpy 1.0b2 -- TEO -2006-08-04 Added draw_markers to PDF backend. - JKS +2006-08-04 + Added draw_markers to PDF backend. - JKS -2006-08-01 Fixed a bug in postscript's rendering of dashed lines - DSD +2006-08-01 + Fixed a bug in postscript's rendering of dashed lines - DSD -2006-08-01 figure.py: savefig() update docstring to add support for 'format' - argument. - backend_cairo.py: print_figure() add support 'format' argument. - SC +2006-08-01 + figure.py: savefig() update docstring to add support for 'format' argument. + backend_cairo.py: print_figure() add support 'format' argument. - SC -2006-07-31 Don't let postscript's xpdf distiller compress images - DSD +2006-07-31 + Don't let postscript's xpdf distiller compress images - DSD -2006-07-31 Added shallowcopy() methods to all Transformations; - removed copy_bbox_transform and copy_bbox_transform_shallow - from transforms.py; - added offset_copy() function to transforms.py to - facilitate positioning artists with offsets. - See examples/transoffset.py. - EF +2006-07-31 + Added shallowcopy() methods to all Transformations; removed + copy_bbox_transform and copy_bbox_transform_shallow from transforms.py; + added offset_copy() function to transforms.py to facilitate positioning + artists with offsets. See examples/transoffset.py. - EF -2006-07-31 Don't let postscript's xpdf distiller compress images - DSD +2006-07-31 + Don't let postscript's xpdf distiller compress images - DSD -2006-07-29 Fixed numerix polygon bug reported by Nick Fotopoulos. - Added inverse_numerix_xy() transform method. - Made autoscale_view() preserve axis direction - (e.g., increasing down).- EF +2006-07-29 + Fixed numerix polygon bug reported by Nick Fotopoulos. Added + inverse_numerix_xy() transform method. Made autoscale_view() preserve axis + direction (e.g., increasing down).- EF -2006-07-28 Added shallow bbox copy routine for transforms -- mainly - useful for copying transforms to apply offset to. - JDH +2006-07-28 + Added shallow bbox copy routine for transforms -- mainly useful for copying + transforms to apply offset to. - JDH -2006-07-28 Added resize method to FigureManager class - for Qt and Gtk backend - CM +2006-07-28 + Added resize method to FigureManager class for Qt and Gtk backend - CM -2006-07-28 Added subplots_adjust button to Qt backend - CM +2006-07-28 + Added subplots_adjust button to Qt backend - CM -2006-07-26 Use numerix more in collections. - Quiver now handles masked arrays. - EF +2006-07-26 + Use numerix more in collections. Quiver now handles masked arrays. - EF -2006-07-22 Fixed bug #1209354 - DSD +2006-07-22 + Fixed bug #1209354 - DSD -2006-07-22 make scatter() work with the kwarg "color". Closes bug - 1285750 - DSD +2006-07-22 + make scatter() work with the kwarg "color". Closes bug 1285750 - DSD -2006-07-20 backend_cairo.py: require pycairo 1.2.0. - print_figure() update to output SVG using cairo. +2006-07-20 + backend_cairo.py: require pycairo 1.2.0. print_figure() update to output + SVG using cairo. -2006-07-19 Added blitting for Qt4Agg - CM +2006-07-19 + Added blitting for Qt4Agg - CM -2006-07-19 Added lasso widget and example examples/lasso_demo.py - JDH +2006-07-19 + Added lasso widget and example examples/lasso_demo.py - JDH -2006-07-18 Added blitting for QtAgg backend - CM +2006-07-18 + Added blitting for QtAgg backend - CM -2006-07-17 Fixed bug #1523585: skip nans in semilog plots - DSD +2006-07-17 + Fixed bug #1523585: skip nans in semilog plots - DSD -2006-07-12 Add support to render the scientific notation label - over the right-side y-axis - DSD +2006-07-12 + Add support to render the scientific notation label over the right-side + y-axis - DSD ------------------------------ -2006-07-11 Released 0.87.4 at revision 2558 - -2006-07-07 Fixed a usetex bug with older versions of latex - DSD - -2006-07-07 Add compatibility for NumPy 1.0 - TEO - -2006-06-29 Added a Qt4Agg backend. Thank you James Amundson - DSD - -2006-06-26 Fixed a usetex bug. On Windows, usetex will process - postscript output in the current directory rather than - in a temp directory. This is due to the use of spaces - and tildes in windows paths, which cause problems with - latex. The subprocess module is no longer used. - DSD - -2006-06-22 Various changes to bar(), barh(), and hist(). - Added 'edgecolor' keyword arg to bar() and barh(). - The x and y args in barh() have been renamed to width - and bottom respectively, and their order has been swapped - to maintain a (position, value) order ala matlab. left, - height, width and bottom args can now all be scalars or - sequences. barh() now defaults to edge alignment instead - of center alignment. Added a keyword arg 'align' to bar(), - barh() and hist() that controls between edge or center bar - alignment. Fixed ignoring the rcParams['patch.facecolor'] - for bar color in bar() and barh(). Fixed ignoring the - rcParams['lines.color'] for error bar color in bar() - and barh(). Fixed a bug where patches would be cleared - when error bars were plotted if rcParams['axes.hold'] - was False. - MAS - -2006-06-22 Added support for numerix 2-D arrays as alternatives to - a sequence of (x,y) tuples for specifying paths in - collections, quiver, contour, pcolor, transforms. - Fixed contour bug involving setting limits for - colormapping. Added numpy-style all() to numerix. - EF - -2006-06-20 Added custom FigureClass hook to pylab interface - see - examples/custom_figure_class.py - -2006-06-16 Added colormaps from gist (gist_earth, gist_stern, - gist_rainbow, gist_gray, gist_yarg, gist_heat, gist_ncar) - JW - -2006-06-16 Added a pointer to parent in figure canvas so you can - access the container with fig.canvas.manager. Useful if - you want to set the window title, e.g., in gtk - fig.canvas.manager.window.set_title, though a GUI neutral - method would be preferable JDH - -2006-06-16 Fixed colorbar.py to handle indexed colors (i.e., - norm = no_norm()) by centering each colored region - on its index. - EF - -2006-06-15 Added scalex and scaley to Axes.autoscale_view to support - selective autoscaling just the x or y axis, and supported - these command in plot so you can say plot(something, - scaley=False) and just the x axis will be autoscaled. - Modified axvline and axhline to support this, so for - example axvline will no longer autoscale the y axis. JDH - -2006-06-13 Fix so numpy updates are backward compatible - TEO - -2006-06-12 Updated numerix to handle numpy restructuring of - oldnumeric - TEO - -2006-06-12 Updated numerix.fft to handle numpy restructuring - Added ImportError to numerix.linear_algebra for numpy -TEO - -2006-06-11 Added quiverkey command to pylab and Axes, using - QuiverKey class in quiver.py. Changed pylab and Axes - to use quiver2 if possible, but drop back to the - newly-renamed quiver_classic if necessary. Modified - examples/quiver_demo.py to illustrate the new quiver - and quiverkey. Changed LineCollection implementation - slightly to improve compatibility with PolyCollection. - EF - -2006-06-11 Fixed a usetex bug for windows, running latex on files - with spaces in their names or paths was failing - DSD - -2006-06-09 Made additions to numerix, changes to quiver to make it - work with all numeric flavors. - EF - -2006-06-09 Added quiver2 function to pylab and method to axes, - with implementation via a Quiver class in quiver.py. - quiver2 will replace quiver before the next release; - it is placed alongside it initially to facilitate - testing and transition. See also - examples/quiver2_demo.py. - EF - -2006-06-08 Minor bug fix to make ticker.py draw proper minus signs - with usetex - DSD +2006-07-11 + Released 0.87.4 at revision 2558 + +2006-07-07 + Fixed a usetex bug with older versions of latex - DSD + +2006-07-07 + Add compatibility for NumPy 1.0 - TEO + +2006-06-29 + Added a Qt4Agg backend. Thank you James Amundson - DSD + +2006-06-26 + Fixed a usetex bug. On Windows, usetex will process postscript output in + the current directory rather than in a temp directory. This is due to the + use of spaces and tildes in windows paths, which cause problems with latex. + The subprocess module is no longer used. - DSD + +2006-06-22 + Various changes to bar(), barh(), and hist(). Added 'edgecolor' keyword + arg to bar() and barh(). The x and y args in barh() have been renamed to + width and bottom respectively, and their order has been swapped to maintain + a (position, value) order ala matlab. left, height, width and bottom args + can now all be scalars or sequences. barh() now defaults to edge alignment + instead of center alignment. Added a keyword arg 'align' to bar(), barh() + and hist() that controls between edge or center bar alignment. Fixed + ignoring the rcParams['patch.facecolor'] for bar color in bar() and barh(). + Fixed ignoring the rcParams['lines.color'] for error bar color in bar() and + barh(). Fixed a bug where patches would be cleared when error bars were + plotted if rcParams['axes.hold'] was False. - MAS + +2006-06-22 + Added support for numerix 2-D arrays as alternatives to a sequence of (x,y) + tuples for specifying paths in collections, quiver, contour, pcolor, + transforms. Fixed contour bug involving setting limits for colormapping. + Added numpy-style all() to numerix. - EF + +2006-06-20 + Added custom FigureClass hook to pylab interface - see + examples/custom_figure_class.py + +2006-06-16 + Added colormaps from gist (gist_earth, gist_stern, gist_rainbow, gist_gray, + gist_yarg, gist_heat, gist_ncar) - JW + +2006-06-16 + Added a pointer to parent in figure canvas so you can access the container + with fig.canvas.manager. Useful if you want to set the window title, e.g., + in gtk fig.canvas.manager.window.set_title, though a GUI neutral method + would be preferable JDH + +2006-06-16 + Fixed colorbar.py to handle indexed colors (i.e., norm = no_norm()) by + centering each colored region on its index. - EF + +2006-06-15 + Added scalex and scaley to Axes.autoscale_view to support selective + autoscaling just the x or y axis, and supported these command in plot so + you can say plot(something, scaley=False) and just the x axis will be + autoscaled. Modified axvline and axhline to support this, so for example + axvline will no longer autoscale the y axis. JDH + +2006-06-13 + Fix so numpy updates are backward compatible - TEO + +2006-06-12 + Updated numerix to handle numpy restructuring of oldnumeric - TEO + +2006-06-12 + Updated numerix.fft to handle numpy restructuring Added ImportError to + numerix.linear_algebra for numpy -TEO + +2006-06-11 + Added quiverkey command to pylab and Axes, using QuiverKey class in + quiver.py. Changed pylab and Axes to use quiver2 if possible, but drop + back to the newly-renamed quiver_classic if necessary. Modified + examples/quiver_demo.py to illustrate the new quiver and quiverkey. + Changed LineCollection implementation slightly to improve compatibility + with PolyCollection. - EF + +2006-06-11 + Fixed a usetex bug for windows, running latex on files with spaces in their + names or paths was failing - DSD + +2006-06-09 + Made additions to numerix, changes to quiver to make it work with all + numeric flavors. - EF + +2006-06-09 + Added quiver2 function to pylab and method to axes, with implementation via + a Quiver class in quiver.py. quiver2 will replace quiver before the next + release; it is placed alongside it initially to facilitate testing and + transition. See also examples/quiver2_demo.py. - EF + +2006-06-08 + Minor bug fix to make ticker.py draw proper minus signs with usetex - DSD ----------------------- -2006-06-06 Released 0.87.3 at revision 2432 - -2006-05-30 More partial support for polygons with outline or fill, - but not both. Made LineCollection inherit from - ScalarMappable. - EF +2006-06-06 + Released 0.87.3 at revision 2432 -2006-05-29 Yet another revision of aspect-ratio handling. - EF +2006-05-30 + More partial support for polygons with outline or fill, but not both. Made + LineCollection inherit from ScalarMappable. - EF -2006-05-27 Committed a patch to prevent stroking zero-width lines in - the svg backend - DSD +2006-05-29 + Yet another revision of aspect-ratio handling. - EF -2006-05-24 Fixed colorbar positioning bug identified by Helge - Avlesen, and improved the algorithm; added a 'pad' - kwarg to control the spacing between colorbar and - parent axes. - EF +2006-05-27 + Committed a patch to prevent stroking zero-width lines in the svg backend - + DSD -2006-05-23 Changed color handling so that collection initializers - can take any mpl color arg or sequence of args; deprecated - float as grayscale, replaced by string representation of - float. - EF +2006-05-24 + Fixed colorbar positioning bug identified by Helge Avlesen, and improved + the algorithm; added a 'pad' kwarg to control the spacing between colorbar + and parent axes. - EF -2006-05-19 Fixed bug: plot failed if all points were masked - EF +2006-05-23 + Changed color handling so that collection initializers can take any mpl + color arg or sequence of args; deprecated float as grayscale, replaced by + string representation of float. - EF -2006-05-19 Added custom symbol option to scatter - JDH +2006-05-19 + Fixed bug: plot failed if all points were masked - EF -2006-05-18 New example, multi_image.py; colorbar fixed to show - offset text when the ScalarFormatter is used; FixedFormatter - augmented to accept and display offset text. - EF +2006-05-19 + Added custom symbol option to scatter - JDH -2006-05-14 New colorbar; old one is renamed to colorbar_classic. - New colorbar code is in colorbar.py, with wrappers in - figure.py and pylab.py. - Fixed aspect-handling bug reported by Michael Mossey. - Made backend_bases.draw_quad_mesh() run.- EF +2006-05-18 + New example, multi_image.py; colorbar fixed to show offset text when the + ScalarFormatter is used; FixedFormatter augmented to accept and display + offset text. - EF -2006-05-08 Changed handling of end ranges in contourf: replaced - "clip-ends" kwarg with "extend". See docstring for - details. -EF +2006-05-14 + New colorbar; old one is renamed to colorbar_classic. New colorbar code is + in colorbar.py, with wrappers in figure.py and pylab.py. Fixed + aspect-handling bug reported by Michael Mossey. Made + backend_bases.draw_quad_mesh() run.- EF -2006-05-08 Added axisbelow to rc - JDH +2006-05-08 + Changed handling of end ranges in contourf: replaced "clip-ends" kwarg with + "extend". See docstring for details. -EF -2006-05-08 If using PyGTK require version 2.2+ - SC +2006-05-08 + Added axisbelow to rc - JDH -2006-04-19 Added compression support to PDF backend, controlled by - new pdf.compression rc setting. - JKS +2006-05-08 + If using PyGTK require version 2.2+ - SC -2006-04-19 Added Jouni's PDF backend +2006-04-19 + Added compression support to PDF backend, controlled by new pdf.compression + rc setting. - JKS -2006-04-18 Fixed a bug that caused agg to not render long lines +2006-04-19 + Added Jouni's PDF backend -2006-04-16 Masked array support for pcolormesh; made pcolormesh support the - same combinations of X,Y,C dimensions as pcolor does; - improved (I hope) description of grid used in pcolor, - pcolormesh. - EF +2006-04-18 + Fixed a bug that caused agg to not render long lines -2006-04-14 Reorganized axes.py - EF +2006-04-16 + Masked array support for pcolormesh; made pcolormesh support the same + combinations of X,Y,C dimensions as pcolor does; improved (I hope) + description of grid used in pcolor, pcolormesh. - EF -2006-04-13 Fixed a bug Ryan found using usetex with sans-serif fonts and - exponential tick labels - DSD +2006-04-14 + Reorganized axes.py - EF -2006-04-11 Refactored backend_ps and backend_agg to prevent module-level - texmanager imports. Now these imports only occur if text.usetex - rc setting is true - DSD +2006-04-13 + Fixed a bug Ryan found using usetex with sans-serif fonts and exponential + tick labels - DSD -2006-04-10 Committed changes required for building mpl on win32 - platforms with visual studio. This allows wxpython - blitting for fast animations. - CM +2006-04-11 + Refactored backend_ps and backend_agg to prevent module-level texmanager + imports. Now these imports only occur if text.usetex rc setting is true - + DSD -2006-04-10 Fixed an off-by-one bug in Axes.change_geometry. +2006-04-10 + Committed changes required for building mpl on win32 platforms with visual + studio. This allows wxpython blitting for fast animations. - CM -2006-04-10 Fixed bug in pie charts where wedge wouldn't have label in - legend. Submitted by Simon Hildebrandt. - ADS +2006-04-10 + Fixed an off-by-one bug in Axes.change_geometry. -2006-05-06 Usetex makes temporary latex and dvi files in a temporary - directory, rather than in the user's current working - directory - DSD +2006-04-10 + Fixed bug in pie charts where wedge wouldn't have label in legend. + Submitted by Simon Hildebrandt. - ADS -2006-04-05 Applied Ken's wx deprecation warning patch closing sf patch - #1465371 - JDH +2006-05-06 + Usetex makes temporary latex and dvi files in a temporary directory, rather + than in the user's current working directory - DSD -2006-04-05 Added support for the new API in the postscript backend. - Allows values to be masked using nan's, and faster file - creation - DSD +2006-04-05 + Applied Ken's wx deprecation warning patch closing sf patch #1465371 - JDH -2006-04-05 Use python's subprocess module for usetex calls to - external programs. subprocess catches when they exit - abnormally so an error can be raised. - DSD +2006-04-05 + Added support for the new API in the postscript backend. Allows values to + be masked using nan's, and faster file creation - DSD -2006-04-03 Fixed the bug in which widgets would not respond to - events. This regressed the twinx functionality, so I - also updated subplots_adjust to update axes that share - an x or y with a subplot instance. - CM +2006-04-05 + Use python's subprocess module for usetex calls to external programs. + subprocess catches when they exit abnormally so an error can be raised. - + DSD -2006-04-02 Moved PBox class to transforms and deleted pbox.py; - made pylab axis command a thin wrapper for Axes.axis; - more tweaks to aspect-ratio handling; fixed Axes.specgram - to account for the new imshow default of unit aspect - ratio; made contour set the Axes.dataLim. - EF +2006-04-03 + Fixed the bug in which widgets would not respond to events. This regressed + the twinx functionality, so I also updated subplots_adjust to update axes + that share an x or y with a subplot instance. - CM -2006-03-31 Fixed the Qt "Underlying C/C++ object deleted" bug. - JRE +2006-04-02 + Moved PBox class to transforms and deleted pbox.py; made pylab axis command + a thin wrapper for Axes.axis; more tweaks to aspect-ratio handling; fixed + Axes.specgram to account for the new imshow default of unit aspect ratio; + made contour set the Axes.dataLim. - EF -2006-03-31 Applied Vasily Sulatskov's Qt Navigation Toolbar enhancement. - JRE +2006-03-31 + Fixed the Qt "Underlying C/C++ object deleted" bug. - JRE -2006-03-31 Ported Norbert's rewriting of Halldor's stineman_interp - algorithm to make it numerix compatible and added code to - matplotlib.mlab. See examples/interp_demo.py - JDH +2006-03-31 + Applied Vasily Sulatskov's Qt Navigation Toolbar enhancement. - JRE -2006-03-30 Fixed a bug in aspect ratio handling; blocked potential - crashes when panning with button 3; added axis('image') - support. - EF +2006-03-31 + Ported Norbert's rewriting of Halldor's stineman_interp algorithm to make + it numerix compatible and added code to matplotlib.mlab. See + examples/interp_demo.py - JDH -2006-03-28 More changes to aspect ratio handling; new PBox class - in new file pbox.py to facilitate resizing and repositioning - axes; made PolarAxes maintain unit aspect ratio. - EF +2006-03-30 + Fixed a bug in aspect ratio handling; blocked potential crashes when + panning with button 3; added axis('image') support. - EF -2006-03-23 Refactored TextWithDash class to inherit from, rather than - delegate to, the Text class. Improves object inspection - and closes bug # 1357969 - DSD +2006-03-28 + More changes to aspect ratio handling; new PBox class in new file pbox.py + to facilitate resizing and repositioning axes; made PolarAxes maintain unit + aspect ratio. - EF -2006-03-22 Improved aspect ratio handling, including pylab interface. - Interactive resizing, pan, zoom of images and plots - (including panels with a shared axis) should work. - Additions and possible refactoring are still likely. - EF +2006-03-23 + Refactored TextWithDash class to inherit from, rather than delegate to, the + Text class. Improves object inspection and closes bug # 1357969 - DSD -2006-03-21 Added another colorbrewer colormap (RdYlBu) - JSWHIT +2006-03-22 + Improved aspect ratio handling, including pylab interface. Interactive + resizing, pan, zoom of images and plots (including panels with a shared + axis) should work. Additions and possible refactoring are still likely. - + EF -2006-03-21 Fixed tickmarks for logscale plots over very large ranges. - Closes bug # 1232920 - DSD +2006-03-21 + Added another colorbrewer colormap (RdYlBu) - JSWHIT -2006-03-21 Added Rob Knight's arrow code; see examples/arrow_demo.py - JDH +2006-03-21 + Fixed tickmarks for logscale plots over very large ranges. Closes bug # + 1232920 - DSD -2006-03-20 Added support for masking values with nan's, using ADS's - isnan module and the new API. Works for \*Agg backends - DSD +2006-03-21 + Added Rob Knight's arrow code; see examples/arrow_demo.py - JDH -2006-03-20 Added contour.negative_linestyle rcParam - ADS +2006-03-20 + Added support for masking values with nan's, using ADS's isnan module and + the new API. Works for \*Agg backends - DSD -2006-03-20 Added _isnan extension module to test for nan with Numeric - - ADS +2006-03-20 + Added contour.negative_linestyle rcParam - ADS -2006-03-17 Added Paul and Alex's support for faceting with quadmesh - in sf patch 1411223 - JDH +2006-03-20 + Added _isnan extension module to test for nan with Numeric - ADS -2006-03-17 Added Charle Twardy's pie patch to support colors=None. - Closes sf patch 1387861 - JDH +2006-03-17 + Added Paul and Alex's support for faceting with quadmesh in sf patch + 1411223 - JDH -2006-03-17 Applied sophana's patch to support overlapping axes with - toolbar navigation by toggling activation with the 'a' key. - Closes sf patch 1432252 - JDH +2006-03-17 + Added Charle Twardy's pie patch to support colors=None. Closes sf patch + 1387861 - JDH -2006-03-17 Applied Aarre's linestyle patch for backend EMF; closes sf - patch 1449279 - JDH +2006-03-17 + Applied sophana's patch to support overlapping axes with toolbar navigation + by toggling activation with the 'a' key. Closes sf patch 1432252 - JDH -2006-03-17 Applied Jordan Dawe's patch to support kwarg properties - for grid lines in the grid command. Closes sf patch - 1451661 - JDH +2006-03-17 + Applied Aarre's linestyle patch for backend EMF; closes sf patch 1449279 - + JDH -2006-03-17 Center postscript output on page when using usetex - DSD +2006-03-17 + Applied Jordan Dawe's patch to support kwarg properties for grid lines in + the grid command. Closes sf patch 1451661 - JDH -2006-03-17 subprocess module built if Python <2.4 even if subprocess - can be imported from an egg - ADS +2006-03-17 + Center postscript output on page when using usetex - DSD -2006-03-17 Added _subprocess.c from Python upstream and hopefully - enabled building (without breaking) on Windows, although - not tested. - ADS +2006-03-17 + subprocess module built if Python <2.4 even if subprocess can be imported + from an egg - ADS -2006-03-17 Updated subprocess.py to latest Python upstream and - reverted name back to subprocess.py - ADS +2006-03-17 + Added _subprocess.c from Python upstream and hopefully enabled building + (without breaking) on Windows, although not tested. - ADS -2006-03-16 Added John Porter's 3D handling code +2006-03-17 + Updated subprocess.py to latest Python upstream and reverted name back to + subprocess.py - ADS +2006-03-16 + Added John Porter's 3D handling code ------------------------ -2006-03-16 Released 0.87.2 at revision 2150 +2006-03-16 + Released 0.87.2 at revision 2150 -2006-03-15 Fixed bug in MaxNLocator revealed by daigos@infinito.it. - The main change is that Locator.nonsingular now adjusts - vmin and vmax if they are nearly the same, not just if - they are equal. A new kwarg, "tiny", sets the threshold. - - EF +2006-03-15 + Fixed bug in MaxNLocator revealed by daigos@infinito.it. The main change + is that Locator.nonsingular now adjusts vmin and vmax if they are nearly + the same, not just if they are equal. A new kwarg, "tiny", sets the + threshold. - EF -2006-03-14 Added import of compatibility library for newer numpy - linear_algebra - TEO +2006-03-14 + Added import of compatibility library for newer numpy linear_algebra - TEO -2006-03-12 Extended "load" function to support individual columns and - moved "load" and "save" into matplotlib.mlab so they can be - used outside of pylab -- see examples/load_converter.py - - JDH +2006-03-12 + Extended "load" function to support individual columns and moved "load" and + "save" into matplotlib.mlab so they can be used outside of pylab -- see + examples/load_converter.py - JDH -2006-03-12 Added AutoDateFormatter and AutoDateLocator submitted - by James Evans. Try the load_converter.py example for a - demo. - ADS +2006-03-12 + Added AutoDateFormatter and AutoDateLocator submitted by James Evans. Try + the load_converter.py example for a demo. - ADS -2006-03-11 Added subprocess module from python-2.4 - DSD +2006-03-11 + Added subprocess module from python-2.4 - DSD -2006-03-11 Fixed landscape orientation support with the usetex - option. The backend_ps print_figure method was - getting complicated, I added a _print_figure_tex - method to maintain some degree of sanity - DSD +2006-03-11 + Fixed landscape orientation support with the usetex option. The backend_ps + print_figure method was getting complicated, I added a _print_figure_tex + method to maintain some degree of sanity - DSD -2006-03-11 Added "papertype" savefig kwarg for setting - postscript papersizes. papertype and ps.papersize - rc setting can also be set to "auto" to autoscale - pagesizes - DSD +2006-03-11 + Added "papertype" savefig kwarg for setting postscript papersizes. + papertype and ps.papersize rc setting can also be set to "auto" to + autoscale pagesizes - DSD -2006-03-09 Apply P-J's patch to make pstoeps work on windows - patch report # 1445612 - DSD +2006-03-09 + Apply P-J's patch to make pstoeps work on windows patch report # 1445612 - + DSD -2006-03-09 Make backend rc parameter case-insensitive - DSD +2006-03-09 + Make backend rc parameter case-insensitive - DSD -2006-03-07 Fixed bug in backend_ps related to C0-C6 papersizes, - which were causing problems with postscript viewers. - Supported page sizes include letter, legal, ledger, - A0-A10, and B0-B10 - DSD +2006-03-07 + Fixed bug in backend_ps related to C0-C6 papersizes, which were causing + problems with postscript viewers. Supported page sizes include letter, + legal, ledger, A0-A10, and B0-B10 - DSD ------------------------------------ -2006-03-07 Released 0.87.1 +2006-03-07 + Released 0.87.1 -2006-03-04 backend_cairo.py: - fix get_rgb() bug reported by Keith Briggs. - Require pycairo 1.0.2. - Support saving png to file-like objects. - SC +2006-03-04 + backend_cairo.py: fix get_rgb() bug reported by Keith Briggs. Require + pycairo 1.0.2. Support saving png to file-like objects. - SC -2006-03-03 Fixed pcolor handling of vmin, vmax - EF +2006-03-03 + Fixed pcolor handling of vmin, vmax - EF -2006-03-02 improve page sizing with usetex with the latex - geometry package. Closes bug # 1441629 - DSD +2006-03-02 + improve page sizing with usetex with the latex geometry package. Closes bug + # 1441629 - DSD -2006-03-02 Fixed dpi problem with usetex png output. Accepted a - modified version of patch # 1441809 - DSD +2006-03-02 + Fixed dpi problem with usetex png output. Accepted a modified version of + patch # 1441809 - DSD -2006-03-01 Fixed axis('scaled') to deal with case xmax < xmin - JSWHIT +2006-03-01 + Fixed axis('scaled') to deal with case xmax < xmin - JSWHIT -2006-03-01 Added reversed colormaps (with '_r' appended to name) - JSWHIT +2006-03-01 + Added reversed colormaps (with '_r' appended to name) - JSWHIT -2006-02-27 Improved eps bounding boxes with usetex - DSD +2006-02-27 + Improved eps bounding boxes with usetex - DSD -2006-02-27 Test svn commit, again! +2006-02-27 + Test svn commit, again! -2006-02-27 Fixed two dependency checking bugs related to usetex - on Windows - DSD +2006-02-27 + Fixed two dependency checking bugs related to usetex on Windows - DSD -2006-02-27 Made the rc deprecation warnings a little more human - readable. +2006-02-27 + Made the rc deprecation warnings a little more human readable. -2006-02-26 Update the previous gtk.main_quit() bug fix to use gtk.main_level() - - SC +2006-02-26 + Update the previous gtk.main_quit() bug fix to use gtk.main_level() - SC -2006-02-24 Implemented alpha support in contour and contourf - EF +2006-02-24 + Implemented alpha support in contour and contourf - EF -2006-02-22 Fixed gtk main quit bug when quit was called before - mainloop. - JDH +2006-02-22 + Fixed gtk main quit bug when quit was called before mainloop. - JDH -2006-02-22 Small change to colors.py to workaround apparent - bug in numpy masked array module - JSWHIT +2006-02-22 + Small change to colors.py to workaround apparent bug in numpy masked array + module - JSWHIT -2006-02-22 Fixed bug in ScalarMappable.to_rgba() reported by - Ray Jones, and fixed incorrect fix found by Jeff - Whitaker - EF +2006-02-22 + Fixed bug in ScalarMappable.to_rgba() reported by Ray Jones, and fixed + incorrect fix found by Jeff Whitaker - EF -------------------------------- -2006-02-22 Released 0.87 +2006-02-22 + Released 0.87 -2006-02-21 Fixed portrait/landscape orientation in postscript backend - DSD +2006-02-21 + Fixed portrait/landscape orientation in postscript backend - DSD -2006-02-21 Fix bug introduced in yesterday's bug fix - SC +2006-02-21 + Fix bug introduced in yesterday's bug fix - SC -2006-02-20 backend_gtk.py FigureCanvasGTK.draw(): fix bug reported by - David Tremouilles - SC +2006-02-20 + backend_gtk.py FigureCanvasGTK.draw(): fix bug reported by David + Tremouilles - SC -2006-02-20 Remove the "pygtk.require('2.4')" error from - examples/embedding_in_gtk2.py - SC +2006-02-20 + Remove the "pygtk.require('2.4')" error from examples/embedding_in_gtk2.py + - SC -2006-02-18 backend_gtk.py FigureCanvasGTK.draw(): simplify to use (rather than - duplicate) the expose_event() drawing code - SC +2006-02-18 + backend_gtk.py FigureCanvasGTK.draw(): simplify to use (rather than + duplicate) the expose_event() drawing code - SC -2006-02-12 Added stagger or waterfall plot capability to LineCollection; - illustrated in examples/collections.py. - EF +2006-02-12 + Added stagger or waterfall plot capability to LineCollection; illustrated + in examples/collections.py. - EF -2006-02-11 Massive cleanup of the usetex code in the postscript backend. Possibly - fixed the clipping issue users were reporting with older versions of - ghostscript - DSD +2006-02-11 + Massive cleanup of the usetex code in the postscript backend. Possibly + fixed the clipping issue users were reporting with older versions of + ghostscript - DSD -2006-02-11 Added autolim kwarg to axes.add_collection. Changed - collection get_verts() methods accordingly. - EF +2006-02-11 + Added autolim kwarg to axes.add_collection. Changed collection get_verts() + methods accordingly. - EF -2006-02-09 added a temporary rc parameter text.dvipnghack, to allow Mac users to get nice - results with the usetex option. - DSD +2006-02-09 + added a temporary rc parameter text.dvipnghack, to allow Mac users to get + nice results with the usetex option. - DSD -2006-02-09 Fixed a bug related to setting font sizes with the usetex option. - DSD +2006-02-09 + Fixed a bug related to setting font sizes with the usetex option. - DSD -2006-02-09 Fixed a bug related to usetex's latex code. - DSD +2006-02-09 + Fixed a bug related to usetex's latex code. - DSD -2006-02-09 Modified behavior of font.size rc setting. You should define font.size in pts, - which will set the "medium" or default fontsize. Special text sizes like axis - labels or tick labels can be given relative font sizes like small, large, - x-large, etc. and will scale accordingly. - DSD +2006-02-09 + Modified behavior of font.size rc setting. You should define font.size in + pts, which will set the "medium" or default fontsize. Special text sizes + like axis labels or tick labels can be given relative font sizes like + small, large, x-large, etc. and will scale accordingly. - DSD -2006-02-08 Added py2exe specific datapath check again. Also added new - py2exe helper function get_py2exe_datafiles for use in py2exe - setup.py scripts. - CM +2006-02-08 + Added py2exe specific datapath check again. Also added new py2exe helper + function get_py2exe_datafiles for use in py2exe setup.py scripts. - CM -2006-02-02 Added box function to pylab +2006-02-02 + Added box function to pylab -2006-02-02 Fixed a problem in setupext.py, tk library formatted in unicode - caused build problems - DSD +2006-02-02 + Fixed a problem in setupext.py, tk library formatted in unicode caused + build problems - DSD -2006-02-01 Dropped TeX engine support in usetex to focus on LaTeX. - DSD +2006-02-01 + Dropped TeX engine support in usetex to focus on LaTeX. - DSD -2006-01-29 Improved usetex option to respect the serif, sans-serif, monospace, - and cursive rc settings. Removed the font.latex.package rc setting, - it is no longer required - DSD +2006-01-29 + Improved usetex option to respect the serif, sans-serif, monospace, and + cursive rc settings. Removed the font.latex.package rc setting, it is no + longer required - DSD -2006-01-29 Fixed tex's caching to include font.family rc information - DSD +2006-01-29 + Fixed tex's caching to include font.family rc information - DSD -2006-01-29 Fixed subpixel rendering bug in \*Agg that was causing - uneven gridlines - JDH +2006-01-29 + Fixed subpixel rendering bug in \*Agg that was causing uneven gridlines - + JDH -2006-01-28 Added fontcmd to backend_ps's RendererPS.draw_tex, to support other - font families in eps output - DSD +2006-01-28 + Added fontcmd to backend_ps's RendererPS.draw_tex, to support other font + families in eps output - DSD -2006-01-28 Added MaxNLocator to ticker.py, and changed contour.py to - use it by default. - EF +2006-01-28 + Added MaxNLocator to ticker.py, and changed contour.py to use it by + default. - EF -2006-01-28 Added fontcmd to backend_ps's RendererPS.draw_tex, to support other - font families in eps output - DSD +2006-01-28 + Added fontcmd to backend_ps's RendererPS.draw_tex, to support other font + families in eps output - DSD -2006-01-27 Buffered reading of matplotlibrc parameters in order to allow - 'verbose' settings to be processed first (allows verbose.report - during rc validation process) - DSD +2006-01-27 + Buffered reading of matplotlibrc parameters in order to allow 'verbose' + settings to be processed first (allows verbose.report during rc validation + process) - DSD -2006-01-27 Removed setuptools support from setup.py and created a - separate setupegg.py file to replace it. - CM +2006-01-27 + Removed setuptools support from setup.py and created a separate setupegg.py + file to replace it. - CM -2006-01-26 Replaced the ugly datapath logic with a cleaner approach from - http://wiki.python.org/moin/DistutilsInstallDataScattered. - Overrides the install_data command. - CM +2006-01-26 + Replaced the ugly datapath logic with a cleaner approach from + http://wiki.python.org/moin/DistutilsInstallDataScattered. Overrides the + install_data command. - CM -2006-01-24 Don't use character typecodes in cntr.c --- changed to use - defined typenumbers instead. - TEO +2006-01-24 + Don't use character typecodes in cntr.c --- changed to use defined + typenumbers instead. - TEO -2006-01-24 Fixed some bugs in usetex's and ps.usedistiller's dependency +2006-01-24 + Fixed some bugs in usetex's and ps.usedistiller's dependency -2006-01-24 Added masked array support to scatter - EF +2006-01-24 + Added masked array support to scatter - EF -2006-01-24 Fixed some bugs in usetex's and ps.usedistiller's dependency - checking - DSD +2006-01-24 + Fixed some bugs in usetex's and ps.usedistiller's dependency checking - DSD ------------------------------- -2006-01-24 Released 0.86.2 - -2006-01-20 Added a converters dict to pylab load to convert selected - columns to float -- especially useful for files with date - strings, uses a datestr2num converter - JDH +2006-01-24 + Released 0.86.2 -2006-01-20 Added datestr2num to matplotlib dates to convert a string - or sequence of strings to a matplotlib datenum +2006-01-20 + Added a converters dict to pylab load to convert selected columns to float + -- especially useful for files with date strings, uses a datestr2num + converter - JDH -2006-01-18 Added quadrilateral pcolormesh patch 1409190 by Alex Mont - and Paul Kienzle -- this is \*Agg only for now. See - examples/quadmesh_demo.py - JDH +2006-01-20 + Added datestr2num to matplotlib dates to convert a string or sequence of + strings to a matplotlib datenum -2006-01-18 Added Jouni's boxplot patch - JDH +2006-01-18 + Added quadrilateral pcolormesh patch 1409190 by Alex Mont and Paul Kienzle + -- this is \*Agg only for now. See examples/quadmesh_demo.py - JDH -2006-01-18 Added comma delimiter for pylab save - JDH +2006-01-18 + Added Jouni's boxplot patch - JDH -2006-01-12 Added Ryan's legend patch - JDH +2006-01-18 + Added comma delimiter for pylab save - JDH +2006-01-12 + Added Ryan's legend patch - JDH -2006-1-12 Fixed numpy / numeric to use .dtype.char to keep in SYNC with numpy SVN +2006-01-12 + Fixed numpy / numeric to use .dtype.char to keep in SYNC with numpy SVN --------------------------- -2006-1-11 Released 0.86.1 +2006-01-11 + Released 0.86.1 -2006-1-11 Fixed setup.py for win32 build and added rc template to the MANIFEST.in +2006-01-11 + Fixed setup.py for win32 build and added rc template to the MANIFEST.in -2006-1-10 Added xpdf distiller option. matplotlibrc ps.usedistiller can now be - none, false, ghostscript, or xpdf. Validation checks for - dependencies. This needs testing, but the xpdf option should produce - the highest-quality output and small file sizes - DSD +2006-01-10 + Added xpdf distiller option. matplotlibrc ps.usedistiller can now be none, + false, ghostscript, or xpdf. Validation checks for dependencies. This needs + testing, but the xpdf option should produce the highest-quality output and + small file sizes - DSD -2006-01-10 For the usetex option, backend_ps now does all the LaTeX work in the - os's temp directory - DSD +2006-01-10 + For the usetex option, backend_ps now does all the LaTeX work in the os's + temp directory - DSD -2006-1-10 Added checks for usetex dependencies. - DSD +2006-01-10 + Added checks for usetex dependencies. - DSD --------------------------------- -2006-1-9 Released 0.86 +2006-01-09 + Released 0.86 -2006-1-4 Changed to support numpy (new name for scipy_core) - TEO +2006-01-04 + Changed to support numpy (new name for scipy_core) - TEO -2006-1-4 Added Mark's scaled axes patch for shared axis +2006-01-04 + Added Mark's scaled axes patch for shared axis -2005-12-28 Added Chris Barker's build_wxagg patch - JDH +2005-12-28 + Added Chris Barker's build_wxagg patch - JDH -2005-12-27 Altered numerix/scipy to support new scipy package - structure - TEO +2005-12-27 + Altered numerix/scipy to support new scipy package structure - TEO -2005-12-20 Fixed Jame's Boyles date tick reversal problem - JDH +2005-12-20 + Fixed Jame's Boyles date tick reversal problem - JDH -2005-12-20 Added Jouni's rc patch to support lists of keys to set on - - JDH +2005-12-20 + Added Jouni's rc patch to support lists of keys to set on - JDH -2005-12-12 Updated pyparsing and mathtext for some speed enhancements - (Thanks Paul McGuire) and minor fixes to scipy numerix and - setuptools +2005-12-12 + Updated pyparsing and mathtext for some speed enhancements (Thanks Paul + McGuire) and minor fixes to scipy numerix and setuptools -2005-12-12 Matplotlib data is now installed as package_data in - the matplotlib module. This gets rid of checking the - many possibilities in matplotlib._get_data_path() - CM +2005-12-12 + Matplotlib data is now installed as package_data in the matplotlib module. + This gets rid of checking the many possibilities in + matplotlib._get_data_path() - CM -2005-12-11 Support for setuptools/pkg_resources to build and use - matplotlib as an egg. Still allows matplotlib to exist - using a traditional distutils install. - ADS +2005-12-11 + Support for setuptools/pkg_resources to build and use matplotlib as an egg. + Still allows matplotlib to exist using a traditional distutils install. - + ADS -2005-12-03 Modified setup to build matplotlibrc based on compile time - findings. It will set numerix in the order of scipy, - numarray, Numeric depending on which are founds, and - backend as in preference order GTKAgg, WXAgg, TkAgg, GTK, - Agg, PS +2005-12-03 + Modified setup to build matplotlibrc based on compile time findings. It + will set numerix in the order of scipy, numarray, Numeric depending on + which are founds, and backend as in preference order GTKAgg, WXAgg, TkAgg, + GTK, Agg, PS -2005-12-03 Modified scipy patch to support Numeric, scipy and numarray - Some work remains to be done because some of the scipy - imports are broken if only the core is installed. e.g., - apparently we need from scipy.basic.fftpack import * rather - than from scipy.fftpack import * +2005-12-03 + Modified scipy patch to support Numeric, scipy and numarray Some work + remains to be done because some of the scipy imports are broken if only the + core is installed. e.g., apparently we need from scipy.basic.fftpack + import * rather than from scipy.fftpack import * -2005-12-03 Applied some fixes to Nicholas Young's nonuniform image - patch +2005-12-03 + Applied some fixes to Nicholas Young's nonuniform image patch -2005-12-01 Applied Alex Gontmakher hatch patch - PS only for now +2005-12-01 + Applied Alex Gontmakher hatch patch - PS only for now -2005-11-30 Added Rob McMullen's EMF patch +2005-11-30 + Added Rob McMullen's EMF patch -2005-11-30 Added Daishi's patch for scipy +2005-11-30 + Added Daishi's patch for scipy -2005-11-30 Fixed out of bounds draw markers segfault in agg +2005-11-30 + Fixed out of bounds draw markers segfault in agg -2005-11-28 Got TkAgg blitting working 100% (cross fingers) correctly. - CM +2005-11-28 + Got TkAgg blitting working 100% (cross fingers) correctly. - CM -2005-11-27 Multiple changes in cm.py, colors.py, figure.py, image.py, - contour.py, contour_demo.py; new _cm.py, examples/image_masked.py. - 1) Separated the color table data from cm.py out into - a new file, _cm.py, to make it easier to find the actual - code in cm.py and to add new colormaps. Also added - some line breaks to the color data dictionaries. Everything - from _cm.py is imported by cm.py, so the split should be - transparent. - 2) Enabled automatic generation of a colormap from - a list of colors in contour; see modified - examples/contour_demo.py. - 3) Support for imshow of a masked array, with the - ability to specify colors (or no color at all) for - masked regions, and for regions that are above or - below the normally mapped region. See - examples/image_masked.py. - 4) In support of the above, added two new classes, - ListedColormap, and no_norm, to colors.py, and modified - the Colormap class to include common functionality. Added - a clip kwarg to the normalize class. Reworked color - handling in contour.py, especially in the ContourLabeller - mixin. - - EF +2005-11-27 + Multiple changes in cm.py, colors.py, figure.py, image.py, contour.py, + contour_demo.py; new _cm.py, examples/image_masked.py. -2005-11-25 Changed text.py to ensure color is hashable. EF + 1. Separated the color table data from cm.py out into a new file, _cm.py, + to make it easier to find the actual code in cm.py and to add new + colormaps. Also added some line breaks to the color data dictionaries. + Everything from _cm.py is imported by cm.py, so the split should be + transparent. + 2. Enabled automatic generation of a colormap from a list of colors in + contour; see modified examples/contour_demo.py. + 3. Support for imshow of a masked array, with the ability to specify colors + (or no color at all) for masked regions, and for regions that are above + or below the normally mapped region. See examples/image_masked.py. + 4. In support of the above, added two new classes, ListedColormap, and + no_norm, to colors.py, and modified the Colormap class to include common + functionality. Added a clip kwarg to the normalize class. Reworked + color handling in contour.py, especially in the ContourLabeller mixin. --------------------------------- + - EF -2005-11-16 Released 0.85 +2005-11-25 + Changed text.py to ensure color is hashable. EF -2005-11-16 Changed the default default linewidth in rc to 1.0 +-------------------------------- -2005-11-16 Replaced agg_to_gtk_drawable with pure pygtk pixbuf code in - backend_gtkagg. When the equivalent is doe for blit, the - agg extension code will no longer be needed +2005-11-16 + Released 0.85 -2005-11-16 Added a maxdict item to cbook to prevent caches from - growing w/o bounds +2005-11-16 + Changed the default linewidth in rc to 1.0 -2005-11-15 Fixed a colorup/colordown reversal bug in finance.py -- - Thanks Gilles +2005-11-16 + Replaced agg_to_gtk_drawable with pure pygtk pixbuf code in backend_gtkagg. + When the equivalent is doe for blit, the agg extension code will no longer + be needed -2005-11-15 Applied Jouni K Steppanen's boxplot patch SF patch#1349997 - - JDH +2005-11-16 + Added a maxdict item to cbook to prevent caches from growing w/o bounds +2005-11-15 + Fixed a colorup/colordown reversal bug in finance.py -- Thanks Gilles -2005-11-09 added axisbelow attr for Axes to determine whether ticks and such - are above or below the actors +2005-11-15 + Applied Jouni K Steppanen's boxplot patch SF patch#1349997 - JDH -2005-11-08 Added Nicolas' irregularly spaced image patch +2005-11-09 + added axisbelow attr for Axes to determine whether ticks and such are above + or below the actors +2005-11-08 + Added Nicolas' irregularly spaced image patch -2005-11-08 Deprecated HorizontalSpanSelector and replaced with - SpanSelection that takes a third arg, direction. The - new SpanSelector supports horizontal and vertical span - selection, and the appropriate min/max is returned. - CM +2005-11-08 + Deprecated HorizontalSpanSelector and replaced with SpanSelection that + takes a third arg, direction. The new SpanSelector supports horizontal and + vertical span selection, and the appropriate min/max is returned. - CM -2005-11-08 Added lineprops dialog for gtk +2005-11-08 + Added lineprops dialog for gtk -2005-11-03 Added FIFOBuffer class to mlab to support real time feeds - and examples/fifo_buffer.py +2005-11-03 + Added FIFOBuffer class to mlab to support real time feeds and + examples/fifo_buffer.py -2005-11-01 Contributed Nickolas Young's patch for afm mathtext to - support mathtext based upon the standard postscript Symbol - font when ps.usetex = True. +2005-11-01 + Contributed Nickolas Young's patch for afm mathtext to support mathtext + based upon the standard postscript Symbol font when ps.usetex = True. -2005-10-26 Added support for scatter legends - thanks John Gill +2005-10-26 + Added support for scatter legends - thanks John Gill -2005-10-20 Fixed image clipping bug that made some tex labels - disappear. JDH +2005-10-20 + Fixed image clipping bug that made some tex labels disappear. JDH -2005-10-14 Removed sqrt from dvipng 1.6 alpha channel mask. +2005-10-14 + Removed sqrt from dvipng 1.6 alpha channel mask. -2005-10-14 Added width kwarg to hist function +2005-10-14 + Added width kwarg to hist function -2005-10-10 Replaced all instances of os.rename with shutil.move +2005-10-10 + Replaced all instances of os.rename with shutil.move -2005-10-05 Added Michael Brady's ydate patch +2005-10-05 + Added Michael Brady's ydate patch -2005-10-04 Added rkern's texmanager patch +2005-10-04 + Added rkern's texmanager patch -2005-09-25 contour.py modified to use a single ContourSet class - that handles filled contours, line contours, and labels; - added keyword arg (clip_ends) to contourf. - Colorbar modified to work with new ContourSet object; - if the ContourSet has lines rather than polygons, the - colorbar will follow suit. Fixed a bug introduced in - 0.84, in which contourf(...,colors=...) was broken - EF +2005-09-25 + contour.py modified to use a single ContourSet class that handles filled + contours, line contours, and labels; added keyword arg (clip_ends) to + contourf. Colorbar modified to work with new ContourSet object; if the + ContourSet has lines rather than polygons, the colorbar will follow suit. + Fixed a bug introduced in 0.84, in which contourf(...,colors=...) was + broken - EF ------------------------------- -2005-09-19 Released 0.84 +2005-09-19 + Released 0.84 -2005-09-14 Added a new 'resize_event' which triggers a callback with a - backend_bases.ResizeEvent object - JDH +2005-09-14 + Added a new 'resize_event' which triggers a callback with a + backend_bases.ResizeEvent object - JDH -2005-09-14 font_manager.py: removed chkfontpath from x11FontDirectory() - SC +2005-09-14 + font_manager.py: removed chkfontpath from x11FontDirectory() - SC -2005-09-14 Factored out auto date locator/formatter factory code into - matplotlib.date.date_ticker_factory; applies John Bryne's - quiver patch. +2005-09-14 + Factored out auto date locator/formatter factory code into + matplotlib.date.date_ticker_factory; applies John Bryne's quiver patch. -2005-09-13 Added Mark's axes positions history patch #1286915 +2005-09-13 + Added Mark's axes positions history patch #1286915 -2005-09-09 Added support for auto canvas resizing with - fig.set_figsize_inches(9,5,forward=True) # inches - OR - fig.resize(400,300) # pixels +2005-09-09 + Added support for auto canvas resizing with:: -2005-09-07 figure.py: update Figure.draw() to use the updated - renderer.draw_image() so that examples/figimage_demo.py works again. - examples/stock_demo.py: remove data_clipping (which no longer - exists) - SC + fig.set_figsize_inches(9,5,forward=True) # inches -2005-09-06 Added Eric's tick.direction patch: in or out in rc + OR:: -2005-09-06 Added Martin's rectangle selector widget + fig.resize(400,300) # pixels -2005-09-04 Fixed a logic err in text.py that was preventing rgxsuper - from matching - JDH +2005-09-07 + figure.py: update Figure.draw() to use the updated renderer.draw_image() so + that examples/figimage_demo.py works again. examples/stock_demo.py: remove + data_clipping (which no longer exists) - SC -2005-08-29 Committed Ken's wx blit patch #1275002 +2005-09-06 + Added Eric's tick.direction patch: in or out in rc -2005-08-26 colorbar modifications - now uses contourf instead of imshow - so that colors used by contourf are displayed correctly. - Added two new keyword args (cspacing and clabels) that are - only relevant for ContourMappable images - JSWHIT +2005-09-06 + Added Martin's rectangle selector widget -2005-08-24 Fixed a PS image bug reported by Darren - JDH +2005-09-04 + Fixed a logic err in text.py that was preventing rgxsuper from matching - + JDH -2005-08-23 colors.py: change hex2color() to accept unicode strings as well as - normal strings. Use isinstance() instead of types.IntType etc - SC +2005-08-29 + Committed Ken's wx blit patch #1275002 -2005-08-16 removed data_clipping line and rc property - JDH +2005-08-26 + colorbar modifications - now uses contourf instead of imshow so that colors + used by contourf are displayed correctly. Added two new keyword args + (cspacing and clabels) that are only relevant for ContourMappable images - + JSWHIT -2005-08-22 backend_svg.py: Remove redundant "x=0.0 y=0.0" from svg element. - Increase svg version from 1.0 to 1.1. Add viewBox attribute to svg - element to allow SVG documents to scale-to-fit into an arbitrary - viewport - SC +2005-08-24 + Fixed a PS image bug reported by Darren - JDH -2005-08-16 Added Eric's dot marker patch - JDH +2005-08-23 + colors.py: change hex2color() to accept unicode strings as well as normal + strings. Use isinstance() instead of types.IntType etc - SC -2005-08-08 Added blitting/animation for TkAgg - CM +2005-08-16 + removed data_clipping line and rc property - JDH -2005-08-05 Fixed duplicate tickline bug - JDH - -2005-08-05 Fixed a GTK animation bug that cropped up when doing - animations in gtk//gtkagg canvases that had widgets packed - above them - -2005-08-05 Added Clovis Goldemberg patch to the tk save dialog - -2005-08-04 Removed origin kwarg from backend.draw_image. origin is - handled entirely by the frontend now. - -2005-07-03 Fixed a bug related to TeX commands in backend_ps - -2005-08-03 Fixed SVG images to respect upper and lower origins. - -2005-08-03 Added flipud method to image and removed it from to_str. - -2005-07-29 Modified figure.figaspect to take an array or number; - modified backend_svg to write utf-8 - JDH - -2005-07-30 backend_svg.py: embed png image files in svg rather than linking - to a separate png file, fixes bug #1245306 (thanks to Norbert Nemec - for the patch) - SC - ---------------------------- +2005-08-22 + backend_svg.py: Remove redundant "x=0.0 y=0.0" from svg element. Increase + svg version from 1.0 to 1.1. Add viewBox attribute to svg element to allow + SVG documents to scale-to-fit into an arbitrary viewport - SC -2005-07-29 Released 0.83.2 +2005-08-16 + Added Eric's dot marker patch - JDH -2005-07-27 Applied SF patch 1242648: minor rounding error in - IndexDateFormatter in dates.py +2005-08-08 + Added blitting/animation for TkAgg - CM -2005-07-27 Applied sf patch 1244732: Scale axis such that circle - looks like circle - JDH +2005-08-05 + Fixed duplicate tickline bug - JDH -2005-07-29 Improved message reporting in texmanager and backend_ps - DSD +2005-08-05 + Fixed a GTK animation bug that cropped up when doing animations in + gtk//gtkagg canvases that had widgets packed above them -2005-07-28 backend_gtk.py: update FigureCanvasGTK.draw() (needed due to the - recent expose_event() change) so that examples/anim.py works in the - usual way - SC +2005-08-05 + Added Clovis Goldemberg patch to the tk save dialog -2005-07-26 Added new widgets Cursor and HorizontalSpanSelector to - matplotlib.widgets. See examples/widgets/cursor.py and - examples/widgets/span_selector.py - JDH +2005-08-04 + Removed origin kwarg from backend.draw_image. origin is handled entirely + by the frontend now. -2005-07-26 added draw event to mpl event hierarchy -- triggered on - figure.draw +2005-07-03 + Fixed a bug related to TeX commands in backend_ps -2005-07-26 backend_gtk.py: allow 'f' key to toggle window fullscreen mode +2005-08-03 + Fixed SVG images to respect upper and lower origins. -2005-07-26 backend_svg.py: write "<.../>" elements all on one line and remove - surplus spaces - SC +2005-08-03 + Added flipud method to image and removed it from to_str. -2005-07-25 backend_svg.py: simplify code by deleting GraphicsContextSVG and - RendererSVG.new_gc(), and moving the gc.get_capstyle() code into - RendererSVG._get_gc_props_svg() - SC +2005-07-29 + Modified figure.figaspect to take an array or number; modified backend_svg + to write utf-8 - JDH -2005-07-24 backend_gtk.py: call FigureCanvasBase.motion_notify_event() on - all motion-notify-events, not just ones where a modifier key or - button has been pressed (fixes bug report from Niklas Volbers) - SC +2005-07-30 + backend_svg.py: embed png image files in svg rather than linking to a + separate png file, fixes bug #1245306 (thanks to Norbert Nemec for the + patch) - SC -2005-07-24 backend_gtk.py: modify print_figure() use own pixmap, fixing - problems where print_figure() overwrites the display pixmap. - return False from all button/key etc events - to allow the event - to propagate further - SC - -2005-07-23 backend_gtk.py: change expose_event from using set_back_pixmap(); - clear() to draw_drawable() - SC - -2005-07-23 backend_gtk.py: removed pygtk.require() - matplotlib/__init__.py: delete 'FROZEN' and 'McPLError' which are - no longer used - SC - -2005-07-22 backend_gdk.py: removed pygtk.require() - SC - -2005-07-21 backend_svg.py: Remove unused imports. Remove methods doc strings - which just duplicate the docs from backend_bases.py. Rename - draw_mathtext to _draw_mathtext. - SC - -2005-07-17 examples/embedding_in_gtk3.py: new example demonstrating placing - a FigureCanvas in a gtk.ScrolledWindow - SC - -2005-07-14 Fixed a Windows related bug (#1238412) in texmanager - DSD - -2005-07-11 Fixed color kwarg bug, setting color=1 or 0 caused an - exception - DSD - -2005-07-07 Added Eric's MA set_xdata Line2D fix - JDH - -2005-07-06 Made HOME/.matplotlib the new config dir where the - matplotlibrc file, the ttf.cache, and the tex.cache live. - The new default filenames in .matplotlib have no leading - dot and are not hidden. e.g., the new names are matplotlibrc - tex.cache ttffont.cache. This is how ipython does it so it - must be right. If old files are found, a warning is issued - and they are moved to the new location. Also fixed - texmanager to put all files, including temp files in - ~/.matplotlib/tex.cache, which allows you to usetex in - non-writable dirs. - -2005-07-05 Fixed bug #1231611 in subplots adjust layout. The problem - was that the text caching mechanism was not using the - transformation affine in the key. - JDH - -2005-07-05 Fixed default backend import problem when using API (SF bug - # 1209354 - see API_CHANGES for more info - JDH - -2005-07-04 backend_gtk.py: require PyGTK version 2.0.0 or higher - SC - -2005-06-30 setupext.py: added numarray_inc_dirs for building against - numarray when not installed in standard location - ADS - -2005-06-27 backend_svg.py: write figure width, height as int, not float. - Update to fix some of the pychecker warnings - SC - -2005-06-23 Updated examples/agg_test.py to demonstrate curved paths - and fills - JDH +--------------------------- -2005-06-21 Moved some texmanager and backend_agg tex caching to class - level rather than instance level - JDH +2005-07-29 + Released 0.83.2 -2005-06-20 setupext.py: fix problem where _nc_backend_gdk is installed to the - wrong directory - SC +2005-07-27 + Applied SF patch 1242648: minor rounding error in IndexDateFormatter in + dates.py -2005-06-19 Added 10.4 support for CocoaAgg. - CM +2005-07-27 + Applied sf patch 1244732: Scale axis such that circle looks like circle - + JDH -2005-06-18 Move Figure.get_width_height() to FigureCanvasBase and return - int instead of float. - SC +2005-07-29 + Improved message reporting in texmanager and backend_ps - DSD -2005-06-18 Applied Ted Drain's QtAgg patch: 1) Changed the toolbar to - be a horizontal bar of push buttons instead of a QToolbar - and updated the layout algorithms in the main window - accordingly. This eliminates the ability to drag and drop - the toolbar and detach it from the window. 2) Updated the - resize algorithm in the main window to show the correct - size for the plot widget as requested. This works almost - correctly right now. It looks to me like the final size of - the widget is off by the border of the main window but I - haven't figured out a way to get that information yet. We - could just add a small margin to the new size but that - seems a little hacky. 3) Changed the x/y location label to - be in the toolbar like the Tk backend instead of as a - status line at the bottom of the widget. 4) Changed the - toolbar pixmaps to use the ppm files instead of the png - files. I noticed that the Tk backend buttons looked much - nicer and it uses the ppm files so I switched them. +2005-07-28 + backend_gtk.py: update FigureCanvasGTK.draw() (needed due to the recent + expose_event() change) so that examples/anim.py works in the usual way - SC -2005-06-17 Modified the gtk backend to not queue mouse motion events. - This allows for live updates when dragging a slider. - CM +2005-07-26 + Added new widgets Cursor and HorizontalSpanSelector to matplotlib.widgets. + See examples/widgets/cursor.py and examples/widgets/span_selector.py - JDH -2005-06-17 Added starter CocoaAgg backend. Only works on OS 10.3 for - now and requires PyObjC. (10.4 is high priority) - CM +2005-07-26 + added draw event to mpl event hierarchy -- triggered on figure.draw -2005-06-17 Upgraded pyparsing and applied Paul McGuire's suggestions - for speeding things up. This more than doubles the speed - of mathtext in my simple tests. JDH +2005-07-26 + backend_gtk.py: allow 'f' key to toggle window fullscreen mode -2005-06-16 Applied David Cooke's subplot make_key patch +2005-07-26 + backend_svg.py: write "<.../>" elements all on one line and remove surplus + spaces - SC + +2005-07-25 + backend_svg.py: simplify code by deleting GraphicsContextSVG and + RendererSVG.new_gc(), and moving the gc.get_capstyle() code into + RendererSVG._get_gc_props_svg() - SC + +2005-07-24 + backend_gtk.py: call FigureCanvasBase.motion_notify_event() on all + motion-notify-events, not just ones where a modifier key or button has been + pressed (fixes bug report from Niklas Volbers) - SC + +2005-07-24 + backend_gtk.py: modify print_figure() use own pixmap, fixing problems where + print_figure() overwrites the display pixmap. return False from all + button/key etc events - to allow the event to propagate further - SC + +2005-07-23 + backend_gtk.py: change expose_event from using set_back_pixmap(); clear() + to draw_drawable() - SC + +2005-07-23 + backend_gtk.py: removed pygtk.require() matplotlib/__init__.py: delete + 'FROZEN' and 'McPLError' which are no longer used - SC + +2005-07-22 + backend_gdk.py: removed pygtk.require() - SC + +2005-07-21 + backend_svg.py: Remove unused imports. Remove methods doc strings which + just duplicate the docs from backend_bases.py. Rename draw_mathtext to + _draw_mathtext. - SC + +2005-07-17 + examples/embedding_in_gtk3.py: new example demonstrating placing a + FigureCanvas in a gtk.ScrolledWindow - SC + +2005-07-14 + Fixed a Windows related bug (#1238412) in texmanager - DSD + +2005-07-11 + Fixed color kwarg bug, setting color=1 or 0 caused an exception - DSD + +2005-07-07 + Added Eric's MA set_xdata Line2D fix - JDH + +2005-07-06 + Made HOME/.matplotlib the new config dir where the matplotlibrc file, the + ttf.cache, and the tex.cache live. The new default filenames in + .matplotlib have no leading dot and are not hidden. e.g., the new names + are matplotlibrc tex.cache ttffont.cache. This is how ipython does it so + it must be right. If old files are found, a warning is issued and they are + moved to the new location. Also fixed texmanager to put all files, + including temp files in ~/.matplotlib/tex.cache, which allows you to usetex + in non-writable dirs. + +2005-07-05 + Fixed bug #1231611 in subplots adjust layout. The problem was that the + text caching mechanism was not using the transformation affine in the key. + - JDH + +2005-07-05 + Fixed default backend import problem when using API (SF bug # 1209354 - + see API_CHANGES for more info - JDH + +2005-07-04 + backend_gtk.py: require PyGTK version 2.0.0 or higher - SC + +2005-06-30 + setupext.py: added numarray_inc_dirs for building against numarray when not + installed in standard location - ADS + +2005-06-27 + backend_svg.py: write figure width, height as int, not float. Update to + fix some of the pychecker warnings - SC + +2005-06-23 + Updated examples/agg_test.py to demonstrate curved paths and fills - JDH + +2005-06-21 + Moved some texmanager and backend_agg tex caching to class level rather + than instance level - JDH + +2005-06-20 + setupext.py: fix problem where _nc_backend_gdk is installed to the wrong + directory - SC + +2005-06-19 + Added 10.4 support for CocoaAgg. - CM + +2005-06-18 + Move Figure.get_width_height() to FigureCanvasBase and return int instead + of float. - SC + +2005-06-18 + Applied Ted Drain's QtAgg patch: 1) Changed the toolbar to be a horizontal + bar of push buttons instead of a QToolbar and updated the layout algorithms + in the main window accordingly. This eliminates the ability to drag and + drop the toolbar and detach it from the window. 2) Updated the resize + algorithm in the main window to show the correct size for the plot widget + as requested. This works almost correctly right now. It looks to me like + the final size of the widget is off by the border of the main window but I + haven't figured out a way to get that information yet. We could just add a + small margin to the new size but that seems a little hacky. 3) Changed the + x/y location label to be in the toolbar like the Tk backend instead of as a + status line at the bottom of the widget. 4) Changed the toolbar pixmaps to + use the ppm files instead of the png files. I noticed that the Tk backend + buttons looked much nicer and it uses the ppm files so I switched them. + +2005-06-17 + Modified the gtk backend to not queue mouse motion events. This allows for + live updates when dragging a slider. - CM + +2005-06-17 + Added starter CocoaAgg backend. Only works on OS 10.3 for now and requires + PyObjC. (10.4 is high priority) - CM + +2005-06-17 + Upgraded pyparsing and applied Paul McGuire's suggestions for speeding + things up. This more than doubles the speed of mathtext in my simple + tests. JDH + +2005-06-16 + Applied David Cooke's subplot make_key patch ---------------------------------- -2005-06-15 0.82 released +0.82 (2005-06-15) +----------------- -2005-06-15 Added subplot config tool to GTK* backends -- note you must - now import the NavigationToolbar2 from your backend of - choice rather than from backend_gtk because it needs to - know about the backend specific canvas -- see - examples/embedding_in_gtk2.py. Ditto for wx backend -- see - examples/embedding_in_wxagg.py +2005-06-15 + Added subplot config tool to GTK* backends -- note you must now import the + NavigationToolbar2 from your backend of choice rather than from backend_gtk + because it needs to know about the backend specific canvas -- see + examples/embedding_in_gtk2.py. Ditto for wx backend -- see + examples/embedding_in_wxagg.py -2005-06-15 backend_cairo.py: updated to use pycairo 0.5.0 - SC +2005-06-15 + backend_cairo.py: updated to use pycairo 0.5.0 - SC -2005-06-14 Wrote some GUI neutral widgets (Button, Slider, - RadioButtons, CheckButtons) in matplotlib.widgets. See - examples/widgets/\*.py - JDH +2005-06-14 + Wrote some GUI neutral widgets (Button, Slider, RadioButtons, CheckButtons) + in matplotlib.widgets. See examples/widgets/\*.py - JDH -2005-06-14 Exposed subplot parameters as rc vars and as the fig - SubplotParams instance subplotpars. See - figure.SubplotParams, figure.Figure.subplots_adjust and the - pylab method subplots_adjust and - examples/subplots_adjust.py . Also added a GUI neutral - widget for adjusting subplots, see - examples/subplot_toolbar.py - JDH +2005-06-14 + Exposed subplot parameters as rc vars and as the fig SubplotParams instance + subplotpars. See figure.SubplotParams, figure.Figure.subplots_adjust and + the pylab method subplots_adjust and examples/subplots_adjust.py . Also + added a GUI neutral widget for adjusting subplots, see + examples/subplot_toolbar.py - JDH -2005-06-13 Exposed cap and join style for lines with new rc params and - line properties +2005-06-13 + Exposed cap and join style for lines with new rc params and line properties:: lines.dash_joinstyle : miter # miter|round|bevel lines.dash_capstyle : butt # butt|round|projecting @@ -4037,1405 +4750,1654 @@ the `API changes <../../api/api_changes.html>`_. lines.solid_capstyle : projecting # butt|round|projecting -2005-06-13 Added kwargs to Axes init +2005-06-13 + Added kwargs to Axes init -2005-06-13 Applied Baptiste's tick patch - JDH +2005-06-13 + Applied Baptiste's tick patch - JDH -2005-06-13 Fixed rc alias 'l' bug reported by Fernando by removing - aliases for mainlevel rc options. - JDH +2005-06-13 + Fixed rc alias 'l' bug reported by Fernando by removing aliases for + mainlevel rc options. - JDH -2005-06-10 Fixed bug #1217637 in ticker.py - DSD +2005-06-10 + Fixed bug #1217637 in ticker.py - DSD -2005-06-07 Fixed a bug in texmanager.py: .aux files not being removed - DSD +2005-06-07 + Fixed a bug in texmanager.py: .aux files not being removed - DSD -2005-06-08 Added Sean Richard's hist binning fix -- see API_CHANGES - JDH +2005-06-08 + Added Sean Richard's hist binning fix -- see API_CHANGES - JDH -2005-06-07 Fixed a bug in texmanager.py: .aux files not being removed - - DSD +2005-06-07 + Fixed a bug in texmanager.py: .aux files not being removed - DSD ---------------------- -2005-06-07 matplotlib-0.81 released +0.81 (2005-06-07) +----------------- -2005-06-06 Added autoscale_on prop to axes +2005-06-06 + Added autoscale_on prop to axes -2005-06-06 Added Nick's picker "among" patch - JDH +2005-06-06 + Added Nick's picker "among" patch - JDH -2005-06-05 Fixed a TeX/LaTeX font discrepency in backend_ps. - DSD +2005-06-05 + Fixed a TeX/LaTeX font discrepancy in backend_ps. - DSD -2005-06-05 Added a ps.distill option in rc settings. If True, postscript - output will be distilled using ghostscript, which should trim - the file size and allow it to load more quickly. Hopefully this - will address the issue of large ps files due to font - definitions. Tested with gnu-ghostscript-8.16. - DSD +2005-06-05 + Added a ps.distill option in rc settings. If True, postscript output will + be distilled using ghostscript, which should trim the file size and allow + it to load more quickly. Hopefully this will address the issue of large ps + files due to font definitions. Tested with gnu-ghostscript-8.16. - DSD -2005-06-03 Improved support for tex handling of text in backend_ps. - DSD +2005-06-03 + Improved support for tex handling of text in backend_ps. - DSD -2005-06-03 Added rc options to render text with tex or latex, and to select - the latex font package. - DSD +2005-06-03 + Added rc options to render text with tex or latex, and to select the latex + font package. - DSD -2005-06-03 Fixed a bug in ticker.py causing a ZeroDivisionError +2005-06-03 + Fixed a bug in ticker.py causing a ZeroDivisionError -2005-06-02 backend_gtk.py remove DBL_BUFFER, add line to expose_event to - try to fix pygtk 2.6 redraw problem - SC +2005-06-02 + backend_gtk.py remove DBL_BUFFER, add line to expose_event to try to fix + pygtk 2.6 redraw problem - SC -2005-06-01 The default behavior of ScalarFormatter now renders scientific - notation and large numerical offsets in a label at the end of - the axis. - DSD +2005-06-01 + The default behavior of ScalarFormatter now renders scientific notation and + large numerical offsets in a label at the end of the axis. - DSD -2005-06-01 Added Nicholas' frombyte image patch - JDH +2005-06-01 + Added Nicholas' frombyte image patch - JDH -2005-05-31 Added vertical TeX support for agg - JDH +2005-05-31 + Added vertical TeX support for agg - JDH -2005-05-31 Applied Eric's cntr patch - JDH +2005-05-31 + Applied Eric's cntr patch - JDH -2005-05-27 Finally found the pesky agg bug (which Maxim was kind - enough to fix within hours) that was causing a segfault in - the win32 cached marker drawing. Now windows users can get - the enormouse performance benefits of caced markers w/o - those occasional pesy screenshots. - JDH +2005-05-27 + Finally found the pesky agg bug (which Maxim was kind enough to fix within + hours) that was causing a segfault in the win32 cached marker drawing. Now + windows users can get the enormous performance benefits of cached markers + w/o those occasional pesy screenshots. - JDH -2005-05-27 Got win32 build system working again, using a more recent - version of gtk and pygtk in the win32 build, gtk 2.6 from - https://web.archive.org/web/20050527002647/https://www.gimp.org/~tml/gimp/win32/downloads.html (you - will also need libpng12.dll to use these). I haven't - tested whether this binary build of mpl for win32 will work - with older gtk runtimes, so you may need to upgrade. +2005-05-27 + Got win32 build system working again, using a more recent version of gtk + and pygtk in the win32 build, gtk 2.6 from + https://web.archive.org/web/20050527002647/https://www.gimp.org/~tml/gimp/win32/downloads.html + (you will also need libpng12.dll to use these). I haven't tested whether + this binary build of mpl for win32 will work with older gtk runtimes, so + you may need to upgrade. -2005-05-27 Fixed bug where 2nd wxapp could be started if using wxagg - backend. - ADS +2005-05-27 + Fixed bug where 2nd wxapp could be started if using wxagg backend. - ADS -2005-05-26 Added Daishi text with dash patch -- see examples/dashtick.py +2005-05-26 + Added Daishi text with dash patch -- see examples/dashtick.py -2005-05-26 Moved backend_latex functionality into backend_ps. If - text.usetex=True, the PostScript backend will use LaTeX to - generate the .ps or .eps file. Ghostscript is required for - eps output. - DSD +2005-05-26 + Moved backend_latex functionality into backend_ps. If text.usetex=True, the + PostScript backend will use LaTeX to generate the .ps or .eps file. + Ghostscript is required for eps output. - DSD -2005-05-24 Fixed alignment and color issues in latex backend. - DSD +2005-05-24 + Fixed alignment and color issues in latex backend. - DSD -2005-05-21 Fixed raster problem for small rasters with dvipng -- looks - like it was a premultipled alpha problem - JDH +2005-05-21 + Fixed raster problem for small rasters with dvipng -- looks like it was a + premultiplied alpha problem - JDH -2005-05-20 Added linewidth and faceted kwarg to scatter to control - edgewidth and color. Also added autolegend patch to - inspect line segments. +2005-05-20 + Added linewidth and faceted kwarg to scatter to control edgewidth and + color. Also added autolegend patch to inspect line segments. -2005-05-18 Added Orsay and JPL qt fixes - JDH +2005-05-18 + Added Orsay and JPL qt fixes - JDH -2005-05-17 Added a psfrag latex backend -- some alignment issues need - to be worked out. Run with -dLaTeX and a *.tex file and - *.eps file are generated. latex and dvips the generated - latex file to get ps output. Note xdvi *does* not work, - you must generate ps.- JDH +2005-05-17 + Added a psfrag latex backend -- some alignment issues need to be worked + out. Run with -dLaTeX and a *.tex file and *.eps file are generated. latex + and dvips the generated latex file to get ps output. Note xdvi *does* not + work, you must generate ps.- JDH -2005-05-13 Added Florent Rougon's Axis set_label1 - patch +2005-05-13 + Added Florent Rougon's Axis set_label1 patch -2005-05-17 pcolor optimization, fixed bug in previous pcolor patch - JSWHIT +2005-05-17 + pcolor optimization, fixed bug in previous pcolor patch - JSWHIT -2005-05-16 Added support for masked arrays in pcolor - JSWHIT +2005-05-16 + Added support for masked arrays in pcolor - JSWHIT -2005-05-12 Started work on TeX text for antigrain using pngdvi -- see - examples/tex_demo.py and the new module - matplotlib.texmanager. Rotated text not supported and - rendering small glyps is not working right yet. BUt large - fontsizes and/or high dpi saved figs work great. +2005-05-12 + Started work on TeX text for antigrain using pngdvi -- see + examples/tex_demo.py and the new module matplotlib.texmanager. Rotated + text not supported and rendering small glyphs is not working right yet. But + large fontsizes and/or high dpi saved figs work great. -2005-05-10 New image resize options interpolation options. New values - for the interp kwarg are +2005-05-10 + New image resize options interpolation options. New values for the interp + kwarg are 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'blackman' - See help(imshow) for details, particularly the - interpolation, filternorm and filterrad kwargs + See help(imshow) for details, particularly the interpolation, filternorm + and filterrad kwargs -2005-05-10 Applied Eric's contour mem leak fixes - JDH +2005-05-10 + Applied Eric's contour mem leak fixes - JDH -2005-05-10 Extended python agg wrapper and started implementing - backend_agg2, an agg renderer based on the python wrapper. - This will be more flexible and easier to extend than the - current backend_agg. See also examples/agg_test.py - JDH +2005-05-10 + Extended python agg wrapper and started implementing backend_agg2, an agg + renderer based on the python wrapper. This will be more flexible and + easier to extend than the current backend_agg. See also + examples/agg_test.py - JDH -2005-05-09 Added Marcin's no legend patch to exclude lines from the - autolegend builder +2005-05-09 + Added Marcin's no legend patch to exclude lines from the autolegend builder:: plot(x, y, label='nolegend') -2005-05-05 Upgraded to agg23 +2005-05-05 + Upgraded to agg23 -2005-05-05 Added newscalarformatter_demo.py to examples. -DSD +2005-05-05 + Added newscalarformatter_demo.py to examples. -DSD -2005-05-04 Added NewScalarFormatter. Improved formatting of ticklabels, - scientific notation, and the ability to plot large large - numbers with small ranges, by determining a numerical offset. - See ticker.NewScalarFormatter for more details. -DSD +2005-05-04 + Added NewScalarFormatter. Improved formatting of ticklabels, scientific + notation, and the ability to plot large numbers with small ranges, by + determining a numerical offset. See ticker.NewScalarFormatter for more + details. -DSD -2005-05-03 Added the option to specify a delimiter in pylab.load -DSD +2005-05-03 + Added the option to specify a delimiter in pylab.load -DSD -2005-04-28 Added Darren's line collection example +2005-04-28 + Added Darren's line collection example -2005-04-28 Fixed aa property in agg - JDH +2005-04-28 + Fixed aa property in agg - JDH -2005-04-27 Set postscript page size in .matplotlibrc - DSD +2005-04-27 + Set postscript page size in .matplotlibrc - DSD -2005-04-26 Added embedding in qt example. - JDH +2005-04-26 + Added embedding in qt example. - JDH -2005-04-14 Applied Michael Brady's qt backend patch: 1) fix a bug - where keyboard input was grabbed by the figure and not - released 2) turn on cursor changes 3) clean up a typo - and commented-out print statement. - JDH +2005-04-14 + Applied Michael Brady's qt backend patch: 1) fix a bug where keyboard input + was grabbed by the figure and not released 2) turn on cursor changes 3) + clean up a typo and commented-out print statement. - JDH +2005-04-14 + Applied Eric Firing's masked data lines patch and contour patch. Support + for masked arrays has been added to the plot command and to the Line2D + object. Only the valid points are plotted. A "valid_only" kwarg was added + to the get_xdata() and get_ydata() methods of Line2D; by default it is + False, so that the original data arrays are returned. Setting it to True + returns the plottable points. - see examples/masked_demo.py - JDH -2005-04-14 Applied Eric Firing's masked data lines patch and contour - patch. Support for masked arrays has been added to the - plot command and to the Line2D object. Only the valid - points are plotted. A "valid_only" kwarg was added to the - get_xdata() and get_ydata() methods of Line2D; by default - it is False, so that the original data arrays are - returned. Setting it to True returns the plottable points. - - see examples/masked_demo.py - JDH - -2005-04-13 Applied Tim Leslie's arrow key event handling patch - JDH - +2005-04-13 + Applied Tim Leslie's arrow key event handling patch - JDH --------------------------- -0.80 released +0.80 +---- -2005-04-11 Applied a variant of rick's xlim/ylim/axis patch. These - functions now take kwargs to let you selectively alter only - the min or max if desired. e.g., xlim(xmin=2) or - axis(ymax=3). They always return the new lim. - JDH +2005-04-11 + Applied a variant of rick's xlim/ylim/axis patch. These functions now take + kwargs to let you selectively alter only the min or max if desired. e.g., + xlim(xmin=2) or axis(ymax=3). They always return the new lim. - JDH -2005-04-11 Incorporated Werner's wx patch -- wx backend should be - compatible with wxpython2.4 and recent versions of 2.5. - Some early versions of wxpython 2.5 will not work because - there was a temporary change in the dc API that was rolled - back to make it 2.4 compliant +2005-04-11 + Incorporated Werner's wx patch -- wx backend should be compatible with + wxpython2.4 and recent versions of 2.5. Some early versions of wxpython + 2.5 will not work because there was a temporary change in the dc API that + was rolled back to make it 2.4 compliant -2005-04-11 modified tkagg show so that new figure window pops up on - call to figure +2005-04-11 + modified tkagg show so that new figure window pops up on call to figure -2005-04-11 fixed wxapp init bug +2005-04-11 + fixed wxapp init bug -2005-04-02 updated backend_ps.draw_lines, draw_markers for use with the - new API - DSD +2005-04-02 + updated backend_ps.draw_lines, draw_markers for use with the new API - DSD -2005-04-01 Added editable polygon example +2005-04-01 + Added editable polygon example ------------------------------ -2005-03-31 0.74 released +0.74 (2005-03-31) +----------------- -2005-03-30 Fixed and added checks for floating point inaccuracy in - ticker.Base - DSD +2005-03-30 + Fixed and added checks for floating point inaccuracy in ticker.Base - DSD -2005-03-30 updated /ellipse definition in backend_ps.py to address bug - #1122041 - DSD +2005-03-30 + updated /ellipse definition in backend_ps.py to address bug #1122041 - DSD -2005-03-29 Added unicode support for Agg and PS - JDH +2005-03-29 + Added unicode support for Agg and PS - JDH -2005-03-28 Added Jarrod's svg patch for text - JDH +2005-03-28 + Added Jarrod's svg patch for text - JDH -2005-03-28 Added Ludal's arrow and quiver patch - JDH +2005-03-28 + Added Ludal's arrow and quiver patch - JDH -2005-03-28 Added label kwarg to Axes to facilitate forcing the - creation of new Axes with otherwise identical attributes +2005-03-28 + Added label kwarg to Axes to facilitate forcing the creation of new Axes + with otherwise identical attributes -2005-03-28 Applied boxplot and OSX font search patches +2005-03-28 + Applied boxplot and OSX font search patches -2005-03-27 Added ft2font NULL check to fix Japanase font bug - JDH +2005-03-27 + Added ft2font NULL check to fix Japanese font bug - JDH -2005-03-27 Added sprint legend patch plus John Gill's tests and fix -- - see examples/legend_auto.py - JDH +2005-03-27 + Added sprint legend patch plus John Gill's tests and fix -- see + examples/legend_auto.py - JDH --------------------------- -2005-03-19 0.73.1 released +0.73.1 (2005-03-19) +------------------- -2005-03-19 Reverted wxapp handling because it crashed win32 - JDH +2005-03-19 + Reverted wxapp handling because it crashed win32 - JDH -2005-03-18 Add .number attribute to figure objects returned by figure() - FP +2005-03-18 + Add .number attribute to figure objects returned by figure() - FP --------------------------- -2005-03-18 0.73 released - -2005-03-16 Fixed labelsep bug - -2005-03-16 Applied Darren's ticker fix for small ranges - JDH - -2005-03-16 Fixed tick on horiz colorbar - JDH - -2005-03-16 Added Japanese winreg patch - JDH - -2005-03-15 backend_gtkagg.py: changed to use double buffering, this fixes - the problem reported Joachim Berdal Haga - "Parts of plot lagging - from previous frame in animation". Tested with anim.py and it makes - no noticeable difference to performance (23.7 before, 23.6 after) - - SC - -2005-03-14 add src/_backend_gdk.c extension to provide a substitute function - for pixbuf.get_pixels_array(). Currently pixbuf.get_pixels_array() - only works with Numeric, and then only works if pygtk has been - compiled with Numeric support. The change provides a function - pixbuf_get_pixels_array() which works with Numeric and numarray and - is always available. It means that backend_gtk should be able to - display images and mathtext in all circumstances. - SC - -2005-03-11 Upgraded CXX to 5.3.1 - -2005-03-10 remove GraphicsContextPS.set_linestyle() - and GraphicsContextSVG.set_linestyle() since they do no more than - the base class GraphicsContext.set_linestyle() - SC - -2005-03-09 Refactored contour functionality into dedicated module - -2005-03-09 Added Eric's contourf updates and Nadia's clabel functionality - -2005-03-09 Moved colorbar to figure.Figure to expose it for API developers - - JDH - -2005-03-09 backend_cairo.py: implemented draw_markers() - SC - -2005-03-09 cbook.py: only use enumerate() (the python version) if the builtin - version is not available. - Add new function 'izip' which is set to itertools.izip if available - and the python equivalent if not available. - SC - -2005-03-07 backend_gdk.py: remove PIXELS_PER_INCH from points_to_pixels(), but - still use it to adjust font sizes. This allows the GTK version of - line_styles.py to more closely match GTKAgg, previously the markers - were being drawn too large. - SC - -2005-03-01 Added Eric's contourf routines - -2005-03-01 Added start of proper agg SWIG wrapper. I would like to - expose agg functionality directly a the user level and this - module will serve that purpose eventually, and will - hopefully take over most of the functionality of the - current _image and _backend_agg modules. - JDH - -2005-02-28 Fixed polyfit / polyval to convert input args to float - arrays - JDH - - -2005-02-25 Add experimental feature to backend_gtk.py to enable/disable - double buffering (DBL_BUFFER=True/False) - SC - -2005-02-24 colors.py change ColorConverter.to_rgb() so it always returns rgb - (and not rgba), allow cnames keys to be cached, change the exception - raised from RuntimeError to ValueError (like hex2color()) - hex2color() use a regular expression to check the color string is - valid - SC - - -2005-02-23 Added rc param ps.useafm so backend ps can use native afm - fonts or truetype. afme breaks mathtext but causes much - smaller font sizes and may result in images that display - better in some contexts (e.g., pdfs incorporated into latex - docs viewed in acrobat reader). I would like to extend - this approach to allow the user to use truetype only for - mathtext, which should be easy. - -2005-02-23 Used sequence protocol rather than tuple in agg collection - drawing routines for greater flexibility - JDH - +0.73 (2005-03-18) +----------------- + +2005-03-16 + Fixed labelsep bug + +2005-03-16 + Applied Darren's ticker fix for small ranges - JDH + +2005-03-16 + Fixed tick on horiz colorbar - JDH + +2005-03-16 + Added Japanese winreg patch - JDH + +2005-03-15 + backend_gtkagg.py: changed to use double buffering, this fixes the problem + reported Joachim Berdal Haga - "Parts of plot lagging from previous frame + in animation". Tested with anim.py and it makes no noticeable difference to + performance (23.7 before, 23.6 after) - SC + +2005-03-14 + add src/_backend_gdk.c extension to provide a substitute function for + pixbuf.get_pixels_array(). Currently pixbuf.get_pixels_array() only works + with Numeric, and then only works if pygtk has been compiled with Numeric + support. The change provides a function pixbuf_get_pixels_array() which + works with Numeric and numarray and is always available. It means that + backend_gtk should be able to display images and mathtext in all + circumstances. - SC + +2005-03-11 + Upgraded CXX to 5.3.1 + +2005-03-10 + remove GraphicsContextPS.set_linestyle() and + GraphicsContextSVG.set_linestyle() since they do no more than the base + class GraphicsContext.set_linestyle() - SC + +2005-03-09 + Refactored contour functionality into dedicated module + +2005-03-09 + Added Eric's contourf updates and Nadia's clabel functionality + +2005-03-09 + Moved colorbar to figure.Figure to expose it for API developers - JDH + +2005-03-09 + backend_cairo.py: implemented draw_markers() - SC + +2005-03-09 + cbook.py: only use enumerate() (the python version) if the builtin version + is not available. Add new function 'izip' which is set to itertools.izip + if available and the python equivalent if not available. - SC + +2005-03-07 + backend_gdk.py: remove PIXELS_PER_INCH from points_to_pixels(), but still + use it to adjust font sizes. This allows the GTK version of line_styles.py + to more closely match GTKAgg, previously the markers were being drawn too + large. - SC + +2005-03-01 + Added Eric's contourf routines + +2005-03-01 + Added start of proper agg SWIG wrapper. I would like to expose agg + functionality directly a the user level and this module will serve that + purpose eventually, and will hopefully take over most of the functionality + of the current _image and _backend_agg modules. - JDH + +2005-02-28 + Fixed polyfit / polyval to convert input args to float arrays - JDH + +2005-02-25 + Add experimental feature to backend_gtk.py to enable/disable double + buffering (DBL_BUFFER=True/False) - SC + +2005-02-24 + colors.py change ColorConverter.to_rgb() so it always returns rgb (and not + rgba), allow cnames keys to be cached, change the exception raised from + RuntimeError to ValueError (like hex2color()) hex2color() use a regular + expression to check the color string is valid - SC + +2005-02-23 + Added rc param ps.useafm so backend ps can use native afm fonts or + truetype. afme breaks mathtext but causes much smaller font sizes and may + result in images that display better in some contexts (e.g., pdfs + incorporated into latex docs viewed in acrobat reader). I would like to + extend this approach to allow the user to use truetype only for mathtext, + which should be easy. + +2005-02-23 + Used sequence protocol rather than tuple in agg collection drawing routines + for greater flexibility - JDH -------------------------------- -2005-02-22 0.72.1 released - -2005-02-21 fixed linestyles for collections -- contour now dashes for - levels <0 +0.72.1 (2005-02-22) +------------------- -2005-02-21 fixed ps color bug - JDH +2005-02-21 + fixed linestyles for collections -- contour now dashes for levels <0 -2005-02-15 fixed missing qt file +2005-02-21 + fixed ps color bug - JDH -2005-02-15 banished error_msg and report_error. Internal backend - methods like error_msg_gtk are preserved. backend writers, - check your backends, and diff against 0.72 to make sure I - did the right thing! - JDH +2005-02-15 + fixed missing qt file +2005-02-15 + banished error_msg and report_error. Internal backend methods like + error_msg_gtk are preserved. backend writers, check your backends, and + diff against 0.72 to make sure I did the right thing! - JDH -2005-02-14 Added enthought traits to matplotlib tree - JDH +2005-02-14 + Added enthought traits to matplotlib tree - JDH ------------------------ -2005-02-14 0.72 released +0.72 (2005-02-14) +----------------- -2005-02-14 fix bug in cbook alltrue() and onetrue() - SC +2005-02-14 + fix bug in cbook alltrue() and onetrue() - SC -2005-02-11 updated qtagg backend from Ted - JDH +2005-02-11 + updated qtagg backend from Ted - JDH -2005-02-11 matshow fixes for figure numbering, return value and docs - FP +2005-02-11 + matshow fixes for figure numbering, return value and docs - FP -2005-02-09 new zorder example for fine control in zorder_demo.py - FP +2005-02-09 + new zorder example for fine control in zorder_demo.py - FP -2005-02-09 backend renderer draw_lines now has transform in backend, - as in draw_markers; use numerix in _backend_agg, aded small - line optimization to agg +2005-02-09 + backend renderer draw_lines now has transform in backend, as in + draw_markers; use numerix in _backend_agg, added small line optimization to + agg -2005-02-09 subplot now deletes axes that it overlaps +2005-02-09 + subplot now deletes axes that it overlaps -2005-02-08 Added transparent support for gzipped files in load/save - Fernando - Perez (FP from now on). +2005-02-08 + Added transparent support for gzipped files in load/save - Fernando Perez + (FP from now on). -2005-02-08 Small optimizations in PS backend. They may have a big impact for - large plots, otherwise they don't hurt - FP +2005-02-08 + Small optimizations in PS backend. They may have a big impact for large + plots, otherwise they don't hurt - FP -2005-02-08 Added transparent support for gzipped files in load/save - Fernando - Perez (FP from now on). +2005-02-08 + Added transparent support for gzipped files in load/save - Fernando Perez + (FP from now on). -2005-02-07 Added newstyle path drawing for markers - only implemented - in agg currently - JDH +2005-02-07 + Added newstyle path drawing for markers - only implemented in agg currently + - JDH -2005-02-05 Some superscript text optimizations for ticking log plots +2005-02-05 + Some superscript text optimizations for ticking log plots -2005-02-05 Added some default key press events to pylab figures: 'g' - toggles grid - JDH +2005-02-05 + Added some default key press events to pylab figures: 'g' toggles grid - + JDH -2005-02-05 Added some support for handling log switching for lines - that have nonpos data - JDH +2005-02-05 + Added some support for handling log switching for lines that have nonpos + data - JDH -2005-02-04 Added Nadia's contour patch - contour now has matlab - compatible syntax; this also fixed an unequal sized contour - array bug- JDH +2005-02-04 + Added Nadia's contour patch - contour now has matlab compatible syntax; + this also fixed an unequal sized contour array bug- JDH -2005-02-04 Modified GTK backends to allow the FigureCanvas to be resized - smaller than its original size - SC +2005-02-04 + Modified GTK backends to allow the FigureCanvas to be resized smaller than + its original size - SC -2005-02-02 Fixed a bug in dates mx2num - JDH +2005-02-02 + Fixed a bug in dates mx2num - JDH -2005-02-02 Incorporated Fernando's matshow - JDH +2005-02-02 + Incorporated Fernando's matshow - JDH -2005-02-01 Added Fernando's figure num patch, including experimental - support for pylab backend switching, LineCOllection.color - warns, savefig now a figure method, fixed a close(fig) bug - - JDH +2005-02-01 + Added Fernando's figure num patch, including experimental support for pylab + backend switching, LineCOllection.color warns, savefig now a figure method, + fixed a close(fig) bug - JDH -2005-01-31 updated datalim in contour - JDH +2005-01-31 + updated datalim in contour - JDH -2005-01-30 Added backend_qtagg.py provided by Sigve Tjora - SC +2005-01-30 + Added backend_qtagg.py provided by Sigve Tjora - SC -2005-01-28 Added tk.inspect rc param to .matplotlibrc. IDLE users - should set tk.pythoninspect:True and interactive:True and - backend:TkAgg +2005-01-28 + Added tk.inspect rc param to .matplotlibrc. IDLE users should set + tk.pythoninspect:True and interactive:True and backend:TkAgg -2005-01-28 Replaced examples/interactive.py with an updated script from - Fernando Perez - SC +2005-01-28 + Replaced examples/interactive.py with an updated script from Fernando Perez + - SC -2005-01-27 Added support for shared x or y axes. See - examples/shared_axis_demo.py and examples/ganged_plots.py +2005-01-27 + Added support for shared x or y axes. See examples/shared_axis_demo.py and + examples/ganged_plots.py -2005-01-27 Added Lee's patch for missing symbols \leq and \LEFTbracket - to _mathtext_data - JDH +2005-01-27 + Added Lee's patch for missing symbols \leq and \LEFTbracket to + _mathtext_data - JDH -2005-01-26 Added Baptiste's two scales patch -- see help(twinx) in the - pylab interface for more info. See also - examples/two_scales.py +2005-01-26 + Added Baptiste's two scales patch -- see help(twinx) in the pylab interface + for more info. See also examples/two_scales.py -2005-01-24 Fixed a mathtext parser bug that prevented font changes in - sub/superscripts - JDH +2005-01-24 + Fixed a mathtext parser bug that prevented font changes in sub/superscripts + - JDH -2005-01-24 Fixed contour to work w/ interactive changes in colormaps, - clim, etc - JDH +2005-01-24 + Fixed contour to work w/ interactive changes in colormaps, clim, etc - JDH ----------------------------- -2005-01-21 matplotlib-0.71 released +0.71 (2005-01-21) +----------------- -2005-01-21 Refactored numerix to solve vexing namespace issues - JDH +2005-01-21 + Refactored numerix to solve vexing namespace issues - JDH -2005-01-21 Applied Nadia's contour bug fix - JDH +2005-01-21 + Applied Nadia's contour bug fix - JDH -2005-01-20 Made some changes to the contour routine - particularly - region=1 seems t fix a lot of the zigzag strangeness. - Added colormaps as default for contour - JDH +2005-01-20 + Made some changes to the contour routine - particularly region=1 seems t + fix a lot of the zigzag strangeness. Added colormaps as default for + contour - JDH -2005-01-19 Restored builtin names which were overridden (min, max, - abs, round, and sum) in pylab. This is a potentially - significant change for those who were relying on an array - version of those functions that previously overrode builtin - function names. - ADS +2005-01-19 + Restored builtin names which were overridden (min, max, abs, round, and + sum) in pylab. This is a potentially significant change for those who were + relying on an array version of those functions that previously overrode + builtin function names. - ADS -2005-01-18 Added accents to mathtext: \hat, \breve, \grave, \bar, - \acute, \tilde, \vec, \dot, \ddot. All of them have the - same syntax, e.g., to make an overbar you do \bar{o} or to - make an o umlaut you do \ddot{o}. The shortcuts are also - provided, e.g., \"o \'e \`e \~n \.x \^y - JDH +2005-01-18 + Added accents to mathtext: \hat, \breve, \grave, \bar, \acute, \tilde, + \vec, \dot, \ddot. All of them have the same syntax, e.g., to make an + overbar you do \bar{o} or to make an o umlaut you do \ddot{o}. The + shortcuts are also provided, e.g., \"o \'e \`e \~n \.x \^y - JDH -2005-01-18 Plugged image resize memory leaks - JDH +2005-01-18 + Plugged image resize memory leaks - JDH -2005-01-18 Fixed some mathtext parser problems relating to superscripts +2005-01-18 + Fixed some mathtext parser problems relating to superscripts -2005-01-17 Fixed a yticklabel problem for colorbars under change of - clim - JDH +2005-01-17 + Fixed a yticklabel problem for colorbars under change of clim - JDH -2005-01-17 Cleaned up Destroy handling in wx reducing memleak/fig from - approx 800k to approx 6k- JDH +2005-01-17 + Cleaned up Destroy handling in wx reducing memleak/fig from approx 800k to + approx 6k- JDH -2005-01-17 Added kappa to latex_to_bakoma - JDH +2005-01-17 + Added kappa to latex_to_bakoma - JDH -2005-01-15 Support arbitrary colorbar axes and horizontal colorbars - JDH +2005-01-15 + Support arbitrary colorbar axes and horizontal colorbars - JDH -2005-01-15 Fixed colormap number of colors bug so that the colorbar - has the same discretization as the image - JDH +2005-01-15 + Fixed colormap number of colors bug so that the colorbar has the same + discretization as the image - JDH -2005-01-15 Added Nadia's x,y contour fix - JDH +2005-01-15 + Added Nadia's x,y contour fix - JDH -2005-01-15 backend_cairo: added PDF support which requires pycairo 0.1.4. - Its not usable yet, but is ready for when the Cairo PDF backend - matures - SC +2005-01-15 + backend_cairo: added PDF support which requires pycairo 0.1.4. Its not + usable yet, but is ready for when the Cairo PDF backend matures - SC -2005-01-15 Added Nadia's x,y contour fix +2005-01-15 + Added Nadia's x,y contour fix -2005-01-12 Fixed set clip_on bug in artist - JDH +2005-01-12 + Fixed set clip_on bug in artist - JDH -2005-01-11 Reverted pythoninspect in tkagg - JDH +2005-01-11 + Reverted pythoninspect in tkagg - JDH -2005-01-09 Fixed a backend_bases event bug caused when an event is - triggered when location is None - JDH +2005-01-09 + Fixed a backend_bases event bug caused when an event is triggered when + location is None - JDH -2005-01-07 Add patch from Stephen Walton to fix bug in pylab.load() - when the % character is included in a comment. - ADS +2005-01-07 + Add patch from Stephen Walton to fix bug in pylab.load() when the % + character is included in a comment. - ADS -2005-01-07 Added markerscale attribute to Legend class. This allows - the marker size in the legend to be adjusted relative to - that in the plot. - ADS +2005-01-07 + Added markerscale attribute to Legend class. This allows the marker size + in the legend to be adjusted relative to that in the plot. - ADS -2005-01-06 Add patch from Ben Vanhaeren to make the FigureManagerGTK vbox a - public attribute - SC +2005-01-06 + Add patch from Ben Vanhaeren to make the FigureManagerGTK vbox a public + attribute - SC ---------------------------- -2004-12-30 Release 0.70 +2004-12-30 + Release 0.70 -2004-12-28 Added coord location to key press and added a - examples/picker_demo.py +2004-12-28 + Added coord location to key press and added a examples/picker_demo.py -2004-12-28 Fixed coords notification in wx toolbar - JDH +2004-12-28 + Fixed coords notification in wx toolbar - JDH -2004-12-28 Moved connection and disconnection event handling to the - FigureCanvasBase. Backends now only need to connect one - time for each of the button press, button release and key - press/release functions. The base class deals with - callbacks and multiple connections. This fixes flakiness - on some backends (tk, wx) in the presence of multiple - connections and/or disconnect - JDH +2004-12-28 + Moved connection and disconnection event handling to the FigureCanvasBase. + Backends now only need to connect one time for each of the button press, + button release and key press/release functions. The base class deals with + callbacks and multiple connections. This fixes flakiness on some backends + (tk, wx) in the presence of multiple connections and/or disconnect - JDH -2004-12-27 Fixed PS mathtext bug where color was not set - Jochen - please verify correct - JDH +2004-12-27 + Fixed PS mathtext bug where color was not set - Jochen please verify + correct - JDH -2004-12-27 Added Shadow class and added shadow kwarg to legend and pie - for shadow effect - JDH +2004-12-27 + Added Shadow class and added shadow kwarg to legend and pie for shadow + effect - JDH -2004-12-27 Added pie charts and new example/pie_demo.py +2004-12-27 + Added pie charts and new example/pie_demo.py -2004-12-23 Fixed an agg text rotation alignment bug, fixed some text - kwarg processing bugs, and added examples/text_rotation.py - to explain and demonstrate how text rotations and alignment - work in matplotlib. - JDH +2004-12-23 + Fixed an agg text rotation alignment bug, fixed some text kwarg processing + bugs, and added examples/text_rotation.py to explain and demonstrate how + text rotations and alignment work in matplotlib. - JDH ----------------------- -2004-12-22 0.65.1 released - JDH +0.65.1 (2004-12-22) +------------------- -2004-12-22 Fixed colorbar bug which caused colorbar not to respond to - changes in colormap in some instances - JDH +2004-12-22 + Fixed colorbar bug which caused colorbar not to respond to changes in + colormap in some instances - JDH -2004-12-22 Refactored NavigationToolbar in tkagg to support app - embedding , init now takes (canvas, window) rather than - (canvas, figman) - JDH +2004-12-22 + Refactored NavigationToolbar in tkagg to support app embedding , init now + takes (canvas, window) rather than (canvas, figman) - JDH -2004-12-21 Refactored axes and subplot management - removed - add_subplot and add_axes from the FigureManager. classic - toolbar updates are done via an observer pattern on the - figure using add_axobserver. Figure now maintains the axes - stack (for gca) and supports axes deletion. Ported changes - to GTK, Tk, Wx, and FLTK. Please test! Added delaxes - JDH +2004-12-21 + Refactored axes and subplot management - removed add_subplot and add_axes + from the FigureManager. classic toolbar updates are done via an observer + pattern on the figure using add_axobserver. Figure now maintains the axes + stack (for gca) and supports axes deletion. Ported changes to GTK, Tk, Wx, + and FLTK. Please test! Added delaxes - JDH -2004-12-21 Lots of image optimizations - 4x performance boost over - 0.65 JDH +2004-12-21 + Lots of image optimizations - 4x performance boost over 0.65 JDH -2004-12-20 Fixed a figimage bug where the axes is shown and modified - tkagg to move the destroy binding into the show method. +2004-12-20 + Fixed a figimage bug where the axes is shown and modified tkagg to move the + destroy binding into the show method. -2004-12-18 Minor refactoring of NavigationToolbar2 to support - embedding in an application - JDH +2004-12-18 + Minor refactoring of NavigationToolbar2 to support embedding in an + application - JDH -2004-12-14 Added linestyle to collections (currently broken) - JDH +2004-12-14 + Added linestyle to collections (currently broken) - JDH -2004-12-14 Applied Nadia's setupext patch to fix libstdc++ link - problem with contour and solaris -JDH +2004-12-14 + Applied Nadia's setupext patch to fix libstdc++ link problem with contour + and solaris -JDH -2004-12-14 A number of pychecker inspired fixes, including removal of - True and False from cbook which I erroneously thought was - needed for python2.2 - JDH +2004-12-14 + A number of pychecker inspired fixes, including removal of True and False + from cbook which I erroneously thought was needed for python2.2 - JDH -2004-12-14 Finished porting doc strings for set introspection. - Used silent_list for many get funcs that return - lists. JDH +2004-12-14 + Finished porting doc strings for set introspection. Used silent_list for + many get funcs that return lists. JDH -2004-12-13 dates.py: removed all timezone() calls, except for UTC - SC +2004-12-13 + dates.py: removed all timezone() calls, except for UTC - SC ---------------------------- -2004-12-13 0.65 released - JDH - -2004-12-13 colors.py: rgb2hex(), hex2color() made simpler (and faster), also - rgb2hex() - added round() instead of integer truncation - hex2color() - changed 256.0 divisor to 255.0, so now - '#ffffff' becomes (1.0,1.0,1.0) not (0.996,0.996,0.996) - SC +0.65 (2004-12-13) +----------------- -2004-12-11 Added ion and ioff to pylab interface - JDH +2004-12-13 + colors.py: rgb2hex(), hex2color() made simpler (and faster), also rgb2hex() + - added round() instead of integer truncation hex2color() - changed 256.0 + divisor to 255.0, so now '#ffffff' becomes (1.0,1.0,1.0) not + (0.996,0.996,0.996) - SC -2004-12-11 backend_template.py: delete FigureCanvasTemplate.realize() - most - backends don't use it and its no longer needed +2004-12-11 + Added ion and ioff to pylab interface - JDH - backend_ps.py, backend_svg.py: delete show() and - draw_if_interactive() - they are not needed for image backends +2004-12-11 + backend_template.py: delete FigureCanvasTemplate.realize() - most backends + don't use it and its no longer needed - backend_svg.py: write direct to file instead of StringIO - - SC + backend_ps.py, backend_svg.py: delete show() and draw_if_interactive() - + they are not needed for image backends -2004-12-10 Added zorder to artists to control drawing order of lines, - patches and text in axes. See examples/zoder_demo.py - JDH + backend_svg.py: write direct to file instead of StringIO -2004-12-10 Fixed colorbar bug with scatter - JDH + - SC -2004-12-10 Added Nadia Dencheva contour code - JDH +2004-12-10 + Added zorder to artists to control drawing order of lines, patches and text + in axes. See examples/zoder_demo.py - JDH -2004-12-10 backend_cairo.py: got mathtext working - SC +2004-12-10 + Fixed colorbar bug with scatter - JDH -2004-12-09 Added Norm Peterson's svg clipping patch +2004-12-10 + Added Nadia Dencheva contour code - JDH -2004-12-09 Added Matthew Newville's wx printing patch +2004-12-10 + backend_cairo.py: got mathtext working - SC -2004-12-09 Migrated matlab to pylab - JDH +2004-12-09 + Added Norm Peterson's svg clipping patch -2004-12-09 backend_gtk.py: split into two parts - - backend_gdk.py - an image backend - - backend_gtk.py - A GUI backend that uses GDK - SC +2004-12-09 + Added Matthew Newville's wx printing patch -2004-12-08 backend_gtk.py: remove quit_after_print_xvfb(\*args), show_xvfb(), - Dialog_MeasureTool(gtk.Dialog) one month after sending mail to - matplotlib-users asking if anyone still uses these functions - SC +2004-12-09 + Migrated matlab to pylab - JDH -2004-12-02 backend_bases.py, backend_template.py: updated some of the method - documentation to make them consistent with each other - SC +2004-12-09 + backend_gtk.py: split into two parts -2004-12-04 Fixed multiple bindings per event for TkAgg mpl_connect and - mpl_disconnect. Added a "test_disconnect" command line - parameter to coords_demo.py JTM + - backend_gdk.py - an image backend + - backend_gtk.py - A GUI backend that uses GDK - SC -2004-12-04 Fixed some legend bugs JDH +2004-12-08 + backend_gtk.py: remove quit_after_print_xvfb(\*args), show_xvfb(), + Dialog_MeasureTool(gtk.Dialog) one month after sending mail to + matplotlib-users asking if anyone still uses these functions - SC -2004-11-30 Added over command for oneoff over plots. e.g., over(plot, x, - y, lw=2). Works with any plot function. +2004-12-02 + backend_bases.py, backend_template.py: updated some of the method + documentation to make them consistent with each other - SC -2004-11-30 Added bbox property to text - JDH +2004-12-04 + Fixed multiple bindings per event for TkAgg mpl_connect and mpl_disconnect. + Added a "test_disconnect" command line parameter to coords_demo.py JTM -2004-11-29 Zoom to rect now respect reversed axes limits (for both - linear and log axes). - GL +2004-12-04 + Fixed some legend bugs JDH -2004-11-29 Added the over command to the matlab interface. over - allows you to add an overlay plot regardless of hold - state. - JDH +2004-11-30 + Added over command for oneoff over plots. e.g., over(plot, x, y, lw=2). + Works with any plot function. -2004-11-25 Added Printf to mplutils for printf style format string - formatting in C++ (should help write better exceptions) +2004-11-30 + Added bbox property to text - JDH -2004-11-24 IMAGE_FORMAT: remove from agg and gtkagg backends as its no longer - used - SC +2004-11-29 + Zoom to rect now respect reversed axes limits (for both linear and log + axes). - GL -2004-11-23 Added matplotlib compatible set and get introspection. See - set_and_get.py +2004-11-29 + Added the over command to the matlab interface. over allows you to add an + overlay plot regardless of hold state. - JDH -2004-11-23 applied Norbert's patched and exposed legend configuration - to kwargs - JDH +2004-11-25 + Added Printf to mplutils for printf style format string formatting in C++ + (should help write better exceptions) -2004-11-23 backend_gtk.py: added a default exception handler - SC +2004-11-24 + IMAGE_FORMAT: remove from agg and gtkagg backends as its no longer used - + SC -2004-11-18 backend_gtk.py: change so that the backend knows about all image - formats and does not need to use IMAGE_FORMAT in other backends - SC +2004-11-23 + Added matplotlib compatible set and get introspection. See set_and_get.py -2004-11-18 Fixed some report_error bugs in string interpolation as - reported on SF bug tracker- JDH +2004-11-23 + applied Norbert's patched and exposed legend configuration to kwargs - JDH -2004-11-17 backend_gtkcairo.py: change so all print_figure() calls render using - Cairo and get saved using backend_gtk.print_figure() - SC +2004-11-23 + backend_gtk.py: added a default exception handler - SC -2004-11-13 backend_cairo.py: Discovered the magic number (96) required for - Cairo PS plots to come out the right size. Restored Cairo PS output - and added support for landscape mode - SC +2004-11-18 + backend_gtk.py: change so that the backend knows about all image formats + and does not need to use IMAGE_FORMAT in other backends - SC -2004-11-13 Added ishold - JDH +2004-11-18 + Fixed some report_error bugs in string interpolation as reported on SF bug + tracker- JDH -2004-11-12 Added many new matlab colormaps - autumn bone cool copper - flag gray hot hsv jet pink prism spring summer winter - PG +2004-11-17 + backend_gtkcairo.py: change so all print_figure() calls render using Cairo + and get saved using backend_gtk.print_figure() - SC -2004-11-11 greatly simplify the emitted postscript code - JV +2004-11-13 + backend_cairo.py: Discovered the magic number (96) required for Cairo PS + plots to come out the right size. Restored Cairo PS output and added + support for landscape mode - SC -2004-11-12 Added new plotting functions spy, spy2 for sparse matrix - visualization - JDH +2004-11-13 + Added ishold - JDH -2004-11-11 Added rgrids, thetragrids for customizing the grid - locations and labels for polar plots - JDH +2004-11-12 + Added many new matlab colormaps - autumn bone cool copper flag gray hot hsv + jet pink prism spring summer winter - PG -2004-11-11 make the Gtk backends build without an X-server connection - JV +2004-11-11 + greatly simplify the emitted postscript code - JV -2004-11-10 matplotlib/__init__.py: Added FROZEN to signal we are running under - py2exe (or similar) - is used by backend_gtk.py - SC +2004-11-12 + Added new plotting functions spy, spy2 for sparse matrix visualization - + JDH -2004-11-09 backend_gtk.py: Made fix suggested by maffew@cat.org.au - to prevent problems when py2exe calls pygtk.require(). - SC +2004-11-11 + Added rgrids, thetragrids for customizing the grid locations and labels for + polar plots - JDH -2004-11-09 backend_cairo.py: Added support for printing to a fileobject. - Disabled cairo PS output which is not working correctly. - SC +2004-11-11 + make the Gtk backends build without an X-server connection - JV ----------------------------------- +2004-11-10 + matplotlib/__init__.py: Added FROZEN to signal we are running under py2exe + (or similar) - is used by backend_gtk.py - SC -2004-11-08 matplotlib-0.64 released +2004-11-09 + backend_gtk.py: Made fix suggested by maffew@cat.org.au to prevent problems + when py2exe calls pygtk.require(). - SC -2004-11-04 Changed -dbackend processing to only use known backends, so - we don't clobber other non-matplotlib uses of -d, like -debug. +2004-11-09 + backend_cairo.py: Added support for printing to a fileobject. Disabled + cairo PS output which is not working correctly. - SC -2004-11-04 backend_agg.py: added IMAGE_FORMAT to list the formats that the - backend can save to. - backend_gtkagg.py: added support for saving JPG files by using the - GTK backend - SC +---------------------------------- -2004-10-31 backend_cairo.py: now produces png and ps files (although the figure - sizing needs some work). pycairo did not wrap all the necessary - functions, so I wrapped them myself, they are included in the - backend_cairo.py doc string. - SC +0.64 (2004-11-08) +----------------- + +2004-11-04 + Changed -dbackend processing to only use known backends, so we don't + clobber other non-matplotlib uses of -d, like -debug. + +2004-11-04 + backend_agg.py: added IMAGE_FORMAT to list the formats that the backend can + save to. backend_gtkagg.py: added support for saving JPG files by using + the GTK backend - SC -2004-10-31 backend_ps.py: clean up the generated PostScript code, use - the PostScript stack to hold itermediate values instead of - storing them in the dictionary. - JV +2004-10-31 + backend_cairo.py: now produces png and ps files (although the figure sizing + needs some work). pycairo did not wrap all the necessary functions, so I + wrapped them myself, they are included in the backend_cairo.py doc string. + - SC -2004-10-30 backend_ps.py, ft2font.cpp, ft2font.h: fix the position of - text in the PostScript output. The new FT2Font method - get_descent gives the distance between the lower edge of - the bounding box and the baseline of a string. In - backend_ps the text is shifted upwards by this amount. - JV +2004-10-31 + backend_ps.py: clean up the generated PostScript code, use the PostScript + stack to hold intermediate values instead of storing them in the dictionary. + - JV -2004-10-30 backend_ps.py: clean up the code a lot. Change the - PostScript output to be more DSC compliant. All - definitions for the generated PostScript are now in a - PostScript dictionary 'mpldict'. Moved the long comment - about drawing ellipses from the PostScript output into a - Python comment. - JV +2004-10-30 + backend_ps.py, ft2font.cpp, ft2font.h: fix the position of text in the + PostScript output. The new FT2Font method get_descent gives the distance + between the lower edge of the bounding box and the baseline of a string. + In backend_ps the text is shifted upwards by this amount. - JV -2004-10-30 backend_gtk.py: removed FigureCanvasGTK.realize() as its no longer - needed. Merged ColorManager into GraphicsContext - backend_bases.py: For set_capstyle/joinstyle() only set cap or - joinstyle if there is no error. - SC +2004-10-30 + backend_ps.py: clean up the code a lot. Change the PostScript output to be + more DSC compliant. All definitions for the generated PostScript are now + in a PostScript dictionary 'mpldict'. Moved the long comment about drawing + ellipses from the PostScript output into a Python comment. - JV -2004-10-30 backend_gtk.py: tidied up print_figure() and removed some of the - dependency on widget events - SC +2004-10-30 + backend_gtk.py: removed FigureCanvasGTK.realize() as its no longer needed. + Merged ColorManager into GraphicsContext backend_bases.py: For + set_capstyle/joinstyle() only set cap or joinstyle if there is no error. - + SC -2004-10-28 backend_cairo.py: The renderer is complete except for mathtext, - draw_image() and clipping. gtkcairo works reasonably well. cairo - does not yet create any files since I can't figure how to set the - 'target surface', I don't think pycairo wraps the required functions - - SC +2004-10-30 + backend_gtk.py: tidied up print_figure() and removed some of the dependency + on widget events - SC -2004-10-28 backend_gtk.py: Improved the save dialog (GTK 2.4 only) so it - presents the user with a menu of supported image formats - SC +2004-10-28 + backend_cairo.py: The renderer is complete except for mathtext, + draw_image() and clipping. gtkcairo works reasonably well. cairo does not + yet create any files since I can't figure how to set the 'target surface', + I don't think pycairo wraps the required functions - SC -2004-10-28 backend_svg.py: change print_figure() to restore original face/edge - color - backend_ps.py : change print_figure() to ensure original face/edge - colors are restored even if there's an IOError - SC +2004-10-28 + backend_gtk.py: Improved the save dialog (GTK 2.4 only) so it presents the + user with a menu of supported image formats - SC -2004-10-27 Applied Norbert's errorbar patch to support barsabove kwarg +2004-10-28 + backend_svg.py: change print_figure() to restore original face/edge color + backend_ps.py : change print_figure() to ensure original face/edge colors + are restored even if there's an IOError - SC -2004-10-27 Applied Norbert's legend patch to support None handles +2004-10-27 + Applied Norbert's errorbar patch to support barsabove kwarg -2004-10-27 Added two more backends: backend_cairo.py, backend_gtkcairo.py - They are not complete yet, currently backend_gtkcairo just renders - polygons, rectangles and lines - SC +2004-10-27 + Applied Norbert's legend patch to support None handles -2004-10-21 Added polar axes and plots - JDH +2004-10-27 + Added two more backends: backend_cairo.py, backend_gtkcairo.py They are not + complete yet, currently backend_gtkcairo just renders polygons, rectangles + and lines - SC -2004-10-20 Fixed corrcoef bug exposed by corrcoef(X) where X is matrix - - JDH +2004-10-21 + Added polar axes and plots - JDH -2004-10-19 Added kwarg support to xticks and yticks to set ticklabel - text properties -- thanks to T. Edward Whalen for the suggestion +2004-10-20 + Fixed corrcoef bug exposed by corrcoef(X) where X is matrix - JDH -2004-10-19 Added support for PIL images in imshow(), image.py - ADS +2004-10-19 + Added kwarg support to xticks and yticks to set ticklabel text properties + -- thanks to T. Edward Whalen for the suggestion -2004-10-19 Re-worked exception handling in _image.py and _transforms.py - to avoid masking problems with shared libraries. - JTM +2004-10-19 + Added support for PIL images in imshow(), image.py - ADS -2004-10-16 Streamlined the matlab interface wrapper, removed the - noplot option to hist - just use mlab.hist instead. +2004-10-19 + Re-worked exception handling in _image.py and _transforms.py to avoid + masking problems with shared libraries. - JTM -2004-09-30 Added Andrew Dalke's strftime code to extend the range of - dates supported by the DateFormatter - JDH +2004-10-16 + Streamlined the matlab interface wrapper, removed the noplot option to hist + - just use mlab.hist instead. -2004-09-30 Added barh - JDH +2004-09-30 + Added Andrew Dalke's strftime code to extend the range of dates supported + by the DateFormatter - JDH -2004-09-30 Removed fallback to alternate array package from numerix - so that ImportErrors are easier to debug. JTM +2004-09-30 + Added barh - JDH -2004-09-30 Add GTK+ 2.4 support for the message in the toolbar. SC +2004-09-30 + Removed fallback to alternate array package from numerix so that + ImportErrors are easier to debug. - JTM -2004-09-30 Made some changes to support python22 - lots of doc - fixes. - JDH +2004-09-30 + Add GTK+ 2.4 support for the message in the toolbar. SC -2004-09-29 Added a Verbose class for reporting - JDH +2004-09-30 + Made some changes to support python22 - lots of doc fixes. - JDH + +2004-09-29 + Added a Verbose class for reporting - JDH ------------------------------------ -2004-09-28 Released 0.63.0 - -2004-09-28 Added save to file object for agg - see - examples/print_stdout.py - -2004-09-24 Reorganized all py code to lib subdir - -2004-09-24 Fixed axes resize image edge effects on interpolation - - required upgrade to agg22 which fixed an agg bug related to - this problem - -2004-09-20 Added toolbar2 message display for backend_tkagg. JTM - - -2004-09-17 Added coords formatter attributes. These must be callable, - and return a string for the x or y data. These will be used - to format the x and y data for the coords box. Default is - the axis major formatter. e.g.: - - # format the coords message box - def price(x): return '$%1.2f'%x - ax.format_xdata = DateFormatter('%Y-%m-%d') - ax.format_ydata = price - - -2004-09-17 Total rewrite of dates handling to use python datetime with - num2date, date2num and drange. pytz for timezone handling, - dateutils for spohisticated ticking. date ranges from - 0001-9999 are supported. rrules allow arbitrary date - ticking. examples/date_demo*.py converted to show new - usage. new example examples/date_demo_rrule.py shows how - to use rrules in date plots. The date locators are much - more general and almost all of them have different - constructors. See matplotlib.dates for more info. - -2004-09-15 Applied Fernando's backend __init__ patch to support easier - backend maintenance. Added his numutils to mlab. JDH - -2004-09-16 Re-designated all files in matplotlib/images as binary and - w/o keyword substitution using "cvs admin -kb \*.svg ...". - See binary files in "info cvs" under Linux. This was messing - up builds from CVS on windows since CVS was doing lf -> cr/lf - and keyword substitution on the bitmaps. - JTM - -2004-09-15 Modified setup to build array-package-specific extensions - for those extensions which are array-aware. Setup builds - extensions automatically for either Numeric, numarray, or - both, depending on what you have installed. Python proxy - modules for the array-aware extensions import the version - optimized for numarray or Numeric determined by numerix. - - JTM - -2004-09-15 Moved definitions of infinity from mlab to numerix to avoid - divide by zero warnings for numarray - JTM - -2004-09-09 Added axhline, axvline, axhspan and axvspan +2004-09-28 + Released 0.63.0 + +2004-09-28 + Added save to file object for agg - see examples/print_stdout.py + +2004-09-24 + Reorganized all py code to lib subdir + +2004-09-24 + Fixed axes resize image edge effects on interpolation - required upgrade to + agg22 which fixed an agg bug related to this problem + +2004-09-20 + Added toolbar2 message display for backend_tkagg. JTM + +2004-09-17 + Added coords formatter attributes. These must be callable, and return a + string for the x or y data. These will be used to format the x and y data + for the coords box. Default is the axis major formatter. e.g.:: + + # format the coords message box + def price(x): return '$%1.2f'%x + ax.format_xdata = DateFormatter('%Y-%m-%d') + ax.format_ydata = price + +2004-09-17 + Total rewrite of dates handling to use python datetime with num2date, + date2num and drange. pytz for timezone handling, dateutils for + spohisticated ticking. date ranges from 0001-9999 are supported. rrules + allow arbitrary date ticking. examples/date_demo*.py converted to show new + usage. new example examples/date_demo_rrule.py shows how to use rrules in + date plots. The date locators are much more general and almost all of them + have different constructors. See matplotlib.dates for more info. + +2004-09-15 + Applied Fernando's backend __init__ patch to support easier backend + maintenance. Added his numutils to mlab. JDH + +2004-09-16 + Re-designated all files in matplotlib/images as binary and w/o keyword + substitution using "cvs admin -kb \*.svg ...". See binary files in "info + cvs" under Linux. This was messing up builds from CVS on windows since CVS + was doing lf -> cr/lf and keyword substitution on the bitmaps. - JTM + +2004-09-15 + Modified setup to build array-package-specific extensions for those + extensions which are array-aware. Setup builds extensions automatically + for either Numeric, numarray, or both, depending on what you have + installed. Python proxy modules for the array-aware extensions import the + version optimized for numarray or Numeric determined by numerix. - JTM + +2004-09-15 + Moved definitions of infinity from mlab to numerix to avoid divide by zero + warnings for numarray - JTM + +2004-09-09 + Added axhline, axvline, axhspan and axvspan ------------------------------- -2004-08-30 matplotlib 0.62.4 released +0.62.4 (2004-08-30) +------------------- -2004-08-30 Fixed a multiple images with different extent bug, - Fixed markerfacecolor as RGB tuple +2004-08-30 + Fixed a multiple images with different extent bug, Fixed markerfacecolor as + RGB tuple -2004-08-27 Mathtext now more than 5x faster. Thanks to Paul Mcguire - for fixes both to pyparsing and to the matplotlib grammar! - mathtext broken on python2.2 +2004-08-27 + Mathtext now more than 5x faster. Thanks to Paul Mcguire for fixes both to + pyparsing and to the matplotlib grammar! mathtext broken on python2.2 -2004-08-25 Exposed Darren's and Greg's log ticking and formatting - options to semilogx and friends +2004-08-25 + Exposed Darren's and Greg's log ticking and formatting options to semilogx + and friends -2004-08-23 Fixed grid w/o args to toggle grid state - JDH +2004-08-23 + Fixed grid w/o args to toggle grid state - JDH -2004-08-11 Added Gregory's log patches for major and minor ticking +2004-08-11 + Added Gregory's log patches for major and minor ticking -2004-08-18 Some pixel edge effects fixes for images +2004-08-18 + Some pixel edge effects fixes for images -2004-08-18 Fixed TTF files reads in backend_ps on win32. +2004-08-18 + Fixed TTF files reads in backend_ps on win32. -2004-08-18 Added base and subs properties for logscale plots, user - modifiable using - set_[x,y]scale('log',base=b,subs=[mt1,mt2,...]) - GL +2004-08-18 + Added base and subs properties for logscale plots, user modifiable using + set_[x,y]scale('log',base=b,subs=[mt1,mt2,...]) - GL -2004-08-18 fixed a bug exposed by trying to find the HOME dir on win32 - thanks to Alan Issac for pointing to the light - JDH +2004-08-18 + fixed a bug exposed by trying to find the HOME dir on win32 thanks to Alan + Issac for pointing to the light - JDH -2004-08-18 fixed errorbar bug in setting ecolor - JDH +2004-08-18 + fixed errorbar bug in setting ecolor - JDH -2004-08-12 Added Darren Dale's exponential ticking patch +2004-08-12 + Added Darren Dale's exponential ticking patch -2004-08-11 Added Gregory's fltkagg backend +2004-08-11 + Added Gregory's fltkagg backend ------------------------------ -2004-08-09 matplotlib-0.61.0 released +0.61.0 (2004-08-09) +------------------- -2004-08-08 backend_gtk.py: get rid of the final PyGTK deprecation warning by - replacing gtkOptionMenu with gtkMenu in the 2.4 version of the - classic toolbar. +2004-08-08 + backend_gtk.py: get rid of the final PyGTK deprecation warning by replacing + gtkOptionMenu with gtkMenu in the 2.4 version of the classic toolbar. -2004-08-06 Added Tk zoom to rect rectangle, proper idle drawing, and - keybinding - JDH +2004-08-06 + Added Tk zoom to rect rectangle, proper idle drawing, and keybinding - JDH -2004-08-05 Updated installing.html and INSTALL - JDH +2004-08-05 + Updated installing.html and INSTALL - JDH -2004-08-01 backend_gtk.py: move all drawing code into the expose_event() +2004-08-01 + backend_gtk.py: move all drawing code into the expose_event() -2004-07-28 Added Greg's toolbar2 and backend_*agg patches - JDH +2004-07-28 + Added Greg's toolbar2 and backend_*agg patches - JDH -2004-07-28 Added image.imread with support for loading png into - numerix arrays +2004-07-28 + Added image.imread with support for loading png into numerix arrays -2004-07-28 Added key modifiers to events - implemented dynamic updates - and rubber banding for interactive pan/zoom - JDH +2004-07-28 + Added key modifiers to events - implemented dynamic updates and rubber + banding for interactive pan/zoom - JDH -2004-07-27 did a readthrough of SVG, replacing all the string - additions with string interps for efficiency, fixed some - layout problems, added font and image support (through - external pngs) - JDH +2004-07-27 + did a readthrough of SVG, replacing all the string additions with string + interps for efficiency, fixed some layout problems, added font and image + support (through external pngs) - JDH -2004-07-25 backend_gtk.py: modify toolbar2 to make it easier to support GTK+ - 2.4. Add GTK+ 2.4 toolbar support. - SC +2004-07-25 + backend_gtk.py: modify toolbar2 to make it easier to support GTK+ 2.4. Add + GTK+ 2.4 toolbar support. - SC -2004-07-24 backend_gtk.py: Simplified classic toolbar creation - SC +2004-07-24 + backend_gtk.py: Simplified classic toolbar creation - SC -2004-07-24 Added images/matplotlib.svg to be used when GTK+ windows are - minimised - SC +2004-07-24 + Added images/matplotlib.svg to be used when GTK+ windows are minimised - SC -2004-07-22 Added right mouse click zoom for NavigationToolbar2 panning - mode. - JTM +2004-07-22 + Added right mouse click zoom for NavigationToolbar2 panning mode. - JTM -2004-07-22 Added NavigationToolbar2 support to backend_tkagg. - Minor tweak to backend_bases. - JTM +2004-07-22 + Added NavigationToolbar2 support to backend_tkagg. Minor tweak to + backend_bases. - JTM -2004-07-22 Incorporated Gergory's renderer cache and buffer object - cache - JDH +2004-07-22 + Incorporated Gergory's renderer cache and buffer object cache - JDH -2004-07-22 Backend_gtk.py: Added support for GtkFileChooser, changed - FileSelection/FileChooser so that only one instance pops up, - and made them both modal. - SC +2004-07-22 + Backend_gtk.py: Added support for GtkFileChooser, changed + FileSelection/FileChooser so that only one instance pops up, and made them + both modal. - SC -2004-07-21 Applied backend_agg memory leak patch from hayden - - jocallo@online.no. Found and fixed a leak in binary - operations on transforms. Moral of the story: never incref - where you meant to decref! Fixed several leaks in ft2font: - moral of story: almost always return Py::asObject over - Py::Object - JDH +2004-07-21 + Applied backend_agg memory leak patch from hayden - jocallo@online.no. + Found and fixed a leak in binary operations on transforms. Moral of the + story: never incref where you meant to decref! Fixed several leaks in + ft2font: moral of story: almost always return Py::asObject over Py::Object + - JDH -2004-07-21 Fixed a to string memory allocation bug in agg and image - modules - JDH +2004-07-21 + Fixed a to string memory allocation bug in agg and image modules - JDH -2004-07-21 Added mpl_connect and mpl_disconnect to matlab interface - - JDH +2004-07-21 + Added mpl_connect and mpl_disconnect to matlab interface - JDH -2004-07-21 Added beginnings of users_guide to CVS - JDH +2004-07-21 + Added beginnings of users_guide to CVS - JDH -2004-07-20 ported toolbar2 to wx +2004-07-20 + ported toolbar2 to wx -2004-07-20 upgraded to agg21 - JDH +2004-07-20 + upgraded to agg21 - JDH -2004-07-20 Added new icons for toolbar2 - JDH +2004-07-20 + Added new icons for toolbar2 - JDH -2004-07-19 Added vertical mathtext for \*Agg and GTK - thanks Jim - Benson! - JDH +2004-07-19 + Added vertical mathtext for \*Agg and GTK - thanks Jim Benson! - JDH -2004-07-16 Added ps/eps/svg savefig options to wx and gtk JDH +2004-07-16 + Added ps/eps/svg savefig options to wx and gtk JDH -2004-07-15 Fixed python framework tk finder in setupext.py - JDH +2004-07-15 + Fixed python framework tk finder in setupext.py - JDH -2004-07-14 Fixed layer images demo which was broken by the 07/12 image - extent fixes - JDH +2004-07-14 + Fixed layer images demo which was broken by the 07/12 image extent fixes - + JDH -2004-07-13 Modified line collections to handle arbitrary length - segments for each line segment. - JDH +2004-07-13 + Modified line collections to handle arbitrary length segments for each line + segment. - JDH -2004-07-13 Fixed problems with image extent and origin - - set_image_extent deprecated. Use imshow(blah, blah, - extent=(xmin, xmax, ymin, ymax) instead - JDH +2004-07-13 + Fixed problems with image extent and origin - set_image_extent deprecated. + Use imshow(blah, blah, extent=(xmin, xmax, ymin, ymax) instead - JDH -2004-07-12 Added prototype for new nav bar with codifed event - handling. Use mpl_connect rather than connect for - matplotlib event handling. toolbar style determined by rc - toolbar param. backend status: gtk: prototype, wx: in - progress, tk: not started - JDH +2004-07-12 + Added prototype for new nav bar with codified event handling. Use + mpl_connect rather than connect for matplotlib event handling. toolbar + style determined by rc toolbar param. backend status: gtk: prototype, wx: + in progress, tk: not started - JDH -2004-07-11 backend_gtk.py: use builtin round() instead of redefining it. - - SC +2004-07-11 + backend_gtk.py: use builtin round() instead of redefining it. - SC -2004-07-10 Added embedding_in_wx3 example - ADS +2004-07-10 + Added embedding_in_wx3 example - ADS -2004-07-09 Added dynamic_image_wxagg to examples - ADS +2004-07-09 + Added dynamic_image_wxagg to examples - ADS -2004-07-09 added support for embedding TrueType fonts in PS files - PEB +2004-07-09 + added support for embedding TrueType fonts in PS files - PEB -2004-07-09 fixed a sfnt bug exposed if font cache is not built +2004-07-09 + fixed a sfnt bug exposed if font cache is not built -2004-07-09 added default arg None to matplotlib.matlab grid command to - toggle current grid state +2004-07-09 + added default arg None to matplotlib.matlab grid command to toggle current + grid state --------------------- -2004-07-08 0.60.2 released +0.60.2 (2004-07-08) +------------------- -2004-07-08 fixed a mathtext bug for '6' +2004-07-08 + fixed a mathtext bug for '6' -2004-07-08 added some numarray bug workarounds +2004-07-08 + added some numarray bug workarounds -------------------------- -2004-07-07 0.60 released - -2004-07-07 Fixed a bug in dynamic_demo_wx - +0.60 (2004-07-07) +----------------- -2004-07-07 backend_gtk.py: raise SystemExit immediately if - 'import pygtk' fails - SC +2004-07-07 + Fixed a bug in dynamic_demo_wx -2004-07-05 Added new mathtext commands \over{sym1}{sym2} and - \under{sym1}{sym2} +2004-07-07 + backend_gtk.py: raise SystemExit immediately if 'import pygtk' fails - SC -2004-07-05 Unified image and patch collections colormapping and - scaling args. Updated docstrings for all - JDH +2004-07-05 + Added new mathtext commands \over{sym1}{sym2} and \under{sym1}{sym2} -2004-07-05 Fixed a figure legend bug and added - examples/figlegend_demo.py - JDH +2004-07-05 + Unified image and patch collections colormapping and scaling args. Updated + docstrings for all - JDH -2004-07-01 Fixed a memory leak in image and agg to string methods +2004-07-05 + Fixed a figure legend bug and added examples/figlegend_demo.py - JDH -2004-06-25 Fixed fonts_demo spacing problems and added a kwargs - version of the fonts_demo fonts_demo_kw.py - JDH +2004-07-01 + Fixed a memory leak in image and agg to string methods -2004-06-25 finance.py: handle case when urlopen() fails - SC +2004-06-25 + Fixed fonts_demo spacing problems and added a kwargs version of the + fonts_demo fonts_demo_kw.py - JDH -2004-06-24 Support for multiple images on axes and figure, with - blending. Support for upper and lower image origins. - clim, jet and gray functions in matlab interface operate on - current image - JDH +2004-06-25 + finance.py: handle case when urlopen() fails - SC -2004-06-23 ported code to Perry's new colormap and norm scheme. Added - new rc attributes image.aspect, image.interpolation, - image.cmap, image.lut, image.origin +2004-06-24 + Support for multiple images on axes and figure, with blending. Support for + upper and lower image origins. clim, jet and gray functions in matlab + interface operate on current image - JDH -2004-06-20 backend_gtk.py: replace gtk.TRUE/FALSE with True/False. - simplified _make_axis_menu(). - SC +2004-06-23 + ported code to Perry's new colormap and norm scheme. Added new rc + attributes image.aspect, image.interpolation, image.cmap, image.lut, + image.origin -2004-06-19 anim_tk.py: Updated to use TkAgg by default (not GTK) - backend_gtk_py: Added '_' in front of private widget - creation functions - SC +2004-06-20 + backend_gtk.py: replace gtk.TRUE/FALSE with True/False. simplified + _make_axis_menu(). - SC -2004-06-17 backend_gtk.py: Create a GC once in realise(), not every - time draw() is called. - SC +2004-06-19 + anim_tk.py: Updated to use TkAgg by default (not GTK) backend_gtk_py: Added + '_' in front of private widget creation functions - SC -2004-06-16 Added new py2exe FAQ entry and added frozen support in - get_data_path for py2exe - JDH +2004-06-17 + backend_gtk.py: Create a GC once in realise(), not every time draw() is + called. - SC -2004-06-16 Removed GTKGD, which was always just a proof-of-concept - backend - JDH +2004-06-16 + Added new py2exe FAQ entry and added frozen support in get_data_path for + py2exe - JDH -2004-06-16 backend_gtk.py updates to replace deprecated functions - gtk.mainquit(), gtk.mainloop(). - Update NavigationToolbar to use the new GtkToolbar API - SC +2004-06-16 + Removed GTKGD, which was always just a proof-of-concept backend - JDH -2004-06-15 removed set_default_font from font_manager to unify font - customization using the new function rc. See API_CHANGES - for more info. The examples fonts_demo.py and - fonts_demo_kw.py are ported to the new API - JDH +2004-06-16 + backend_gtk.py updates to replace deprecated functions gtk.mainquit(), + gtk.mainloop(). Update NavigationToolbar to use the new GtkToolbar API - + SC -2004-06-15 Improved (yet again!) axis scaling to properly handle - singleton plots - JDH +2004-06-15 + removed set_default_font from font_manager to unify font customization + using the new function rc. See API_CHANGES for more info. The examples + fonts_demo.py and fonts_demo_kw.py are ported to the new API - JDH -2004-06-15 Restored the old FigureCanvasGTK.draw() - SC +2004-06-15 + Improved (yet again!) axis scaling to properly handle singleton plots - JDH -2004-06-11 More memory leak fixes in transforms and ft2font - JDH +2004-06-15 + Restored the old FigureCanvasGTK.draw() - SC -2004-06-11 Eliminated numerix .numerix file and environment variable - NUMERIX. Fixed bug which prevented command line overrides: - --numarray or --numeric. - JTM +2004-06-11 + More memory leak fixes in transforms and ft2font - JDH -2004-06-10 Added rc configuration function rc; deferred all rc param - setting until object creation time; added new rc attrs: - lines.markerfacecolor, lines.markeredgecolor, - lines.markeredgewidth, patch.linewidth, patch.facecolor, - patch.edgecolor, patch.antialiased; see - examples/customize_rc.py for usage - JDH +2004-06-11 + Eliminated numerix .numerix file and environment variable NUMERIX. Fixed + bug which prevented command line overrides: --numarray or --numeric. - JTM +2004-06-10 + Added rc configuration function rc; deferred all rc param setting until + object creation time; added new rc attrs: lines.markerfacecolor, + lines.markeredgecolor, lines.markeredgewidth, patch.linewidth, + patch.facecolor, patch.edgecolor, patch.antialiased; see + examples/customize_rc.py for usage - JDH --------------------------------------------------------------- -2004-06-09 0.54.2 released +0.54.2 (2004-06-09) +------------------- -2004-06-08 Rewrote ft2font using CXX as part of general memory leak - fixes; also fixed transform memory leaks - JDH +2004-06-08 + Rewrote ft2font using CXX as part of general memory leak fixes; also fixed + transform memory leaks - JDH -2004-06-07 Fixed several problems with log ticks and scaling - JDH +2004-06-07 + Fixed several problems with log ticks and scaling - JDH -2004-06-07 Fixed width/height issues for images - JDH +2004-06-07 + Fixed width/height issues for images - JDH -2004-06-03 Fixed draw_if_interactive bug for semilogx; +2004-06-03 + Fixed draw_if_interactive bug for semilogx; -2004-06-02 Fixed text clipping to clip to axes - JDH +2004-06-02 + Fixed text clipping to clip to axes - JDH -2004-06-02 Fixed leading newline text and multiple newline text - JDH +2004-06-02 + Fixed leading newline text and multiple newline text - JDH -2004-06-02 Fixed plot_date to return lines - JDH +2004-06-02 + Fixed plot_date to return lines - JDH -2004-06-01 Fixed plot to work with x or y having shape N,1 or 1,N - JDH +2004-06-01 + Fixed plot to work with x or y having shape N,1 or 1,N - JDH -2004-05-31 Added renderer markeredgewidth attribute of Line2D. - ADS +2004-05-31 + Added renderer markeredgewidth attribute of Line2D. - ADS -2004-05-29 Fixed tick label clipping to work with navigation. +2004-05-29 + Fixed tick label clipping to work with navigation. -2004-05-28 Added renderer grouping commands to support groups in +2004-05-28 + Added renderer grouping commands to support groups in SVG/PS. - JDH -2004-05-28 Fixed, this time I really mean it, the singleton plot - plot([0]) scaling bug; Fixed Flavio's shape = N,1 bug - JDH +2004-05-28 + Fixed, this time I really mean it, the singleton plot plot([0]) scaling + bug; Fixed Flavio's shape = N,1 bug - JDH -2004-05-28 added colorbar - JDH +2004-05-28 + added colorbar - JDH -2004-05-28 Made some changes to the matplotlib.colors.Colormap to - properly support clim - JDH +2004-05-28 + Made some changes to the matplotlib.colors.Colormap to properly support + clim - JDH ----------------------------------------------------------------- -2004-05-27 0.54.1 released +0.54.1 (2004-05-27) +------------------- -2004-05-27 Lots of small bug fixes: rotated text at negative angles, - errorbar capsize and autoscaling, right tick label - position, gtkagg on win98, alpha of figure background, - singleton plots - JDH +2004-05-27 + Lots of small bug fixes: rotated text at negative angles, errorbar capsize + and autoscaling, right tick label position, gtkagg on win98, alpha of + figure background, singleton plots - JDH -2004-05-26 Added Gary's errorbar stuff and made some fixes for length - one plots and constant data plots - JDH +2004-05-26 + Added Gary's errorbar stuff and made some fixes for length one plots and + constant data plots - JDH -2004-05-25 Tweaked TkAgg backend so that canvas.draw() works - more like the other backends. Fixed a bug resulting - in 2 draws per figure manager show(). - JTM +2004-05-25 + Tweaked TkAgg backend so that canvas.draw() works more like the other + backends. Fixed a bug resulting in 2 draws per figure manager show(). + - JTM ------------------------------------------------------------ -2004-05-19 0.54 released +0.54 (2004-05-19) +----------------- -2004-05-18 Added newline separated text with rotations to text.Text - layout - JDH +2004-05-18 + Added newline separated text with rotations to text.Text layout - JDH -2004-05-16 Added fast pcolor using PolyCollections. - JDH +2004-05-16 + Added fast pcolor using PolyCollections. - JDH -2004-05-14 Added fast polygon collections - changed scatter to use - them. Added multiple symbols to scatter. 10x speedup on - large scatters using \*Agg and 5X speedup for ps. - JDH +2004-05-14 + Added fast polygon collections - changed scatter to use them. Added + multiple symbols to scatter. 10x speedup on large scatters using \*Agg and + 5X speedup for ps. - JDH -2004-05-14 On second thought... created an "nx" namespace in - in numerix which maps type names onto typecodes - the same way for both numarray and Numeric. This - undoes my previous change immediately below. To get a - typename for Int16 usable in a Numeric extension: - say nx.Int16. - JTM +2004-05-14 + On second thought... created an "nx" namespace in in numerix which maps + type names onto typecodes the same way for both numarray and Numeric. This + undoes my previous change immediately below. To get a typename for Int16 + usable in a Numeric extension: say nx.Int16. - JTM -2004-05-15 Rewrote transformation class in extension code, simplified - all the artist constructors - JDH +2004-05-15 + Rewrote transformation class in extension code, simplified all the artist + constructors - JDH -2004-05-14 Modified the type definitions in the numarray side of - numerix so that they are Numeric typecodes and can be - used with Numeric compilex extensions. The original - numarray types were renamed to type. - JTM +2004-05-14 + Modified the type definitions in the numarray side of numerix so that they + are Numeric typecodes and can be used with Numeric compilex extensions. + The original numarray types were renamed to type. - JTM -2004-05-06 Gary Ruben sent me a bevy of new plot symbols and markers. - See matplotlib.matlab.plot - JDH +2004-05-06 + Gary Ruben sent me a bevy of new plot symbols and markers. See + matplotlib.matlab.plot - JDH -2004-05-06 Total rewrite of mathtext - factored ft2font stuff out of - layout engine and defined abstract class for font handling - to lay groundwork for ps mathtext. Rewrote parser and made - layout engine much more precise. Fixed all the layout - hacks. Added spacing commands \/ and \hspace. Added - composite chars and defined angstrom. - JDH +2004-05-06 + Total rewrite of mathtext - factored ft2font stuff out of layout engine and + defined abstract class for font handling to lay groundwork for ps mathtext. + Rewrote parser and made layout engine much more precise. Fixed all the + layout hacks. Added spacing commands \/ and \hspace. Added composite + chars and defined angstrom. - JDH -2004-05-05 Refactored text instances out of backend; aligned - text with arbitrary rotations is now supported - JDH +2004-05-05 + Refactored text instances out of backend; aligned text with arbitrary + rotations is now supported - JDH -2004-05-05 Added a Matrix capability for numarray to numerix. JTM +2004-05-05 + Added a Matrix capability for numarray to numerix. JTM -2004-05-04 Updated whats_new.html.template to use dictionary and - template loop, added anchors for all versions and items; - updated goals.txt to use those for links. PG +2004-05-04 + Updated whats_new.html.template to use dictionary and template loop, added + anchors for all versions and items; updated goals.txt to use those for + links. PG -2004-05-04 Added fonts_demo.py to backend_driver, and AFM and TTF font - caches to font_manager.py - PEB +2004-05-04 + Added fonts_demo.py to backend_driver, and AFM and TTF font caches to + font_manager.py - PEB -2004-05-03 Redid goals.html.template to use a goals.txt file that - has a pseudo restructured text organization. PG +2004-05-03 + Redid goals.html.template to use a goals.txt file that has a pseudo + restructured text organization. PG -2004-05-03 Removed the close buttons on all GUIs and added the python - #! bang line to the examples following Steve Chaplin's - advice on matplotlib dev +2004-05-03 + Removed the close buttons on all GUIs and added the python #! bang line to + the examples following Steve Chaplin's advice on matplotlib dev -2004-04-29 Added CXX and rewrote backend_agg using it; tracked down - and fixed agg memory leak - JDH +2004-04-29 + Added CXX and rewrote backend_agg using it; tracked down and fixed agg + memory leak - JDH -2004-04-29 Added stem plot command - JDH +2004-04-29 + Added stem plot command - JDH -2004-04-28 Fixed PS scaling and centering bug - JDH +2004-04-28 + Fixed PS scaling and centering bug - JDH -2004-04-26 Fixed errorbar autoscale problem - JDH +2004-04-26 + Fixed errorbar autoscale problem - JDH -2004-04-22 Fixed copy tick attribute bug, fixed singular datalim - ticker bug; fixed mathtext fontsize interactive bug. - JDH +2004-04-22 + Fixed copy tick attribute bug, fixed singular datalim ticker bug; fixed + mathtext fontsize interactive bug. - JDH -2004-04-21 Added calls to draw_if_interactive to axes(), legend(), - and pcolor(). Deleted duplicate pcolor(). - JTM +2004-04-21 + Added calls to draw_if_interactive to axes(), legend(), and pcolor(). + Deleted duplicate pcolor(). - JTM ------------------------------------------------------------ -2004-04-21 matplotlib 0.53 release +2004-04-21 + matplotlib 0.53 release -2004-04-19 Fixed vertical alignment bug in PS backend - JDH +2004-04-19 + Fixed vertical alignment bug in PS backend - JDH -2004-04-17 Added support for two scales on the "same axes" with tick - different ticking and labeling left right or top bottom. - See examples/two_scales.py - JDH +2004-04-17 + Added support for two scales on the "same axes" with tick different ticking + and labeling left right or top bottom. See examples/two_scales.py - JDH -2004-04-17 Added default dirs as list rather than single dir in - setupext.py - JDH +2004-04-17 + Added default dirs as list rather than single dir in setupext.py - JDH -2004-04-16 Fixed wx exception swallowing bug (and there was much - rejoicing!) - JDH +2004-04-16 + Fixed wx exception swallowing bug (and there was much rejoicing!) - JDH -2004-04-16 Added new ticker locator a formatter, fixed default font - return - JDH +2004-04-16 + Added new ticker locator a formatter, fixed default font return - JDH -2004-04-16 Added get_name method to FontProperties class. Fixed font lookup - in GTK and WX backends. - PEB +2004-04-16 + Added get_name method to FontProperties class. Fixed font lookup in GTK and + WX backends. - PEB -2004-04-16 Added get- and set_fontstyle msethods. - PEB +2004-04-16 + Added get- and set_fontstyle methods. - PEB -2004-04-10 Mathtext fixes: scaling with dpi, - JDH +2004-04-10 + Mathtext fixes: scaling with dpi, - JDH -2004-04-09 Improved font detection algorithm. - PEB +2004-04-09 + Improved font detection algorithm. - PEB -2004-04-09 Move deprecation warnings from text.py to __init__.py - PEB +2004-04-09 + Move deprecation warnings from text.py to __init__.py - PEB -2004-04-09 Added default font customization - JDH +2004-04-09 + Added default font customization - JDH -2004-04-08 Fixed viewlim set problem on axes and axis. - JDH +2004-04-08 + Fixed viewlim set problem on axes and axis. - JDH -2004-04-07 Added validate_comma_sep_str and font properties parameters to - __init__. Removed font families and added rcParams to - FontProperties __init__ arguments in font_manager. Added - default font property parameters to .matplotlibrc file with - descriptions. Added deprecation warnings to the get\_ - and - set_fontXXX methods of the Text object. - PEB +2004-04-07 + Added validate_comma_sep_str and font properties parameters to __init__. + Removed font families and added rcParams to FontProperties __init__ + arguments in font_manager. Added default font property parameters to + .matplotlibrc file with descriptions. Added deprecation warnings to the + get\_ - and set_fontXXX methods of the Text object. - PEB -2004-04-06 Added load and save commands for ASCII data - JDH +2004-04-06 + Added load and save commands for ASCII data - JDH -2004-04-05 Improved font caching by not reading AFM fonts until needed. - Added better documentation. Changed the behaviour of the - get_family, set_family, and set_name methods of FontProperties. - - PEB +2004-04-05 + Improved font caching by not reading AFM fonts until needed. Added better + documentation. Changed the behaviour of the get_family, set_family, and + set_name methods of FontProperties. - PEB -2004-04-05 Added WXAgg backend - JDH +2004-04-05 + Added WXAgg backend - JDH -2004-04-04 Improved font caching in backend_agg with changes to - font_manager - JDH +2004-04-04 + Improved font caching in backend_agg with changes to font_manager - JDH -2004-03-29 Fixed fontdicts and kwargs to work with new font manager - - JDH +2004-03-29 + Fixed fontdicts and kwargs to work with new font manager - JDH -------------------------------------------- This is the Old, stale, never used changelog -2002-12-10 - Added a TODO file and CHANGELOG. Lots to do -- get - crackin'! +2002-12-10 + - Added a TODO file and CHANGELOG. Lots to do -- get crackin'! - - Fixed y zoom tool bug + - Fixed y zoom tool bug - - Adopted a compromise fix for the y data clipping problem. - The problem was that for solid lines, the y data clipping - (as opposed to the gc clipping) caused artifactual - horizontal solid lines near the ylim boundaries. I did a - 5% offset hack in Axes set_ylim functions which helped, - but didn't cure the problem for very high gain y zooms. - So I disabled y data clipping for connected lines . If - you need extensive y clipping, either plot(y,x) because x - data clipping is always enabled, or change the _set_clip - code to 'if 1' as indicated in the lines.py src. See - _set_clip in lines.py and set_ylim in figure.py for more - information. + - Adopted a compromise fix for the y data clipping problem. The problem + was that for solid lines, the y data clipping (as opposed to the gc + clipping) caused artifactual horizontal solid lines near the ylim + boundaries. I did a 5% offset hack in Axes set_ylim functions which + helped, but didn't cure the problem for very high gain y zooms. So I + disabled y data clipping for connected lines . If you need extensive y + clipping, either plot(y,x) because x data clipping is always enabled, or + change the _set_clip code to 'if 1' as indicated in the lines.py src. + See _set_clip in lines.py and set_ylim in figure.py for more information. +2002-12-11 + - Added a measurement dialog to the figure window to measure axes position + and the delta x delta y with a left mouse drag. These defaults can be + overridden by deriving from Figure and overriding button_press_event, + button_release_event, and motion_notify_event, and _dialog_measure_tool. -2002-12-11 - Added a measurement dialog to the figure window to - measure axes position and the delta x delta y with a left - mouse drag. These defaults can be overridden by deriving - from Figure and overriding button_press_event, - button_release_event, and motion_notify_event, - and _dialog_measure_tool. + - fixed the navigation dialog so you can check the axes the navigation + buttons apply to. - - fixed the navigation dialog so you can check the axes the - navigation buttons apply to. +2003-04-23 + Released matplotlib v0.1 +2003-04-24 + Added a new line style PixelLine2D which is the plots the markers as pixels + (as small as possible) with format symbol ',' + Added a new class Patch with derived classes Rectangle, RegularPolygon and + Circle -2003-04-23 Released matplotlib v0.1 - -2003-04-24 Added a new line style PixelLine2D which is the plots the - markers as pixels (as small as possible) with format - symbol ',' - - Added a new class Patch with derived classes Rectangle, - RegularPolygon and Circle - -2003-04-25 Implemented new functions errorbar, scatter and hist - - Added a new line type '|' which is a vline. syntax is - plot(x, Y, '|') where y.shape = len(x),2 and each row gives - the ymin,ymax for the respective values of x. Previously I - had implemented vlines as a list of lines, but I needed the - efficientcy of the numeric clipping for large numbers of - vlines outside the viewport, so I wrote a dedicated class - Vline2D which derives from Line2D +2003-04-25 + Implemented new functions errorbar, scatter and hist + Added a new line type '|' which is a vline. syntax is plot(x, Y, '|') + where y.shape = len(x),2 and each row gives the ymin,ymax for the + respective values of x. Previously I had implemented vlines as a list of + lines, but I needed the efficiency of the numeric clipping for large + numbers of vlines outside the viewport, so I wrote a dedicated class + Vline2D which derives from Line2D 2003-05-01 - - Fixed ytick bug where grid and tick show outside axis viewport with gc clip + Fixed ytick bug where grid and tick show outside axis viewport with gc clip 2003-05-14 - - Added new ways to specify colors 1) matlab format string 2) - html-style hex string, 3) rgb tuple. See examples/color_demo.py + Added new ways to specify colors 1) matlab format string 2) html-style hex + string, 3) rgb tuple. See examples/color_demo.py 2003-05-28 - - Changed figure rendering to draw form a pixmap to reduce flicker. - See examples/system_monitor.py for an example where the plot is - continusouly updated w/o flicker. This example is meant to - simulate a system monitor that shows free CPU, RAM, etc... + Changed figure rendering to draw form a pixmap to reduce flicker. See + examples/system_monitor.py for an example where the plot is continuously + updated w/o flicker. This example is meant to simulate a system monitor + that shows free CPU, RAM, etc... 2003-08-04 - - Added Jon Anderson's GTK shell, which doesn't require pygtk to - have threading built-in and looks nice! + Added Jon Anderson's GTK shell, which doesn't require pygtk to have + threading built-in and looks nice! 2003-08-25 - - Fixed deprecation warnings for python2.3 and pygtk-1.99.18 + Fixed deprecation warnings for python2.3 and pygtk-1.99.18 2003-08-26 - - Added figure text with new example examples/figtext.py - + Added figure text with new example examples/figtext.py 2003-08-27 + Fixed bugs in figure text with font override dictionaries and fig text that + was placed outside the window bounding box - Fixed bugs i figure text with font override dictionairies and fig - text that was placed outside the window bounding box - -2003-09-1 through 2003-09-15 - - Added a postscript and a GD module backend +2003-09-01 through 2003-09-15 + Added a postscript and a GD module backend 2003-09-16 - - Fixed font scaling and point scaling so circles, squares, etc on - lines will scale with DPI as will fonts. Font scaling is not fully - implemented on the gtk backend because I have not figured out how - to scale fonts to arbitrary sizes with GTK + Fixed font scaling and point scaling so circles, squares, etc on lines will + scale with DPI as will fonts. Font scaling is not fully implemented on the + gtk backend because I have not figured out how to scale fonts to arbitrary + sizes with GTK 2003-09-17 + Fixed figure text bug which crashed X windows on long figure text extending + beyond display area. This was, I believe, due to the vestigial erase + functionality that was no longer needed since I began rendering to a pixmap - Fixed figure text bug which crashed X windows on long figure text - extending beyond display area. This was, I believe, due to the - vestigial erase functionality that was no longer needed since I - began rendering to a pixmap - -2003-09-30 Added legend +2003-09-30 + Added legend -2003-10-01 Fixed bug when colors are specified with rgb tuple or hex - string. +2003-10-01 + Fixed bug when colors are specified with rgb tuple or hex string. +2003-10-21 + Andrew Straw provided some legend code which I modified and incorporated. + Thanks Andrew! -2003-10-21 Andrew Straw provided some legend code which I modified - and incorporated. Thanks Andrew! +2003-10-27 + Fixed a bug in axis.get_view_distance that affected zoom in versus out with + interactive scrolling, and a bug in the axis text reset system that + prevented the text from being redrawn on a interactive gtk view lim set + with the widget -2003-10-27 Fixed a bug in axis.get_view_distance that affected zoom in - versus out with interactive scrolling, and a bug in the axis text - reset system that prevented the text from being redrawn on a - interactive gtk view lim set with the widget + Fixed a bug in that prevented the manual setting of ticklabel strings from + working properly - Fixed a bug in that prevented the manual setting of ticklabel - strings from working properly - -2003-11-02 - Do a nearest neighbor color pick on GD when - allocate fails +2003-11-02 + - Do a nearest neighbor color pick on GD when allocate fails 2003-11-02 - - Added pcolor plot - - Added MRI example - - Fixed bug that screwed up label position if xticks or yticks were - empty - - added nearest neighbor color picker when GD max colors exceeded - - fixed figure background color bug in GD backend + - Added pcolor plot + - Added MRI example + - Fixed bug that screwed up label position if xticks or yticks were empty + - added nearest neighbor color picker when GD max colors exceeded + - fixed figure background color bug in GD backend 2003-11-10 - 2003-11-11 - - major refactoring. - - * Ticks (with labels, lines and grid) handled by dedicated class - * Artist now know bounding box and dpi - * Bounding boxes and transforms handled by dedicated classes - * legend in dedicated class. Does a better job of alignment and - bordering. Can be initialized with specific line instances. - See examples/legend_demo2.py - - -2003-11-14 Fixed legend positioning bug and added new position args + major refactoring. -2003-11-16 Finished porting GD to new axes API + * Ticks (with labels, lines and grid) handled by dedicated class + * Artist now know bounding box and dpi + * Bounding boxes and transforms handled by dedicated classes + * legend in dedicated class. Does a better job of alignment and bordering. + Can be initialized with specific line instances. See + examples/legend_demo2.py +2003-11-14 + Fixed legend positioning bug and added new position args -2003-11-20 - add TM for matlab on website and in docs +2003-11-16 + Finished porting GD to new axes API +2003-11-20 + - add TM for matlab on website and in docs -2003-11-20 - make a nice errorbar and scatter screenshot +2003-11-20 + - make a nice errorbar and scatter screenshot -2003-11-20 - auto line style cycling for multiple line types - broken +2003-11-20 + - auto line style cycling for multiple line types broken -2003-11-18 (using inkrect) :logical rect too big on gtk backend +2003-11-18 + (using inkrect) :logical rect too big on gtk backend -2003-11-18 ticks don't reach edge of axes in gtk mode -- - rounding error? +2003-11-18 + ticks don't reach edge of axes in gtk mode -- rounding error? -2003-11-20 - port Gary's errorbar code to new API before 0.40 +2003-11-20 + - port Gary's errorbar code to new API before 0.40 -2003-11-20 - problem with stale _set_font. legend axes box - doesn't resize on save in GTK backend -- see htdocs legend_demo.py +2003-11-20 + - problem with stale _set_font. legend axes box doesn't resize on save in + GTK backend -- see htdocs legend_demo.py -2003-11-21 - make a dash-dot dict for the GC +2003-11-21 + - make a dash-dot dict for the GC -2003-12-15 - fix install path bug +2003-12-15 + - fix install path bug diff --git a/doc/users/dflt_style_changes.rst b/doc/users/prev_whats_new/dflt_style_changes.rst similarity index 99% rename from doc/users/dflt_style_changes.rst rename to doc/users/prev_whats_new/dflt_style_changes.rst index 5de399adb053..b36a1a343116 100644 --- a/doc/users/dflt_style_changes.rst +++ b/doc/users/prev_whats_new/dflt_style_changes.rst @@ -1,3 +1,5 @@ +.. redirect-from:: /users/dflt_style_changes + ============================== Changes to the default style ============================== diff --git a/doc/users/prev_whats_new/github_stats_3.0.0.rst b/doc/users/prev_whats_new/github_stats_3.0.0.rst new file mode 100644 index 000000000000..ab90e5e79e4e --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.0.0.rst @@ -0,0 +1,1221 @@ +.. _github-stats-3-0-0: + +GitHub statistics for 3.0.0 (Sep 18, 2018) +========================================== + +GitHub statistics for 2017/01/17 (tag: v2.0.0) - 2018/09/18 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 123 issues and merged 598 pull requests. +The full list can be seen `on GitHub `__ + +The following 478 authors contributed 9809 commits. + +* 816-8055 +* Aashil Patel +* AbdealiJK +* Adam +* Adam Williamson +* Adrian Price-Whelan +* Adrien Chardon +* Adrien F. Vincent +* ahed87 +* akrherz +* Akshay Nair +* Alan Bernstein +* Alberto +* alcinos +* Aleksey Bilogur +* Alex Rothberg +* Alexander Buchkovsky +* Alexander Harnisch +* AlexCav +* Alexis Bienvenüe +* Ali Uneri +* Allan Haldane +* Allen Downey +* Alvaro Sanchez +* alvarosg +* AndersonDaniel +* Andras Deak +* Andreas Gustafsson +* Andreas Hilboll +* Andreas Mayer +* Andreas Mueller +* Andrew Nelson +* Andy Mastbaum +* aneda +* Anthony Scopatz +* Anton Akhmerov +* Antony Lee +* aparamon +* apodemus +* Arthur Paulino +* Arvind +* as691454 +* ash13 +* Atharva Khare +* Avinash Sharma +* Bastian Bechtold +* bduick +* Ben +* Ben Root +* Benedikt Daurer +* Benjamin Berg +* Benjamin Congdon +* Bernhard M. Wiedemann +* BHT +* Bianca Gibson +* Björn Dahlgren +* Blaise Thompson +* Boaz Mohar +* Brendan Zhang +* Brennan Magee +* Bruno Zohreh +* BTWS +* buefox +* Cameron Davidson-Pilon +* Cameron Fackler +* cclauss +* ch3rn0v +* Charles Ruan +* chelseatroy +* Chen Karako +* Chris Holdgraf +* Christoph Deil +* Christoph Gohlke +* Cimarron Mittelsteadt +* CJ Carey +* cknd +* cldssty +* clintval +* Cody Scot +* Colin +* Conner R. Phillips +* Craig Citro +* DaCoEx +* dahlbaek +* Dakota Blair +* Damian +* Dan Hickstein +* Dana +* Daniel C. Marcu +* Daniel Laidig +* danielballan +* Danny Hermes +* daronjp +* DaveL17 +* David A +* David Brooks +* David Kent +* David Stansby +* deeenes +* deepyaman +* Derek Kim +* Derek Tropf +* Devashish Deshpande +* Diego Mora Cespedes +* Dietmar Schwertberger +* Dietrich Brunn +* Divyam Madaan +* dlmccaffrey +* Dmitry Shachnev +* Dora Fraeman +* DoriekeMG +* Dorota Jarecka +* Doug Blank +* Drew J. Sonne +* Duncan Macleod +* Dylan Evans +* E. G. Patrick Bos +* Egor Panfilov +* Elijah Schutz +* Elizabeth Seiver +* Elliott Sales de Andrade +* Elvis Stansvik +* Emlyn Price +* endolith +* Eric Dill +* Eric Firing +* Eric Galloway +* Eric Larson +* Eric Wang (Mac) +* Eric Wieser +* Erik M. Bray +* Erin Pintozzi +* et2010 +* Ethan Ligon +* Eugene Yurtsev +* Fabian Kloosterman +* Fabian-Robert Stöter +* FedeMiorelli +* Federico Ariza +* Felix +* Felix Kohlgrüber +* Felix Yan +* Filip Dimitrovski +* Florencia Noriega +* Florian Le Bourdais +* Franco Vaccari +* Francoise Provencher +* Frank Yu +* fredrik-1 +* fuzzythecat +* Gabe +* Gabriel Munteanu +* Gauravjeet +* Gaute Hope +* gcallah +* Geoffrey Spear +* gnaggnoyil +* goldstarwebs +* Graeme Smecher +* greg-roper +* gregorybchris +* Grillard +* Guillermo Breto +* Gustavo Goretkin +* Hajoon Choi +* Hakan Kucukdereli +* hannah +* Hans Moritz Günther +* Harnesser +* Harshal Prakash Patankar +* Harshit Patni +* Hassan Kibirige +* Hastings Greer +* Heath Henley +* Heiko Oberdiek +* Helder +* helmiriawan +* Henning Pohl +* Herbert Kruitbosch +* HHest +* Hubert Holin +* Ian Thomas +* Ida Hjorth +* Ildar Akhmetgaleev +* ilivni +* Ilya Flyamer +* ImportanceOfBeingErnest +* ImSoErgodic +* Isa Hassen +* Isaac Schwabacher +* Isaac Slavitt +* Ismo Toijala +* J Alammar +* J. Goutin +* Jaap Versteegh +* Jacob McDonald +* jacob-on-github +* Jae-Joon Lee +* Jake Vanderplas +* James A. Bednar +* Jamie Nunez +* Jan Koehler +* Jan Schlüter +* Jan Schulz +* Jarrod Millman +* Jason King +* Jason Neal +* Jason Zheng +* jbhopkins +* jdollichon +* Jeffrey Hokanson @ Loki +* JelsB +* Jens Hedegaard Nielsen +* Jerry Lui +* jerrylui803 +* jhelie +* jli +* Jody Klymak +* joelostblom +* Johannes Wienke +* John Hoffman +* John Vandenberg +* Johnny Gill +* JojoBoulix +* jonchar +* Joseph Albert +* Joseph Fox-Rabinovitz +* Joseph Jon Booker +* Joseph Martinot-Lagarde +* Jouni K. Seppänen +* Juan Nunez-Iglesias +* Julia Sprenger +* Julian Mehne +* Julian V. Modesto +* Julien Lhermitte +* Julien Schueller +* Jun Tan +* Justin Cai +* Jörg Dietrich +* Kacper Kowalik (Xarthisius) +* Kanchana Ranasinghe +* Katrin Leinweber +* Keerysanth Sribaskaran +* keithbriggs +* Kenneth Ma +* Kevin Davies +* Kevin Ji +* Kevin Keating +* Kevin Rose +* Kexuan Sun +* khyox +* Kieran Ramos +* Kjartan Myrdal +* Kjell Le +* Klara Gerlei +* klaus +* klonuo +* Kristen M. Thyng +* kshramt +* Kyle Bridgemohansingh +* Kyle Sunden +* Kyler Brown +* Laptop11_ASPP2016 +* lboogaard +* legitz7 +* Leo Singer +* Leon Yin +* Levi Kilcher +* Liam Brannigan +* Lionel Miller +* lspvic +* Luca Verginer +* Luis Pedro Coelho +* luz.paz +* lzkelley +* Maarten Baert +* Magnus Nord +* mamrehn +* Manish Devgan +* Manuel Jung +* Mark Harfouche +* Martin Fitzpatrick +* Martin Spacek +* Massimo Santini +* Matt Hancock +* Matt Newville +* Matthew Bell +* Matthew Brett +* Matthias Bussonnier +* Matthias Lüthi +* Matti Picus +* Maximilian Albert +* Maximilian Maahn +* Maximilian Nöthe +* mcquin +* Mher Kazandjian +* Michael Droettboom +* Michael Scott Cuthbert +* Michael Seifert +* Michiel de Hoon +* Mike Henninger +* Mike Jarvis +* MinRK +* Mitar +* mitch +* mlub +* mobando +* Molly Rossow +* Moritz Boehle +* muahah +* Mudit Surana +* myyc +* Naoya Kanai +* Nathan Goldbaum +* Nathan Musoke +* Nathaniel M. Beaver +* navdeep rana +* nbrunett +* Nelle Varoquaux +* nemanja +* neok-m4700 +* nepix32 +* Nick Forrington +* Nick Garvey +* Nick Papior +* Nico Schlömer +* Nicolas P. Rougier +* Nicolas Tessore +* Nik Quibin +* Nikita Kniazev +* Nils Werner +* Ninad Bhat +* nmartensen +* Norman Fomferra +* ob +* OceanWolf +* Olivier +* Orso Meneghini +* Osarumwense +* Pankaj Pandey +* Paramonov Andrey +* Pastafarianist +* Paul Ganssle +* Paul Hobson +* Paul Ivanov +* Paul Kirow +* Paul Romano +* Paul Seyfert +* Pavol Juhas +* pdubcali +* Pete Huang +* Pete Peterson +* Peter Mackenzie-Helnwein +* Peter Mortensen +* Peter Würtz +* Petr Danecek +* pharshalp +* Phil Elson +* Phil Ruffwind +* Pierre de Buyl +* Pierre Haessig +* Pranav Garg +* productivememberofsociety666 +* PrzemysÅ‚aw DÄ…bek +* Qingpeng "Q.P." Zhang +* RAKOTOARISON Herilalaina +* Ramiro Gómez +* Randy Olson +* rebot +* Richard Gowers +* Rishikesh +* Rob Harrigan +* Robin Dunn +* Robin Neatherway +* Robin Wilson +* Ronald Hartley-Davies +* Roy Smith +* Rui Lopes +* ruin +* rvhbooth +* Ryan +* Ryan May +* Ryan Morshead +* RyanPan +* s0vereign +* Saket Choudhary +* Salganos +* Salil Vanvari +* Salinder Sidhu +* Sam Vaughan +* Samson +* Samuel St-Jean +* Sander +* scls19fr +* Scott Howard +* Scott Lasley +* scott-vsi +* Sean Farley +* Sebastian Raschka +* Sebastián Vanrell +* Seraphim Alvanides +* Sergey B Kirpichev +* serv-inc +* settheory +* shaunwbell +* Simon Gibbons +* simonpf +* sindunuragarp +* Sourav Singh +* Stefan Pfenninger +* Stephan Erb +* Sterling Smith +* Steven Silvester +* Steven Tilley +* stone +* stonebig +* Tadeo Corradi +* Taehoon Lee +* Tanuj +* Taras +* Taras Kuzyo +* TD22057 +* Ted Petrou +* terranjp +* Terrence J. Katzenbaer +* Terrence Katzenbaer +* The Gitter Badger +* Thomas A Caswell +* Thomas Hisch +* Thomas Levine +* Thomas Mansencal +* Thomas Robitaille +* Thomas Spura +* Thomas VINCENT +* Thorsten Liebig +* thuvejan +* Tian Xia +* Till Stensitzki +* Tim Hoffmann +* tmdavison +* Tobias Froehlich +* Tobias Megies +* Tom +* Tom Augspurger +* Tom Dupré la Tour +* tomoemon +* tonyyli +* Trish Gillett-Kawamoto +* Truong Pham +* Tuan Dung Tran +* u55 +* ultra-andy +* V. R +* vab9 +* Valentin Schmidt +* Vedant Nanda +* Vidur Satija +* vraelvrangr +* Víctor Zabalza +* WANG Aiyong +* Warren Weckesser +* watkinrt +* Wieland Hoffmann +* Will Silva +* William Granados +* William Mallard +* Xufeng Wang +* y1thof +* Yao-Yuan Mao +* Yuval Langer +* Zac Hatfield-Dodds +* Zbigniew JÄ™drzejewski-Szmek +* zhangeugenia +* ZhaoZhonglun1991 +* zhoubecky +* ZWL +* Élie Gouzien +* Ðндрей Парамонов + +GitHub issues and pull requests: + +Pull Requests (598): + +* :ghpull:`12145`: Doc final 3.0 docs +* :ghpull:`12143`: Backport PR #12142 on branch v3.0.x (Unbreak formlayout for image edits.) +* :ghpull:`12142`: Unbreak formlayout for image edits. +* :ghpull:`12135`: Backport PR #12131 on branch v3.0.x (Fixes currently release version of cartopy) +* :ghpull:`12131`: Fixes currently release version of cartopy +* :ghpull:`12129`: Backports for 3.0 +* :ghpull:`12132`: Backport PR #12130 on branch v3.0.x (Mention colorbar.minorticks_on/off in references) +* :ghpull:`12130`: Mention colorbar.minorticks_on/off in references +* :ghpull:`12099`: FIX: make sure all ticks show up for colorbar minor tick +* :ghpull:`11962`: Propagate changes to backend loading to setup/setupext. +* :ghpull:`12128`: Unbreak the Sphinx 1.8 build by renaming :math: to :mathmpl:. +* :ghpull:`12126`: Backport PR #12117 on branch v3.0.x (Fix Agg extent calculations for empty draws) +* :ghpull:`12113`: Backport PR #12112 on branch v3.0.x (Reword the LockDraw docstring.) +* :ghpull:`12112`: Reword the LockDraw docstring. +* :ghpull:`12110`: Backport PR #12109 on branch v3.0.x (Pin to sphinx<1.8; unremove sphinxext.mathmpl.) +* :ghpull:`12084`: DOC: link palettable +* :ghpull:`12096`: Backport PR #12092 on branch v3.0.x (Update backend_qt5agg to fix PySide2 mem issues) +* :ghpull:`12083`: Backport PR #12012 on branch v3.0.x (FIX: fallback text renderer to fig._cachedRenderer, if none found) +* :ghpull:`12081`: Backport PR #12037 on branch v3.0.x (Fix ArtistInspector.get_aliases.) +* :ghpull:`12080`: Backport PR #12053 on branch v3.0.x (Fix up some OSX backend issues) +* :ghpull:`12037`: Fix ArtistInspector.get_aliases. +* :ghpull:`12053`: Fix up some OSX backend issues +* :ghpull:`12064`: Backport PR #11971 on branch v3.0.x (FIX: use cached renderer on Legend.get_window_extent) +* :ghpull:`12063`: Backport PR #12036 on branch v3.0.x (Interactive tests update) +* :ghpull:`11928`: Update doc/conf.py to avoid warnings with (future) sphinx 1.8. +* :ghpull:`12048`: Backport PR #12047 on branch v3.0.x (Remove asserting about current backend at the end of mpl_test_settings.) +* :ghpull:`11971`: FIX: use cached renderer on Legend.get_window_extent +* :ghpull:`12036`: Interactive tests update +* :ghpull:`12029`: Backport PR #12022 on branch v3.0.x (Remove intent to deprecate rcParams["backend_fallback"].) +* :ghpull:`12047`: Remove asserting about current backend at the end of mpl_test_settings. +* :ghpull:`12020`: Backport PR #12019 on branch v3.0.x (typo: s/unmultipled/unmultiplied) +* :ghpull:`12022`: Remove intent to deprecate rcParams["backend_fallback"]. +* :ghpull:`12028`: Backport PR #12023 on branch v3.0.x (Fix deprecation check in wx Timer.) +* :ghpull:`12023`: Fix deprecation check in wx Timer. +* :ghpull:`12019`: typo: s/unmultipled/unmultiplied +* :ghpull:`12017`: Backport PR #12016 on branch v3.0.x (Fix AttributeError in GTK3Agg backend) +* :ghpull:`12016`: Fix AttributeError in GTK3Agg backend +* :ghpull:`11991`: Backport PR #11988 on branch v3.0.x +* :ghpull:`11978`: Backport PR #11973 on branch v3.0.x +* :ghpull:`11968`: Backport PR #11963 on branch v3.0.x +* :ghpull:`11967`: Backport PR #11961 on branch v3.0.x +* :ghpull:`11969`: Fix an invalid escape sequence. +* :ghpull:`11963`: Fix some lgtm convention alerts +* :ghpull:`11961`: Downgrade backend_version log to DEBUG level. +* :ghpull:`11953`: Backport PR #11896 on branch v3.0.x +* :ghpull:`11896`: Resolve backend in rcParams.__getitem__("backend"). +* :ghpull:`11950`: Backport PR #11934 on branch v3.0.x +* :ghpull:`11952`: Backport PR #11949 on branch v3.0.x +* :ghpull:`11949`: Remove test2.png from examples. +* :ghpull:`11934`: Suppress the "non-GUI backend" warning from the .. plot:: directive... +* :ghpull:`11918`: Backport PR #11917 on branch v3.0.x +* :ghpull:`11916`: Backport PR #11897 on branch v3.0.x +* :ghpull:`11915`: Backport PR #11591 on branch v3.0.x +* :ghpull:`11897`: HTMLWriter, put initialisation of frames in setup +* :ghpull:`11591`: BUG: correct the scaling in the floating-point slop test. +* :ghpull:`11910`: Backport PR #11907 on branch v3.0.x +* :ghpull:`11907`: Move TOC back to top in axes documentation +* :ghpull:`11904`: Backport PR #11900 on branch v3.0.x +* :ghpull:`11900`: Allow args to pass through _allow_super_init +* :ghpull:`11889`: Backport PR #11847 on branch v3.0.x +* :ghpull:`11890`: Backport PR #11850 on branch v3.0.x +* :ghpull:`11850`: FIX: macosx framework check +* :ghpull:`11883`: Backport PR #11862 on branch v3.0.x +* :ghpull:`11882`: Backport PR #11876 on branch v3.0.x +* :ghpull:`11876`: MAINT Better error message for number of colors versus number of data… +* :ghpull:`11862`: Fix NumPy FutureWarning for non-tuple indexing. +* :ghpull:`11845`: Use Format_ARGB32_Premultiplied instead of RGBA8888 for Qt backends. +* :ghpull:`11843`: Remove unnecessary use of nose. +* :ghpull:`11600`: backend switching -- don't create a public fallback API +* :ghpull:`11833`: adding show inheritance to autosummary template +* :ghpull:`11828`: changed warning in animation +* :ghpull:`11829`: func animation warning changes +* :ghpull:`11826`: DOC documented more of the gridspec options +* :ghpull:`11818`: Merge v2.2.x +* :ghpull:`11821`: DOC: remove multicolumns from examples +* :ghpull:`11819`: DOC: fix minor typo in figure example +* :ghpull:`11722`: Remove unnecessary hacks from setup.py. +* :ghpull:`11802`: gridspec tutorial edits +* :ghpull:`11801`: update annotations +* :ghpull:`11734`: Small cleanups to backend_agg. +* :ghpull:`11785`: Add missing API changes +* :ghpull:`11788`: Fix DeprecationWarning on LocatableAxes +* :ghpull:`11558`: Added xkcd Style for Markers (plot only) +* :ghpull:`11755`: Add description for metadata argument of savefig +* :ghpull:`11703`: FIX: make update-from also set the original face/edgecolor +* :ghpull:`11765`: DOC: reorder examples and fix top level heading +* :ghpull:`11724`: Fix cairo's image inversion and alpha misapplication. +* :ghpull:`11726`: Consolidate agg-buffer examples. +* :ghpull:`11754`: FIX: update spine positions before get extents +* :ghpull:`11779`: Remove unused attribute in tests. +* :ghpull:`11770`: Correct errors in documentation +* :ghpull:`11778`: Unpin pandas in the CI. +* :ghpull:`11772`: Clarifying an error message +* :ghpull:`11760`: Switch grid documentation to numpydoc style +* :ghpull:`11705`: Suppress/fix some test warnings. +* :ghpull:`11763`: Pin OSX CI to numpy<1.15 to unbreak the build. +* :ghpull:`11767`: Add tolerance to csd frequency test +* :ghpull:`11757`: PGF backend output text color even if black +* :ghpull:`11751`: Remove the unused 'verbose' option from setupext. +* :ghpull:`9084`: Require calling a _BoundMethodProxy to get the underlying callable. +* :ghpull:`11752`: Fix section level of Previous Whats New +* :ghpull:`10513`: Replace most uses of getfilesystemencoding by os.fs{en,de}code. +* :ghpull:`11739`: fix tight_layout bug #11737 +* :ghpull:`11744`: minor doc update on axes_grid1's inset_axes +* :ghpull:`11729`: Pass 'figure' as kwarg to FigureCanvasQt5Agg super __init__. +* :ghpull:`11736`: Remove unused needs_sphinx marker; move importorskip to toplevel. +* :ghpull:`11731`: Directly get the size of the renderer buffer from the renderer. +* :ghpull:`11717`: DOC: fix broken link in inset-locator example +* :ghpull:`11723`: Start work on making colormaps picklable. +* :ghpull:`11721`: Remove some references to colorConverter. +* :ghpull:`11713`: Don't assume cwd in test_ipynb. +* :ghpull:`11026`: ENH add an inset_axes to the axes class +* :ghpull:`11712`: Fix drawing on qt+retina. +* :ghpull:`11714`: docstring for Figure.tight_layout don't include renderer parameter +* :ghpull:`8951`: Let QPaintEvent tell us what region to repaint. +* :ghpull:`11234`: Add fig.add_artist method +* :ghpull:`11706`: Remove unused private method. +* :ghpull:`11637`: Split API changes into individual pages +* :ghpull:`10403`: Deprecate LocatableAxes from toolkits +* :ghpull:`11699`: Dedent overindented rst bullet lists. +* :ghpull:`11701`: Use skipif instead of xfail when test dependencies are missing. +* :ghpull:`11700`: Don't use pytest -rw now that pytest-warnings is builtin. +* :ghpull:`11696`: Don't force backend in toolmanager example. +* :ghpull:`11690`: Avoid using private APIs in examples. +* :ghpull:`11684`: Style +* :ghpull:`11666`: TESTS: Increase tolerance for aarch64 tests +* :ghpull:`11680`: Boring style fixes. +* :ghpull:`11678`: Use super() instead of manually fetching supermethods for parasite axes. +* :ghpull:`11679`: Remove pointless draw() at the end of static examples. +* :ghpull:`11676`: Remove unused C++ code. +* :ghpull:`11010`: ENH: Add gridspec method to figure, and subplotspecs +* :ghpull:`11672`: Add comment re: use of lru_cache in PsfontsMap. +* :ghpull:`11674`: Boring style fixes. +* :ghpull:`10954`: Cache various dviread constructs globally. +* :ghpull:`9150`: Don't update style-blacklisted rcparams in rc_* functions +* :ghpull:`10936`: Simplify tkagg C extension. +* :ghpull:`11378`: SVG Backend gouraud_triangle Correction +* :ghpull:`11383`: FIX: Improve *c* (color) kwarg checking in scatter and the related exceptions +* :ghpull:`11627`: FIX: CL avoid fully collapsed axes +* :ghpull:`11504`: Bump pgi requirement to 0.0.11.2. +* :ghpull:`11640`: Fix barplot color if none and alpha is set +* :ghpull:`11443`: changed paths in kwdocs +* :ghpull:`11626`: Minor docstring fixes +* :ghpull:`11631`: DOC: better tight_layout error handling +* :ghpull:`11651`: Remove unused imports in examples +* :ghpull:`11633`: Clean up next api_changes +* :ghpull:`11643`: Fix deprecation messages. +* :ghpull:`9223`: Set norm to log if bins=='log' in hexbin +* :ghpull:`11622`: FIX: be forgiving about the event for enterEvent not having a pos +* :ghpull:`11581`: backend switching. +* :ghpull:`11616`: Fix some doctest issues +* :ghpull:`10872`: Cleanup _plot_args_replacer logic +* :ghpull:`11617`: Clean up what's new +* :ghpull:`11610`: FIX: let colorbar extends work for PowerNorm +* :ghpull:`11615`: Revert glyph warnings +* :ghpull:`11614`: CI: don't run tox to test pytz +* :ghpull:`11603`: Doc merge up +* :ghpull:`11613`: Make flake8 exceptions explicit +* :ghpull:`11611`: Fix css for parameter types +* :ghpull:`10001`: MAINT/BUG: Don't use 5-sided quadrilaterals in Axes3D.plot_surface +* :ghpull:`10234`: PowerNorm: do not clip negative values +* :ghpull:`11398`: Simplify retrieval of cache and config directories +* :ghpull:`10682`: ENH have ax.get_tightbbox have a bbox around all artists attached to axes. +* :ghpull:`11590`: Don't associate Wx timers with the parent frame. +* :ghpull:`10245`: Cache paths of fonts shipped with mpl relative to the mpl data path. +* :ghpull:`11381`: Deprecate text.latex.unicode. +* :ghpull:`11601`: FIX: subplots don't mutate kwargs passed by user. +* :ghpull:`11609`: Remove _macosx.NavigationToolbar. +* :ghpull:`11608`: Remove some conditional branches in examples for wx<4. +* :ghpull:`11604`: TST: Place animation files in a temp dir. +* :ghpull:`11605`: Suppress a spurious missing-glyph warning with ft2font. +* :ghpull:`11360`: Pytzectomy +* :ghpull:`10885`: Move GTK3 setupext checks to within the process. +* :ghpull:`11081`: Help tool for Wx backends +* :ghpull:`10851`: Wx Toolbar for ToolManager +* :ghpull:`11247`: Remove mplDeprecation +* :ghpull:`9795`: Backend switching +* :ghpull:`9426`: Don't mark a patch transform as set if the parent transform is not set. +* :ghpull:`9175`: Warn on freetype missing glyphs. +* :ghpull:`11412`: Make contour and contourf color assignments consistent. +* :ghpull:`11477`: Enable flake8 and re-enable it everywhere +* :ghpull:`11165`: Fix figure window icon +* :ghpull:`11584`: ENH: fix colorbar bad minor ticks +* :ghpull:`11438`: ENH: add get_gridspec convenience method to subplots +* :ghpull:`11451`: Cleanup Matplotlib API docs +* :ghpull:`11579`: DOC update some examples to use constrained_layout=True +* :ghpull:`11594`: Some more docstring cleanups. +* :ghpull:`11593`: Skip wx interactive tests on OSX. +* :ghpull:`11592`: Remove some extra spaces in docstrings/comments. +* :ghpull:`11585`: Some doc cleanup of Triangulation +* :ghpull:`10474`: Use TemporaryDirectory instead of mkdtemp in a few places. +* :ghpull:`11240`: Deprecate the examples.directory rcParam. +* :ghpull:`11370`: Sorting drawn artists by their zorder when blitting using FuncAnimation +* :ghpull:`11576`: Add parameter doc to save_diff_image +* :ghpull:`11573`: Inline setup_external_compile into setupext. +* :ghpull:`11571`: Cleanup stix_fonts_demo example. +* :ghpull:`11563`: Use explicit signature in pyplot.close() +* :ghpull:`9801`: ENH: Change default Autodatelocator *interval_multiples* +* :ghpull:`11570`: More simplifications to FreeType setup on Windows. +* :ghpull:`11401`: Some py3fications. +* :ghpull:`11566`: Cleanups. +* :ghpull:`11520`: Add private API retrieving the current event loop and backend GUI info. +* :ghpull:`11544`: Restore axes sharedness when unpickling. +* :ghpull:`11568`: Figure.text changes +* :ghpull:`11248`: Simplify FreeType Windows build. +* :ghpull:`11556`: Fix colorbar bad ticks +* :ghpull:`11494`: Fix CI install of wxpython. +* :ghpull:`11564`: triinterpolate cleanups. +* :ghpull:`11548`: Use numpydoc-style parameter lists for choices +* :ghpull:`9583`: Add edgecolors kwarg to contourf +* :ghpull:`10275`: Update contour.py and widget.py +* :ghpull:`11547`: Fix example links +* :ghpull:`11555`: Fix spelling in title +* :ghpull:`11404`: FIX: don't include text at -inf in bbox +* :ghpull:`11455`: Fixing the issue where right column and top row generate wrong stream… +* :ghpull:`11297`: Prefer warn_deprecated instead of warnings.warn. +* :ghpull:`11495`: Update the documentation guidelines +* :ghpull:`11545`: Doc: fix x(filled) marker image +* :ghpull:`11287`: Maintain artist addition order in Axes.mouseover_set. +* :ghpull:`11530`: FIX: Ensuring both x and y attrs of LocationEvent are int +* :ghpull:`10336`: Use Integral and Real in typechecks rather than explicit types. +* :ghpull:`10298`: Apply gtk3 background. +* :ghpull:`10297`: Fix gtk3agg alpha channel. +* :ghpull:`9094`: axisbelow should just set zorder. +* :ghpull:`11542`: Documentation polar grids +* :ghpull:`11459`: Doc changes in add_subplot and add_axes +* :ghpull:`10908`: Make draggable callbacks check that artist has not been removed. +* :ghpull:`11522`: Small cleanups. +* :ghpull:`11539`: DOC: talk about sticky edges in Axes.margins +* :ghpull:`11540`: adding axes to module list +* :ghpull:`11537`: Fix invalid value warning when autoscaling with no data limits +* :ghpull:`11512`: Skip 3D rotation example in sphinx gallery +* :ghpull:`11538`: Re-enable pep8 on examples folder +* :ghpull:`11136`: Move remaining examples from api/ +* :ghpull:`11519`: Raise ImportError on failure to import backends. +* :ghpull:`11529`: add documentation for quality in savefig +* :ghpull:`11528`: Replace an unnecessary zip() in mplot3d by numpy ops. +* :ghpull:`11492`: add __repr__ to GridSpecBase +* :ghpull:`11521`: Add missing ``.`` to rcParam +* :ghpull:`11491`: Fixed the source path on windows in rcparam_role +* :ghpull:`11514`: Remove embedding_in_tk_canvas, which demonstrated a private API. +* :ghpull:`11507`: Fix embedding_in_tk_canvas example. +* :ghpull:`11513`: Changed docstrings in Text +* :ghpull:`11503`: Remove various mentions of the now removed GTK(2) backend. +* :ghpull:`11493`: Update a test to a figure-equality test. +* :ghpull:`11501`: Treat empty $MPLBACKEND as an unset value. +* :ghpull:`11395`: Various fixes to deprecated and warn_deprecated. +* :ghpull:`11408`: Figure equality-based tests. +* :ghpull:`11461`: Fixed bug in rendering font property kwargs list +* :ghpull:`11397`: Replace ACCEPTS by standard numpydoc params table. +* :ghpull:`11483`: Use pip requirements files for travis build +* :ghpull:`11481`: remove more pylab references +* :ghpull:`10940`: Run flake8 instead of pep8 on Python 3.6 +* :ghpull:`11476`: Remove pylab references +* :ghpull:`11448`: Link rcParams role to docs +* :ghpull:`11424`: DOC: point align-ylabel demo to new align-label functions +* :ghpull:`11454`: add subplots to axes documentation +* :ghpull:`11470`: Hyperlink DOIs against preferred resolver +* :ghpull:`11421`: DOC: make signature background grey +* :ghpull:`11457`: Search $CPATH for include directories +* :ghpull:`11456`: DOC: fix minor typo in figaspect +* :ghpull:`11293`: Lim parameter naming +* :ghpull:`11447`: Do not use class attributes as defaults for instance attributes +* :ghpull:`11449`: Slightly improve doc sidebar layout +* :ghpull:`11224`: Add deprecation messages for unused kwargs in FancyArrowPatch +* :ghpull:`11437`: Doc markersupdate +* :ghpull:`11417`: FIX: better default spine path (for logit) +* :ghpull:`11406`: Backport PR #11403 on branch v2.2.2-doc +* :ghpull:`11427`: FIX: pathlib in nbagg +* :ghpull:`11428`: Doc: Remove huge note box from examples. +* :ghpull:`11392`: Deprecate the ``verts`` kwarg to ``scatter``. +* :ghpull:`8834`: WIP: Contour log extension +* :ghpull:`11402`: Remove unnecessary str calls. +* :ghpull:`11399`: Autogenerate credits.rst +* :ghpull:`11382`: plt.subplots and plt.figure docstring changes +* :ghpull:`11388`: DOC: Constrained layout tutorial improvements +* :ghpull:`11400`: Correct docstring for axvspan() +* :ghpull:`11396`: Remove some (minor) comments regarding Py2. +* :ghpull:`11210`: FIX: don't pad axes for ticks if they aren't visible or axis off +* :ghpull:`11362`: Fix tox configuration +* :ghpull:`11366`: Improve docstring of Axes.spy +* :ghpull:`11289`: io.open and codecs.open are redundant with open on Py3. +* :ghpull:`11213`: MNT: deprecate patches.YAArrow +* :ghpull:`11352`: Catch a couple of test warnings +* :ghpull:`11292`: Simplify cleanup decorator implementation. +* :ghpull:`11349`: Remove non-existent files from MANIFEST.IN +* :ghpull:`8774`: Git issue #7216 - Add a "ruler" tool to the plot UI +* :ghpull:`11348`: Make OSX's blit() have a consistent signature with other backends. +* :ghpull:`11345`: Revert "Deprecate text.latex.unicode." +* :ghpull:`11250`: [WIP] Add tutorial for LogScale +* :ghpull:`11223`: Add an arrow tutorial +* :ghpull:`10212`: Categorical refactor +* :ghpull:`11339`: Convert Ellipse docstring to numpydoc +* :ghpull:`11255`: Deprecate text.latex.unicode. +* :ghpull:`11338`: Fix typos +* :ghpull:`11332`: Let plt.rc = matplotlib.rc, instead of being a trivial wrapper. +* :ghpull:`11331`: multiprocessing.set_start_method() --> mp.set_start_method() +* :ghpull:`9948`: Add ``ealpha`` option to ``errorbar`` +* :ghpull:`11329`: Minor docstring update of thumbnail +* :ghpull:`9551`: Refactor backend loading +* :ghpull:`11328`: Undeprecate Polygon.xy from #11299 +* :ghpull:`11318`: Improve docstring of imread() and imsave() +* :ghpull:`11311`: Simplify image.thumbnail. +* :ghpull:`11225`: Add stacklevel=2 to some more warnings.warn() calls +* :ghpull:`11313`: Add changelog entry for removal of proprietary sphinx directives. +* :ghpull:`11323`: Fix infinite loop for connectionstyle + add some tests +* :ghpull:`11314`: API changes: use the heading format defined in README.txt +* :ghpull:`11320`: Py3fy multiprocess example. +* :ghpull:`6254`: adds two new cyclic color schemes +* :ghpull:`11268`: DOC: Sanitize some internal documentation links +* :ghpull:`11300`: Start replacing ACCEPTS table by parsing numpydoc. +* :ghpull:`11298`: Automagically set the stacklevel on warnings. +* :ghpull:`11277`: Avoid using MacRoman encoding. +* :ghpull:`11295`: Use sphinx builtin only directive instead of custom one. +* :ghpull:`11305`: Reuse the noninteractivity warning from Figure.show in _Backend.show. +* :ghpull:`11307`: Avoid recursion for subclasses of str that are also "PathLike" in to_filehandle() +* :ghpull:`11304`: Re-remove six from INSTALL.rst. +* :ghpull:`11299`: Fix a bunch of doc/comment typos in patches.py. +* :ghpull:`11301`: Undefined name: cbook --> matplotlib.cbook +* :ghpull:`11254`: Update INSTALL.rst. +* :ghpull:`11267`: FIX: allow nan values in data for plt.hist +* :ghpull:`11271`: Better argspecs for Axes.stem +* :ghpull:`11272`: Remove commented-out code, unused imports +* :ghpull:`11280`: Trivial cleanups +* :ghpull:`10514`: Cleanup/update cairo + gtk compatibility matrix. +* :ghpull:`11282`: Reduce the use of C++ exceptions +* :ghpull:`11263`: Fail gracefully if can't decode font names +* :ghpull:`11278`: Remove conditional path for sphinx <1.3 in plot_directive. +* :ghpull:`11273`: Include template matplotlibrc in package_data. +* :ghpull:`11265`: Minor cleanups. +* :ghpull:`11249`: Simplify FreeType build. +* :ghpull:`11158`: Remove dependency on six - we're Py3 only now! +* :ghpull:`10050`: Update Legend draggable API +* :ghpull:`11206`: More cleanups +* :ghpull:`11001`: DOC: improve legend bbox_to_anchor description +* :ghpull:`11258`: Removed comment in AGG backend that is no longer applicable +* :ghpull:`11062`: FIX: call constrained_layout twice +* :ghpull:`11251`: Re-run boilerplate.py. +* :ghpull:`11228`: Don't bother checking luatex's version. +* :ghpull:`11207`: Update venv gui docs wrt availability of PySide2. +* :ghpull:`11236`: Minor cleanups to setupext. +* :ghpull:`11239`: Reword the timeout error message in cbook._lock_path. +* :ghpull:`11204`: Test that boilerplate.py is correctly run. +* :ghpull:`11172`: ENH add rcparam to legend_title +* :ghpull:`11229`: Simplify lookup of animation external commands. +* :ghpull:`9086`: Add SVG animation. +* :ghpull:`11212`: Fix CirclePolygon __str__ + adding tests +* :ghpull:`6737`: Ternary +* :ghpull:`11216`: Yet another set of simplifications. +* :ghpull:`11056`: Simplify travis setup a bit. +* :ghpull:`11211`: Revert explicit linestyle kwarg on step() +* :ghpull:`11205`: Minor cleanups to pyplot. +* :ghpull:`11174`: Replace numeric loc by position string +* :ghpull:`11208`: Don't crash qt figure options on unknown marker styles. +* :ghpull:`11195`: Some unrelated cleanups. +* :ghpull:`11192`: Don't use deprecated get_texcommand in backend_pgf. +* :ghpull:`11197`: Simplify demo_ribbon_box.py. +* :ghpull:`11137`: Convert ``**kwargs`` to named arguments for a clearer API +* :ghpull:`10982`: Improve docstring of Axes.imshow +* :ghpull:`11182`: Use GLib.MainLoop() instead of deprecated GObject.MainLoop() +* :ghpull:`11185`: Fix undefined name error in backend_pgf. +* :ghpull:`10321`: Ability to scale axis by a fixed factor +* :ghpull:`8787`: Faster path drawing for the cairo backend (cairocffi only) +* :ghpull:`4559`: tight_layout: Use a different default gridspec +* :ghpull:`11179`: Convert internal tk focus helper to a context manager +* :ghpull:`11176`: Allow creating empty closed paths +* :ghpull:`10339`: Pass explicit font paths to fontspec in backend_pgf. +* :ghpull:`9832`: Minor cleanup to Text class. +* :ghpull:`11141`: Remove mpl_examples symlink. +* :ghpull:`10715`: ENH: add title_fontsize to legend +* :ghpull:`11166`: Set stacklevel to 2 for backend_wx +* :ghpull:`10934`: Autogenerate (via boilerplate) more of pyplot. +* :ghpull:`9298`: Cleanup blocking_input. +* :ghpull:`6329`: Set _text to '' if Text.set_text argument is None +* :ghpull:`11157`: Fix contour return link +* :ghpull:`11146`: Explicit args and refactor Axes.margins +* :ghpull:`11145`: Use kwonlyargs instead of popping from kwargs +* :ghpull:`11119`: PGF: Get unitless positions from Text elements (fix #11116) +* :ghpull:`9078`: New anchored direction arrows +* :ghpull:`11144`: Remove toplevel unit/ directory. +* :ghpull:`11148`: remove use of subprocess compatibility shim +* :ghpull:`11143`: Use debug level for debugging messages +* :ghpull:`11142`: Finish removing future imports. +* :ghpull:`11130`: Don't include the postscript title if it is not latin-1 encodable. +* :ghpull:`11093`: DOC: Fixup to AnchoredArtist examples in the gallery +* :ghpull:`11132`: pillow-dependency update +* :ghpull:`10446`: implementation of the copy canvas tool +* :ghpull:`9131`: FIX: prevent the canvas from jump sizes due to DPI changes +* :ghpull:`9454`: Batch ghostscript converter. +* :ghpull:`10545`: Change manual kwargs popping to kwonly arguments. +* :ghpull:`10950`: Actually ignore invalid log-axis limit setting +* :ghpull:`11096`: Remove support for bar(left=...) (as opposed to bar(x=...)). +* :ghpull:`11106`: py3fy art3d. +* :ghpull:`11085`: Use GtkShortcutsWindow for Help tool. +* :ghpull:`11099`: Deprecate certain marker styles that have simpler synonyms. +* :ghpull:`11100`: Some more deprecations of old, old stuff. +* :ghpull:`11098`: Make Marker.get_snap_threshold() always return a scalar. +* :ghpull:`11097`: Schedule a removal date for passing normed (instead of density) to hist. +* :ghpull:`9706`: Masking invalid x and/or weights in hist +* :ghpull:`11080`: Py3fy backend_qt5 + other cleanups to the backend. +* :ghpull:`10967`: updated the pyplot fill_between example to elucidate the premise;maki… +* :ghpull:`11075`: Drop alpha channel when saving comparison failure diff image. +* :ghpull:`9022`: Help tool +* :ghpull:`11045`: Help tool. +* :ghpull:`11076`: Don't create texput.{aux,log} in rootdir everytime tests are run. +* :ghpull:`11073`: py3fication of some tests. +* :ghpull:`11074`: bytes % args is back since py3.5 +* :ghpull:`11066`: Use chained comparisons where reasonable. +* :ghpull:`11061`: Changed tight_layout doc strings +* :ghpull:`11064`: Minor docstring format cleanup +* :ghpull:`11055`: Remove setup_tests_only.py. +* :ghpull:`11057`: Update Ellipse position with ellipse.center +* :ghpull:`10435`: Pathlibify font_manager (only internally, doesn't change the API). +* :ghpull:`10442`: Make the filternorm prop of Images a boolean rather than a {0,1} scalar. +* :ghpull:`9855`: ENH: make ax.get_position apply aspect +* :ghpull:`9987`: MNT: hist2d now uses pcolormesh instead of pcolorfast +* :ghpull:`11014`: Merge v2.2.x into master +* :ghpull:`11000`: FIX: improve Text repr to not error if non-float x and y. +* :ghpull:`10910`: FIX: return proper legend window extent +* :ghpull:`10915`: FIX: tight_layout having negative width axes +* :ghpull:`10408`: Factor out common code in _process_unit_info +* :ghpull:`10960`: Added share_tickers parameter to axes._AxesBase.twinx/y +* :ghpull:`10971`: Skip pillow animation test if pillow not importable +* :ghpull:`10970`: Simplify/fix some manual manipulation of len(args). +* :ghpull:`10958`: Simplify the grouper implementation. +* :ghpull:`10508`: Deprecate FigureCanvasQT.keyAutoRepeat. +* :ghpull:`10607`: Move notify_axes_change to FigureManagerBase class. +* :ghpull:`10215`: Test timers and (a bit) key_press_event for interactive backends. +* :ghpull:`10955`: Py3fy cbook, compare_backend_driver_results +* :ghpull:`10680`: Rewrite the tk C blitting code +* :ghpull:`9498`: Move title up if x-axis is on the top of the figure +* :ghpull:`10942`: Make active param in CheckBottons optional, default false +* :ghpull:`10943`: Allow pie textprops to take alignment and rotation arguments +* :ghpull:`10780`: Fix scaling of RadioButtons +* :ghpull:`10938`: Fix two undefined names +* :ghpull:`10685`: fix plt.show doesn't warn if a non-GUI backend +* :ghpull:`10689`: Declare global variables that are created elsewhere +* :ghpull:`10845`: WIP: first draft at replacing linkcheker +* :ghpull:`10898`: Replace "matplotlibrc" by "rcParams" in the docs where applicable. +* :ghpull:`10926`: Some more removals of deprecated APIs. +* :ghpull:`9173`: dynamically generate pyplot functions +* :ghpull:`10918`: Use function signatures in boilerplate.py. +* :ghpull:`10914`: Changed pie charts default shape to circle and added tests +* :ghpull:`10864`: ENH: Stop mangling default figure file name if file exists +* :ghpull:`10562`: Remove deprecated code in image.py +* :ghpull:`10798`: FIX: axes limits reverting to automatic when sharing +* :ghpull:`10485`: Remove the 'hold' kwarg from codebase +* :ghpull:`10571`: Use np.full{,_like} where appropriate. [requires numpy>=1.12] +* :ghpull:`10913`: Rely a bit more on rc_context. +* :ghpull:`10299`: Invalidate texmanager cache when any text.latex.* rc changes. +* :ghpull:`10906`: Deprecate ImageComparisonTest. +* :ghpull:`10904`: Improve docstring of clabel() +* :ghpull:`10912`: remove unused matplotlib.testing import +* :ghpull:`10876`: [wip] Replace _remove_method by _on_remove list of callbacks +* :ghpull:`10692`: Update afm docs and internal data structures +* :ghpull:`10896`: Update INSTALL.rst. +* :ghpull:`10905`: Inline knownfailureif. +* :ghpull:`10907`: No need to mark (unicode) strings as u"foo" anymore. +* :ghpull:`10903`: Py3fy testing machinery. +* :ghpull:`10901`: Remove Py2/3 portable code guide. +* :ghpull:`10900`: Remove some APIs deprecated in mpl2.1. +* :ghpull:`10902`: Kill some Py2 docs. +* :ghpull:`10887`: Added feature (Make pie charts circular by default #10789) +* :ghpull:`10884`: Style fixes to setupext.py. +* :ghpull:`10879`: Deprecate two-args for cycler() and set_prop_cycle() +* :ghpull:`10865`: DOC: use OO-ish interface in image, contour, field examples +* :ghpull:`8479`: FIX markerfacecolor / mfc not in rcparams +* :ghpull:`10314`: setattr context manager. +* :ghpull:`10013`: Allow rasterization for 3D plots +* :ghpull:`10158`: Allow mplot3d rasterization; adjacent cleanups. +* :ghpull:`10871`: Rely on rglob support rather than os.walk. +* :ghpull:`10878`: Change hardcoded brackets for Toolbar message +* :ghpull:`10708`: Py3fy webagg/nbagg. +* :ghpull:`10862`: py3ify table.py and correct some docstrings +* :ghpull:`10810`: Fix for plt.plot() does not support structured arrays as data= kwarg +* :ghpull:`10861`: More python3 cleanup +* :ghpull:`9903`: ENH: adjustable colorbar ticks +* :ghpull:`10831`: Minor docstring updates on binning related plot functions +* :ghpull:`9571`: Remove LaTeX checking in setup.py. +* :ghpull:`10097`: Reset extents in RectangleSelector when not interactive on press. +* :ghpull:`10686`: fix BboxConnectorPatch does not show facecolor +* :ghpull:`10801`: Fix undefined name. Add animation tests. +* :ghpull:`10857`: FIX: ioerror font cache, second try +* :ghpull:`10796`: Added descriptions for line bars and markers examples +* :ghpull:`10846`: Unsixification +* :ghpull:`10852`: Update docs re: pygobject in venv. +* :ghpull:`10847`: Py3fy axis.py. +* :ghpull:`10834`: Minor docstring updates on spectral plot functions +* :ghpull:`10778`: wx_compat is no more. +* :ghpull:`10609`: More wx cleanup. +* :ghpull:`10826`: Py3fy dates.py. +* :ghpull:`10837`: Correctly display error when running setup.py test. +* :ghpull:`10838`: Don't use private attribute in tk example. Fix Toolbar class rename. +* :ghpull:`10835`: DOC: Make colorbar tutorial examples look like colorbars. +* :ghpull:`10823`: Add some basic smoketesting for webagg (and wx). +* :ghpull:`10828`: Add print_rgba to backend_cairo. +* :ghpull:`10830`: Make function signatures more explicit +* :ghpull:`10829`: Use long color names for default rcParams +* :ghpull:`9776`: WIP: Lockout new converters Part 2 +* :ghpull:`10799`: DOC: make legend docstring interpolated +* :ghpull:`10818`: Deprecate vestigial Annotation.arrow. +* :ghpull:`10817`: Add test to imread from url. +* :ghpull:`10696`: Simplify venv docs. +* :ghpull:`10724`: Py3fication of unicode. +* :ghpull:`10815`: API: shift deprecation of TempCache class to 3.0 +* :ghpull:`10725`: FIX/TST constrained_layout remove test8 duplication +* :ghpull:`10705`: FIX: enable extend kwargs with log scale colorbar +* :ghpull:`10400`: numpydoc-ify art3d docstrings +* :ghpull:`10723`: repr style fixes. +* :ghpull:`10592`: Rely on generalized * and ** unpackings where possible. +* :ghpull:`9475`: Declare property aliases in a single place +* :ghpull:`10793`: A hodgepodge of Py3 & style fixes. +* :ghpull:`10794`: fixed comment typo +* :ghpull:`10768`: Fix crash when imshow encounters longdouble data +* :ghpull:`10774`: Remove dead wx testing code. +* :ghpull:`10756`: Fixes png showing inconsistent inset_axes position +* :ghpull:`10773`: Consider alpha channel from RGBA color of text for SVG backend text opacity rendering +* :ghpull:`10772`: API: check locator and formatter args when passed +* :ghpull:`10713`: Implemented support for 'markevery' in prop_cycle +* :ghpull:`10751`: make centre_baseline legal for Text.set_verticalalignment +* :ghpull:`10771`: FIX/TST OS X builds +* :ghpull:`10742`: FIX: reorder linewidth setting before linestyle +* :ghpull:`10714`: sys.platform is normalized to "linux" on Py3. +* :ghpull:`10542`: Minor cleanup: PEP8, PEP257 +* :ghpull:`10636`: Remove some wx version checks. +* :ghpull:`9731`: Make legend title fontsize obey fontsize kwarg by default +* :ghpull:`10697`: Remove special-casing of _remove_method when pickling. +* :ghpull:`10701`: Autoadd removal version to deprecation message. +* :ghpull:`10699`: Remove incorrect warning in gca(). +* :ghpull:`10674`: Fix getting polar axes in plt.polar() +* :ghpull:`10564`: Nested classes and instancemethods are directly picklable on Py3.5+. +* :ghpull:`10107`: Fix stay_span to reset onclick in SpanSelector. +* :ghpull:`10693`: Make markerfacecolor work for 3d scatterplots +* :ghpull:`10596`: Switch to per-file locking. +* :ghpull:`10532`: Py3fy backend_pgf. +* :ghpull:`10618`: Fixes #10501. python3 support and pep8 in jpl_units +* :ghpull:`10652`: Some py3fication for matplotlib/__init__, setupext. +* :ghpull:`10522`: Py3fy font_manager. +* :ghpull:`10666`: More figure-related doc updates +* :ghpull:`10507`: Remove Python 2 code from C extensions +* :ghpull:`10679`: Small fixes to gtk3 examples. +* :ghpull:`10426`: Delete deprecated backends +* :ghpull:`10488`: Bug Fix - Polar plot rectangle patch not transformed correctly (#8521) +* :ghpull:`9814`: figure_enter_event uses now LocationEvent instead of Event. Fix issue #9812. +* :ghpull:`9918`: Remove old nose testing code +* :ghpull:`10672`: Deprecation fixes. +* :ghpull:`10608`: Remove most APIs deprecated in 2.1. +* :ghpull:`10653`: Mock is in stdlib in Py3. +* :ghpull:`10603`: Remove workarounds for numpy<1.10. +* :ghpull:`10660`: Work towards removing reuse-of-axes-on-collision. +* :ghpull:`10661`: Homebrew python is now python 3 +* :ghpull:`10656`: Minor fixes to event handling docs. +* :ghpull:`10635`: Simplify setupext by using globs. +* :ghpull:`10632`: Support markers from Paths that consist of one line segment +* :ghpull:`10558`: Remove if six.PY2 code paths from boilerplate.py +* :ghpull:`10640`: Fix extra and missing spaces in constrainedlayout warning. +* :ghpull:`10624`: Some trivial py3fications. +* :ghpull:`10548`: Implement PdfPages for backend pgf +* :ghpull:`10614`: Use np.stack instead of list(zip()) in colorbar.py. +* :ghpull:`10621`: Cleanup and py3fy backend_gtk3. +* :ghpull:`10615`: More style fixes. +* :ghpull:`10604`: Minor style fixes. +* :ghpull:`10565`: Strip python 2 code from subprocess.py +* :ghpull:`10605`: Bump a tolerance in test_axisartist_floating_axes. +* :ghpull:`7853`: Use exact types for Py_BuildValue. +* :ghpull:`10591`: Switch to @-matrix multiplication. +* :ghpull:`10570`: Fix check_shared in test_subplots. +* :ghpull:`10569`: Various style fixes. +* :ghpull:`10593`: Use 'yield from' where appropriate. +* :ghpull:`10577`: Minor simplification to Figure.__getstate__ logic. +* :ghpull:`10549`: Source typos +* :ghpull:`10525`: Convert six.moves.xrange() to range() for Python 3 +* :ghpull:`10541`: More argumentless (py3) super() +* :ghpull:`10539`: TST: Replace assert_equal with plain asserts. +* :ghpull:`10534`: Modernize cbook.get_realpath_and_stat. +* :ghpull:`10524`: Remove unused private _StringFuncParser. +* :ghpull:`10470`: Remove Python 2 code from setup +* :ghpull:`10528`: py3fy examples +* :ghpull:`10520`: Py3fy mathtext.py. +* :ghpull:`10527`: Switch to argumentless (py3) super(). +* :ghpull:`10523`: The current master branch is now python 3 only. +* :ghpull:`10515`: Use feature detection instead of version detection +* :ghpull:`10432`: Use some new Python3 types +* :ghpull:`10475`: Use HTTP Secure for matplotlib.org +* :ghpull:`10383`: Fix some C++ warnings +* :ghpull:`10498`: Tell the lgtm checker that the project is Python 3 only +* :ghpull:`10505`: Remove backport of which() +* :ghpull:`10483`: Remove backports.functools_lru_cache +* :ghpull:`10492`: Avoid UnboundLocalError in drag_pan. +* :ghpull:`10491`: Simplify Mac builds on Travis +* :ghpull:`10481`: Remove python 2 compatibility code from dviread +* :ghpull:`10447`: Remove Python 2 compatibility code from backend_pdf.py +* :ghpull:`10468`: Replace is_numlike by isinstance(..., numbers.Number). +* :ghpull:`10439`: mkdir is in the stdlib in Py3. +* :ghpull:`10392`: FIX: make set_text(None) keep string empty instead of "None" +* :ghpull:`10425`: API: only support python 3.5+ +* :ghpull:`10316`: TST FIX pyqt5 5.9 +* :ghpull:`4625`: hist2d() is now using pcolormesh instead of pcolorfast + +Issues (123): + +* :ghissue:`12133`: Streamplot does not work for 29x29 grid +* :ghissue:`4429`: Error calculating scaling for radiobutton widget. +* :ghissue:`3293`: markerfacecolor / mfc not in rcparams +* :ghissue:`8109`: Cannot set the markeredgecolor by default +* :ghissue:`7942`: Extend keyword doesn't work with log scale. +* :ghissue:`5571`: Finish reorganizing examples +* :ghissue:`8307`: Colorbar with imshow(logNorm) shows unexpected minor ticks +* :ghissue:`6992`: plt.hist fails when data contains nan values +* :ghissue:`6483`: Range determination for data with NaNs +* :ghissue:`8059`: BboxConnectorPatch does not show facecolor +* :ghissue:`12134`: tight_layout flips images when making plots without displaying them +* :ghissue:`6739`: Make matplotlib fail more gracefully in headless environments +* :ghissue:`3679`: Runtime detection for default backend +* :ghissue:`11966`: CartoPy code gives attribute error +* :ghissue:`11844`: Backend related issues with matplotlib 3.0.0rc1 +* :ghissue:`12095`: colorbar minorticks (possibly release critical for 3.0) +* :ghissue:`12108`: Broken doc build with sphinx 1.8 +* :ghissue:`7366`: handle repaint requests better it qtAgg +* :ghissue:`11985`: Single shot timer not working correctly with MacOSX backend +* :ghissue:`10948`: OSX backend raises deprecation warning for enter_notify_event +* :ghissue:`11970`: Legend.get_window_extent now requires a renderer +* :ghissue:`8293`: investigate whether using a single instance of ghostscript for ps->png conversion can speed up the Windows build +* :ghissue:`7707`: Replace pep8 by pycodestyle for style checking +* :ghissue:`9135`: rcdefaults, rc_file_defaults, rc_file should not update backend if it has already been selected +* :ghissue:`12015`: AttributeError with GTK3Agg backend +* :ghissue:`11913`: plt.contour levels parameter don't work as intended if receive a single int +* :ghissue:`11846`: macosx backend won't load +* :ghissue:`11792`: Newer versions of ImageMagickWriter not found on windows +* :ghissue:`11858`: Adding "pie of pie" and "bar of pie" functionality +* :ghissue:`11852`: get_backend() backward compatibility +* :ghissue:`11629`: Importing qt_compat when no Qt binding is installed fails with NameError instead of ImportError +* :ghissue:`11842`: Failed nose import in test_annotation_update +* :ghissue:`11252`: Some API removals not documented +* :ghissue:`9404`: Drop support for python 2 +* :ghissue:`2625`: Markers in XKCD style +* :ghissue:`11749`: metadata kwarg to savefig is not documented +* :ghissue:`11702`: Setting alpha on legend handle changes patch color +* :ghissue:`8798`: gtk3cairo draw_image does not respect origin and mishandles alpha +* :ghissue:`11737`: Bug in tight_layout +* :ghissue:`11373`: Passing an incorrectly sized colour list to scatter should raise a relevant error +* :ghissue:`11756`: pgf backend doesn't set color of text when the color is black +* :ghissue:`11766`: test_axes.py::test_csd_freqs failing with numpy 1.15.0 on macOS +* :ghissue:`11750`: previous whats new is overindented on "what's new in mpl3.0 page" +* :ghissue:`11728`: Qt5 Segfaults on window resize +* :ghissue:`11709`: Repaint region is wrong on Retina display with Qt5 +* :ghissue:`11578`: wx segfaulting on OSX travis tests +* :ghissue:`11628`: edgecolor argument not working in matplotlib.pyplot.bar +* :ghissue:`11625`: plt.tight_layout() does not work with plt.subplot2grid +* :ghissue:`4993`: Version ~/.cache/matplotlib +* :ghissue:`7842`: If hexbin has logarithmic bins, use log formatter for colorbar +* :ghissue:`11607`: AttributeError: 'QEvent' object has no attribute 'pos' +* :ghissue:`11486`: Colorbar does not render with PowerNorm and min extend when using imshow +* :ghissue:`11582`: wx segfault +* :ghissue:`11515`: using 'sharex' once in 'subplots' function can affect subsequent calles to 'subplots' +* :ghissue:`10269`: input() blocks any rendering and event handling +* :ghissue:`10345`: Python 3.4 with Matplotlib 1.5 vs Python 3.6 with Matplotlib 2.1 +* :ghissue:`10443`: Drop use of pytz dependency in next major release +* :ghissue:`10572`: contour and contourf treat levels differently +* :ghissue:`11123`: Crash when interactively adding a number of subplots +* :ghissue:`11550`: Undefined names: 'obj_type' and 'cbook' +* :ghissue:`11138`: Only the first figure window has mpl icon, all other figures have default tk icon. +* :ghissue:`11510`: extra minor-ticks on the colorbar when used with the extend option +* :ghissue:`11369`: zorder of Artists not being respected when blitting with FuncAnimation +* :ghissue:`11452`: Streamplot ignores rightmost column and topmost row of velocity data +* :ghissue:`11284`: imshow of multiple images produces old pixel values printed in status bar +* :ghissue:`11496`: MouseEvent.x and .y have different types +* :ghissue:`11534`: Cross-reference margins and sticky edges +* :ghissue:`8556`: Add images of markers to the list of markers +* :ghissue:`11386`: Logit scale doesn't position x/ylabel correctly first draw +* :ghissue:`11384`: Undefined name 'Path' in backend_nbagg.py +* :ghissue:`11426`: nbagg broken on master. 'Path' is not defined... +* :ghissue:`11390`: Internal use of deprecated code +* :ghissue:`11203`: tight_layout reserves tick space even if disabled +* :ghissue:`11361`: Tox.ini does not work out of the box +* :ghissue:`11253`: Problem while changing current figure size in Jupyter notebook +* :ghissue:`11219`: Write an arrow tutorial +* :ghissue:`11322`: Really deprecate Patches.xy? +* :ghissue:`11294`: ConnectionStyle Angle3 hangs with specific parameters +* :ghissue:`9518`: Some ConnectionStyle not working +* :ghissue:`11306`: savefig and path.py +* :ghissue:`11077`: Font "DejaVu Sans" can only be used through fallback +* :ghissue:`10717`: Failure to find matplotlibrc when testing installed distribution +* :ghissue:`9912`: Cleaning up variable argument signatures +* :ghissue:`3701`: unit tests should compare pyplot.py with output from boilerplate.py +* :ghissue:`11183`: Undefined name 'system_fonts' in backend_pgf.py +* :ghissue:`11101`: Crash on empty patches +* :ghissue:`11124`: [Bug] savefig cannot save file with a Unicode name +* :ghissue:`7733`: Trying to set_ylim(bottom=0) on a log scaled axis changes plot +* :ghissue:`10319`: TST: pyqt 5.10 breaks pyqt5 interactive tests +* :ghissue:`10676`: Add source code to documentation +* :ghissue:`9207`: axes has no method to return new position after box is adjusted due to aspect ratio... +* :ghissue:`4615`: hist2d with log xy axis +* :ghissue:`10996`: Plotting text with datetime axis causes warning +* :ghissue:`7582`: Report date and time of cursor position on a plot_date plot +* :ghissue:`10114`: Remove mlab from examples +* :ghissue:`10342`: imshow longdouble not truly supported +* :ghissue:`8062`: tight_layout + lots of subplots + long ylabels inverts yaxis +* :ghissue:`4413`: Long axis title alters xaxis length and direction with ``plt.tight_layout()`` +* :ghissue:`1415`: Plot title should be shifted up when xticks are set to the top of the plot +* :ghissue:`10789`: Make pie charts circular by default +* :ghissue:`10941`: Cannot set text alignment in pie chart +* :ghissue:`7908`: plt.show doesn't warn if a non-GUI backend is being used +* :ghissue:`10502`: 'FigureManager' is an undefined name in backend_wx.py +* :ghissue:`10062`: axes limits revert to automatic on sharing axes? +* :ghissue:`9246`: ENH: make default colorbar ticks adjust as nicely as axes ticks +* :ghissue:`8818`: plt.plot() does not support structured arrays as data= kwarg +* :ghissue:`10533`: Recognize pandas Timestamp objects for DateConverter? +* :ghissue:`8358`: Minor ticks on log-scale colorbar are not cleared +* :ghissue:`10075`: RectangleSelector does not work if start and end points are identical +* :ghissue:`8576`: support 'markevery' in prop_cycle +* :ghissue:`8874`: Crash in python setup.py test +* :ghissue:`3871`: replace use of _tkcanvas with get_tk_widget() +* :ghissue:`10550`: Use long color names for rc defaultParams +* :ghissue:`10722`: Duplicated test name in test_constrainedlayout +* :ghissue:`10419`: svg backend does not respect alpha channel of text *when passed as rgba* +* :ghissue:`10769`: DOC: set_major_locator could check that its getting a Locator (was EngFormatter broken?) +* :ghissue:`10719`: Need better type error checking for linewidth in ax.grid +* :ghissue:`7776`: tex cache lockfile retries should be configurable +* :ghissue:`10556`: Special conversions of xrange() +* :ghissue:`10501`: cmp() is an undefined name in Python 3 +* :ghissue:`9812`: figure_enter_event generates base Event and not LocationEvent +* :ghissue:`10602`: Random image failures with test_curvelinear4 +* :ghissue:`7795`: Incorrect uses of is_numlike diff --git a/doc/users/prev_whats_new/github_stats_3.0.1.rst b/doc/users/prev_whats_new/github_stats_3.0.1.rst new file mode 100644 index 000000000000..95e899d1a9de --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.0.1.rst @@ -0,0 +1,203 @@ +.. _github-stats-3-0-1: + +GitHub statistics for 3.0.1 (Oct 25, 2018) +========================================== + +GitHub statistics for 2018/09/18 (tag: v3.0.0) - 2018/10/25 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 31 issues and merged 127 pull requests. +The full list can be seen `on GitHub `__ + +The following 23 authors contributed 227 commits. + +* Abhinuv Nitin Pitale +* Antony Lee +* Anubhav Shrimal +* Ben Root +* Colin +* Daniele Nicolodi +* David Haberthür +* David Stansby +* Elan Ernest +* Elliott Sales de Andrade +* Eric Firing +* ImportanceOfBeingErnest +* Jody Klymak +* Kai Muehlbauer +* Kevin Rose +* Marcel Martin +* MeeseeksMachine +* Nelle Varoquaux +* Nikita Kniazev +* Ryan May +* teresy +* Thomas A Caswell +* Tim Hoffmann + +GitHub issues and pull requests: + +Pull Requests (127): + +* :ghpull:`12595`: Backport PR #12569 on branch v3.0.x (Don't confuse uintptr_t and Py_ssize_t.) +* :ghpull:`12623`: Backport PR #12285 on branch v3.0.x (FIX: Don't apply tight_layout if axes collapse) +* :ghpull:`12285`: FIX: Don't apply tight_layout if axes collapse +* :ghpull:`12622`: FIX: flake8errors 3.0.x mergeup +* :ghpull:`12619`: Backport PR #12548 on branch v3.0.x (undef _XOPEN_SOURCE breaks the build in AIX) +* :ghpull:`12621`: Backport PR #12607 on branch v3.0.x (STY: fix whitespace and escaping) +* :ghpull:`12616`: Backport PR #12615 on branch v3.0.x (Fix travis OSX build) +* :ghpull:`12594`: Backport PR #12572 on branch v3.0.x (Fix singleton hist labels) +* :ghpull:`12615`: Fix travis OSX build +* :ghpull:`12607`: STY: fix whitespace and escaping +* :ghpull:`12605`: Backport PR #12603 on branch v3.0.x (FIX: don't import macosx to check if eventloop running) +* :ghpull:`12604`: FIX: over-ride 'copy' on RcParams +* :ghpull:`12603`: FIX: don't import macosx to check if eventloop running +* :ghpull:`12602`: Backport PR #12599 on branch v3.0.x (Fix formatting of docstring) +* :ghpull:`12599`: Fix formatting of docstring +* :ghpull:`12593`: Backport PR #12581 on branch v3.0.x (Fix hist() error message) +* :ghpull:`12569`: Don't confuse uintptr_t and Py_ssize_t. +* :ghpull:`12572`: Fix singleton hist labels +* :ghpull:`12581`: Fix hist() error message +* :ghpull:`12575`: Backport PR #12573 on branch v3.0.x (BUG: mplot3d: Don't crash if azim or elev are non-integral) +* :ghpull:`12558`: Backport PR #12555 on branch v3.0.x (Clarify horizontalalignment and verticalalignment in suptitle) +* :ghpull:`12544`: Backport PR #12159 on branch v3.0.x (FIX: colorbar re-check norm before draw for autolabels) +* :ghpull:`12159`: FIX: colorbar re-check norm before draw for autolabels +* :ghpull:`12540`: Backport PR #12501 on branch v3.0.x (Rectified plot error) +* :ghpull:`12531`: Backport PR #12431 on branch v3.0.x (FIX: allow single-string color for scatter) +* :ghpull:`12431`: FIX: allow single-string color for scatter +* :ghpull:`12529`: Backport PR #12216 on branch v3.0.x (Doc: Fix search for sphinx >=1.8) +* :ghpull:`12527`: Backport PR #12461 on branch v3.0.x (FIX: make add_lines work with new colorbar) +* :ghpull:`12461`: FIX: make add_lines work with new colorbar +* :ghpull:`12522`: Backport PR #12241 on branch v3.0.x (FIX: make unused spines invisible) +* :ghpull:`12241`: FIX: make unused spines invisible +* :ghpull:`12519`: Backport PR #12504 on branch v3.0.x (DOC: clarify min supported version wording) +* :ghpull:`12517`: Backport PR #12507 on branch v3.0.x (FIX: make minor ticks formatted with science formatter as well) +* :ghpull:`12507`: FIX: make minor ticks formatted with science formatter as well +* :ghpull:`12512`: Backport PR #12363 on branch v3.0.x +* :ghpull:`12511`: Backport PR #12366 on branch v2.2.x (TST: Update test images for new Ghostscript.) +* :ghpull:`12509`: Backport PR #12478 on branch v3.0.x (MAINT: numpy deprecates asscalar in 1.16) +* :ghpull:`12363`: FIX: errors in get_position changes +* :ghpull:`12497`: Backport PR #12495 on branch v3.0.x (Fix duplicate condition in pathpatch3d example) +* :ghpull:`12490`: Backport PR #12489 on branch v3.0.x (Fix typo in documentation of ylim) +* :ghpull:`12485`: Fix font_manager.OSXInstalledFonts() +* :ghpull:`12484`: Backport PR #12448 on branch v3.0.x (Don't error if some font directories are not readable.) +* :ghpull:`12421`: Backport PR #12360 on branch v3.0.x (Replace axes_grid by axes_grid1 in test) +* :ghpull:`12448`: Don't error if some font directories are not readable. +* :ghpull:`12471`: Backport PR #12468 on branch v3.0.x (Fix ``set_ylim`` unit handling) +* :ghpull:`12475`: Backport PR #12469 on branch v3.0.x (Clarify documentation of offsetbox.AnchoredText's prop kw argument) +* :ghpull:`12468`: Fix ``set_ylim`` unit handling +* :ghpull:`12464`: Backport PR #12457 on branch v3.0.x (Fix tutorial typos.) +* :ghpull:`12432`: Backport PR #12277: FIX: datetime64 now recognized if in a list +* :ghpull:`12277`: FIX: datetime64 now recognized if in a list +* :ghpull:`12426`: Backport PR #12293 on branch v3.0.x (Make pyplot more tolerant wrt. 3rd-party subclasses.) +* :ghpull:`12293`: Make pyplot more tolerant wrt. 3rd-party subclasses. +* :ghpull:`12360`: Replace axes_grid by axes_grid1 in test +* :ghpull:`12412`: Backport PR #12394 on branch v3.0.x (DOC: fix CL tutorial to give same output from saved file and example) +* :ghpull:`12410`: Backport PR #12408 on branch v3.0.x (Don't crash on invalid registry font entries on Windows.) +* :ghpull:`12411`: Backport PR #12366 on branch v3.0.0-doc (TST: Update test images for new Ghostscript.) +* :ghpull:`12408`: Don't crash on invalid registry font entries on Windows. +* :ghpull:`12403`: Backport PR #12149 on branch v3.0.x (Mathtext tutorial fixes) +* :ghpull:`12400`: Backport PR #12257 on branch v3.0.x (Document standard backends in matplotlib.use()) +* :ghpull:`12257`: Document standard backends in matplotlib.use() +* :ghpull:`12399`: Backport PR #12383 on branch v3.0.x (Revert change of parameter name in annotate()) +* :ghpull:`12383`: Revert change of parameter name in annotate() +* :ghpull:`12390`: Backport PR #12385 on branch v3.0.x (CI: Added Appveyor Python 3.7 build) +* :ghpull:`12385`: CI: Added Appveyor Python 3.7 build +* :ghpull:`12381`: Backport PR #12353 on branch v3.0.x (Doc: clarify default parameters in scatter docs) +* :ghpull:`12378`: Backport PR #12366 on branch v3.0.x (TST: Update test images for new Ghostscript.) +* :ghpull:`12375`: Backport PR #11648 on branch v3.0.x (FIX: colorbar placement in constrained layout) +* :ghpull:`11648`: FIX: colorbar placement in constrained layout +* :ghpull:`12350`: Backport PR #12214 on branch v3.0.x +* :ghpull:`12348`: Backport PR #12347 on branch v3.0.x (DOC: add_child_axes to axes_api.rst) +* :ghpull:`12214`: Improve docstring of Annotation +* :ghpull:`12344`: Backport PR #12321 on branch v3.0.x (maint: setupext.py for freetype had a Catch case for missing ft2build.h) +* :ghpull:`12342`: Backport PR #12334 on branch v3.0.x (Improve selection of inset indicator connectors.) +* :ghpull:`12334`: Improve selection of inset indicator connectors. +* :ghpull:`12339`: Backport PR #12297 on branch v3.0.x (Remove some pytest parameterising warnings) +* :ghpull:`12338`: Backport PR #12268 on branch v3.0.x (FIX: remove unnecessary ``self`` in ``super_``-calls, fixes #12265) +* :ghpull:`12336`: Backport PR #12212 on branch v3.0.x (font_manager: Fixed problems with Path(...).suffix) +* :ghpull:`12268`: FIX: remove unnecessary ``self`` in ``super_``-calls, fixes #12265 +* :ghpull:`12212`: font_manager: Fixed problems with Path(...).suffix +* :ghpull:`12331`: Backport PR #12322 on branch v3.0.x (Fix the docs build.) +* :ghpull:`12327`: Backport PR #12326 on branch v3.0.x (fixed minor spelling error in docstring) +* :ghpull:`12320`: Backport PR #12319 on branch v3.0.x (Fix Travis 3.6 builds) +* :ghpull:`12315`: Backport PR #12313 on branch v3.0.x (BUG: Fix typo in view_limits() for MultipleLocator) +* :ghpull:`12313`: BUG: Fix typo in view_limits() for MultipleLocator +* :ghpull:`12305`: Backport PR #12274 on branch v3.0.x (MNT: put back ``_hold`` as read-only attribute on AxesBase) +* :ghpull:`12274`: MNT: put back ``_hold`` as read-only attribute on AxesBase +* :ghpull:`12303`: Backport PR #12163 on branch v3.0.x (TST: Defer loading Qt framework until test is run.) +* :ghpull:`12299`: Backport PR #12294 on branch v3.0.x (Fix expand_dims warnings in triinterpolate) +* :ghpull:`12163`: TST: Defer loading Qt framework until test is run. +* :ghpull:`12301`: Ghostscript 9.0 requirement revisited +* :ghpull:`12294`: Fix expand_dims warnings in triinterpolate +* :ghpull:`12297`: Remove some pytest parameterising warnings +* :ghpull:`12295`: Backport PR #12261 on branch v3.0.x (FIX: parasite axis2 demo) +* :ghpull:`12289`: Backport PR #12278 on branch v3.0.x (Document inheriting docstrings) +* :ghpull:`12287`: Backport PR #12262 on branch v3.0.x (Simplify empty-rasterized pdf test.) +* :ghpull:`12280`: Backport PR #12269 on branch v3.0.x (Add some param docs to BlockingInput methods) +* :ghpull:`12266`: Backport PR #12254 on branch v3.0.x (Improve docstrings of Animations) +* :ghpull:`12262`: Simplify empty-rasterized pdf test. +* :ghpull:`12254`: Improve docstrings of Animations +* :ghpull:`12263`: Backport PR #12258 on branch v3.0.x (Fix CSS for module-level data) +* :ghpull:`12250`: Backport PR #12209 on branch v3.0.x (Doc: Sort named colors example by palette) +* :ghpull:`12248`: Backport PR #12237 on branch v3.0.x (Use (float, float) as parameter type for 2D positions in docstrings) +* :ghpull:`12240`: Backport PR #12236 on branch v3.0.x +* :ghpull:`12237`: Use (float, float) as parameter type for 2D positions in docstrings +* :ghpull:`12242`: Backport PR #12238 on branch v3.0.x (Typo in docs) +* :ghpull:`12236`: Make boilerplate-generated pyplot.py flake8 compliant +* :ghpull:`12234`: Backport PR #12228 on branch v3.0.x (Fix trivial typo in docs.) +* :ghpull:`12230`: Backport PR #12213 on branch v3.0.x (Change win32InstalledFonts return value) +* :ghpull:`12213`: Change win32InstalledFonts return value +* :ghpull:`12223`: Backport PR #11688 on branch v3.0.x (Don't draw axis (spines, ticks, labels) twice when using parasite axes.) +* :ghpull:`12224`: Backport PR #12207 on branch v3.0.x (FIX: dont' check for interactive framework if none required) +* :ghpull:`12207`: FIX: don't check for interactive framework if none required +* :ghpull:`11688`: Don't draw axis (spines, ticks, labels) twice when using parasite axes. +* :ghpull:`12205`: Backport PR #12186 on branch v3.0.x (DOC: fix API note about get_tightbbox) +* :ghpull:`12204`: Backport PR #12203 on branch v3.0.x (Document legend best slowness) +* :ghpull:`12203`: Document legend's slowness when "best" location is used +* :ghpull:`12194`: Backport PR #12164 on branch v3.0.x (Fix Annotation.contains.) +* :ghpull:`12193`: Backport PR #12177 on branch v3.0.x (FIX: remove cwd from mac font path search) +* :ghpull:`12164`: Fix Annotation.contains. +* :ghpull:`12177`: FIX: remove cwd from mac font path search +* :ghpull:`12185`: Backport PR #12183 on branch v3.0.x (Doc: Don't use Sphinx 1.8) +* :ghpull:`12183`: Doc: Don't use Sphinx 1.8 +* :ghpull:`12172`: Backport PR #12157 on branch v3.0.x (Properly declare the interactive framework for the qt4foo backends.) +* :ghpull:`12167`: Backport PR #12166 on branch v3.0.x (Document preference order for backend auto selection) +* :ghpull:`12166`: Document preference order for backend auto selection +* :ghpull:`12157`: Properly declare the interactive framework for the qt4foo backends. +* :ghpull:`12153`: Backport PR #12148 on branch v3.0.x (BLD: pragmatic fix for building basic_unit example on py37) + +Issues (31): + +* :ghissue:`12626`: AttributeError: module 'matplotlib' has no attribute 'artist' +* :ghissue:`12613`: transiently linked interactivity of unshared pair of axes generated with make_axes_locatable +* :ghissue:`12601`: Can't import matplotlib +* :ghissue:`12580`: Incorrect hist error message with bad color size +* :ghissue:`12567`: Calling pyplot.show() with TkAgg backend on x86 machine raises OverflowError. +* :ghissue:`12556`: Matplotlib 3.0.0 import hangs in clean environment +* :ghissue:`12550`: colorbar resizes in animation +* :ghissue:`12155`: Incorrect placement of Colorbar ticks using LogNorm +* :ghissue:`12438`: Scatter doesn't accept a list of strings as color spec. +* :ghissue:`12429`: scatter() does not accept gray strings anymore +* :ghissue:`12458`: add_lines misses lines for matplotlib.colorbar.ColorbarBase +* :ghissue:`12239`: 3d axes are collapsed by tight_layout +* :ghissue:`12488`: inconsistent colorbar tick labels for LogNorm +* :ghissue:`12515`: pyplot.step broken in 3.0.0? +* :ghissue:`12355`: Error for bbox_inches='tight' in savefig with make_axes_locatable +* :ghissue:`12505`: ImageGrid in 3.0 +* :ghissue:`12291`: Importing pyplot crashes on macOS due to missing fontlist-v300.json and then Permission denied: '/opt/local/share/fonts' +* :ghissue:`12288`: New function signatures in pyplot break Cartopy +* :ghissue:`12445`: Error on colorbar +* :ghissue:`12446`: Polar Contour - float() argument must be a string or a number, not 'AxesParasiteParasiteAuxTrans' +* :ghissue:`12271`: error with errorbar with datetime64 +* :ghissue:`12405`: plt.stackplot() does not work with 3.0.0 +* :ghissue:`12406`: Bug with font finding, and here is my fix as well. +* :ghissue:`12325`: Annotation change from "s" to "text" in 3.0- documentation +* :ghissue:`11641`: constrained_layout and colorbar for a subset of axes +* :ghissue:`12352`: TeX rendering broken on master with windows +* :ghissue:`12354`: Too many levels of symbolic links +* :ghissue:`12265`: ParasiteAxesAuxTrans pcolor/pcolormesh and contour/contourf broken +* :ghissue:`12173`: Cannot import pyplot +* :ghissue:`12120`: Default legend behavior (loc='best') very slow for large amounts of data. +* :ghissue:`12176`: import pyplot on MacOS without font cache will search entire subtree of current dir diff --git a/doc/users/prev_whats_new/github_stats_3.0.2.rst b/doc/users/prev_whats_new/github_stats_3.0.2.rst index edafac67e192..395f87acd534 100644 --- a/doc/users/prev_whats_new/github_stats_3.0.2.rst +++ b/doc/users/prev_whats_new/github_stats_3.0.2.rst @@ -1,13 +1,14 @@ .. _github-stats-3-0-2: -GitHub Stats for Matplotlib 3.0.2 -================================= +GitHub statistics for 3.0.2 (Nov 10, 2018) +========================================== -GitHub stats for 2018/09/18 - 2018/11/09 (tag: v3.0.0) +GitHub statistics for 2018/09/18 (tag: v3.0.0) - 2018/11/10 These lists are automatically generated, and may be incomplete or contain duplicates. We closed 170 issues and merged 224 pull requests. +The full list can be seen `on GitHub `__ The following 49 authors contributed 460 commits. diff --git a/doc/users/prev_whats_new/github_stats_3.0.3.rst b/doc/users/prev_whats_new/github_stats_3.0.3.rst new file mode 100644 index 000000000000..5c1271e52e4f --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.0.3.rst @@ -0,0 +1,147 @@ +.. _github-stats-3-0-3: + +GitHub statistics for 3.0.3 (Feb 28, 2019) +========================================== + +GitHub statistics for 2018/11/10 (tag: v3.0.2) - 2019/02/28 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 14 issues and merged 92 pull requests. +The full list can be seen `on GitHub `__ + +The following 19 authors contributed 157 commits. + +* Antony Lee +* Christer Jensen +* Christoph Gohlke +* David Stansby +* Elan Ernest +* Elliott Sales de Andrade +* ImportanceOfBeingErnest +* James Adams +* Jody Klymak +* Johannes H. Jensen +* Matthias Geier +* MeeseeksMachine +* Molly Rossow +* Nelle Varoquaux +* Paul Ivanov +* Pierre Thibault +* Thomas A Caswell +* Tim Hoffmann +* Tobia De Koninck + +GitHub issues and pull requests: + +Pull Requests (92): + +* :ghpull:`13493`: V3.0.3 prep +* :ghpull:`13491`: V3.0.x pi +* :ghpull:`13460`: Backport PR #13455 on branch v3.0.x (BLD: only try to get freetype src if src does not exist) +* :ghpull:`13461`: Backport PR #13426 on branch v3.0.x (FIX: bbox_inches='tight' with only non-finite bounding boxes) +* :ghpull:`13426`: FIX: bbox_inches='tight' with only non-finite bounding boxes +* :ghpull:`13453`: Backport PR #13451 on branch v3.0.x (MNT: fix logic error where we never try the second freetype URL) +* :ghpull:`13451`: MNT: fix logic error where we never try the second freetype URL +* :ghpull:`13446`: Merge pull request #11246 from anntzer/download-jquery +* :ghpull:`13437`: Backport PR #13436 on branch v3.0.x (Add get/set_in_layout to artist API docs.) +* :ghpull:`13436`: Add get/set_in_layout to artist API docs. +* :ghpull:`13432`: Really fix ArtistInspector.get_aliases +* :ghpull:`13416`: Backport PR #13405 on branch v3.0.x (Fix imshow()ing PIL-opened images.) +* :ghpull:`13418`: Backport PR #13412 and #13337 on branch v3.0.x +* :ghpull:`13412`: CI: add additional qt5 deb package on travis +* :ghpull:`13370`: Backport PR #13367 on branch v3.0.x (DOC: fix note of what version hold was deprecated in (2.0 not 2.1)) +* :ghpull:`13366`: Backport PR #13365 on branch v3.0.x (Fix gcc warning) +* :ghpull:`13365`: Fix gcc warning +* :ghpull:`13347`: Backport PR #13289 on branch v3.0.x (Fix unhandled C++ exception) +* :ghpull:`13349`: Backport PR #13234 on branch v3.0.x +* :ghpull:`13281`: MAINT install of pinned vers for travis +* :ghpull:`13289`: Fix unhandled C++ exception +* :ghpull:`13345`: Backport PR #13333 on branch v3.0.x (Fix possible leak of return of PySequence_GetItem.) +* :ghpull:`13333`: Fix possible leak of return of PySequence_GetItem. +* :ghpull:`13337`: Bump to flake8 3.7. +* :ghpull:`13340`: Backport PR #12398 on branch v3.0.x (CI: Don't run AppVeyor/Travis for doc backport branches.) +* :ghpull:`13317`: Backport PR #13316 on branch v3.0.x (Put correct version in constrained layout tutorial) +* :ghpull:`13308`: Backport PR #12678 on branch v3.0.x +* :ghpull:`12678`: FIX: properly set tz for YearLocator +* :ghpull:`13291`: Backport PR #13287 on branch v3.0.x (Fix unsafe use of NULL pointer) +* :ghpull:`13290`: Backport PR #13288 on branch v3.0.x (Fix potential memory leak) +* :ghpull:`13287`: Fix unsafe use of NULL pointer +* :ghpull:`13288`: Fix potential memory leak +* :ghpull:`13273`: Backport PR #13272 on branch v3.0.x (DOC Better description of inset locator and colorbar) +* :ghpull:`12812`: Backport PR #12809 on branch v3.0.x (Fix TypeError when calculating tick_values) +* :ghpull:`13245`: Backport PR #13244 on branch v3.0.x (Fix typo) +* :ghpull:`13176`: Backport PR #13047 on branch v3.0.x (Improve docs on contourf extend) +* :ghpull:`13215`: Backport PR #13212 on branch v3.0.x (Updated the docstring for pyplot.figure to list floats as the type for figsize argument) +* :ghpull:`13158`: Backport PR #13150 on branch v3.0.x (Remove unused add_dicts from example.) +* :ghpull:`13157`: Backport PR #13152 on branch v3.0.x (DOC: Add explanatory comment for colorbar with axes divider example) +* :ghpull:`13221`: Backport PR #13194 on branch v3.0.x (TST: Fix incorrect call to pytest.raises.) +* :ghpull:`13230`: Backport PR #13226 on branch v3.0.x (Avoid triggering warnings in mandelbrot example.) +* :ghpull:`13216`: Backport #13205 on branch v3.0.x (Add xvfb service to travis) +* :ghpull:`13194`: TST: Fix incorrect call to pytest.raises. +* :ghpull:`13212`: Updated the docstring for pyplot.figure to list floats as the type for figsize argument +* :ghpull:`13205`: Add xvfb service to travis +* :ghpull:`13204`: Add xvfb service to travis +* :ghpull:`13175`: Backport PR #13015 on branch v3.0.x (Enable local doc building without git installation) +* :ghpull:`13047`: Improve docs on contourf extend +* :ghpull:`13015`: Enable local doc building without git installation +* :ghpull:`13159`: Revert "Pin pytest to <3.8 (for 3.0.x)" +* :ghpull:`13150`: Remove unused add_dicts from example. +* :ghpull:`13152`: DOC: Add explanatory comment for colorbar with axes divider example +* :ghpull:`13085`: Backport PR #13081 on branch v3.0.x (DOC: forbid a buggy version of pillow for building docs) +* :ghpull:`13082`: Backport PR #13080 on branch v3.0.x (Pin pillow to < 5.4 to fix doc build) +* :ghpull:`13054`: Backport PR #13052 on branch v3.0.x (Small bug fix in image_slices_viewer) +* :ghpull:`13052`: Small bug fix in image_slices_viewer +* :ghpull:`13036`: Backport PR #12949 on branch v3.0.x (Update docstring of Axes3d.scatter) +* :ghpull:`12949`: Update docstring of Axes3d.scatter +* :ghpull:`13004`: Backport PR #13001: Update windows build instructions +* :ghpull:`13011`: Backport PR #13006 on branch v3.0.x (Add category module to docs) +* :ghpull:`13009`: Fix dependencies for travis build with python 3.5 +* :ghpull:`13006`: Add category module to docs +* :ghpull:`13001`: Update windows build instructions +* :ghpull:`12996`: Fix return type in 3D scatter docs +* :ghpull:`12972`: Backport PR #12929 on branch v3.0.x (FIX: skip gtk backend if gobject but not pygtk is installed) +* :ghpull:`12596`: Use sudo:true for nightly builds. +* :ghpull:`12929`: FIX: skip gtk backend if gobject but not pygtk is installed +* :ghpull:`12965`: Backport PR #12960 on branch v3.0.x (Remove animated=True from animation docs) +* :ghpull:`12964`: Backport PR #12938 on branch v3.0.x (Fix xtick.minor.visible only acting on the xaxis) +* :ghpull:`12938`: Fix xtick.minor.visible only acting on the xaxis +* :ghpull:`12937`: Backport PR #12914 on branch 3.0.x: Fix numpydoc formatting +* :ghpull:`12914`: Fix numpydoc formatting +* :ghpull:`12923`: Backport PR #12921 on branch v3.0.x (Fix documentation of vert parameter of Axes.bxp) +* :ghpull:`12921`: Fix documentation of vert parameter of Axes.bxp +* :ghpull:`12912`: Backport PR #12878 on branch v3.0.2-doc (Pin pytest to <3.8 (for 3.0.x)) +* :ghpull:`12906`: Backport PR #12774 on branch v3.0.x +* :ghpull:`12774`: Cairo backend: Fix alpha render of collections +* :ghpull:`12854`: Backport PR #12835 on branch v3.0.x (Don't fail tests if cairo dependency is not installed.) +* :ghpull:`12896`: Backport PR #12848 on branch v3.0.x (Fix spelling of the name Randall Munroe) +* :ghpull:`12894`: Backport PR #12890 on branch v3.0.x (Restrict postscript title to ascii.) +* :ghpull:`12838`: Backport PR #12795 on branch v3.0.x (Fix Bezier degree elevation formula in backend_cairo.) +* :ghpull:`12843`: Backport PR #12824 on branch v3.0.x +* :ghpull:`12890`: Restrict postscript title to ascii. +* :ghpull:`12878`: Pin pytest to <3.8 (for 3.0.x) +* :ghpull:`12870`: Backport PR #12869 on branch v3.0.x (Fix latin-1-ization of Title in eps.) +* :ghpull:`12869`: Fix latin-1-ization of Title in eps. +* :ghpull:`12835`: Don't fail tests if cairo dependency is not installed. +* :ghpull:`12848`: Fix spelling of the name Randall Munroe +* :ghpull:`12795`: Fix Bezier degree elevation formula in backend_cairo. +* :ghpull:`12824`: Add missing datestr2num to docs +* :ghpull:`12791`: Backport PR #12790 on branch v3.0.x (Remove ticks and titles from tight bbox tests.) +* :ghpull:`12790`: Remove ticks and titles from tight bbox tests. + +Issues (14): + +* :ghissue:`10360`: creating PathCollection proxy artist with %matplotlib inline raises ValueError: cannot convert float NaN to integer +* :ghissue:`13276`: calling annotate with nan values for the position still gives error after 3.0.2 +* :ghissue:`13450`: Issues with jquery download caching +* :ghissue:`13223`: label1On set to true when axis.tick_params(axis='both', which='major', length=5) +* :ghissue:`13311`: docs unclear on status of constraint layout +* :ghissue:`12675`: Off-by-one bug in annual axis labels when localized time crosses year boundary +* :ghissue:`13208`: Wrong argument type for figsize in documentation for figure +* :ghissue:`13201`: test_backend_qt tests failing +* :ghissue:`13013`: v3.0.2 local html docs "git describe" error +* :ghissue:`13051`: Missing self in image_slices_viewer +* :ghissue:`12920`: Incorrect return type in mplot3d documentation +* :ghissue:`12907`: Tiny typo in documentation of matplotlib.figure.Figure.colorbar +* :ghissue:`12892`: GTK3Cairo Backend Legend TypeError +* :ghissue:`12815`: DOC: matplotlib.dates datestr2num function documentation is missing diff --git a/doc/users/prev_whats_new/github_stats_3.1.0.rst b/doc/users/prev_whats_new/github_stats_3.1.0.rst index 704de6d09932..97bee1af56b8 100644 --- a/doc/users/prev_whats_new/github_stats_3.1.0.rst +++ b/doc/users/prev_whats_new/github_stats_3.1.0.rst @@ -1,14 +1,14 @@ .. _github-stats-3-1-0: -GitHub Stats for Matplotlib 3.1.0 -================================= +GitHub statistics for 3.1.0 (May 18, 2019) +========================================== -GitHub stats for 2018/09/18 - 2019/05/13 (tag: v3.0.0) +GitHub statistics for 2018/09/18 (tag: v3.0.0) - 2019/05/18 These lists are automatically generated, and may be incomplete or contain duplicates. We closed 161 issues and merged 918 pull requests. -The full list can be seen `on GitHub `__ +The full list can be seen `on GitHub `__ The following 150 authors contributed 3426 commits. diff --git a/doc/users/prev_whats_new/github_stats_3.1.1.rst b/doc/users/prev_whats_new/github_stats_3.1.1.rst index 5de8ba84ade8..3e552c371c55 100644 --- a/doc/users/prev_whats_new/github_stats_3.1.1.rst +++ b/doc/users/prev_whats_new/github_stats_3.1.1.rst @@ -1,9 +1,9 @@ .. _github-stats-3-1-1: -GitHub Stats for Matplotlib 3.1.1 -================================= +GitHub statistics for 3.1.1 (Jul 02, 2019) +========================================== -GitHub stats for 2019/05/18 - 2019/06/30 (tag: v3.1.0) +GitHub statistics for 2019/05/18 (tag: v3.1.0) - 2019/07/02 These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.1.2.rst b/doc/users/prev_whats_new/github_stats_3.1.2.rst index 9f11f34cb78a..e1ed84e26372 100644 --- a/doc/users/prev_whats_new/github_stats_3.1.2.rst +++ b/doc/users/prev_whats_new/github_stats_3.1.2.rst @@ -1,202 +1,186 @@ .. _github-stats-3-1-2: -GitHub Stats for Matplotlib 3.1.2 -================================= +GitHub statistics for 3.1.2 (Nov 21, 2019) +========================================== -GitHub stats for 2019/05/18 - 2019/06/30 (tag: v3.1.0) +GitHub statistics for 2019/07/01 (tag: v3.1.1) - 2019/11/21 These lists are automatically generated, and may be incomplete or contain duplicates. -We closed 30 issues and merged 120 pulnl requests. -The full list can be seen `on GitHub `__ +We closed 28 issues and merged 113 pull requests. +The full list can be seen `on GitHub `__ -The following 30 authors contributed 323 commits. +The following 23 authors contributed 192 commits. -* Adam Gomaa +* Alex Rudy * Antony Lee -* Ben Root -* Christer Jensen -* chuanzhu xu +* Bingyao Liu +* Cong Ma * David Stansby -* Deng Tian -* djdt -* Dora Fraeman Caswell -* Elan Ernest * Elliott Sales de Andrade -* Eric Firing -* Filipe Fernandes -* Ian Thomas +* hannah +* Hanno Rein * ImportanceOfBeingErnest +* joaonsg * Jody Klymak -* Johannes H. Jensen -* Jonas Camillus Jeppesen -* LeiSurrre -* Matt Adamson +* Matthias Bussonnier * MeeseeksMachine -* Molly Rossow -* Nathan Goldbaum +* miquelastein * Nelle Varoquaux +* Patrick Shriwise +* Paul Hoffman * Paul Ivanov -* RoryIAngus * Ryan May +* Samesh * Thomas A Caswell -* Thomas Robitaille * Tim Hoffmann +* Vincent L.M. Mazoyer GitHub issues and pull requests: -Pull Requests (120): +Pull Requests (113): -* :ghpull:`14636`: Don't capture stderr in _check_and_log_subprocess. -* :ghpull:`14655`: Backport PR #14649 on branch v3.1.x (Fix appveyor conda py37) -* :ghpull:`14649`: Fix appveyor conda py37 -* :ghpull:`14646`: Backport PR #14640 on branch v3.1.x (FIX: allow secondary axes to be non-linear) -* :ghpull:`14640`: FIX: allow secondary axes to be non-linear -* :ghpull:`14643`: Second attempt at fixing axis inversion (for mpl3.1). -* :ghpull:`14623`: Fix axis inversion with loglocator and logitlocator. -* :ghpull:`14619`: Backport PR #14598 on branch v3.1.x (Fix inversion of shared axes.) -* :ghpull:`14621`: Backport PR #14613 on branch v3.1.x (Cleanup DateFormatter docstring.) -* :ghpull:`14622`: Backport PR #14611 on branch v3.1.x (Update some axis docstrings.) -* :ghpull:`14611`: Update some axis docstrings. -* :ghpull:`14613`: Cleanup DateFormatter docstring. -* :ghpull:`14598`: Fix inversion of shared axes. -* :ghpull:`14610`: Backport PR #14579 on branch v3.1.x (Fix inversion of 3d axis.) -* :ghpull:`14579`: Fix inversion of 3d axis. -* :ghpull:`14600`: Backport PR #14599 on branch v3.1.x (DOC: Add numpngw to third party packages.) -* :ghpull:`14574`: Backport PR #14568 on branch v3.1.x (Don't assume tk canvas have a manager attached.) -* :ghpull:`14568`: Don't assume tk canvas have a manager attached. -* :ghpull:`14571`: Backport PR #14566 on branch v3.1.x (Move setting of AA_EnableHighDpiScaling before creating QApplication.) -* :ghpull:`14566`: Move setting of AA_EnableHighDpiScaling before creating QApplication. -* :ghpull:`14541`: Backport PR #14535 on branch v3.1.x (Invalidate FT2Font cache when fork()ing.) -* :ghpull:`14535`: Invalidate FT2Font cache when fork()ing. -* :ghpull:`14522`: Backport PR #14040 on branch v3.1.x (Gracefully handle non-finite z in tricontour (issue #10167)) -* :ghpull:`14434`: Backport PR #14296 on branch v3.1.x (Fix barbs to accept array of bool for ``flip_barb``) -* :ghpull:`14518`: Backport PR #14509 on branch v3.1.x (Fix too large icon spacing in Qt5 on non-HiDPI screens) -* :ghpull:`14509`: Fix too large icon spacing in Qt5 on non-HiDPI screens -* :ghpull:`14514`: Backport PR #14256 on branch v3.1.x (Improve docstring of Axes.barbs) -* :ghpull:`14256`: Improve docstring of Axes.barbs -* :ghpull:`14505`: Backport PR #14395 on branch v3.1.x (MAINT: work around non-zero exit status of "pdftops -v" command.) -* :ghpull:`14504`: Backport PR #14445 on branch v3.1.x (FIX: fastpath clipped artists) -* :ghpull:`14502`: Backport PR #14451 on branch v3.1.x (FIX: return points rather than path to fix regression) -* :ghpull:`14445`: FIX: fastpath clipped artists -* :ghpull:`14497`: Backport PR #14491 on branch v3.1.x (Fix uses of PyObject_IsTrue.) -* :ghpull:`14491`: Fix uses of PyObject_IsTrue. -* :ghpull:`14492`: Backport PR #14490 on branch v3.1.x (Fix links of parameter types) -* :ghpull:`14490`: Fix links of parameter types -* :ghpull:`14489`: Backport PR #14459 on branch v3.1.x (Cleanup docstring of DraggableBase.) -* :ghpull:`14459`: Cleanup docstring of DraggableBase. -* :ghpull:`14485`: Backport #14429 on v3.1.x -* :ghpull:`14486`: Backport #14403 on v3.1. -* :ghpull:`14429`: FIX: if the first elements of an array are masked keep checking -* :ghpull:`14481`: Backport PR #14475 on branch v3.1.x (change ginoput docstring to match behavior) -* :ghpull:`14482`: Backport PR #14464 on branch v3.1.x (Mention origin and extent tutorial in API docs for origin kwarg) -* :ghpull:`14464`: Mention origin and extent tutorial in API docs for origin kwarg -* :ghpull:`14468`: Backport PR #14449: Improve docs on gridspec -* :ghpull:`14475`: change ginoput docstring to match behavior -* :ghpull:`14477`: Backport PR #14461 on branch v3.1.x (Fix out of bounds read in backend_tk.) -* :ghpull:`14476`: Backport PR #14474 on branch v3.1.x (Fix default value in docstring of errorbar func) -* :ghpull:`14461`: Fix out of bounds read in backend_tk. -* :ghpull:`14474`: Fix default value in docstring of errorbar func -* :ghpull:`14473`: Backport PR #14472 on branch v3.1.x (Fix NameError in example code for setting label via method) -* :ghpull:`14472`: Fix NameError in example code for setting label via method -* :ghpull:`14449`: Improve docs on gridspec -* :ghpull:`14450`: Backport PR #14422 on branch v3.1.x (Fix ReST note in span selector example) -* :ghpull:`14446`: Backport PR #14438 on branch v3.1.x (Issue #14372 - Add degrees to documentation) -* :ghpull:`14438`: Issue #14372 - Add degrees to documentation -* :ghpull:`14437`: Backport PR #14387 on branch v3.1.x (Fix clearing rubberband on nbagg) -* :ghpull:`14387`: Fix clearing rubberband on nbagg -* :ghpull:`14435`: Backport PR #14425 on branch v3.1.x (Lic restore license paint) -* :ghpull:`14296`: Fix barbs to accept array of bool for ``flip_barb`` -* :ghpull:`14430`: Backport PR #14397 on branch v3.1.x (Correctly set clip_path on pcolorfast return artist.) -* :ghpull:`14397`: Correctly set clip_path on pcolorfast return artist. -* :ghpull:`14409`: Backport PR #14335 on branch v3.1.x (Add explanation of animation.embed_limit to matplotlibrc.template) -* :ghpull:`14335`: Add explanation of animation.embed_limit to matplotlibrc.template -* :ghpull:`14403`: Revert "Preserve whitespace in svg output." -* :ghpull:`14407`: Backport PR #14406 on branch v3.1.x (Remove extra \iint in math_symbol_table for document) -* :ghpull:`14398`: Backport PR #14394 on branch v3.1.x (Update link to "MathML torture test".) -* :ghpull:`14394`: Update link to "MathML torture test". -* :ghpull:`14389`: Backport PR #14388 on branch v3.1.x (Fixed one little spelling error) -* :ghpull:`14385`: Backport PR #14316 on branch v3.1.x (Improve error message for kiwisolver import error (DLL load failed)) -* :ghpull:`14388`: Fixed one little spelling error -* :ghpull:`14384`: Backport PR #14369 on branch v3.1.x (Don't use deprecated mathcircled in docs.) -* :ghpull:`14316`: Improve error message for kiwisolver import error (DLL load failed) -* :ghpull:`14369`: Don't use deprecated mathcircled in docs. -* :ghpull:`14375`: Backport PR #14374 on branch v3.1.x (Check that the figure patch is in bbox_artists before trying to remove.) -* :ghpull:`14374`: Check that the figure patch is in bbox_artists before trying to remove. -* :ghpull:`14040`: Gracefully handle non-finite z in tricontour (issue #10167) -* :ghpull:`14342`: Backport PR #14326 on branch v3.1.x (Correctly apply PNG palette when building ImageBase through Pillow.) -* :ghpull:`14326`: Correctly apply PNG palette when building ImageBase through Pillow. -* :ghpull:`14341`: Backport PR #14337 on branch v3.1.x (Docstring cleanup) -* :ghpull:`14337`: Docstring cleanup -* :ghpull:`14325`: Backport PR #14126 on branch v3.1.x (Simplify grouped bar chart example) -* :ghpull:`14324`: Backport PR #14139 on branch v3.1.x (TST: be more explicit about identifying qt4/qt5 imports) -* :ghpull:`14126`: Simplify grouped bar chart example -* :ghpull:`14323`: Backport PR #14290 on branch v3.1.x (Convert SymmetricalLogScale to numpydoc) -* :ghpull:`14139`: TST: be more explicit about identifying qt4/qt5 imports -* :ghpull:`14290`: Convert SymmetricalLogScale to numpydoc -* :ghpull:`14321`: Backport PR #14313 on branch v3.1.x -* :ghpull:`14313`: Support masked array inputs for to_rgba and to_rgba_array. -* :ghpull:`14320`: Backport PR #14319 on branch v3.1.x (Don't set missing history buttons.) -* :ghpull:`14319`: Don't set missing history buttons. -* :ghpull:`14317`: Backport PR #14295: Fix bug in SymmetricalLogTransform. -* :ghpull:`14302`: Backport PR #14255 on branch v3.1.x (Improve docsstring of Axes.streamplot) -* :ghpull:`14255`: Improve docsstring of Axes.streamplot -* :ghpull:`14295`: Fix bug in SymmetricalLogTransform. -* :ghpull:`14294`: Backport PR #14282 on branch v3.1.x (Fix toolmanager's destroy subplots in tk) -* :ghpull:`14282`: Fix toolmanager's destroy subplots in tk -* :ghpull:`14292`: Backport PR #14289 on branch v3.1.x (BUG: Fix performance regression when plotting values from Numpy array sub-classes) -* :ghpull:`14289`: BUG: Fix performance regression when plotting values from Numpy array sub-classes -* :ghpull:`14287`: Backport PR #14286 on branch v3.1.x (fix minor typo) -* :ghpull:`14284`: Backport PR #14279 on branch v3.1.x (In case fallback to Agg fails, let the exception propagate out.) -* :ghpull:`14254`: Merge up 30x -* :ghpull:`14279`: In case fallback to Agg fails, let the exception propagate out. -* :ghpull:`14268`: Backport PR #14261 on branch v3.1.x (Updated polar documentation) -* :ghpull:`14261`: Updated polar documentation -* :ghpull:`14264`: Backport PR #14260 on branch v3.1.x (Remove old OSX FAQ page) -* :ghpull:`14260`: Remove old OSX FAQ page -* :ghpull:`14249`: Backport PR #14243 on branch v3.1.x (Update docstring of makeMappingArray) -* :ghpull:`14250`: Backport PR #14149 on branch v3.1.x -* :ghpull:`14252`: Backport PR #14248 on branch v3.1.x (Fix TextBox not respecting eventson) -* :ghpull:`14253`: Backport PR #13596 on branch v3.1.x (Normalize properties passed to bxp().) -* :ghpull:`14251`: Backport PR #14241 on branch v3.1.x (Fix linear segmented colormap with one element) -* :ghpull:`13596`: Normalize properties passed to bxp(). -* :ghpull:`14248`: Fix TextBox not respecting eventson -* :ghpull:`14241`: Fix linear segmented colormap with one element -* :ghpull:`14243`: Update docstring of makeMappingArray -* :ghpull:`14238`: Backport PR #14164 on branch v3.1.x (Fix regexp for dvipng version detection) -* :ghpull:`14149`: Avoid using ``axis([xlo, xhi, ylo, yhi])`` in examples. -* :ghpull:`14164`: Fix regexp for dvipng version detection -* :ghpull:`13739`: Fix pressing tab breaks keymap in CanvasTk +* :ghpull:`15664`: Backport PR #15649 on branch v3.1.x (Fix searchindex.js loading when ajax fails (because e.g. CORS in embedded iframes)) +* :ghpull:`15722`: Backport PR #15718 on branch v3.1.x (Update donation link) +* :ghpull:`15667`: Backport PR #15654 on branch v3.1.x (Fix some broken links.) +* :ghpull:`15658`: Backport PR #15647 on branch v3.1.x (Update some links) +* :ghpull:`15582`: Backport PR #15512 on branch v3.1.x +* :ghpull:`15512`: FIX: do not consider webagg and nbagg "interactive" for fallback +* :ghpull:`15558`: Backport PR #15553 on branch v3.1.x (DOC: add cache-buster query string to css path) +* :ghpull:`15550`: Backport PR #15528 on branch v3.1.x (Declutter home page) +* :ghpull:`15547`: Backport PR #15516 on branch v3.1.x (Add logo like font) +* :ghpull:`15511`: DOC: fix nav location +* :ghpull:`15508`: Backport PR #15489 on branch v3.1.x (DOC: adding main nav to site) +* :ghpull:`15494`: Backport PR #15486 on branch v3.1.x (Fixes an error in the documentation of Ellipse) +* :ghpull:`15486`: Fixes an error in the documentation of Ellipse +* :ghpull:`15473`: Backport PR #15464 on branch v3.1.x (Remove unused code (remainder from #15453)) +* :ghpull:`15470`: Backport PR #15460 on branch v3.1.x (Fix incorrect value check in axes_grid.) +* :ghpull:`15464`: Remove unused code (remainder from #15453) +* :ghpull:`15455`: Backport PR #15453 on branch v3.1.x (Improve example for tick locators) +* :ghpull:`15453`: Improve example for tick locators +* :ghpull:`15443`: Backport PR #15439 on branch v3.1.x (DOC: mention discourse main page) +* :ghpull:`15424`: Backport PR #15422 on branch v3.1.x (FIX: typo in attribute lookup) +* :ghpull:`15322`: Backport PR #15297 on branch v3.1.x (Document How-to figure empty) +* :ghpull:`15298`: Backport PR #15296 on branch v3.1.x (Fix typo/bug from 18cecf7) +* :ghpull:`15296`: Fix typo/bug from 18cecf7 +* :ghpull:`15278`: Backport PR #15271 on branch v3.1.x (Fix font weight validation) +* :ghpull:`15271`: Fix font weight validation +* :ghpull:`15218`: Backport PR #15217 on branch v3.1.x (Doc: Add ``plt.show()`` to horizontal bar chart example) +* :ghpull:`15207`: Backport PR #15206: FIX: be more forgiving about expecting internal s… +* :ghpull:`15198`: Backport PR #15197 on branch v3.1.x (Remove mention of now-removed basedir setup option.) +* :ghpull:`15197`: Remove mention of now-removed basedir setup option. +* :ghpull:`15189`: Backport PR #14979: FIX: Don't enable IPython integration if not ente… +* :ghpull:`15190`: Backport PR #14683: For non-html output, let sphinx pick the best format +* :ghpull:`15187`: Backport PR #15140 on branch v3.1.x +* :ghpull:`15185`: Backport PR #15168 on branch v3.1.x (MNT: explicitly cast ``np.bool_`` -> bool to prevent deprecation warning) +* :ghpull:`15168`: MNT: explicitly cast ``np.bool_`` -> bool to prevent deprecation warning +* :ghpull:`15183`: Backport PR #15181 on branch v3.1.x (FIX: proper call to zero_formats) +* :ghpull:`15181`: FIX: proper call to zero_formats +* :ghpull:`15172`: Backport PR #15166 on branch v3.1.x +* :ghpull:`15166`: FIX: indexed pandas bar +* :ghpull:`15153`: Backport PR #14456 on branch v3.1.x (PyQT5 Backend Partial Redraw Fix) +* :ghpull:`14456`: PyQT5 Backend Partial Redraw Fix +* :ghpull:`15140`: Fix ScalarFormatter formatting of masked values +* :ghpull:`15135`: Backport PR #15132 on branch v3.1.x (Update documenting guide on rcParams) +* :ghpull:`15128`: Backport PR #15115 on branch v3.1.x (Doc: highlight rcparams) +* :ghpull:`15125`: Backport PR #15110 on branch v3.1.x (Add inheritance diagram to mpl.ticker docs) +* :ghpull:`15116`: Backport PR #15114 on branch v3.1.x (DOC: update language around NF) +* :ghpull:`15058`: Backport PR #15055 on branch v3.1.x (Remove mention of now-removed feature in docstring.) +* :ghpull:`15055`: Remove mention of now-removed feature in docstring. +* :ghpull:`15047`: Backport PR #14919 on branch v3.1.x (FIX constrained_layout w/ hidden axes) +* :ghpull:`14919`: FIX constrained_layout w/ hidden axes +* :ghpull:`15022`: Backport PR #15020 on branch v3.1.x (Let connectionpatch be drawn on figure level) +* :ghpull:`15020`: Let connectionpatch be drawn on figure level +* :ghpull:`15017`: Backport PR #15007 on branch v3.1.x (FIX: support pandas 0.25) +* :ghpull:`14979`: FIX: Don't enable IPython integration if not entering REPL. +* :ghpull:`14987`: Merge pull request #14915 from AWhetter/fix_14585 +* :ghpull:`14985`: Backport PR #14982 on branch v3.1.x (DOC: correct table docstring) +* :ghpull:`14982`: DOC: correct table docstring +* :ghpull:`14975`: Backport PR #14974 on branch v3.1.x (grammar) +* :ghpull:`14972`: Backport PR #14971 on branch v3.1.x (typo) +* :ghpull:`14965`: Fix typo in documentation of table +* :ghpull:`14951`: Backport PR #14934 on branch v3.1.x (DOC: update axes_demo to directly manipulate fig, ax) +* :ghpull:`14938`: Backport PR #14905 on branch v3.1.x (Gracefully handle encoding problems when querying external executables.) +* :ghpull:`14935`: Backport PR #14933 on branch v3.1.x (DOC: typo x2 costum -> custom) +* :ghpull:`14936`: Backport PR #14932 on branch v3.1.x (DOC: Update invert_example to directly manipulate axis.) +* :ghpull:`14905`: Gracefully handle encoding problems when querying external executables. +* :ghpull:`14933`: DOC: typo x2 costum -> custom +* :ghpull:`14910`: Backport PR #14901 on branch v3.1.x (Fix GH14900: numpy 1.17.0 breaks test_colors.) +* :ghpull:`14864`: Backport PR #14830 on branch v3.1.x (FIX: restore special casing of shift-enter in notebook) +* :ghpull:`14861`: Don't use pandas 0.25.0 for testing +* :ghpull:`14855`: Backport PR #14839 on branch v3.1.x +* :ghpull:`14839`: Improve docstring of Axes.hexbin +* :ghpull:`14837`: Backport PR #14757 on branch v3.1.x (Remove incorrect color/cmap docstring line in contour.py) +* :ghpull:`14836`: Backport PR #14764 on branch v3.1.x (DOC: Fixes the links in the see-also section of Axes.get_tightbbox) +* :ghpull:`14818`: Backport PR #14510 on branch v3.1.x (Improve example for fill_between) +* :ghpull:`14819`: Backport PR #14704 on branch v3.1.x (Small patches on Docs (Tutorials and FAQ)) +* :ghpull:`14820`: Backport PR #14765 on branch v3.1.x (DOC: Fix documentation location for patheffects) +* :ghpull:`14821`: Backport PR #14741 on branch v3.1.x (DOC: Update description of properties of Line2D in 'plot' documentation.) +* :ghpull:`14822`: Backport PR #14714 on branch v3.1.x (Point towards how to save output of non-interactive backends) +* :ghpull:`14823`: Backport PR #14784 on branch v3.1.x (Tiny docs/comments cleanups.) +* :ghpull:`14824`: Backport PR #14798 on branch v3.1.x (Cleanup dates.py module docstrings.) +* :ghpull:`14825`: Backport PR #14802 on branch v3.1.x (Fix some broken refs in the docs.) +* :ghpull:`14826`: Backport PR #14806 on branch v3.1.x (Remove unnecessary uses of transFigure from examples.) +* :ghpull:`14827`: Backport PR #14525 on branch v3.1.x (improve documentation of OffsetBox) +* :ghpull:`14828`: Backport PR #14548: Link to matplotlibrc of used version +* :ghpull:`14817`: Backport PR #14697 on branch v3.1.x (Fix NavigationToolbar2QT height) +* :ghpull:`14692`: Backport PR #14688 on branch v3.1.x (Revise the misleading title for subplots demo) +* :ghpull:`14816`: Backport PR #14677 on branch v3.1.x (Don't misclip axis when calling set_ticks on inverted axes.) +* :ghpull:`14815`: Backport PR #14658 on branch v3.1.x (Fix numpydoc formatting) +* :ghpull:`14813`: Backport PR #14488 on branch v3.1.x (Make sure EventCollection doesn't modify input in-place) +* :ghpull:`14806`: Remove unnecessary uses of transFigure from examples. +* :ghpull:`14802`: Fix some broken refs in the docs. +* :ghpull:`14798`: Cleanup dates.py module docstrings. +* :ghpull:`14784`: Tiny docs/comments cleanups. +* :ghpull:`14764`: DOC: Fixes the links in the see-also section of Axes.get_tightbbox +* :ghpull:`14777`: Backport PR #14775 on branch v3.1.x (DOC: Fix CircleCI builds) +* :ghpull:`14769`: Backport PR #14759 on branch v3.1.x (DOC: note about having to rebuild after switching to local freetype) +* :ghpull:`14714`: Point towards how to save output of non-interactive backends +* :ghpull:`14741`: DOC: Update description of properties of Line2D in 'plot' documentation. +* :ghpull:`14771`: Backport PR #14760 on branch v3.1.x (DOC: minor CoC wording change) +* :ghpull:`14765`: DOC: Fix documentation location for patheffects +* :ghpull:`14735`: Backport PR #14734 on branch v3.1.x (Add geoplot to third-party example libraries page.) +* :ghpull:`14711`: Backport PR #14706 on branch v3.1.x (Mention gr backend in docs.) +* :ghpull:`14704`: Small patches on Docs (Tutorials and FAQ) +* :ghpull:`14700`: Backport PR #14698 on branch v3.1.x (Make property name be consistent with rc parameter.) +* :ghpull:`14510`: Improve example for fill_between +* :ghpull:`14683`: For non-html output, let sphinx pick the best format. +* :ghpull:`14697`: Fix NavigationToolbar2QT height +* :ghpull:`14677`: Don't misclip axis when calling set_ticks on inverted axes. +* :ghpull:`14658`: Fix numpydoc formatting +* :ghpull:`14488`: Make sure EventCollection doesn't modify input in-place +* :ghpull:`14570`: Remove print statements +* :ghpull:`14525`: improve documentation of OffsetBox +* :ghpull:`14548`: Link to matplotlibrc of used version +* :ghpull:`14395`: MAINT: work around non-zero exit status of "pdftops -v" command. -Issues (30): +Issues (28): -* :ghissue:`14620`: Plotting on a log/logit scale overwrites axis inverting -* :ghissue:`14615`: Inverting an axis using its limits does not work for log scale -* :ghissue:`14577`: Calling invert_yaxis() on a 3D plot has either no effect or removes ticks -* :ghissue:`14602`: NavigationToolbar2Tk save_figure function bug -* :ghissue:`1219`: Show fails on figures created with the object-oriented system -* :ghissue:`10167`: Segmentation fault with tricontour -* :ghissue:`13723`: RuntimeError when saving PDFs via parallel processes (not threads!) -* :ghissue:`14315`: Improvement: Better error message if kiwisolver fails to import -* :ghissue:`14356`: matplotlib.units.ConversionError on scatter of dates with a NaN in the first position -* :ghissue:`14467`: Docs for plt.ginput() have the wrong default value for show_clicks keyword argument. -* :ghissue:`14225`: Matplotlib crashes on windows while maximizing plot window when using Multicursor -* :ghissue:`14458`: DOC: small inconsistency in errobar docstring -* :ghissue:`14372`: Document that view_init() arguments should be in degrees -* :ghissue:`12201`: issues clearing rubberband on nbagg at non-default browser zoom -* :ghissue:`13576`: pcolorfast misbehaves when changing axis limits -* :ghissue:`14303`: Unable to import matplotlib on Windows 10 v1903 -* :ghissue:`14283`: RendererSVG CSS 'white-space' property conflicts with default HTML CSS -* :ghissue:`14293`: imshow() producing "inverted" colors since 3.0.3 -* :ghissue:`14322`: Cannot import matplotlib with Python 3.7.x on Win10Pro -* :ghissue:`14137`: Qt5 test auto-skip is not working correctly -* :ghissue:`14301`: scatter() fails on nan-containing input when providing edgecolor -* :ghissue:`14318`: Don't try to set missing history buttons. -* :ghissue:`14265`: symlog looses some points since 3.1.0 (example given) -* :ghissue:`14274`: BUG: plotting with Numpy array subclasses is slow with Matplotlib 3.1.0 (regression) -* :ghissue:`14263`: import pyplot issue - -* :ghissue:`14227`: Update "working with Mpl on OSX" docs -* :ghissue:`13448`: boxplot doesn't normalize properties before applying them -* :ghissue:`14226`: Modify matplotlib TextBox value without triggering callback -* :ghissue:`14232`: LinearSegmentedColormap with N=1 gives confusing error message -* :ghissue:`10365`: Scatter plot with non-sequence ´c´ color should give a better Error message. +* :ghissue:`15295`: Can't install matplotlib with pip for Python 3.8b4 +* :ghissue:`15714`: Publish 3.8 wheels +* :ghissue:`15706`: Python 3.8 - Installation error: TypeError: stat: path should be string, bytes, os.PathLike or integer, not NoneType +* :ghissue:`15690`: Should xlim support single-entry arrays? +* :ghissue:`15608`: imshow rendering changed from 3.1.0 to 3.1.1 +* :ghissue:`14903`: 'MPLBACKEND=webagg' is overwritten by agg when $DISPLAY is not set on Linux +* :ghissue:`15351`: Bar width expands between subsequent bars +* :ghissue:`15240`: Can't specify integer ``font.weight`` in custom style sheet any more +* :ghissue:`15255`: ``imshow`` in ``v3.1.1``: y-axis chopped-off +* :ghissue:`15186`: 3D quiver plot fails when pivot = "middle" +* :ghissue:`14160`: PySide2/PyQt5: Graphics issues in QScrollArea for OSX +* :ghissue:`15178`: mdates.ConciseDateFormatter() doesn't work with zero_formats parameter +* :ghissue:`15179`: Patch 3.1.1 broke imshow() heatmaps: Tiles cut off on y-axis +* :ghissue:`15162`: axes.bar fails when x is int-indexed pandas.Series +* :ghissue:`15103`: Colorbar for imshow messes interactive cursor with masked data +* :ghissue:`8744`: ConnectionPatch hidden by plots +* :ghissue:`14950`: plt.ioff() not supressing figure generation +* :ghissue:`14959`: Typo in Docs +* :ghissue:`14902`: from matplotlib import animation UnicodeDecodeError +* :ghissue:`14897`: New yticks behavior in 3.1.1 vs 3.1.0 +* :ghissue:`14811`: How to save hexbin binned data in a text file. +* :ghissue:`14551`: Non functional API links break docs builds downstream +* :ghissue:`14720`: Line2D properties should state units +* :ghissue:`10891`: Toolbar icons too large in PyQt5 (Qt5Agg backend) +* :ghissue:`14675`: Heatmaps are being truncated when using with seaborn +* :ghissue:`14487`: eventplot sorts np.array positions, but not list positions +* :ghissue:`14547`: Changing mplstyle: axes.titlelocation causes Bad Key error +* :ghissue:`10410`: eventplot alters data in some cases diff --git a/doc/users/prev_whats_new/github_stats_3.1.3.rst b/doc/users/prev_whats_new/github_stats_3.1.3.rst new file mode 100644 index 000000000000..b4706569df02 --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.1.3.rst @@ -0,0 +1,87 @@ +.. _github-stats-3-1-3: + +GitHub statistics for 3.1.3 (Feb 03, 2020) +========================================== + +GitHub statistics for 2019/11/05 (tag: v3.1.2) - 2020/02/03 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 7 issues and merged 45 pull requests. +The full list can be seen `on GitHub `__ + +The following 13 authors contributed 125 commits. + +* Antony Lee +* David Stansby +* Elliott Sales de Andrade +* hannah +* Jody Klymak +* MeeseeksMachine +* Nelle Varoquaux +* Nikita Kniazev +* Paul Ivanov +* SamSchott +* Steven G. Johnson +* Thomas A Caswell +* Tim Hoffmann + +GitHub issues and pull requests: + +Pull Requests (45): + +* :ghpull:`16382`: Backport PR #16379 on branch v3.1.x (FIX: catch on message content, not module) +* :ghpull:`16362`: Backport PR #16347: FIX: catch warnings from pandas in cbook._check_1d +* :ghpull:`16356`: Backport PR #16330 on branch v3.1.x (Clearer signal handling) +* :ghpull:`16330`: Clearer signal handling +* :ghpull:`16348`: Backport PR #16255 on branch v3.1.x (Move version info to sidebar) +* :ghpull:`16345`: Backport PR #16298 on branch v3.1.x (Don't recursively call draw_idle when updating artists at draw time.) +* :ghpull:`16298`: Don't recursively call draw_idle when updating artists at draw time. +* :ghpull:`16322`: Backport PR #16250: Fix zerolen intersect +* :ghpull:`16320`: Backport PR #16311 on branch v3.1.x (don't override non-Python signal handlers) +* :ghpull:`16311`: don't override non-Python signal handlers +* :ghpull:`16250`: Fix zerolen intersect +* :ghpull:`16237`: Backport PR #16235 on branch v3.1.x (FIX: AttributeError in TimerBase.start) +* :ghpull:`16235`: FIX: AttributeError in TimerBase.start +* :ghpull:`16208`: Backport PR #15556 on branch v3.1.x (Fix test suite compat with ghostscript 9.50.) +* :ghpull:`16213`: Backport PR #15763 on branch v3.1.x (Skip webagg test if tornado is not available.) +* :ghpull:`16167`: Backport PR #16166 on branch v3.1.x (Add badge for citing 3.1.2) +* :ghpull:`16166`: Add badge for citing 3.1.2 +* :ghpull:`16144`: Backport PR #16053 on branch v3.1.x (Fix v_interval setter) +* :ghpull:`16053`: Fix v_interval setter +* :ghpull:`16136`: Backport PR #16112 on branch v3.1.x (CI: Fail when failed to install dependencies) +* :ghpull:`16131`: Backport PR #16126 on branch v3.1.x (TST: test_fork: Missing join) +* :ghpull:`16126`: TST: test_fork: Missing join +* :ghpull:`16091`: Backport PR #16086 on branch v3.1.x (FIX: use supported attribute to check pillow version) +* :ghpull:`16040`: Backport PR #16031 on branch v3.1.x (Fix docstring of hillshade().) +* :ghpull:`16032`: Backport PR #16028 on branch v3.1.x (Prevent FigureCanvasQT_draw_idle recursively calling itself.) +* :ghpull:`16028`: Prevent FigureCanvasQT_draw_idle recursively calling itself. +* :ghpull:`16020`: Backport PR #16007 on branch v3.1.x (Fix search on nested pages) +* :ghpull:`16018`: Backport PR #15735 on branch v3.1.x (Cleanup some mplot3d docstrings.) +* :ghpull:`16007`: Fix search on nested pages +* :ghpull:`15957`: Backport PR #15953 on branch v3.1.x (Update donation link) +* :ghpull:`15763`: Skip webagg test if tornado is not available. +* :ghpull:`15881`: Backport PR #15859 on branch v3.1.x (Doc: Move search field into nav bar) +* :ghpull:`15863`: Backport PR #15244 on branch v3.1.x: Change documentation format of rcParams defaults +* :ghpull:`15859`: Doc: Move search field into nav bar +* :ghpull:`15860`: Backport PR #15851 on branch v3.1.x (ffmpeg is available on default ubuntu packages now) +* :ghpull:`15851`: ffmpeg is available on default ubuntu packages now. +* :ghpull:`15843`: Backport PR #15737 on branch v3.1.x (Fix env override in WebAgg backend test.) +* :ghpull:`15760`: Backport PR #15752 on branch v3.1.x (Update boxplot/violinplot faq.) +* :ghpull:`15757`: Backport PR #15751 on branch v3.1.x (Modernize FAQ entry for plt.show().) +* :ghpull:`15735`: Cleanup some mplot3d docstrings. +* :ghpull:`15753`: Backport PR #15661 on branch v3.1.x (Document scope of 3D scatter depthshading.) +* :ghpull:`15741`: Backport PR #15729 on branch v3.1.x (Catch correct parse errror type for dateutil >= 2.8.1) +* :ghpull:`15729`: Catch correct parse errror type for dateutil >= 2.8.1 +* :ghpull:`15737`: Fix env override in WebAgg backend test. +* :ghpull:`15244`: Change documentation format of rcParams defaults + +Issues (7): + +* :ghissue:`16294`: BUG: Interactive mode slow +* :ghissue:`15842`: Path.intersects_path returns True when it shouldn't +* :ghissue:`16163`: libpng error: Read Error when using matplotlib after setting usetex=True +* :ghissue:`15960`: v3.1.2 - test suite "frozen" after it finishes +* :ghissue:`16083`: Pillow 7.0.0 Support +* :ghissue:`15481`: Recursion error +* :ghissue:`15717`: Move search field into nav bar diff --git a/doc/users/prev_whats_new/github_stats_3.2.0.rst b/doc/users/prev_whats_new/github_stats_3.2.0.rst index f5cee3ad245c..5fd75f7c57d0 100644 --- a/doc/users/prev_whats_new/github_stats_3.2.0.rst +++ b/doc/users/prev_whats_new/github_stats_3.2.0.rst @@ -1,9 +1,9 @@ .. _github-stats-3-2-0: -GitHub Stats for Matplotlib 3.2.0 -================================= +GitHub statistics for 3.2.0 (Mar 04, 2020) +========================================== -GitHub stats for 2019/05/18 - 2020/03/03 (tag: v3.1.0) +GitHub statistics for 2019/05/18 (tag: v3.1.0) - 2020/03/04 These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.2.1.rst b/doc/users/prev_whats_new/github_stats_3.2.1.rst index ec95cf4a7887..4f865dbb5429 100644 --- a/doc/users/prev_whats_new/github_stats_3.2.1.rst +++ b/doc/users/prev_whats_new/github_stats_3.2.1.rst @@ -1,9 +1,9 @@ .. _github-stats-3-2-1: -GitHub Stats for Matplotlib 3.2.1 -================================= +GitHub statistics for 3.2.1 (Mar 18, 2020) +========================================== -GitHub stats for 2020/03/03 - 2020/03/17 (tag: v3.2.0) +GitHub statistics for 2020/03/03 (tag: v3.2.0) - 2020/03/18 These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.2.2.rst b/doc/users/prev_whats_new/github_stats_3.2.2.rst index 8cd4e4eef1d7..9026d518ce4d 100644 --- a/doc/users/prev_whats_new/github_stats_3.2.2.rst +++ b/doc/users/prev_whats_new/github_stats_3.2.2.rst @@ -1,9 +1,9 @@ .. _github-stats-3-2-2: -GitHub Stats for Matplotlib 3.2.2 -================================= +GitHub statistics for 3.2.2 (Jun 17, 2020) +========================================== -GitHub stats for 2020/03/18 - 2020/06/17 (tag: v3.2.1) +GitHub statistics for 2020/03/18 (tag: v3.2.1) - 2020/06/17 These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.3.0.rst b/doc/users/prev_whats_new/github_stats_3.3.0.rst index c57d4cda8cba..c2e6cd132c2d 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.0.rst +++ b/doc/users/prev_whats_new/github_stats_3.3.0.rst @@ -1,9 +1,9 @@ .. _github-stats-3-3-0: -GitHub Stats for Matplotlib 3.3.0 -================================= +GitHub statistics for 3.3.0 (Jul 16, 2020) +========================================== -GitHub stats for 2020/03/03 - 2020/07/16 (tag: v3.2.0) +GitHub statistics for 2020/03/03 (tag: v3.2.0) - 2020/07/16 These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.3.1.rst b/doc/users/prev_whats_new/github_stats_3.3.1.rst index ed6638638959..49212587a17a 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.1.rst +++ b/doc/users/prev_whats_new/github_stats_3.3.1.rst @@ -1,9 +1,9 @@ .. _github-stats-3-3-1: -GitHub Stats for Matplotlib 3.3.1 -================================= +GitHub statistics for 3.3.1 (Aug 13, 2020) +========================================== -GitHub stats for 2020/07/16 - 2020/08/13 (tag: v3.3.0) +GitHub statistics for 2020/07/16 (tag: v3.3.0) - 2020/08/13 These lists are automatically generated, and may be incomplete or contain duplicates. 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 index 8f9bb9a6eceb..0bc03cbc83ee 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.2.rst +++ b/doc/users/prev_whats_new/github_stats_3.3.2.rst @@ -1,9 +1,9 @@ .. _github-stats-3-3-2: -GitHub Stats for Matplotlib 3.3.2 -================================= +GitHub statistics for 3.3.2 (Sep 15, 2020) +========================================== -GitHub stats for 2020/08/14 - 2020/09/15 (tag: v3.3.1) +GitHub statistics for 2020/08/14 - 2020/09/15 (tag: v3.3.1) These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.3.3.rst b/doc/users/prev_whats_new/github_stats_3.3.3.rst index d7ba592d7339..5475a5209eed 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.3.rst +++ b/doc/users/prev_whats_new/github_stats_3.3.3.rst @@ -1,9 +1,9 @@ .. _github-stats-3-3-3: -GitHub Stats for Matplotlib 3.3.3 -================================= +GitHub statistics for 3.3.3 (Nov 11, 2020) +========================================== -GitHub stats for 2020/09/15 - 2020/11/11 (tag: v3.3.2) +GitHub statistics for 2020/09/15 (tag: v3.3.2) - 2020/11/11 These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.3.4.rst b/doc/users/prev_whats_new/github_stats_3.3.4.rst index e61309836034..afff8b384b8e 100644 --- a/doc/users/prev_whats_new/github_stats_3.3.4.rst +++ b/doc/users/prev_whats_new/github_stats_3.3.4.rst @@ -1,9 +1,9 @@ .. _github-stats-3-3-4: -GitHub Stats for Matplotlib 3.3.4 -================================= +GitHub statistics for 3.3.4 (Jan 28, 2021) +========================================== -GitHub stats for 2020/11/12 - 2021/01/28 (tag: v3.3.3) +GitHub statistics for 2020/11/12 (tag: v3.3.3) - 2021/01/28 These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.4.0.rst b/doc/users/prev_whats_new/github_stats_3.4.0.rst index a9c62990714d..fe49e673a660 100644 --- a/doc/users/prev_whats_new/github_stats_3.4.0.rst +++ b/doc/users/prev_whats_new/github_stats_3.4.0.rst @@ -1,9 +1,9 @@ .. _github-stats-3-4-0: -GitHub Stats for Matplotlib 3.4.0 -================================= +GitHub statistics for 3.4.0 (Mar 26, 2021) +========================================== -GitHub stats for 2020/07/16 - 2021/03/25 (tag: v3.3.0) +GitHub statistics for 2020/07/16 (tag: v3.3.0) - 2021/03/26 These lists are automatically generated, and may be incomplete or contain duplicates. @@ -244,9 +244,9 @@ Pull Requests (772): * :ghpull:`19611`: Fix double picks. * :ghpull:`19640`: Backport PR #19639 on branch v3.4.x (FIX: do not allow single element list of str in subplot_mosaic) * :ghpull:`19639`: FIX: do not allow single element list of str in subplot_mosaic -* :ghpull:`19638`: Backport PR #19632 on branch v3.4.x (Fix handling of warn keyword in in Figure.show.) +* :ghpull:`19638`: Backport PR #19632 on branch v3.4.x (Fix handling of warn keyword in Figure.show.) * :ghpull:`19637`: Backport PR #19582 on branch v3.4.x (Add kerning to single-byte strings in PDFs) -* :ghpull:`19632`: Fix handling of warn keyword in in Figure.show. +* :ghpull:`19632`: Fix handling of warn keyword in Figure.show. * :ghpull:`19582`: Add kerning to single-byte strings in PDFs * :ghpull:`19629`: Backport PR #19548 on branch v3.4.x (Increase tolerances for other arches.) * :ghpull:`19630`: Backport PR #19596 on branch v3.4.x (Fix for issue 17769: wx interactive figure close cause crash) @@ -1016,7 +1016,7 @@ Issues (204): * :ghissue:`18648`: Drop support for directly imread()ing urls. * :ghissue:`19366`: Current CI doc builds fail * :ghissue:`19372`: matplotlib.axes.Axes.indicate_inset default label value is incompatible with LaTeX -* :ghissue:`17100`: Is it a better solution to acess one of the spines by class atrribute? +* :ghissue:`17100`: Is it a better solution to access one of the spines by class attribute? * :ghissue:`17375`: Proposal: add_subfigs.... * :ghissue:`19339`: constrained_layout + fixed-aspect axes + bbox_inches="tight" * :ghissue:`19308`: Reduce whitespace in Choosing Colormaps tutorial plots diff --git a/doc/users/prev_whats_new/github_stats_3.4.1.rst b/doc/users/prev_whats_new/github_stats_3.4.1.rst index 220ed67a489a..0819a6850a3e 100644 --- a/doc/users/prev_whats_new/github_stats_3.4.1.rst +++ b/doc/users/prev_whats_new/github_stats_3.4.1.rst @@ -1,9 +1,9 @@ .. _github-stats-3-4-1: -GitHub Stats for Matplotlib 3.4.1 -================================= +GitHub statistics for 3.4.1 (Mar 31, 2021) +========================================== -GitHub stats for 2021/03/26 - 2021/03/31 (tag: v3.4.0) +GitHub statistics for 2021/03/26 (tag: v3.4.0) - 2021/03/31 These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.4.2.rst b/doc/users/prev_whats_new/github_stats_3.4.2.rst index badd0a589785..22b4797c2fc2 100644 --- a/doc/users/prev_whats_new/github_stats_3.4.2.rst +++ b/doc/users/prev_whats_new/github_stats_3.4.2.rst @@ -1,9 +1,9 @@ .. _github-stats-3-4-2: -GitHub Stats for Matplotlib 3.4.2 -================================= +GitHub statistics for 3.4.2 (May 08, 2021) +========================================== -GitHub stats for 2021/03/31 - 2021/05/07 (tag: v3.4.1) +GitHub statistics for 2021/03/31 (tag: v3.4.1) - 2021/05/08 These lists are automatically generated, and may be incomplete or contain duplicates. diff --git a/doc/users/prev_whats_new/github_stats_3.4.3.rst b/doc/users/prev_whats_new/github_stats_3.4.3.rst new file mode 100644 index 000000000000..b248bf69b6ef --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.4.3.rst @@ -0,0 +1,133 @@ +.. _github-stats-3-4-3: + +GitHub statistics for 3.4.3 (August 21, 2021) +============================================= + +GitHub statistics for 2021/05/08 - 2021/08/12 (tag: v3.4.2) + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 22 issues and merged 69 pull requests. +The full list can be seen `on GitHub `__ + +The following 20 authors contributed 95 commits. + +* Antony Lee +* David Stansby +* Diego +* Diego Leal Petrola +* Diego Petrola +* Elliott Sales de Andrade +* Eric Firing +* Frank Sauerburger +* Greg Lucas +* Ian Hunt-Isaak +* Jash Shah +* Jody Klymak +* Jouni K. Seppänen +* MichaÅ‚ Górny +* sandipanpanda +* Slava Ostroukh +* Thomas A Caswell +* Tim Hoffmann +* Viacheslav Ostroukh +* Xianxiang Li + +GitHub issues and pull requests: + +Pull Requests (69): + +* :ghpull:`20830`: Backport PR #20826 on branch v3.4.x (Fix clear of Axes that are shared.) +* :ghpull:`20826`: Fix clear of Axes that are shared. +* :ghpull:`20823`: Backport PR #20817 on branch v3.4.x (Make test_change_epoch more robust.) +* :ghpull:`20817`: Make test_change_epoch more robust. +* :ghpull:`20820`: Backport PR #20771 on branch v3.4.x (FIX: tickspacing for subfigures) +* :ghpull:`20771`: FIX: tickspacing for subfigures +* :ghpull:`20777`: FIX: dpi and scatter for subfigures now correct +* :ghpull:`20787`: Backport PR #20786 on branch v3.4.x (Fixed typo in _constrained_layout.py (#20782)) +* :ghpull:`20786`: Fixed typo in _constrained_layout.py (#20782) +* :ghpull:`20763`: Backport PR #20761 on branch v3.4.x (Fix suplabel autopos) +* :ghpull:`20761`: Fix suplabel autopos +* :ghpull:`20751`: Backport PR #20748 on branch v3.4.x (Ensure _static directory exists before copying CSS.) +* :ghpull:`20748`: Ensure _static directory exists before copying CSS. +* :ghpull:`20713`: Backport PR #20710 on branch v3.4.x (Fix tests with Inkscape 1.1.) +* :ghpull:`20687`: Enable PyPy wheels for v3.4.x +* :ghpull:`20710`: Fix tests with Inkscape 1.1. +* :ghpull:`20696`: Backport PR #20662 on branch v3.4.x (Don't forget to disable autoscaling after interactive zoom.) +* :ghpull:`20662`: Don't forget to disable autoscaling after interactive zoom. +* :ghpull:`20683`: Backport PR #20645 on branch v3.4.x (Fix leak if affine_transform is passed invalid vertices.) +* :ghpull:`20645`: Fix leak if affine_transform is passed invalid vertices. +* :ghpull:`20642`: Backport PR #20629 on branch v3.4.x (Add protection against out-of-bounds read in ttconv) +* :ghpull:`20643`: Backport PR #20597 on branch v3.4.x +* :ghpull:`20629`: Add protection against out-of-bounds read in ttconv +* :ghpull:`20597`: Fix TTF headers for type 42 stix font +* :ghpull:`20624`: Backport PR #20609 on branch v3.4.x (FIX: fix figbox deprecation) +* :ghpull:`20609`: FIX: fix figbox deprecation +* :ghpull:`20594`: Backport PR #20590 on branch v3.4.x (Fix class docstrings for Norms created from Scales.) +* :ghpull:`20590`: Fix class docstrings for Norms created from Scales. +* :ghpull:`20587`: Backport PR #20584: FIX: do not simplify path in LineCollection.get_s… +* :ghpull:`20584`: FIX: do not simplify path in LineCollection.get_segments +* :ghpull:`20578`: Backport PR #20511 on branch v3.4.x (Fix calls to np.ma.masked_where) +* :ghpull:`20511`: Fix calls to np.ma.masked_where +* :ghpull:`20568`: Backport PR #20565 on branch v3.4.x (FIX: PILLOW asarray bug) +* :ghpull:`20566`: Backout pillow=8.3.0 due to a crash +* :ghpull:`20565`: FIX: PILLOW asarray bug +* :ghpull:`20503`: Backport PR #20488 on branch v3.4.x (FIX: Include 0 when checking lognorm vmin) +* :ghpull:`20488`: FIX: Include 0 when checking lognorm vmin +* :ghpull:`20483`: Backport PR #20480 on branch v3.4.x (Fix str of empty polygon.) +* :ghpull:`20480`: Fix str of empty polygon. +* :ghpull:`20478`: Backport PR #20473 on branch v3.4.x (_GSConverter: handle stray 'GS' in output gracefully) +* :ghpull:`20473`: _GSConverter: handle stray 'GS' in output gracefully +* :ghpull:`20456`: Backport PR #20453 on branch v3.4.x (Remove ``Tick.apply_tickdir`` from 3.4 deprecations.) +* :ghpull:`20441`: Backport PR #20416 on branch v3.4.x (Fix missing Patch3DCollection._z_markers_idx) +* :ghpull:`20416`: Fix missing Patch3DCollection._z_markers_idx +* :ghpull:`20417`: Backport PR #20395 on branch v3.4.x (Pathing issue) +* :ghpull:`20395`: Pathing issue +* :ghpull:`20404`: Backport PR #20403: FIX: if we have already subclassed mixin class ju… +* :ghpull:`20403`: FIX: if we have already subclassed mixin class just return +* :ghpull:`20383`: Backport PR #20381 on branch v3.4.x (Prevent corrections and completions in search field) +* :ghpull:`20307`: Backport PR #20154 on branch v3.4.x (ci: Bump Ubuntu to 18.04 LTS.) +* :ghpull:`20285`: Backport PR #20275 on branch v3.4.x (Fix some examples that are skipped in docs build) +* :ghpull:`20275`: Fix some examples that are skipped in docs build +* :ghpull:`20267`: Backport PR #20265 on branch v3.4.x (Legend edgecolor face) +* :ghpull:`20265`: Legend edgecolor face +* :ghpull:`20260`: Fix legend edgecolor face +* :ghpull:`20259`: Backport PR #20248 on branch v3.4.x (Replace pgf image-streaming warning by error.) +* :ghpull:`20248`: Replace pgf image-streaming warning by error. +* :ghpull:`20241`: Backport PR #20212 on branch v3.4.x (Update span_selector.py) +* :ghpull:`20212`: Update span_selector.py +* :ghpull:`19980`: Tidy up deprecation messages in ``_subplots.py`` +* :ghpull:`20234`: Backport PR #20225 on branch v3.4.x (FIX: correctly handle ax.legend(..., legendcolor='none')) +* :ghpull:`20225`: FIX: correctly handle ax.legend(..., legendcolor='none') +* :ghpull:`20232`: Backport PR #19636 on branch v3.4.x (Correctly check inaxes for multicursor) +* :ghpull:`20228`: Backport PR #19849 on branch v3.4.x (FIX DateFormatter for month names when usetex=True) +* :ghpull:`19849`: FIX DateFormatter for month names when usetex=True +* :ghpull:`20154`: ci: Bump Ubuntu to 18.04 LTS. +* :ghpull:`20186`: Backport PR #19975 on branch v3.4.x (CI: remove workflow to push commits to macpython/matplotlib-wheels) +* :ghpull:`19975`: CI: remove workflow to push commits to macpython/matplotlib-wheels +* :ghpull:`19636`: Correctly check inaxes for multicursor + +Issues (22): + +* :ghissue:`20219`: Regression: undocumented change of behaviour in mpl 3.4.2 with axis ticks direction +* :ghissue:`20721`: ax.clear() adds extra ticks, un-hides shared-axis tick labels +* :ghissue:`20765`: savefig re-scales xticks and labels of some (but not all) subplots +* :ghissue:`20782`: [Bug]: _supylabel get_in_layout() typo? +* :ghissue:`20747`: [Bug]: _copy_css_file assumes that the _static directory already exists +* :ghissue:`20617`: tests fail with new inkscape +* :ghissue:`20519`: Toolbar zoom doesn't change autoscale status for versions 3.2.0 and above +* :ghissue:`20628`: Out-of-bounds read leads to crash or broken TrueType fonts +* :ghissue:`20612`: Broken EPS for Type 42 STIX +* :ghissue:`19982`: regression for 3.4.x - ax.figbox replacement incompatible to all version including 3.3.4 +* :ghissue:`19938`: unuseful deprecation warning figbox +* :ghissue:`16400`: Inconsistent behavior between Normalizers when input is Dataframe +* :ghissue:`20583`: Lost class descriptions since 3.4 docs +* :ghissue:`20551`: set_segments(get_segments()) makes lines coarse +* :ghissue:`20560`: test_png is failing +* :ghissue:`20487`: test_huge_range_log is failing... +* :ghissue:`20472`: test_backend_pgf.py::test_xelatex[pdf] - ValueError: invalid literal for int() with base 10: b'ate missing from Resources. [...] +* :ghissue:`20328`: Path.intersects_path sometimes returns incorrect values +* :ghissue:`20258`: Using edgecolors='face' with stackplot causes value error when using plt.legend() +* :ghissue:`20200`: examples/widgets/span_selector.py is brittle +* :ghissue:`20231`: MultiCursor bug +* :ghissue:`19836`: Month names not set as text when using usetex diff --git a/doc/users/prev_whats_new/github_stats_3.5.0.rst b/doc/users/prev_whats_new/github_stats_3.5.0.rst new file mode 100644 index 000000000000..70d599f10c5d --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.5.0.rst @@ -0,0 +1,1292 @@ +.. _github-stats-3-5-0: + +GitHub statistics for 3.5.0 (Nov 15, 2021) +========================================== + +GitHub statistics for 2021/03/26 (tag: v3.4.0) - 2021/11/15 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 187 issues and merged 939 pull requests. +The full list can be seen `on GitHub `__ + +The following 144 authors contributed 3406 commits. + +* Aaron Rogers +* Abhinav Sagar +* Adrian Price-Whelan +* Adrien F. Vincent +* ain-soph +* Aitik Gupta +* Akiomi Kamakura +* AkM-2018 +* Andrea PIERRÉ +* andthum +* Antony Lee +* Antti Soininen +* apodemus +* astromancer +* Bruno Beltran +* Carlos Cerqueira +* Casper da Costa-Luis +* ceelo777 +* Christian Baumann +* dan +* Dan Zimmerman +* David Matos +* David Poznik +* David Stansby +* dependabot[bot] +* Diego Leal Petrola +* Dmitriy Fishman +* Ellert van der Velden +* Elliott Sales de Andrade +* Engjell Avdiu +* Eric Firing +* Eric Larson +* Eric Prestat +* Ewan Sutherland +* Felix Nößler +* Fernando +* fourpoints +* Frank Sauerburger +* Gleb Fedorov +* Greg Lucas +* hannah +* Hannes Breytenbach +* Hans Meine +* Harshal Prakash Patankar +* harupy +* Harutaka Kawamura +* Hildo Guillardi Júnior +* Holtz Yan +* Hood +* Ian Hunt-Isaak +* Ian Thomas +* ianhi +* Illviljan +* ImportanceOfBeingErnest +* Isha Mehta +* iury simoes-sousa +* Jake Bowhay +* Jakub Klus +* Jan-Hendrik Müller +* Janakarajan Natarajan +* Jann Paul Mattern +* Jash Shah +* Jay Joshi +* jayjoshi112711 +* jeffreypaul15 +* Jerome F. Villegas +* Jerome Villegas +* Jesus Briales +* Jody Klymak +* Jonathan Yong +* Joschua Conrad +* Joschua-Conrad +* Jouni K. Seppänen +* K-Monty +* katrielester +* kdpenner +* Kent +* Kent Gauen +* kentcr +* kir0ul +* kislovskiy +* KIU Shueng Chuan +* KM Goh +* Konstantin Popov +* kyrogon +* Leeh Peter +* Leo Singer +* lgfunderburk +* Liam Toney +* luz paz +* luzpaz +* Madhav Humagain +* MalikIdreesHasa +* Marat Kopytjuk +* Marco Rigobello +* Marco Salathe +* Markus Wesslén +* martinRenou +* Matthias Bussonnier +* MeeseeksMachine +* MichaÅ‚ Górny +* Mihai Anton +* Navid C. Constantinou +* Nico Schlömer +* Phil Nagel +* Philip Schiff +* Philipp Nagel +* pwohlhart +* Péter Leéh +* Quentin Peter +* Ren Pang +* rgbmrc +* Richard Barnes +* richardsheridan +* Rike-Benjamin Schuppner +* Roberto Toro +* Ruth Comer +* ryahern +* Ryan May +* Sam Van Kooten +* sandipanpanda +* Simon Hoxbro +* Slava Ostroukh +* Stefan Appelhoff +* Stefanie Molin +* takimata +* tdpetrou +* theOehrly +* Thomas A Caswell +* Tim Hoffmann +* tohc1 +* Tom Charrett +* Tom Neep +* Tomas Hrnciar +* Tortar +* Tranquilled +* Vagrant Cascadian +* Viacheslav Ostroukh +* Vishnu V K +* Xianxiang Li +* Yannic Schroeder +* Yo Yehudi +* Zexi +* znstrider + +GitHub issues and pull requests: + +Pull Requests (939): + +* :ghpull:`21645`: Backport PR #21628 on branch v3.5.x (Fix METH_VARARGS method signatures ) +* :ghpull:`21644`: Backport PR #21640 on branch v3.5.x (DOC: remove sample_plots from tutorials) +* :ghpull:`21628`: Fix METH_VARARGS method signatures +* :ghpull:`21640`: DOC: remove sample_plots from tutorials +* :ghpull:`21636`: Backport PR #21604 on branch v3.5.x (Fix centre square rectangle selector part 1) +* :ghpull:`21604`: Fix centre square rectangle selector part 1 +* :ghpull:`21633`: Backport PR #21501 on branch v3.5.x (Refix for pyparsing compat.) +* :ghpull:`21606`: BLD: limit support of pyparsing to <3 +* :ghpull:`21501`: Refix for pyparsing compat. +* :ghpull:`21624`: Backport PR #21621 on branch v3.5.x (Fix GhostScript error handling types) +* :ghpull:`21625`: Backport PR #21568 on branch v3.5.x (Enhancing support for tex and datetimes) +* :ghpull:`21568`: Enhancing support for tex and datetimes +* :ghpull:`21621`: Fix GhostScript error handling types +* :ghpull:`21623`: Backport PR #21619 on branch v3.5.x (Revert "Pin sphinx to fix sphinx-gallery") +* :ghpull:`21619`: Revert "Pin sphinx to fix sphinx-gallery" +* :ghpull:`21618`: Backport PR #21617 on branch v3.5.x (FIX: Make sure we do not over-write eps short cuts) +* :ghpull:`21622`: Backport PR #21350 on branch v3.5.x (Remove plot_gallery setting from conf.py) +* :ghpull:`21617`: FIX: Make sure we do not over-write eps short cuts +* :ghpull:`21616`: Backport PR #21613 on branch v3.5.x (SEC/DOC update supported versions) +* :ghpull:`21615`: Backport PR #21607 on branch v3.5.x (DOC: link to cheatsheets site, not github repo) +* :ghpull:`21614`: Backport PR #21609 on branch v3.5.x (Fix documentation link with renaming ``voxels`` to ``voxelarray``) +* :ghpull:`21613`: SEC/DOC update supported versions +* :ghpull:`21607`: DOC: link to cheatsheets site, not github repo +* :ghpull:`21609`: Fix documentation link with renaming ``voxels`` to ``voxelarray`` +* :ghpull:`21605`: Backport PR #21317 on branch v3.5.x (Move label hiding rectilinear-only check into _label_outer_{x,y}axis.) +* :ghpull:`21317`: Move label hiding rectilinear-only check into _label_outer_{x,y}axis. +* :ghpull:`21602`: Backport PR #21586 on branch v3.5.x (Defer enforcement of hatch validation) +* :ghpull:`21601`: Backport PR #21530 on branch v3.5.x (Fix interrupting GTK on plain Python) +* :ghpull:`21603`: Backport PR #21596 on branch v3.5.x (Pin sphinx to fix sphinx-gallery) +* :ghpull:`21586`: Defer enforcement of hatch validation +* :ghpull:`21530`: Fix interrupting GTK on plain Python +* :ghpull:`21397`: Support for pre 2.7.1 freetype savannah versions +* :ghpull:`21599`: Backport PR #21592 on branch v3.5.x ([BUG in 3.5.0rc1] - Anatomy of a Figure has the legend in the wrong spot) +* :ghpull:`21587`: Backport PR #21581 on branch v3.5.x (Fix RangeSlider.reset) +* :ghpull:`21592`: [BUG in 3.5.0rc1] - Anatomy of a Figure has the legend in the wrong spot +* :ghpull:`21596`: Pin sphinx to fix sphinx-gallery +* :ghpull:`21577`: Backport PR #21527 on branch v3.5.x (Add more 3.5 release notes) +* :ghpull:`21527`: Add more 3.5 release notes +* :ghpull:`21573`: Backport PR #21570 on branch v3.5.x (Raise correct exception out of Spines.__getattr__) +* :ghpull:`21563`: Backport PR #21559 on branch v3.5.x (Fix eventplot units) +* :ghpull:`21560`: Backport PR #21553 on branch v3.5.x (Fix check for manager presence in blocking_input.) +* :ghpull:`21561`: Backport PR #21555 on branch v3.5.x (MNT: reject more possibly unsafe strings in validate_cycler) +* :ghpull:`21555`: MNT: reject more possibly unsafe strings in validate_cycler +* :ghpull:`21553`: Fix check for manager presence in blocking_input. +* :ghpull:`21559`: Fix eventplot units +* :ghpull:`21543`: Backport PR #21443 on branch v3.5.x (FIX: re-instate ability to have position in axes) +* :ghpull:`21550`: Ignore transOffset if no offsets passed to Collection +* :ghpull:`21443`: FIX: re-instate ability to have position in axes +* :ghpull:`21531`: Backport PR #21491 on branch v3.5.x (Relocate inheritance diagram to the top of the document) +* :ghpull:`21491`: Relocate inheritance diagram to the top of the document +* :ghpull:`21504`: Backport PR #21481 on branch v3.5.x (FIX: spanning subfigures) +* :ghpull:`21481`: FIX: spanning subfigures +* :ghpull:`21483`: Backport PR #21387 on branch v3.5.x (Fix path simplification of closed loops) +* :ghpull:`21486`: Backport PR #21478 on branch v3.5.x (Fix GTK4 embedding example) +* :ghpull:`21497`: Backport PR #21484 on branch v3.5.x (Replacement for imread should return an array) +* :ghpull:`21484`: Replacement for imread should return an array +* :ghpull:`21495`: Backport PR #21492 on branch v3.5.x (added parameter documentation for MultiCursor) +* :ghpull:`21493`: Backport PR #21488 on branch v3.5.x (Added to contour docs) +* :ghpull:`21492`: added parameter documentation for MultiCursor +* :ghpull:`21488`: Added to contour docs +* :ghpull:`21478`: Fix GTK4 embedding example +* :ghpull:`21387`: Fix path simplification of closed loops +* :ghpull:`21479`: Backport PR #21472 on branch v3.5.x (Clarify set_parse_math documentation.) +* :ghpull:`21472`: Clarify set_parse_math documentation. +* :ghpull:`21471`: Backport PR #21470 on branch v3.5.x (Hide fully transparent latex text in PS output) +* :ghpull:`21470`: Hide fully transparent latex text in PS output +* :ghpull:`21469`: Backport PR #21468 on branch v3.5.x (Fix some typos in examples) +* :ghpull:`21468`: Fix some typos in examples +* :ghpull:`21461`: Backport #21429 from jklymak/doc-use-mpl-sphinx +* :ghpull:`21464`: Backport PR #21460 on branch v3.5.x (Clip slider init marker to slider track.) +* :ghpull:`21460`: Clip slider init marker to slider track. +* :ghpull:`21458`: Backport: #21429 from jklymak/doc-use-mpl-sphinx +* :ghpull:`21454`: Fix error with pyparsing 3 for 3.5.x +* :ghpull:`21459`: Backport PR #21423 on branch v3.5.x (Change CircleCI job title to "Rendered docs") +* :ghpull:`21423`: Change CircleCI job title to "Rendered docs" +* :ghpull:`21457`: Backport PR #21455 on branch v3.5.x (Hide note linking to the download section at the bottom of galleries) +* :ghpull:`21456`: Backport PR #21453 on branch v3.5.x (Cleanup index.rst sectioning) +* :ghpull:`21455`: Hide note linking to the download section at the bottom of galleries +* :ghpull:`21453`: Cleanup index.rst sectioning +* :ghpull:`21224`: DOC: Nav-bar: Add icon linking to contents +* :ghpull:`21451`: Backport PR #21445 on branch v3.5.x (Mnt pin pyparsing) +* :ghpull:`21429`: DOC: use mpl-sphinx-theme for navbar, social, logo +* :ghpull:`21450`: Backport PR #21449 on branch v3.5.x (Less verbose install info on index page) +* :ghpull:`21449`: Less verbose install info on index page +* :ghpull:`21446`: Also exclude pyparsing 3.0.0 in setup.py. +* :ghpull:`21445`: Mnt pin pyparsing +* :ghpull:`21439`: Backport PR #21420 on branch v3.5.x (Enable Python 3.10 wheel building on all systems) +* :ghpull:`21438`: Backport PR #21427 on branch v3.5.x (Update docstrings of get_{view,data}_interval.) +* :ghpull:`21437`: Backport PR #21435 on branch v3.5.x (DOC: Fix selection of parameter names in HTML theme) +* :ghpull:`21420`: Enable Python 3.10 wheel building on all systems +* :ghpull:`21427`: Update docstrings of get_{view,data}_interval. +* :ghpull:`21435`: DOC: Fix selection of parameter names in HTML theme +* :ghpull:`21428`: Backport PR #21422 on branch v3.5.x (More doc reorganization) +* :ghpull:`21422`: More doc reorganization +* :ghpull:`21421`: Backport PR #21411 on branch v3.5.x (Document webagg in docs.) +* :ghpull:`21419`: Backport PR #21251 on branch v3.5.x (DOC: more site re-org) +* :ghpull:`21411`: Document webagg in docs. +* :ghpull:`21251`: DOC: more site re-org +* :ghpull:`21416`: Backport PR #21326 on branch v3.5.x (Add ability to scale BBox with just x or y values) +* :ghpull:`21418`: Backport PR #21414 on branch v3.5.x (Support pathological tmpdirs in TexManager.) +* :ghpull:`21410`: Backport PR #20591 on branch v3.5.x (Webagg backend: get rid of tornado) +* :ghpull:`21414`: Support pathological tmpdirs in TexManager. +* :ghpull:`21326`: Add ability to scale BBox with just x or y values +* :ghpull:`20591`: Webagg backend: get rid of tornado +* :ghpull:`21406`: Backport PR #21212 on branch v3.5.x (Fix set_size_inches on HiDPI and also GTK4) +* :ghpull:`21405`: Backport PR #21365 on branch v3.5.x (Convert macosx backend to use device_pixel_ratio) +* :ghpull:`18274`: Improve initial macosx device scale +* :ghpull:`21212`: Fix set_size_inches on HiDPI and also GTK4 +* :ghpull:`21365`: Convert macosx backend to use device_pixel_ratio +* :ghpull:`21372`: Backport PR #20708 on branch v3.5.x (Describe possible need for loading the 'lmodern' package when using PGF files) +* :ghpull:`20708`: Describe possible need for loading the 'lmodern' package when using PGF files +* :ghpull:`21359`: Add GHA testing whether files were added and deleted in the same PR. +* :ghpull:`21360`: Backport PR #21335 on branch v3.5.x (DOC: move usage tutorial info to Users guide rst) +* :ghpull:`21363`: Backport PR #21287 on branch v3.5.x (Inherit more docstrings.) +* :ghpull:`21361`: Fix flake8 from #21335 +* :ghpull:`21287`: Inherit more docstrings. +* :ghpull:`21335`: DOC: move usage tutorial info to Users guide rst +* :ghpull:`21358`: Backport PR #21357 on branch v3.5.x (DOC: remove test from README.rst) +* :ghpull:`21357`: DOC: remove test from README.rst +* :ghpull:`21350`: Remove plot_gallery setting from conf.py +* :ghpull:`21340`: Backport PR #21332 on branch v3.5.x (Fix default value for ``shading`` in``pyplot.pcolormesh`` docstring) +* :ghpull:`21332`: Fix default value for ``shading`` in``pyplot.pcolormesh`` docstring +* :ghpull:`21334`: Backport PR #21330 on branch v3.5.x (Fix medical image caption in tutorial) +* :ghpull:`21329`: Backport PR #21321 on branch v3.5.x (DOC Update description of ax.contour method, resolves #21310) +* :ghpull:`21330`: Fix medical image caption in tutorial +* :ghpull:`21321`: DOC Update description of ax.contour method, resolves #21310 +* :ghpull:`21327`: Backport PR #21313 on branch v3.5.x (DOC: Minimal getting started page) +* :ghpull:`21313`: DOC: Minimal getting started page +* :ghpull:`21316`: Backport PR #21312 on branch v3.5.x (Update link to Agg website) +* :ghpull:`21312`: Update link to Agg website +* :ghpull:`21308`: Backport PR #21307 on branch v3.5.x (Use in-tree builds for PyPy wheels) +* :ghpull:`21307`: Use in-tree builds for PyPy wheels +* :ghpull:`21306`: Backport PR #21303 on branch v3.5.x (Pin macOS to 10.15 for wheels) +* :ghpull:`21305`: Backport PR #21286 on branch v3.5.x (Clarify FigureBase.tight_bbox as different from all other artists.) +* :ghpull:`21286`: Clarify FigureBase.tight_bbox as different from all other artists. +* :ghpull:`21302`: Backport PR #21291 on branch v3.5.x (DOC: Bump to the sphinx-gallery release) +* :ghpull:`21304`: Backport PR #21294 on branch v3.5.x (Disable blitting on GTK4 backends) +* :ghpull:`21294`: Disable blitting on GTK4 backends +* :ghpull:`21277`: Backport PR #21263 on branch v3.5.x (Ensure internal FreeType matches Python compile) +* :ghpull:`21291`: DOC: Bump to the sphinx-gallery release +* :ghpull:`21296`: Backport PR #21288 on branch v3.5.x (Allow macosx thread safety test on macOS11) +* :ghpull:`21297`: Backport PR #21293 on branch v3.5.x (Fix snap argument to pcolormesh) +* :ghpull:`21293`: Fix snap argument to pcolormesh +* :ghpull:`21288`: Allow macosx thread safety test on macOS11 +* :ghpull:`21279`: Fix freetype wheel building +* :ghpull:`21292`: Backport PR #21290 on branch v3.5.x (DOC: Fix some lists in animation examples) +* :ghpull:`21290`: DOC: Fix some lists in animation examples +* :ghpull:`21284`: Backport PR #21282 on branch v3.5.x (Fix incorrect markup in example.) +* :ghpull:`21282`: Fix incorrect markup in example. +* :ghpull:`21281`: Backport PR #21275 on branch v3.5.x (Fix format_cursor_data for values close to float resolution.) +* :ghpull:`21275`: Fix format_cursor_data for values close to float resolution. +* :ghpull:`21263`: Ensure internal FreeType matches Python compile +* :ghpull:`21273`: Backport PR #21269 on branch v3.5.x (Don't use pixelDelta() on X11.) +* :ghpull:`21269`: Don't use pixelDelta() on X11. +* :ghpull:`21268`: Backport PR #21236: DOC: Update interactive colormap example +* :ghpull:`21265`: Backport PR #21264 on branch v3.5.x (DOC: Fix footnote that breaks PDF builds) +* :ghpull:`21264`: DOC: Fix footnote that breaks PDF builds +* :ghpull:`21236`: DOC: Update interactive colormap example +* :ghpull:`21262`: Backport PR #21250 on branch v3.5.x (DOC: Remove examples/README) +* :ghpull:`21260`: DOC: Fix source links to prereleases +* :ghpull:`21261`: Backport PR #21240: DOC: Fix source links and flake8 cleanup +* :ghpull:`21248`: Backport PR #21247 on branch v3.5.x (Fix release notes typos.) +* :ghpull:`21254`: Backport PR #21249 on branch v3.5.x (Fix some syntax highlights in coding and contributing guide.) +* :ghpull:`21250`: DOC: Remove examples/README +* :ghpull:`21249`: Fix some syntax highlights in coding and contributing guide. +* :ghpull:`20652`: Fixed Comments and Clarification +* :ghpull:`21240`: DOC: Fix source links and flake8 cleanup +* :ghpull:`21247`: Fix release notes typos. +* :ghpull:`21244`: Backport PR #20907 on branch v3.5.x (Move sigint tests into subprocesses) +* :ghpull:`21245`: Backport PR #21226 on branch v3.5.x (DOC: Adapt some colors in examples) +* :ghpull:`21226`: DOC: Adapt some colors in examples +* :ghpull:`20907`: Move sigint tests into subprocesses +* :ghpull:`21241`: Backport PR #21237 on branch v3.5.x (DOC: Add fill_between to plot_types) +* :ghpull:`21237`: DOC: Add fill_between to plot_types +* :ghpull:`21235`: Backport PR #20852 on branch v3.5.x (Prepare docs for 3.5) +* :ghpull:`20852`: Prepare docs for 3.5 +* :ghpull:`21234`: Backport PR #21221 on branch v3.5.x (Updates to plot types) +* :ghpull:`21232`: Backport PR #21228 on branch v3.5.x (Small doc nits.) +* :ghpull:`21233`: Backport PR #21229 on branch v3.5.x (Shorten PdfPages FAQ entry.) +* :ghpull:`21221`: Updates to plot types +* :ghpull:`21229`: Shorten PdfPages FAQ entry. +* :ghpull:`21228`: Small doc nits. +* :ghpull:`21227`: Backport PR #20730 on branch v3.5.x (DOC: Add a release mode tag) +* :ghpull:`20730`: DOC: Add a release mode tag +* :ghpull:`21225`: Backport PR #21223 on branch v3.5.x (Fix nav link for "Usage guide" and remove release/date info from that page) +* :ghpull:`21223`: Fix nav link for "Usage guide" and remove release/date info from that page +* :ghpull:`21222`: Backport PR #21211 on branch v3.5.x (updated resources) +* :ghpull:`21211`: updated resources +* :ghpull:`21219`: Backport PR #21216 on branch v3.5.x (Use correct confidence interval) +* :ghpull:`21216`: Use correct confidence interval +* :ghpull:`21217`: Backport PR #21215 on branch v3.5.x (Fix more edge cases in psd, csd.) +* :ghpull:`21215`: Fix more edge cases in psd, csd. +* :ghpull:`21210`: Backport PR #21191 on branch v3.5.x (Fix very-edge case in csd(), plus small additional cleanups.) +* :ghpull:`21209`: Backport PR #21188 on branch v3.5.x (Rework headers for individual backend docs.) +* :ghpull:`21191`: Fix very-edge case in csd(), plus small additional cleanups. +* :ghpull:`21188`: Rework headers for individual backend docs. +* :ghpull:`21208`: Backport PR #21203 on branch v3.5.x (Rework plot types quiver) +* :ghpull:`21203`: Rework plot types quiver +* :ghpull:`21207`: Backport PR #21198 on branch v3.5.x (Update coding_guide.rst) +* :ghpull:`21206`: Backport PR #21201 on branch v3.5.x (Fix signature of barh() in plot types) +* :ghpull:`21204`: Backport PR #21193 on branch v3.5.x (Update contributing guide.) +* :ghpull:`21198`: Update coding_guide.rst +* :ghpull:`21201`: Fix signature of barh() in plot types +* :ghpull:`21200`: Backport PR #21196 on branch v3.5.x (Update fonts.rst) +* :ghpull:`21199`: Backport PR #21026 on branch v3.5.x (Place 3D contourf patches between levels) +* :ghpull:`21197`: Backport PR #21186 on branch v3.5.x (Fixed typos using codespell. (previous pull request was told not to change the agg files) ) +* :ghpull:`21196`: Update fonts.rst +* :ghpull:`21026`: Place 3D contourf patches between levels +* :ghpull:`21186`: Fixed typos using codespell. (previous pull request was told not to change the agg files) +* :ghpull:`21195`: Backport PR #21189 on branch v3.5.x (Small doc fixes.) +* :ghpull:`21194`: Backport PR #21192 on branch v3.5.x (Discourage making style changes to extern/.) +* :ghpull:`21189`: Small doc fixes. +* :ghpull:`21192`: Discourage making style changes to extern/. +* :ghpull:`21193`: Update contributing guide. +* :ghpull:`21184`: Backport PR #21172 on branch v3.5.x (skip QImage leak workaround for PySide2 >= 5.12) +* :ghpull:`21183`: Backport PR #21081 on branch v3.5.x (Improve docs for to_jshtml()) +* :ghpull:`21172`: skip QImage leak workaround for PySide2 >= 5.12 +* :ghpull:`21181`: Backport PR #21166 on branch v3.5.x (Cleanup contour(f)3d examples.) +* :ghpull:`21182`: Backport PR #21180 on branch v3.5.x (Remove uninformative ``.. figure::`` titles in docs.) +* :ghpull:`21081`: Improve docs for to_jshtml() +* :ghpull:`21180`: Remove uninformative ``.. figure::`` titles in docs. +* :ghpull:`21166`: Cleanup contour(f)3d examples. +* :ghpull:`21174`: Backport PR #19343 on branch v3.5.x (Enh improve agg chunks error) +* :ghpull:`19343`: Enh improve agg chunks error +* :ghpull:`21171`: Backport PR #20951 on branch v3.5.x ([ENH]: data kwarg support for mplot3d #20912) +* :ghpull:`21169`: Backport PR #21126 on branch v3.5.x (Deprecate passing formatting parameters positionally to stem()) +* :ghpull:`21126`: Deprecate passing formatting parameters positionally to stem() +* :ghpull:`21164`: Backport PR #21039 on branch v3.5.x (Fix ``hexbin`` marginals and log scaling) +* :ghpull:`21039`: Fix ``hexbin`` marginals and log scaling +* :ghpull:`21160`: Backport PR #21136 on branch v3.5.x (More (minor) plot types gallery fixes.) +* :ghpull:`21136`: More (minor) plot types gallery fixes. +* :ghpull:`21158`: Backport PR #21140 on branch v3.5.x (Docstring cleanups around DATA_PARAMETER_PLACEHOLDER.) +* :ghpull:`21159`: Backport PR #21127 on branch v3.5.x (Simplify argument parsing in stem().) +* :ghpull:`21157`: Backport PR #21153 on branch v3.5.x (Improve curve_error_band example.) +* :ghpull:`21156`: Backport PR #21154 on branch v3.5.x (Increase marker size in double_pendulum example.) +* :ghpull:`21127`: Simplify argument parsing in stem(). +* :ghpull:`21140`: Docstring cleanups around DATA_PARAMETER_PLACEHOLDER. +* :ghpull:`21153`: Improve curve_error_band example. +* :ghpull:`21154`: Increase marker size in double_pendulum example. +* :ghpull:`21149`: Backport PR #21146 on branch v3.5.x (Fix clim handling for pcolor{,mesh}.) +* :ghpull:`21151`: Backport PR #21141 on branch v3.5.x (Fix DATA_PARAMETER_PLACEHOLDER interpolation for quiver&contour{,f}.) +* :ghpull:`21150`: Backport PR #21145 on branch v3.5.x (Fix format_cursor_data with nans.) +* :ghpull:`21141`: Fix DATA_PARAMETER_PLACEHOLDER interpolation for quiver&contour{,f}. +* :ghpull:`21145`: Fix format_cursor_data with nans. +* :ghpull:`21146`: Fix clim handling for pcolor{,mesh}. +* :ghpull:`21148`: Backport PR #21142 on branch v3.5.x (Mac qt ctrl) +* :ghpull:`21142`: Mac qt ctrl +* :ghpull:`21144`: Backport PR #21122 on branch v3.5.x (CTRL does not fix aspect in zoom-to-rect mode.) +* :ghpull:`21143`: Backport PR #19515 on branch v3.5.x (Colorbar axis zoom and pan) +* :ghpull:`21122`: CTRL does not fix aspect in zoom-to-rect mode. +* :ghpull:`19515`: Colorbar axis zoom and pan +* :ghpull:`21138`: Backport PR #21131 on branch v3.5.x (Fix polar() regression on second call failure) +* :ghpull:`21134`: Backport PR #21124 on branch v3.5.x (Tweak streamplot plot_types example.) +* :ghpull:`21133`: Backport PR #21114 on branch v3.5.x (Add contour and tricontour plots to plot types) +* :ghpull:`21132`: Backport PR #21093 on branch v3.5.x (DOC: clarify what we mean by object oriented in pyplot api) +* :ghpull:`21124`: Tweak streamplot plot_types example. +* :ghpull:`21114`: Add contour and tricontour plots to plot types +* :ghpull:`21130`: Backport PR #21129 on branch v3.5.x (Fix decenter of image in gallery thumbnails) +* :ghpull:`21093`: DOC: clarify what we mean by object oriented in pyplot api +* :ghpull:`21129`: Fix decenter of image in gallery thumbnails +* :ghpull:`21125`: Backport PR #21086 on branch v3.5.x (Capitalization fixes in example section titles.) +* :ghpull:`21128`: Backport PR #21123 on branch v3.5.x (Simplify/uniformize sample data setup in plot_types examples.) +* :ghpull:`21123`: Simplify/uniformize sample data setup in plot_types examples. +* :ghpull:`21121`: Backport PR #21111 on branch v3.5.x (Rename section title Gallery -> Examples) +* :ghpull:`21086`: Capitalization fixes in example section titles. +* :ghpull:`21120`: Backport PR #21115 on branch v3.5.x (Improve errorbar plot types example) +* :ghpull:`21119`: Backport PR #21116 on branch v3.5.x (Adapt css so that galleries have four columns) +* :ghpull:`21116`: Adapt css so that galleries have four columns +* :ghpull:`21118`: Backport PR #21112 on branch v3.5.x (Fix make_norm_from_scale ``__name__`` when used inline.) +* :ghpull:`21111`: Rename section title Gallery -> Examples +* :ghpull:`21112`: Fix make_norm_from_scale ``__name__`` when used inline. +* :ghpull:`20951`: [ENH]: data kwarg support for mplot3d #20912 +* :ghpull:`21115`: Improve errorbar plot types example +* :ghpull:`21109`: Backport PR #21104 on branch v3.5.x (Remove the index and module index pages) +* :ghpull:`21104`: Remove the index and module index pages +* :ghpull:`21102`: Backport PR #21100 on branch v3.5.x (Cleanup demo_tight_layout.) +* :ghpull:`21106`: Backport PR #21034 on branch v3.5.x (Make rcParams["backend"] backend fallback check rcParams identity first.) +* :ghpull:`21105`: Backport PR #21083 on branch v3.5.x (Fix capitalizations) +* :ghpull:`21103`: Backport PR #21089 on branch v3.5.x (Update sticky_edges docstring to new behavior.) +* :ghpull:`21034`: Make rcParams["backend"] backend fallback check rcParams identity first. +* :ghpull:`21083`: Fix capitalizations +* :ghpull:`21099`: Backport PR #20935 on branch v3.5.x (Add ColormapsRegistry as experimental and add it to pyplot) +* :ghpull:`21100`: Cleanup demo_tight_layout. +* :ghpull:`21098`: Backport PR #20903 on branch v3.5.x (Use release-branch version scheme ) +* :ghpull:`20935`: Add ColormapsRegistry as experimental and add it to pyplot +* :ghpull:`20903`: Use release-branch version scheme +* :ghpull:`21089`: Update sticky_edges docstring to new behavior. +* :ghpull:`21084`: Backport PR #20988 on branch v3.5.x (Add HiDPI support in GTK.) +* :ghpull:`21085`: Backport PR #21082 on branch v3.5.x (Fix layout of sidebar entries) +* :ghpull:`20345`: ENH: call update_ticks before we return them to the user +* :ghpull:`21082`: Fix layout of sidebar entries +* :ghpull:`20988`: Add HiDPI support in GTK. +* :ghpull:`21080`: Backport PR #19619 on branch v3.5.x (Fix bug in shape assignment) +* :ghpull:`19619`: Fix bug in shape assignment +* :ghpull:`21079`: Backport PR #21078 on branch v3.5.x (Cache build dependencies on Circle) +* :ghpull:`21078`: Cache build dependencies on Circle +* :ghpull:`21077`: Backport PR #21076 on branch v3.5.x (Break links between twinned axes when removing) +* :ghpull:`21076`: Break links between twinned axes when removing +* :ghpull:`21073`: Backport PR #21072 on branch v3.5.x (Use sysconfig directly instead of through distutils) +* :ghpull:`21072`: Use sysconfig directly instead of through distutils +* :ghpull:`21071`: Backport PR #21061 on branch v3.5.x (Remove most visible dependencies on distutils.) +* :ghpull:`21061`: Remove most visible dependencies on distutils. +* :ghpull:`21070`: Backport PR #21025 on branch v3.5.x (Fix Cairo backends on HiDPI screens) +* :ghpull:`21065`: Backport PR #20819 on branch v3.5.x (Add CPython 3.10 wheels) +* :ghpull:`21069`: Backport PR #21051 on branch v3.5.x (set_dashes does not support offset=None anymore.) +* :ghpull:`21068`: Backport PR #21067 on branch v3.5.x (Remove generated file accidentally added in #20867) +* :ghpull:`21025`: Fix Cairo backends on HiDPI screens +* :ghpull:`21051`: set_dashes does not support offset=None anymore. +* :ghpull:`21067`: Remove generated file accidentally added in #20867 +* :ghpull:`21066`: Backport PR #21060 on branch v3.5.x (Correct the default for fillstyle parameter in MarkerStyle()) +* :ghpull:`20819`: Add CPython 3.10 wheels +* :ghpull:`21064`: Backport PR #20913 on branch v3.5.x ([Doc] colors.to_hex input & output) +* :ghpull:`20913`: [Doc] colors.to_hex input & output +* :ghpull:`21063`: Backport PR #21062 on branch v3.5.x (Fix typo in template of current dev-docs) +* :ghpull:`21062`: Fix typo in template of current dev-docs +* :ghpull:`21060`: Correct the default for fillstyle parameter in MarkerStyle() +* :ghpull:`21058`: Backport PR #21053 on branch v3.5.x (Fix validate_markevery docstring markup.) +* :ghpull:`21053`: Fix validate_markevery docstring markup. +* :ghpull:`21052`: Backport PR #20867 on branch v3.5.x ("inner" index reorganization) +* :ghpull:`21047`: Backport PR #21040 on branch v3.5.x (Document ``handleheight`` parameter of ``Legend`` constructor) +* :ghpull:`21048`: Backport PR #21044 on branch v3.5.x (Support for forward/back mousebuttons on WX backend) +* :ghpull:`20867`: "inner" index reorganization +* :ghpull:`21044`: Support for forward/back mousebuttons on WX backend +* :ghpull:`21040`: Document ``handleheight`` parameter of ``Legend`` constructor +* :ghpull:`21045`: Backport PR #21041 on branch v3.5.x (Prefer "none" to "None" in docs, examples and comments.) +* :ghpull:`21041`: Prefer "none" to "None" in docs, examples and comments. +* :ghpull:`21037`: Backport PR #20949 on branch v3.5.x (Improve formatting of imshow() cursor data independently of colorbar.) +* :ghpull:`21035`: Backport PR #21031 on branch v3.5.x (Make date.{converter,interval_multiples} rcvalidators side-effect free.) +* :ghpull:`20949`: Improve formatting of imshow() cursor data independently of colorbar. +* :ghpull:`21031`: Make date.{converter,interval_multiples} rcvalidators side-effect free. +* :ghpull:`21032`: Backport PR #21017 on branch v3.5.x (FIX: Don't subslice lines if non-standard transform) +* :ghpull:`21030`: Backport PR #20980 on branch v3.5.x (FIX: remove colorbar from list of colorbars on axes) +* :ghpull:`21029`: Backport PR #21028 on branch v3.5.x (Minor homogeneization of markup for MEP titles.) +* :ghpull:`21028`: Minor homogeneization of markup for MEP titles. +* :ghpull:`21022`: Backport PR #20518 on branch v3.5.x ( Support sketch_params in pgf backend) +* :ghpull:`20518`: Support sketch_params in pgf backend +* :ghpull:`21018`: Backport PR #20976 on branch v3.5.x (Separate tick and spine examples) +* :ghpull:`20976`: Separate tick and spine examples +* :ghpull:`21014`: Backport PR #20994 on branch v3.5.x (Remove unused icon_filename, window_icon globals.) +* :ghpull:`21013`: Backport PR #21012 on branch v3.5.x (Use numpydoc for GridSpecFromSubplotSpec.__init__) +* :ghpull:`20994`: Remove unused icon_filename, window_icon globals. +* :ghpull:`21012`: Use numpydoc for GridSpecFromSubplotSpec.__init__ +* :ghpull:`21011`: Backport PR #21003 on branch v3.5.x (Deemphasize mpl_toolkits in API docs.) +* :ghpull:`21003`: Deemphasize mpl_toolkits in API docs. +* :ghpull:`21002`: Backport PR #20987 on branch v3.5.x (FIX: colorbar with boundary norm, proportional, extend) +* :ghpull:`20987`: FIX: colorbar with boundary norm, proportional, extend +* :ghpull:`21000`: Backport PR #20997 on branch v3.5.x (Fix ToolManager + TextBox support.) +* :ghpull:`20997`: Fix ToolManager + TextBox support. +* :ghpull:`20985`: Backport PR #20942 on branch v3.5.x (DOC Use 'Axes' instead of 'axes' in axes._base.py) +* :ghpull:`20983`: Backport PR #20973 on branch v3.5.x (Docstring cleanups.) +* :ghpull:`20982`: Backport PR #20972 on branch v3.5.x (Cleanup some dviread docstrings.) +* :ghpull:`20942`: DOC Use 'Axes' instead of 'axes' in axes._base.py +* :ghpull:`20981`: Backport PR #20975 on branch v3.5.x (Clarify support for 2D coordinate inputs to streamplot.) +* :ghpull:`20972`: Cleanup some dviread docstrings. +* :ghpull:`20975`: Clarify support for 2D coordinate inputs to streamplot. +* :ghpull:`20973`: Docstring cleanups. +* :ghpull:`20971`: Backport PR #20970 on branch v3.5.x (Build wheels for Apple Silicon.) +* :ghpull:`20970`: Build wheels for Apple Silicon. +* :ghpull:`20969`: Backport PR #20321 on branch v3.5.x (Add a GTK4 backend.) +* :ghpull:`20321`: Add a GTK4 backend. +* :ghpull:`20966`: Backport PR #19553 on branch v3.5.x (ENH: Adding callbacks to Norms for update signals) +* :ghpull:`20967`: Backport PR #20965 on branch v3.5.x (BUG: Fix f_back is None handling) +* :ghpull:`20965`: BUG: Fix f_back is None handling +* :ghpull:`19553`: ENH: Adding callbacks to Norms for update signals +* :ghpull:`20960`: Backport PR #20745 on branch v3.5.x (Clean up some Event class docs.) +* :ghpull:`20745`: Clean up some Event class docs. +* :ghpull:`20959`: Backport PR #20952 on branch v3.5.x (Redirect to new 3rd party packages page) +* :ghpull:`20952`: Redirect to new 3rd party packages page +* :ghpull:`20958`: Backport PR #20956 on branch v3.5.x (Make warning for no-handles legend more explicit.) +* :ghpull:`20956`: Make warning for no-handles legend more explicit. +* :ghpull:`20954`: Backport PR #20931 on branch v3.5.x (API: rename draw_no_output to draw_without_rendering) +* :ghpull:`20931`: API: rename draw_no_output to draw_without_rendering +* :ghpull:`20934`: Backport PR #20919 on branch v3.5.x (Improve various release notes)" +* :ghpull:`20948`: Backport PR #20944 on branch v3.5.x (Switch documented deprecations in mathtext by ``__getattr__`` deprecations) +* :ghpull:`20944`: Switch documented deprecations in mathtext by ``__getattr__`` deprecations +* :ghpull:`20947`: Backport PR #20941 on branch v3.5.x (Fix variable capitalization in plot types headings) +* :ghpull:`20941`: Fix variable capitalization in plot types headings +* :ghpull:`20939`: Backport PR #20937 on branch v3.5.x (Fix documented allowed values for Patch.set_edgecolor.) +* :ghpull:`20940`: Backport PR #20938 on branch v3.5.x (Fix missorted changelog entry.) +* :ghpull:`20938`: Fix missorted changelog entry. +* :ghpull:`20937`: Fix documented allowed values for Patch.set_edgecolor. +* :ghpull:`20933`: Backport PR #20916 on branch v3.5.x (Improve deleted Animation warning) +* :ghpull:`20916`: Improve deleted Animation warning +* :ghpull:`20919`: Improve various release notes +* :ghpull:`20928`: Backport PR #20889 on branch v3.5.x (Fix clearing selector) +* :ghpull:`20927`: Backport PR #20924 on branch v3.5.x (Improve ``path.py`` docstrings a bit) +* :ghpull:`20889`: Fix clearing selector +* :ghpull:`20922`: Backport PR #20920 on branch v3.5.x (Fix cubic curve code in ``Path.__doc__``) +* :ghpull:`20925`: Backport PR #20917 on branch v3.5.x (Move installing FAQ to installing page.) +* :ghpull:`20924`: Improve ``path.py`` docstrings a bit +* :ghpull:`20917`: Move installing FAQ to installing page. +* :ghpull:`20920`: Fix cubic curve code in ``Path.__doc__`` +* :ghpull:`20918`: Backport PR #20915 on branch v3.5.x ([Doc] boxplot typo) +* :ghpull:`20915`: [Doc] boxplot typo +* :ghpull:`20908`: [Doc] FigureCanvasBase draw +* :ghpull:`20899`: Backport PR #20885 on branch v3.5.x (Fix broken QApplication init in a test.) +* :ghpull:`20885`: Fix broken QApplication init in a test. +* :ghpull:`20894`: Backport PR #20891 on branch v3.5.x (Add dependency link for 3.5) +* :ghpull:`20893`: Backport PR #20892 on branch v3.5.x (Label pylab as "discouraged" instead of "disapproved") +* :ghpull:`20891`: Add dependency link for 3.5 +* :ghpull:`20888`: Backport PR #20864 on branch v3.5.x (Add Python 3.10 testing.) +* :ghpull:`20890`: Backport PR #20693 on branch v3.5.x (Fix setting artists properties of selectors) +* :ghpull:`20892`: Label pylab as "discouraged" instead of "disapproved" +* :ghpull:`20693`: Fix setting artists properties of selectors +* :ghpull:`20864`: Add Python 3.10 testing. +* :ghpull:`20886`: Backport PR #20884 on branch v3.5.x (Ensure full environment is passed to headless test.) +* :ghpull:`20884`: Ensure full environment is passed to headless test. +* :ghpull:`20883`: Make pywin32 optional in Ctrl+C Qt test. +* :ghpull:`20874`: Add additional external resource. +* :ghpull:`20875`: Use mpl.colormaps in examples +* :ghpull:`20586`: Deprecate matplotlib.test() +* :ghpull:`19892`: Add Figure parameter layout and discourage tight_layout / constrained_layout +* :ghpull:`20882`: Don't add QtNetwork to the API exported by qt_compat. +* :ghpull:`20881`: Deprecate some old globals in qt_compat. +* :ghpull:`13306`: Qt5: SIGINT kills just the mpl window and not the process itself +* :ghpull:`20876`: DOC: Fix dependency link. +* :ghpull:`20878`: Use tables for Locator and Formatter docs +* :ghpull:`20873`: Remove mplutils.cpp; shorten mplutils.h. +* :ghpull:`20872`: Remove some boilerplate from C extension inits. +* :ghpull:`20871`: Move setup.cfg to mplsetup.cfg. +* :ghpull:`20869`: Ignore errors trying to delete make_release_tree. +* :ghpull:`20868`: Fix qt key mods +* :ghpull:`20856`: TST: Add unit test to catch recurrences of #20822, #20855 +* :ghpull:`20857`: Propose a less error-prone helper for module-level getattrs. +* :ghpull:`20840`: Speed up Tkagg blit with Tk_PhotoPutBlock +* :ghpull:`20805`: Ensure all params are restored after ``reset_ticks``. +* :ghpull:`20863`: new github citation format +* :ghpull:`20859`: Allow SubFigure legends +* :ghpull:`20848`: Fix PyPy wheels and tests +* :ghpull:`20862`: Fix minor typo in setupext.py +* :ghpull:`20814`: FIX: Avoid copying source script when ``plot_html_show_source_link`` is False in plot directive +* :ghpull:`20855`: BUG: __getattr__ must raise AttributeError if name not found (again) +* :ghpull:`20079`: Prepare axes_divider for simpler(?) indexing-based API. +* :ghpull:`20444`: Delete _Bracket and update the _Curve to be able to ']->' and '<-[' +* :ghpull:`20812`: Clarify tutorial "Customizing Matplotlib with style sheets and rcParams" +* :ghpull:`20806`: Deprecate matplotlib.cm.LUTSIZE +* :ghpull:`20818`: Swap Cap/Cup glyphs when using STIX font. +* :ghpull:`20849`: Add external resources to devdoc landing page +* :ghpull:`20846`: Re-re-remove deprecated Qt globals. +* :ghpull:`18503`: Add a dedicated ColormapRegistry class +* :ghpull:`20603`: Deprecate unused LassoSelector event handlers. +* :ghpull:`20679`: Fix selector onselect call when the selector is removed by an "empty" click and add ``ignore_event_outside`` argument +* :ghpull:`11358`: FIX/ENH: Introduce a monolithic legend handler for Line2D +* :ghpull:`20699`: FIX/ENH: Introduce a monolithic legend handler for Line2D +* :ghpull:`20837`: Merge branch v3.4.x +* :ghpull:`18782`: ENH: allow image to interpolate post RGBA +* :ghpull:`20829`: TST: neither warned and pytest upstream deprecated this usage +* :ghpull:`20828`: Increase test timeouts to 60 s to aid slower architectures +* :ghpull:`20816`: ENH: Add the ability to block callback signals +* :ghpull:`20646`: Handle NaN values in ``plot_surface`` zsort +* :ghpull:`20725`: ``Axes3D.plot_surface``: Allow masked arrays and ``NaN`` values +* :ghpull:`20825`: Fix image triage tool with Qt6 +* :ghpull:`20229`: ENH: Only do constrained layout at draw... +* :ghpull:`20822`: BUG: __getattr__ must raise AttributeError if name not found +* :ghpull:`20815`: circle: Switch to next-gen image. +* :ghpull:`20813`: add doc-link to dufte +* :ghpull:`20799`: MNT: Rename callbacksSM to callbacks +* :ghpull:`20803`: Re-remove deprecated Qt globals. +* :ghpull:`17810`: FIX: don't fail on first show if animation already exhausted +* :ghpull:`20733`: Deprecate globals using module-level ``__getattr__``. +* :ghpull:`20788`: FIX: Check for colorbar creation with multi-dimensional alpha +* :ghpull:`20115`: ENH: pass extra kwargs in FigureBase, SubFigure, Figure to set +* :ghpull:`20795`: TST/MNT: deprecate unused fixture +* :ghpull:`20792`: Change legend guide to object oriented approach +* :ghpull:`20717`: Fix collection offsets +* :ghpull:`20673`: Point [SOURCE] documents to github +* :ghpull:`19255`: Support for PyQt6/PySide6. +* :ghpull:`20772`: Implement remove_rubberband rather than release_zoom. +* :ghpull:`20783`: Document how to check for the existence of current figure/axes. +* :ghpull:`20778`: Dedupe handling of mouse buttons in macos backend. +* :ghpull:`20749`: Cleanup font subsetting code +* :ghpull:`20775`: Remove some remnants of qt4 support. +* :ghpull:`20659`: Add HiDPI-related config for mathmpl +* :ghpull:`20767`: Factor out latex ifpackageloaded pattern. +* :ghpull:`20769`: Simplify backend_ps._nums_to_str. +* :ghpull:`20768`: Avoid using gca() in examples. +* :ghpull:`20766`: Fix line dash offset format in PS output +* :ghpull:`20706`: Include ``underscore.sty`` +* :ghpull:`20729`: Support vmin/vmax with bins='log' in hexbin +* :ghpull:`20753`: Deprecate support for case-insensitive scales. +* :ghpull:`20602`: Merge EllipseSelector example together with RectangleSelector. +* :ghpull:`20744`: Add an example showing alternate mouse cursors. +* :ghpull:`20758`: FIX: pass colorbar.set_ticklabels down to long_axis +* :ghpull:`20759`: Modernize mathtext examples +* :ghpull:`20739`: Small simplifications to streamplot. +* :ghpull:`20756`: Add new external resource: Python Graph Gallery +* :ghpull:`20330`: Fix cla colorbar +* :ghpull:`20688`: issue form files +* :ghpull:`20743`: Set the canvas cursor when using a SpanSelector +* :ghpull:`20391`: Type42 subsetting in PS/PDF +* :ghpull:`20737`: DOC: new index page +* :ghpull:`20686`: Fix interaction between make_keyword_only and pyplot generation. +* :ghpull:`20731`: Improved implementation of Path.copy and deepcopy +* :ghpull:`20732`: Fix style in ``assert(x)``. +* :ghpull:`20620`: Move set_cursor from the toolbar to FigureCanvas. +* :ghpull:`20728`: Fix broken link in 'Contributing' docs +* :ghpull:`20727`: DOC/TST make circle faster +* :ghpull:`20726`: DOC: Provide alternative to cbar.patch +* :ghpull:`20719`: Fix color normalization in plot types scatter +* :ghpull:`20634`: Implement Type-1 decryption +* :ghpull:`20633`: Emit non BMP chars as XObjects in PDF +* :ghpull:`20709`: Fix Circle merge on master branch. +* :ghpull:`20701`: Small cleanup to GTK backend +* :ghpull:`20670`: Support markevery on figure-level lines. +* :ghpull:`20707`: Rename a confusingly named variable in backend_pdf. +* :ghpull:`20680`: CI: Build merged version on CircleCI +* :ghpull:`20471`: add interactive colorbar example to gallery +* :ghpull:`20692`: Small cleanups to hatch.py. +* :ghpull:`20702`: DOC: add note about contouring algorithm +* :ghpull:`18869`: Add __version_info__ as a tuple-based version identifier +* :ghpull:`20689`: Fix some very unlikely leaks in extensions. +* :ghpull:`20254`: Define FloatingAxes boundary patch in data coordinates. +* :ghpull:`20682`: Bump codecov/codecov-action from 1 to 2 +* :ghpull:`20544`: Support of different locations for the text fixing cursor of TextBox +* :ghpull:`20648`: Simplify barchart_demo +* :ghpull:`20606`: Dynamically generate CbarAxes. +* :ghpull:`20405`: ENH: expose make_norm_from_scale +* :ghpull:`20555`: Fix the way to get xs length in set_3d_properties() +* :ghpull:`20546`: Improve tutorial figures in the new theme +* :ghpull:`20676`: Fix bounds when initialising ``SpanSelector`` +* :ghpull:`20678`: Clarify comment about backend_pgf.writeln. +* :ghpull:`20675`: Shorten the ``@deprecated`` docs. +* :ghpull:`20585`: Rename parameter selectors +* :ghpull:`20672`: Remove outdated parts of MatplotlibDeprecationWarning docs. +* :ghpull:`20671`: Standardize description of kwargs in legend_handler. +* :ghpull:`20669`: Cleanup related to usage of axs +* :ghpull:`20664`: Reword docs about fallbacks on headless linux. +* :ghpull:`20663`: Document $MPLSETUPCFG. +* :ghpull:`20638`: Small simplifications to FixedAxisArtistHelper. +* :ghpull:`20626`: Simplify curvilinear grid examples. +* :ghpull:`20088`: fix some http: -> https: URLs +* :ghpull:`20654`: Remove some usages of plt.setp() +* :ghpull:`20615`: Font 42 kerning +* :ghpull:`20636`: Use set_xticks(ticks, labels) instead of a separate set_xticklabels() +* :ghpull:`20450`: [Doc] Font Types and Font Subsetting +* :ghpull:`20582`: Fix twoslopenorm colorbar +* :ghpull:`20632`: Use ticklabels([]) instead of ticklabels('') +* :ghpull:`20608`: doc/conf.py: if set, use SOURCE_DATE_EPOCH to set copyright year. +* :ghpull:`20605`: Add \dddot and \ddddot as accents in mathtext +* :ghpull:`20621`: TST/DOC: just run circle once... +* :ghpull:`20498`: Adapt the release guide to the new release notes structure +* :ghpull:`20601`: Hide some ``_SelectorWidget`` state internals. +* :ghpull:`20600`: Inline _print_svg into its only call site (print_svg). +* :ghpull:`20589`: Add directional sizing cursors +* :ghpull:`20481`: Deprecate Colorbar.patch. +* :ghpull:`20598`: Don't forget to propagate kwargs from print_svgz to print_svg. +* :ghpull:`19495`: Move svg basename detection down to RendererSVG. +* :ghpull:`20501`: Colorbar redo again! +* :ghpull:`20407`: Turn shared_axes, stale_viewlims into {axis_name: value} dicts. +* :ghpull:`18966`: PR: Remove modality of figure options +* :ghpull:`19265`: Change styling of slider widgets +* :ghpull:`20593`: DOC: fix various typos +* :ghpull:`20374`: Check modification times of included RST files +* :ghpull:`20569`: Better signature and docstring for Artist.set +* :ghpull:`20574`: Add tricontourf hatching example +* :ghpull:`18666`: Remove unused/deprecated ``AVConv`` classes +* :ghpull:`20514`: Fix example for rcParams['autolimit_mode'] +* :ghpull:`20571`: Switch default ArrowStyle angle values from None to zero. +* :ghpull:`20510`: Consistent capitalization of section headers +* :ghpull:`20573`: Move the marker path example into the marker reference +* :ghpull:`20572`: Clarify allowable backend switches in matplotlib.use(). +* :ghpull:`20538`: Show box/arrowstyle parameters in reference examples. +* :ghpull:`20515`: Shorten the implementation of bxp(). +* :ghpull:`20562`: More concise how to for subplot adjustment +* :ghpull:`20570`: Reduce vertical margins in property tables +* :ghpull:`20563`: Expire deprecation of passing nbins to MaxNLocator in two ways +* :ghpull:`20561`: Fix limits in plot types example hist(x) +* :ghpull:`20559`: Fix deprecation of encoding in plot_directive. +* :ghpull:`20547`: Raise if passed invalid kwargs to set_constrained_layout_pads. +* :ghpull:`20527`: Factor out DEBUG_TRUETYPE checks in ttconv, & removals of unused defs. +* :ghpull:`20465`: Remove remaining 3.3 deprecations +* :ghpull:`20558`: Rename recently introduced parameters in SpanSelector +* :ghpull:`20535`: Improve the documentation guide +* :ghpull:`20113`: Interactive span selector improvement +* :ghpull:`20524`: Dedupe some box anchoring code between legend.py and offsetbox.py. +* :ghpull:`20451`: Add initial TextBox widget testing +* :ghpull:`20543`: Deprecate ``@pytest.mark.style(...)``. +* :ghpull:`20530`: Plot nothing for incompatible 0 shape in x,y data +* :ghpull:`20367`: Add parse_math in Text and default it False for TextBox +* :ghpull:`20509`: Cleanup plot types +* :ghpull:`20537`: Don't sort boxstyles/arrowstyles/etc. alphabetically. +* :ghpull:`20542`: Fix ScalarFormatter.format_ticks for non-ordered tick locations. +* :ghpull:`20533`: Rename (N, M) -> (M, N) array-like +* :ghpull:`20540`: Deprecate :encoding: option to .. plot::, which has no effect since 2011 +* :ghpull:`20541`: Minor fix +* :ghpull:`20539`: Document defaults in plot_directive. +* :ghpull:`20536`: Make most of annotation tutorial a comment, and remove figure titles. +* :ghpull:`20439`: Remove dead code from LGTM alerts. +* :ghpull:`20528`: Merge subplot_demo into subplot example. +* :ghpull:`20493`: Cleanup AnchoredOffsetbox-related demos. +* :ghpull:`20513`: Shorten the bxp docstring. +* :ghpull:`20507`: Merge subplot_toolbar example into subplots_adjust. +* :ghpull:`20505`: Add rc_context to customizing tutorial +* :ghpull:`20449`: Suppress repeated logwarns in postscript output. +* :ghpull:`20500`: DOC: Add twitter icon and fix logo link +* :ghpull:`20499`: Simplify plot types pie() +* :ghpull:`20495`: Fix shape of Z in contour docs +* :ghpull:`20497`: Remove obsolete footnote on pyside +* :ghpull:`20485`: DOC: hexbin 'extent' must be 4-tuple of float, not float +* :ghpull:`20466`: Various cleanups to pgf backend. +* :ghpull:`20474`: Make lack of support more explicit for non-postscript fonts + usetex. +* :ghpull:`20476`: give Font a root widget +* :ghpull:`20477`: remove _master attribute from FigureCanvasTk +* :ghpull:`19731`: DOC: first pass at switching to pydata theme +* :ghpull:`20475`: Less pyplot, more OO in docs. +* :ghpull:`20467`: Small cleanups to sphinxext.plot_directive. +* :ghpull:`20437`: Use packaging to do version comparisons. +* :ghpull:`20354`: Merge Colorbar and ColorbarBase. +* :ghpull:`20464`: tinypages/conf.py doesn't need to manipulate sys.path. +* :ghpull:`20420`: Add a select_overload helper for signature-overloaded functions. +* :ghpull:`20460`: Shorten the AnchoredOffsetbox docstring. +* :ghpull:`20458`: Set the axes of legend text +* :ghpull:`20438`: Fix deprecation of ``Tick.apply_tickdir``. +* :ghpull:`20457`: Rename data variables in histogram example. +* :ghpull:`20442`: Fix dvi baseline detector when ``\usepackage{chemformula}`` is used. +* :ghpull:`20454`: Tell LGTM to use Python 3 explicitly. +* :ghpull:`20446`: Make used tex packages consistent between ps and other backends. +* :ghpull:`20447`: Remove Figure/Axes/Axis deprecations from 3.3 +* :ghpull:`20414`: ENH: add colorbar info to gridspec cbar +* :ghpull:`20436`: Add missing super __init__ in subclasses +* :ghpull:`20284`: Use a GtkApplication in GTK backend. +* :ghpull:`20400`: Make pdftex.map parsing stricter +* :ghpull:`20292`: Cleanup plot types docs +* :ghpull:`20445`: Small cleanups to backend_ps. +* :ghpull:`20399`: Improve example for 3D polygons +* :ghpull:`20432`: Small doc cleanups. +* :ghpull:`20398`: Document Axes.get_aspect() +* :ghpull:`20428`: Deprecate public use of get_path_in_displaycoord. +* :ghpull:`20397`: Improve hexbin() documentation +* :ghpull:`20430`: Improve fancyarrow_demo. +* :ghpull:`20431`: Fix indentation of Arrow/Box/Connection styles tables. +* :ghpull:`20427`: Fix references in ArrowStyle docstring. +* :ghpull:`20346`: Clarify/Improve docs on family-names vs generic-families +* :ghpull:`20410`: PGF: Clip lines/markers to maximum LaTeX dimensions. +* :ghpull:`20363`: Don't disable path clipping on paths with codes. +* :ghpull:`20244`: Inline and simplify SubplotToolQt. +* :ghpull:`20165`: Slightly improve output of dvi debug utilities, and tiny cleanups. +* :ghpull:`20390`: Cleanup arrow_demo. +* :ghpull:`20408`: Remove mention of now-removed Encoding class. +* :ghpull:`20327`: FIX: fix colorbars with no scales +* :ghpull:`20215`: Quadmesh.set_array validates dimensions +* :ghpull:`20293`: Simplify font setting in usetex mode +* :ghpull:`20386`: Merge arrow_simple_demo into arrow_guide. +* :ghpull:`20348`: codecs.getwriter has simpler lifetime semantics than TextIOWrapper. +* :ghpull:`20132`: Create release notes page +* :ghpull:`20331`: Remove Axis, Tick, and Axes deprecations from 3.3 +* :ghpull:`20373`: Handle direction="column" in axes_grid.Grid +* :ghpull:`20394`: Remove separate section for support of 3d subplots. +* :ghpull:`20393`: Remove non-informative figure captions. +* :ghpull:`17453`: Displaying colorbars with specified boundaries correctly +* :ghpull:`20369`: Switch version scheme to release-branch-semver. +* :ghpull:`20377`: Cleanup some examples titles & texts. +* :ghpull:`20378`: Redirect agg_buffer{,_to_array} examples to canvasagg. +* :ghpull:`20376`: Small improvements to canvasagg example. +* :ghpull:`20365`: Reorganize a bit text-related rcs in matplotlibrc. +* :ghpull:`20362`: Add research notice +* :ghpull:`20353`: Remove incorrect statement about data-kwarg interface. +* :ghpull:`20343`: Fix exception handling when constructing C-level PathGenerator. +* :ghpull:`20349`: Fix missing write in TTStreamWriter::printf. +* :ghpull:`20347`: Fix possible refleak in PathGenerator. +* :ghpull:`20339`: Cleanup autoscale-related docstrings. +* :ghpull:`20338`: Fix some indent-related style lints. +* :ghpull:`20337`: Small unit-related cleanups. +* :ghpull:`20168`: FIX: clean up re-limiting hysteresis +* :ghpull:`20336`: Deduplicate color format specification +* :ghpull:`20334`: Remove need for ConversionInterface to support unitless values. +* :ghpull:`20020`: For polar plots, report cursor position with correct precision. +* :ghpull:`20319`: DOC: Tweaks to module API pages +* :ghpull:`20332`: Quadmesh's default value of shading is now set to 'flat' instead of False +* :ghpull:`20333`: Better align param comments in ``Legend.__init__`` signature. +* :ghpull:`20323`: Adding cla and remove to ColorbarAxes +* :ghpull:`20320`: Fix remaining E265 exceptions. +* :ghpull:`20318`: DOC: Fix missing refs in what's new pages +* :ghpull:`20315`: Fix spelling. +* :ghpull:`20291`: Write data parameter docs as regular parameter not as note (v2) +* :ghpull:`19908`: Implement get_cursor_data for QuadMesh. +* :ghpull:`20314`: MAINT: Removing deprecated colorbar functions. +* :ghpull:`20310`: Add test for font selection by texmanager. +* :ghpull:`19348`: Make YearLocator a subclass of RRuleLocator +* :ghpull:`20208`: Rewrite blocking_input to something much simpler. +* :ghpull:`19033`: Templatize class factories. +* :ghpull:`20309`: DOC: Spell out args/kwargs in examples/tutorials +* :ghpull:`20305`: Merge two axisartist examples and point to standard methods. +* :ghpull:`20306`: Document legend(handles=handles) signature +* :ghpull:`20311`: Warn if a non-str is passed to an rcParam requiring a str. +* :ghpull:`18472`: Adding a get_coordinates() method to Quadmesh collections +* :ghpull:`20032`: axvline()/axvspan() should not update r limits in polar plots. +* :ghpull:`20304`: Don't mention dviread in the PsfontsMap "missing entry" error message. +* :ghpull:`20308`: Remove outdated comment re: pgf/windows. +* :ghpull:`20302`: Further remove use of meshWidth, meshHeight in QuadMesh. +* :ghpull:`20101`: Fix ``Text`` class bug when ``font`` argument is provided without ``math_fontfamily`` +* :ghpull:`15436`: Allow imshow from float16 data +* :ghpull:`20299`: Simplify tfm parsing. +* :ghpull:`20290`: Support imshow(). +* :ghpull:`20303`: Remove tilde in code links where not necessary +* :ghpull:`19873`: Allow changing the vertical axis in 3d plots +* :ghpull:`19558`: Use luatex in --luaonly mode to query kpsewhich. +* :ghpull:`20301`: Clarify the effect of PolygonCollection properties on Quiver +* :ghpull:`20235`: Warn user when mathtext font is used for ticks +* :ghpull:`20237`: Make QuadMesh arguments with defaults keyword_only +* :ghpull:`20054`: Enh better colorbar axes +* :ghpull:`20164`: Auto-generate required kwdoc entries into docstring.interpd. +* :ghpull:`19677`: Convert axis limit units in Qt plot options widget +* :ghpull:`14913`: Reimplement NonUniformImage, PcolorImage in Python, not C. +* :ghpull:`20295`: Replace text._wrap_text by _cm_set(). +* :ghpull:`19859`: Write data parameter docs as regular parameter not as note +* :ghpull:`20273`: Fix cursor with toolmanager on GTK3. +* :ghpull:`20288`: Small markup fixes in api docs. +* :ghpull:`20276`: Tiny fixes to mathtext/usetex tutorials. +* :ghpull:`20084`: Add legend.labelcolor in rcParams +* :ghpull:`19253`: Improve font spec for SVG font referencing. +* :ghpull:`20278`: Deprecate public access to certain texmanager attributes. +* :ghpull:`19375`: Don't composite path-clipped image; forward suppressComposite as needed. +* :ghpull:`20190`: Simplify handling of uncomparable formats in tests. +* :ghpull:`20277`: Fix ordering of tex font usepackages. +* :ghpull:`20279`: Slightly reword intros of mpl_toolkits API docs. +* :ghpull:`20272`: De-duplicate fonts in LaTeX preamble. +* :ghpull:`15604`: Deprecate auto-removal of grid by pcolor/pcolormesh. +* :ghpull:`20193`: Simplify HostAxes.draw and its interaction with ParasiteAxes. +* :ghpull:`19441`: Make backend_gtk3foo importable on headless environments. +* :ghpull:`20126`: Simplify font_manager font enumeration logic. +* :ghpull:`19869`: Factor out x/y lo/hi handling in errorbar. +* :ghpull:`20173`: Rename (with deprecation) first parameter of grid() from b to visible. +* :ghpull:`19499`: Fully fold overset/underset into _genset. +* :ghpull:`20268`: Api pcolorargs deprecation +* :ghpull:`20264`: Fix blitting selector +* :ghpull:`20081`: Limit documenting special members to __call__ +* :ghpull:`20245`: MAINT: Removing deprecated ``offset_position`` from Collection +* :ghpull:`20218`: Update Axes showcase in "Embedding in Tk" example +* :ghpull:`20019`: Example: Cursor widget with text +* :ghpull:`20242`: Add comments and format Axis._get_coord_info +* :ghpull:`20207`: Move axisartist towards using standard Transforms. +* :ghpull:`20247`: Explicitly reject black autoformatting. +* :ghpull:`20217`: ci: Export sphinx-gallery run results to CircleCI. +* :ghpull:`20238`: Clarify docstring of ScalarMappable.set/get_array() +* :ghpull:`20239`: Style tables in style guide +* :ghpull:`19894`: Remove deprecated Qt4 backends +* :ghpull:`19937`: Add washing machine to Axes3D +* :ghpull:`20233`: Add a Ubuntu 20.04 / Python 3.9 CI run +* :ghpull:`20227`: Adding an equals method to colormaps +* :ghpull:`20216`: Documentation Style Guide for contributors +* :ghpull:`20222`: Fix C coverage +* :ghpull:`20221`: DOC: clarify that savefig(..., transparent=False) has no effect +* :ghpull:`20047`: Add labels parameter to set_ticks() +* :ghpull:`20118`: Convert FontEntry to a data class +* :ghpull:`19167`: Add support for HiDPI in TkAgg on Windows +* :ghpull:`18397`: fix cmr10 negative sign in cmsy10 (RuntimeWarning: Glyph 8722 missing) +* :ghpull:`20170`: SubplotParams.validate-associated fixes. +* :ghpull:`19467`: Shorten the implementation of violin(). +* :ghpull:`12226`: FIX: pcolor/pcolormesh honour edgecolors kwarg when facecolors is set 'none' +* :ghpull:`18870`: Expand ScalarMappable.set_array to accept array-like inputs +* :ghpull:`20073`: Support SubFigures in AxesDivider. +* :ghpull:`20209`: Deprecate cbook.report_memory. +* :ghpull:`20211`: Use check_getitem in legend location resolution. +* :ghpull:`20206`: Cleanup axisartist in preparation for future changes. +* :ghpull:`20191`: Small simplifications to FloatingAxesBase. +* :ghpull:`20189`: Add tests for ginput and waitforbuttonpress. +* :ghpull:`20199`: Make set_marker{edge,face}color(None) more consistent. +* :ghpull:`16943`: Changing get_cmap to return copies of the registered colormaps. +* :ghpull:`19483`: MNT: deprecate epoch2num/num2epoch +* :ghpull:`20201`: Simplify _process_plot_var_args.set_prop_cycle. +* :ghpull:`20197`: Speedup Line2D marker color setting. +* :ghpull:`20194`: Fix markup on MEP22. +* :ghpull:`20198`: Fix validation of Line2D color. +* :ghpull:`20046`: Deprecation warning +* :ghpull:`20144`: More tight_layout cleanups +* :ghpull:`20105`: Shorten Curve arrowstyle implementations. +* :ghpull:`19401`: Simplify axisartist line clipping. +* :ghpull:`19260`: Update round fix +* :ghpull:`20196`: Replaced links to colormap packages with link to third-party packages list in MPL docs +* :ghpull:`18819`: Usage guide edit +* :ghpull:`18346`: Soft-deprecate Axes.plot_date() +* :ghpull:`20187`: Merge v3.4.x up into master +* :ghpull:`15333`: Enh: DivergingNorm Fair +* :ghpull:`20188`: Remove 3.3 deprecations in cbook. +* :ghpull:`20177`: Fix broken test re: polar tick visibility. +* :ghpull:`20026`: DOC: move third-party packages to new page +* :ghpull:`19994`: Don't hide shared "x/y"ticklabels for grids of non-rectilinear axes. +* :ghpull:`20150`: Rename mosaic layout +* :ghpull:`19369`: Add Artist._cm_set for temporarily setting an Artist property. +* :ghpull:`15889`: Add svg logo icon +* :ghpull:`20140`: DOC: make 2x versions of all gallery figures +* :ghpull:`20155`: Fix wheel builds on CI +* :ghpull:`19951`: Convert Qhull wrapper to C++ and array_view +* :ghpull:`19918`: Cleanup some consistency in contour extensions +* :ghpull:`20153`: Fix wheel builds on CI +* :ghpull:`19363`: Create box3d example +* :ghpull:`20129`: Cleanup some "variable assigned but not used" lints. +* :ghpull:`20107`: Support full-sharex/y in subplot_mosaic. +* :ghpull:`20094`: Switch _auto_adjust_subplotpars to take rowspan/colspan as input. +* :ghpull:`16368`: Improve warning for unsupported scripts. +* :ghpull:`19660`: Allow PolygonSelector points to be removed +* :ghpull:`16291`: Split Norm and LinearNorm up +* :ghpull:`20119`: Cleanup flake8 exceptions for examples +* :ghpull:`20109`: Fix trailing text in doctest-syntax plot_directive. +* :ghpull:`19538`: Speedup pdftex.map parsing. +* :ghpull:`20003`: Bump minimum NumPy to 1.17 +* :ghpull:`20074`: Copy-edit axes_grid tutorial. +* :ghpull:`20124`: Remove workaround unneeded on Py3.7+, which we require now. +* :ghpull:`20120`: Cleanup subsetting tool. +* :ghpull:`20108`: Skip back-and-forth between pixels and points in contour code. +* :ghpull:`20106`: Shorten bracket arrowstyle docs. +* :ghpull:`20090`: Cleanup anchored_artists, inset_locator docstrings. +* :ghpull:`20097`: Use nullcontext more as do-nothing context manager. +* :ghpull:`20095`: Remove 3.3 ticker deprecations +* :ghpull:`20064`: Expire deprecation of AxesDivider defaulting to zero pads. +* :ghpull:`20091`: Cleanup tight_layout. +* :ghpull:`20069`: Don't make VBoxDivider inherit from HBoxDivider. +* :ghpull:`20078`: Remove some usages of OrderedDict +* :ghpull:`20077`: Expire Artist.set() property reordering +* :ghpull:`20070`: Harmonize descriptions of the 'anchor' parameter. +* :ghpull:`20011`: Move development dependencies to dependencies page +* :ghpull:`20072`: Improve labeling in simple_axes_divider1 example. +* :ghpull:`20063`: Deprecate some untested, never used axes_grid1 methods. +* :ghpull:`20065`: Deprecate AxesDivider.append_axes(..., add_to_figure=True). +* :ghpull:`20066`: Cleanup axes_divider docstrings, and detail calculations. +* :ghpull:`20059`: Include left and right titles for labeling axes in qt axes selector. +* :ghpull:`20052`: Remove axes_grid/axisartist APIs deprecated in Matplotlib 3.3. +* :ghpull:`18807`: make FancyArrow animatable +* :ghpull:`15281`: Don't use ImageGrid in demo_text_rotation_mode. +* :ghpull:`20051`: Remove offsetbox APIs deprecated in Matplotlib 3.3. +* :ghpull:`14854`: Improved dev installation documentation +* :ghpull:`18900`: Enh better colorbar axes +* :ghpull:`20042`: DOC: fix typos +* :ghpull:`13860`: Deprecate {Locator,Formatter}.set_{{view,data}_interval,bounds}. +* :ghpull:`20028`: Shorten the repr of scaling transforms. +* :ghpull:`20027`: Fix axvspan for drawing slices on polar plots. +* :ghpull:`20024`: Small fixes to latex-related docs. +* :ghpull:`20023`: Simplify _redo_transform_rel_fig. +* :ghpull:`20012`: Fix default theta tick locations for non-full-circle polar plots. +* :ghpull:`20021`: DOC: fix typos +* :ghpull:`20013`: Move restriction of polar theta scales to ThetaAxis._set_scale. +* :ghpull:`20010`: DOC: fix heading level for plot_types/stats +* :ghpull:`20000`: Remove ax fixture from category tests. +* :ghpull:`20007`: Correct minor typos in legend.py and autoscale.py +* :ghpull:`20005`: DOC: Fix numpydoc syntax, and parameters names. +* :ghpull:`19996`: Small simplification to RadialLocator. +* :ghpull:`19968`: ENH: draw no output +* :ghpull:`19657`: Allow Selectors to be dragged from anywhere within their patch +* :ghpull:`19304`: Add legend title font properties +* :ghpull:`19977`: Fix doc build +* :ghpull:`19974`: CI: update the ssh key used to push the devdocs +* :ghpull:`9888`: Add an Annulus patch class +* :ghpull:`13680`: Update seaborn style +* :ghpull:`19967`: ENH: add user-facing no-output draw +* :ghpull:`19765`: ENH: use canvas renderer in draw +* :ghpull:`19525`: Don't create page transparency group in pdf output (for pdftex compat). +* :ghpull:`19952`: avoid implicit np.array -> float conversion +* :ghpull:`19931`: Remove now unused patches to ttconv. +* :ghpull:`19934`: Deprecate drawtype to RectangleSelector +* :ghpull:`19941`: Simplify 3D random walk example +* :ghpull:`19926`: Move custom scales/custom projections docs to module docstrings. +* :ghpull:`19898`: Remove 3.3 backend deprecations +* :ghpull:`19901`: Remove 3.3 rcParam deprecations +* :ghpull:`19900`: Remove 3.3 text deprecations +* :ghpull:`19922`: Remove 3.3 deprecated modules +* :ghpull:`19925`: Include projections.geo in api docs. +* :ghpull:`19924`: Discourage use of imread & improve its docs. +* :ghpull:`19866`: Switch to asciiart for boxplot illustration. +* :ghpull:`19912`: Add symlog to figureoptions scalings +* :ghpull:`19564`: Micro-optimize type1font loading +* :ghpull:`19623`: FIX: Contour lines rendered incorrectly when closed loops +* :ghpull:`19902`: Implement ``ArtistList.__[r]add__``. +* :ghpull:`19904`: Don't set zoom/pan cursor for non-navigatable axes. +* :ghpull:`19909`: Use unicode when interactively displaying 3d azim/elev. +* :ghpull:`19905`: pyplot: do not apply kwargs twice in to x/yticklabels +* :ghpull:`19126`: Move pixel ratio handling into FigureCanvasBase +* :ghpull:`19897`: DOC/MNT fix make clean for plot_types +* :ghpull:`19858`: Move Line2D units handling to Axes & deprecate "units finalize" signal. +* :ghpull:`19889`: Include length in ArtistList repr. +* :ghpull:`19887`: Fix E265 in test files. +* :ghpull:`19882`: Use ax.set() for a more compact notation of styling in plot types docs +* :ghpull:`17231`: Fix errobar order +* :ghpull:`19703`: DOC: new plot gallery +* :ghpull:`19825`: Factor out machinery for running subprocess tk tests. +* :ghpull:`19872`: Fix unit handling in errorbar for astropy. +* :ghpull:`19526`: Apply unit conversion early in errorbar(). +* :ghpull:`19855`: Correct handle default backend. +* :ghpull:`18216`: Combine Axes.{lines,images,collections,patches,text,tables} into single list +* :ghpull:`19853`: Consistent corner variables names in widgets.py +* :ghpull:`19575`: Deprecate Text.get_prop_tup. +* :ghpull:`19810`: Remove JPEG-specific parameters and rcParams. +* :ghpull:`19666`: Change dictionary to list of tuples to permit duplicate keys +* :ghpull:`19400`: Fix tk event coordinates in the presence of scrollbars. +* :ghpull:`19603`: Remove matplotlibrc.template. +* :ghpull:`19835`: Merge v3.4.x into master +* :ghpull:`19821`: Hide stderr output from subprocess call in test suite. +* :ghpull:`19819`: Correct small typos in _axes.py and legend.py +* :ghpull:`19795`: Remove usetex-related APIs deprecated in Matplotlib 3.3. +* :ghpull:`19789`: Fix zorder handling for OffsetBoxes and subclasses. +* :ghpull:`19796`: Expire ````keymap.all_axes````-related deprecations. +* :ghpull:`19806`: Remove outdated api changes notes. +* :ghpull:`19801`: Expire deprecation of mathtext.fallback_to_cm. +* :ghpull:`12744`: Explicit plotorder +* :ghpull:`19681`: Merge branch 'v3.4.x' into master +* :ghpull:`18971`: Switch to setuptools_scm. +* :ghpull:`19727`: DOC: simplify API index +* :ghpull:`19760`: Speed up _delete_parameter. +* :ghpull:`19756`: Minor cleanup of documentation guide +* :ghpull:`19752`: Cleanup backend_tools docstrings, and minor refactorings. +* :ghpull:`19552`: Remove scalarmappable private update attributes +* :ghpull:`19728`: Factor out clip-path attr handling in backend_svg. +* :ghpull:`19540`: Share subplots() label visibility handling with label_outer(). +* :ghpull:`19753`: Cleanup string formatting in backend_pgf. +* :ghpull:`19750`: Simplify maxdict implementation. +* :ghpull:`19749`: Remove unused _find_dedent_regex & _dedent_regex. +* :ghpull:`19751`: Update some matplotlib.lines docstrings. +* :ghpull:`13072`: ENH: add figure.legend; outside kwarg for better layout outside subplots +* :ghpull:`19740`: Minor backend docstring fixes. +* :ghpull:`19734`: Remove unused _fonts attribute in RendererSVG. +* :ghpull:`19733`: Reword AutoDateFormatter docs. +* :ghpull:`19718`: Small style fixes to matplotlibrc.template. +* :ghpull:`19679`: Add inheritance diagram to patches docs +* :ghpull:`19717`: Don't sort lexicographially entries in SVG output. +* :ghpull:`19716`: Fix colon placement in issue template. +* :ghpull:`19704`: Cleanup license page in docs +* :ghpull:`19487`: Deprecate unused \*args to print_. +* :ghpull:`19654`: Dedupe various method implementations using functools.partialmethod. +* :ghpull:`19655`: Deprecate Tick.apply_tickdir. +* :ghpull:`19653`: deprecate_privatize_attribute also works for privatizing methods. +* :ghpull:`19646`: Add angle setter/getter to Rectangle +* :ghpull:`19659`: Improve docs for rgba conversion +* :ghpull:`19641`: Fix Bbox.frozen() not copying minposx/minposy +* :ghpull:`19626`: Clean up E265 in examples. +* :ghpull:`19622`: Prefer Axes.remove() over Figure.delaxes() in docs. +* :ghpull:`19621`: Dedupe docstrings of Figure.{get_axes,axes}. +* :ghpull:`19600`: DOC: better intro for dates.py +* :ghpull:`19606`: Remove versionadded notes; correct doc link +* :ghpull:`19620`: Remove suggestion to remove rk4/rk45 integrators from streamplot. +* :ghpull:`19586`: DOC: more improve date example +* :ghpull:`19566`: add docstring to ax.quiver +* :ghpull:`19601`: Handle None entries in sys.modules. +* :ghpull:`19517`: Deprecate toplevel is_url, URL_REGEX helpers. +* :ghpull:`19570`: Dedupe part of error message in check_in_list. +* :ghpull:`14508`: Add force_zorder parameter +* :ghpull:`19585`: Deprecate trivial helpers in style.core. +* :ghpull:`19534`: BUG: fill_between with interpolate=True and NaN. +* :ghpull:`18887`: FIX: Generalize Colorbar Scale Handling +* :ghpull:`16788`: Adding png image return for inline backend figures with _repr_html_ + +Issues (187): + +* :ghissue:`21518`: [Bug]: Datetime axis with usetex is unclear +* :ghissue:`21509`: [Bug]: Text sometimes is missing when figure saved to EPS +* :ghissue:`21569`: [Bug]: AttributeError: 'NoneType' object has no attribute 'dpi' after drawing and removing contours inside artist +* :ghissue:`21612`: [Bug]: Security.md out of date +* :ghissue:`21608`: [Doc]: ``ax.voxels`` links to wrong method. +* :ghissue:`21528`: [Doc]: Outdated QT_API docs +* :ghissue:`21517`: [Bug]: this example shows ok on matplotlib-3.4.3, but not in matplotlib-3.5.0 master of october 30th +* :ghissue:`21548`: [Bug]: blocking_input +* :ghissue:`21552`: [Bug]: eventplot cannot handle multiple datetime-based series +* :ghissue:`21441`: [Bug]: axes(position = [...]) behavior +* :ghissue:`10346`: Passing clim as keyword argument to pcolormesh does not change limits. +* :ghissue:`21480`: [Bug]: Subfigure breaks for some ``Gridspec`` slices when using ``constrained_layout`` +* :ghissue:`20989`: [Bug]: regression with setting ticklabels for colorbars in matplotlib 3.5.0b1 +* :ghissue:`21474`: [Doc]: Suggestion to use PIL.image.open is not a 1:1 replacement for imread +* :ghissue:`19634`: Multicursor docstring missing a Parameters Section +* :ghissue:`20847`: [Bug]: Contourf not filling contours. +* :ghissue:`21300`: [Bug]: zooming in on contour plot gives false extra contour lines +* :ghissue:`21466`: [Bug]: EPS export shows hidden tick labels when using tex for text rendering +* :ghissue:`21463`: [Bug]: Plotting lables with Greek latters in math mode produces Parsing error when plt.show() runs +* :ghissue:`20534`: Document formatting for for sections +* :ghissue:`21246`: [Doc]: Install info takes up too much room on new front page +* :ghissue:`21432`: [Doc]: Double clicking parameter name also highlights next item of text +* :ghissue:`21310`: [Bug]: contour on 3d plot fails if x and y are 1d and different lengths +* :ghissue:`18213`: Figure out why test_interactive_backend fails on Travis macOS +* :ghissue:`21090`: [MNT]: Should set_size_inches be updated to use device_pixel_ratio? +* :ghissue:`13948`: Allow colorbar.ax.set_ylim to set the colorbar limits? +* :ghissue:`21314`: Inconsistensy in ``pyplot.pcolormesh`` docstring regarding default value for ``shading`` +* :ghissue:`21320`: [Doc]: Incorrect image caption in imshow() example +* :ghissue:`21311`: [Doc]: dead link for agg +* :ghissue:`20929`: [Bug]: PyPy Win64 wheels use incorrect version +* :ghissue:`21202`: [Bug]: python3.7/site-packages/matplotlib/ft2font.so: Undefined symbol "FT_Done_Glyph" +* :ghissue:`20932`: Qt Ctrl-C broken on windows +* :ghissue:`21230`: [Doc]: [source] links is devdocs are broken +* :ghissue:`20906`: 3.5.0b1: ax.contour generates different artists +* :ghissue:`21161`: [Doc]: In new docs, "Usage guide" entry in the top menu does not link to the "Usage guide" +* :ghissue:`21016`: [Bug] Error: 'PathCollection' object has no attribute 'do_3d_projection' when doing contourf in 3d with extend = 'both' +* :ghissue:`21135`: [Doc]: Data parameter description is not always replaced +* :ghissue:`4132`: Support clim kwarg in pcolor-type plots +* :ghissue:`21110`: Qt swapping ctrl and cmd on OSX +* :ghissue:`20912`: [ENH]: data kwarg support for mplot3d +* :ghissue:`15005`: Cleanup API for setting ticks +* :ghissue:`21095`: [ENH]: A data-type check is missed in cm.ScalarMappable.set_array() +* :ghissue:`7711`: Colorbar: changing the norm does not update the Formatter +* :ghissue:`18925`: Removing axes created by twiny() leads to an error +* :ghissue:`21057`: [Bug]: distutils deprecation +* :ghissue:`21024`: [ENH]: Cairo backends do not fully support HiDPI +* :ghissue:`20811`: Python 3.10 manylinux wheels +* :ghissue:`11509`: On making the rc-validators function know the rcParam affected instance +* :ghissue:`20516`: Sketch params ignored when using PGF backend +* :ghissue:`20963`: [Bug]: broken 'proportional' colorbar when using contourf+cmap+norm+extend +* :ghissue:`13974`: [DOC] Undocumented behavior in streamplot +* :ghissue:`16251`: API changes are too hard to find in the rendered docs +* :ghissue:`20770`: [Doc]: How to replicate behaviour of ``plt.gca(projection=...)``? +* :ghissue:`17052`: Colorbar update error with clim change in multi_image.py example +* :ghissue:`4387`: make ``Normalize`` objects notifiy scalar-mappables on changes +* :ghissue:`20001`: rename fig.draw_no_output +* :ghissue:`20936`: [Bug]: edgecolor 'auto' doesn't work properly +* :ghissue:`20909`: [Bug]: Animation error message +* :ghissue:`6864`: Add release dates to what's new page +* :ghissue:`20905`: [Bug]: error plotting z-axis array with np.nan -- does not plot with cmap option (surface plot) +* :ghissue:`20618`: BUG: Lost functionality of interactive selector update +* :ghissue:`20791`: [Bug]: spines and ticklabels +* :ghissue:`20723`: Adding a legend to a ``SubFigure`` doesn't work +* :ghissue:`20637`: PyPy wheels are pinned to v3.3, so pypy-based wheels for latest versions are not available +* :ghissue:`19160`: pypy failures +* :ghissue:`20385`: Add ']->' , '<-[' arrowstyles +* :ghissue:`19016`: Move away from set_ticklabels() +* :ghissue:`20800`: [Bug]: Setting backend in custom style sheet raises UserWarning +* :ghissue:`20809`: [Bug]: \Cap and \Cup in mathtext are inconsistent +* :ghissue:`20762`: [Doc]: Add external resources to devdoc landing page +* :ghissue:`18490`: Add a method to access the list of registered colormaps +* :ghissue:`20666`: Interactive SpanSelector no longer notifies when the selector is removed by an "empty" click +* :ghissue:`20552`: Expose legend's line: ``legline._legmarker`` as public +* :ghissue:`18391`: Bug? Legend Picking Not Working on Marker +* :ghissue:`11357`: Unable to retrieve marker from legend handle +* :ghissue:`2035`: legend marker update bug +* :ghissue:`19748`: Incorrect & inconsistent coloring in .imshow() with LogNorm +* :ghissue:`18735`: imshow padding around NaN values +* :ghissue:`7928`: [Bug] backend_bases.key_press_handler sneakily uses digit keys +* :ghissue:`20802`: Add ability to disable callbacks temporarily +* :ghissue:`16470`: Inconsistent Corner Masking w/ plot_surface +* :ghissue:`12395`: Rendering issue occurs when plotting 3D surfaces at a discontinuity +* :ghissue:`8222`: matplotlib 3D surface - gaps / holes in surface +* :ghissue:`4941`: Axes3d plot_surface not supporting masked arrays? +* :ghissue:`487`: Plotting masked arrays with plot_surface() +* :ghissue:`20794`: [Doc]: "Bachelor's degrees by gender" example is more or less dufte +* :ghissue:`20557`: Have ``[Source]`` in api docs link to github +* :ghissue:`20754`: [Doc]: legend guide should be OO +* :ghissue:`17770`: animation.save and fig.savefig interfere with each other and raise StopIteration +* :ghissue:`20785`: [Bug]: Colorbar creation from pcolormesh with cell specific alpha values +* :ghissue:`19843`: collection with alpha + colorer +* :ghissue:`20698`: collections.Collections offset improvements +* :ghissue:`17774`: Cannot make Latex plots when Pandas dataframe has underscore in variable name +* :ghissue:`19884`: Better document Axes.set() +* :ghissue:`20760`: [Bug]: subfigure position shifts on y-axis when x kwarg added to supxlabel +* :ghissue:`20296`: colorbar set_ticklabels - text properties not working +* :ghissue:`18191`: PostScript Type42 embedding is broken in various ways +* :ghissue:`11303`: Using fonttype 42 will make the produced PDF size considerably larger when the image has Chinese characters +* :ghissue:`20735`: The top level of the docs needs modification +* :ghissue:`20684`: make_keyword_only doesn't work for pyplot-wrapped methods +* :ghissue:`20635`: DOC: Document patch deprecation +* :ghissue:`17473`: Issue with appearance of RectangleSelector +* :ghissue:`20616`: Type 42 chars beyond BMP not displayed in PDF +* :ghissue:`20658`: MAINT: CircleCI build merged PRs +* :ghissue:`18312`: Add easily comparable version info to toplevel +* :ghissue:`20665`: interactive SpanSelector incorrectly forces axes limits to include 0 +* :ghissue:`20614`: Missing kerning in PDFs with Type 42 font +* :ghissue:`20640`: Column direction breaks label mode L for AxesGrid. +* :ghissue:`20581`: Change in custom norm colour map display +* :ghissue:`20595`: Triple and quadruple dot Mathtext accents don't stack or align. +* :ghissue:`19755`: Avoid showing a black background before the plot is ready with Qt5agg backend +* :ghissue:`10235`: Why not get the same clear image on a high-resolution screen? +* :ghissue:`20479`: ColorbarAxes is an imperfect proxy for the Axes passed to Colorbar +* :ghissue:`18965`: Figure options with qt backend breaks +* :ghissue:`19256`: New Styling for Sliders +* :ghissue:`14148`: zorder ignored in mplot3d +* :ghissue:`20523`: plot_directive is confused by include directives, part 2 (context option) +* :ghissue:`17860`: Plot directive may be confused by ``..include::`` +* :ghissue:`19431`: Tricontour documentation and examples should be updated in line with contour +* :ghissue:`20508`: rcParams['axes.autolimit_mode'] = 'round_numbers' is broken +* :ghissue:`20289`: Simplify font setting in usetex mode +* :ghissue:`20370`: Test Coverage for TextBox +* :ghissue:`20522`: Improve 'Writing ReST Pages' section on docs +* :ghissue:`19259`: Set legend title font properties +* :ghissue:`20049`: add legend.labelcolor "argument" to mplstyle stylesheet +* :ghissue:`20452`: Wrong/not useful error message when plotting incompatible x and y +* :ghissue:`20266`: "$$" can not be displayed by ax.text() +* :ghissue:`20517`: Wrong shape of Z in documentation of contour +* :ghissue:`19423`: Switch to pydata-sphinx-theme +* :ghissue:`20435`: Legend Text's ``axes`` attribute is ``None`` +* :ghissue:`20379`: Change name of variables in histogram example +* :ghissue:`20440`: Wrong text vertical position with LaTeX enabled +* :ghissue:`10042`: Inconsistent use of graphicx and color packages in LaTeX preambles +* :ghissue:`4482`: PGF Backend: "Dimension too large" error while processing log-scale plot +* :ghissue:`20324`: New colorbar doesn't handle norms without a scale properly... +* :ghissue:`17508`: Quadmesh.set_array should validate dimensions +* :ghissue:`20372`: Incorrect axes positioning in axes_grid.Grid with direction='column' +* :ghissue:`19419`: Dev version hard to check +* :ghissue:`17310`: Matplotlib git master version fails to pass serveral pytest's tests. +* :ghissue:`7742`: plot_date() after axhline() doesn't rescale axes +* :ghissue:`20322`: QuadMesh default for shading inadvertently changed. +* :ghissue:`9653`: SVG savefig + LaTeX extremely slow on macOS +* :ghissue:`20099`: ``fontset`` from ``mathtext`` throwing error after setting Text ``font=`` +* :ghissue:`18399`: How to get Quadmesh coordinates +* :ghissue:`15432`: Add support in matplotlib.pyplot.imshow for float16 +* :ghissue:`20298`: plt.quiver linestyle option doesn't work?..... +* :ghissue:`19075`: Qt backend's Figure options to support axis units +* :ghissue:`15039`: NonUniformImage wrong image when using large values for axis +* :ghissue:`18499`: Saving as a pdf ignores ``set_clip_path`` when there is more than one of them. +* :ghissue:`15600`: Grid disappear after pcolormesh apply +* :ghissue:`20080`: API docs currently include entries for class ``__dict__``, ``__module__``, ``__weakref__`` +* :ghissue:`20159`: Zoom in NavigationToolbar2Tk stops working after updating the canvas figure. +* :ghissue:`17007`: Computer Modern Glyph Error +* :ghissue:`19494`: Update azure ubuntu images to 18.04, or update texlive in CI +* :ghissue:`18841`: ScalarMappable should copy its input and allow non-arrays +* :ghissue:`20121`: Adding cmocean and CMasher to the colormaps tutorial +* :ghissue:`18154`: Deprecate plot_date() +* :ghissue:`7413`: Autoscaling has fundamental problems +* :ghissue:`19627`: Replace use of Python/C API with numpy::array_view in _tri.cpp and qhull_wrap.c +* :ghissue:`19111`: plot_directive errantly tries to run code +* :ghissue:`11007`: BUG: Plot directive fails if its content ends with a normal text line (sphinxext) +* :ghissue:`19929`: Selecting axes when customizing gives +* :ghissue:`19578`: bisect very hard with rcParam changes +* :ghissue:`19506`: Allow saving PDF files without a page group +* :ghissue:`19906`: symlog is not in scale setting +* :ghissue:`19568`: Contour lines are rendered incorrectly when closed loops +* :ghissue:`19890`: Should ArtistList implement ``__add__``? +* :ghissue:`14405`: ENH: Add HiDPI physical to logical pixel ratio property +* :ghissue:`17139`: errorbar doesn't follow plot order +* :ghissue:`18277`: Create new sphinx gallery page for "Chart Types" +* :ghissue:`15446`: the python script in Catalina dock icon display wrong +* :ghissue:`19848`: ValueError: Key backend: '' is not a valid value for backend +* :ghissue:`1622`: zorder is not respected by all parts of ``errorbar`` +* :ghissue:`17247`: Move towards making Axes.lines, Axes.patches, ... read-only views of a single child list. +* :ghissue:`19842`: UserWarning: "Trying to register the cmap '...' which already exists" is not very helpful. +* :ghissue:`7962`: pip interprets Matplotlib dev version as stable +* :ghissue:`19607`: Curves with same label not appearing in Figure options (only the last one) +* :ghissue:`17584`: NavigationToolbar2Tk behave unexpected when using it in with Tkinter Canvas +* :ghissue:`19838`: Unexpected behaviour of imshow default interpolation +* :ghissue:`7650`: anchored_artists don't support zorder argument +* :ghissue:`19687`: License doc cleanup +* :ghissue:`19635`: Multicursor updates to events for any axis +* :ghissue:`17967`: Document how to use mathtext to obtain unicode minus instead of dashes for negative numbers +* :ghissue:`8519`: Closed figures linger in memory +* :ghissue:`14175`: RFC: Allow users to force zorder in 3D plots +* :ghissue:`19464`: Quiver docs don't have a return section +* :ghissue:`18986`: fill_between issue with interpolation & NaN diff --git a/doc/users/prev_whats_new/github_stats_3.5.1.rst b/doc/users/prev_whats_new/github_stats_3.5.1.rst new file mode 100644 index 000000000000..7eb37b769d6c --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.5.1.rst @@ -0,0 +1,151 @@ +.. _github-stats-3-5-1: + +GitHub statistics for 3.5.1 (Dec 11, 2021) +========================================== + +GitHub statistics for 2021/11/16 (tag: v3.5.0) - 2021/12/11 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 29 issues and merged 84 pull requests. +The full list can be seen `on GitHub `__ + +The following 17 authors contributed 123 commits. + +* Antony Lee +* Constantine Evans +* David Stansby +* Elliott Sales de Andrade +* franzhaas +* Greg Lucas +* Hansin Ahuja +* Hood Chatham +* Jake Lishman +* Jody Klymak +* Matthias Bussonnier +* Ryan May +* Steffen Rehberg +* Sven Eschlbeck +* sveneschlbeck +* Thomas A Caswell +* Tim Hoffmann + +GitHub issues and pull requests: + +Pull Requests (84): + +* :ghpull:`21926`: Backport PR #21913 on branch v3.5.x (Make colorbar boundaries work again) +* :ghpull:`21924`: Backport PR #21861 on branch v3.5.x (DOC: Small formatting improvement to set_markevery) +* :ghpull:`21913`: Make colorbar boundaries work again +* :ghpull:`21922`: Backport PR #21753 on branch v3.5.x (DOC: update anatomy of figure) +* :ghpull:`21861`: DOC: Small formatting improvement to set_markevery +* :ghpull:`21919`: Fix use_data_coordinates docstring +* :ghpull:`21912`: Backport PR #21900 on branch v3.5.x (Include test notebooks in test package) +* :ghpull:`21900`: Include test notebooks in test package +* :ghpull:`21908`: Backport PR #21834 on branch v3.5.x (MAINT Fix signature qhull version function ) +* :ghpull:`21907`: Backport PR #21905 on branch v3.5.x (Fix image testing decorator in pytest importlib mode) +* :ghpull:`21906`: Backport PR #21773 on branch v3.5.x (FIX: Reset label of axis to center) +* :ghpull:`21834`: MAINT Fix signature qhull version function +* :ghpull:`21905`: Fix image testing decorator in pytest importlib mode +* :ghpull:`21773`: FIX: Reset label of axis to center +* :ghpull:`21902`: Backport PR #21884 on branch v3.5.x (FIX: be more careful about coercing unit-full containers to ndarray) +* :ghpull:`21884`: FIX: be more careful about coercing unit-full containers to ndarray +* :ghpull:`21899`: Backport PR #21859 on branch v3.5.x (Fix streamline plotting from upper edges of grid) +* :ghpull:`21859`: Fix streamline plotting from upper edges of grid +* :ghpull:`21896`: Backport PR #21890 on branch v3.5.x (Drop retina images when building PDF docs) +* :ghpull:`21891`: Backport PR #21887 on branch v3.5.x (Make figure target links relative) +* :ghpull:`21883`: Backport PR #21872 on branch v3.5.x (FIX: colorbars with NoNorm) +* :ghpull:`21872`: FIX: colorbars with NoNorm +* :ghpull:`21869`: Backport PR #21866 on branch v3.5.x (Shorten some inset_locator docstrings.) +* :ghpull:`21866`: Shorten some inset_locator docstrings. +* :ghpull:`21865`: Backport PR #21864 on branch v3.5.x (Delete "Load converter" example) +* :ghpull:`21864`: Delete "Load converter" example +* :ghpull:`21857`: Backport PR #21837 on branch v3.5.x (Display example figures in a single column) +* :ghpull:`21856`: Backport PR #21853 on branch v3.5.x (DOC: Fix Annotation arrow style reference example) +* :ghpull:`21853`: DOC: Fix Annotation arrow style reference example +* :ghpull:`21852`: Backport PR #21818 on branch v3.5.x (Fix collections coerce float) +* :ghpull:`21818`: Fix collections coerce float +* :ghpull:`21849`: Backport PR #21845 on branch v3.5.x (FIX: bbox subfigures) +* :ghpull:`21845`: FIX: bbox subfigures +* :ghpull:`21832`: Backport PR #21820 on branch v3.5.x (Drop setuptools-scm requirement in wheels) +* :ghpull:`21820`: Drop setuptools-scm requirement in wheels +* :ghpull:`21829`: Backport PR #21823 on branch v3.5.x (DOC: Misc rst syntax fixes) +* :ghpull:`21823`: DOC: Misc rst syntax fixes +* :ghpull:`21826`: Backport PR #21800 on branch v3.5.x (DOC: Update Basic Usage tutorial) +* :ghpull:`21814`: Manual backport of #21794 +* :ghpull:`21812`: Backport #21641 +* :ghpull:`21810`: Backport PR #21743 on branch v3.5.x (Clarify Annotation arrowprops docs) +* :ghpull:`21808`: Backport PR #21785 on branch v3.5.x (Fix ConciseDateFormatter offset during zoom) +* :ghpull:`21807`: Backport PR #21791 on branch v3.5.x (Refix check for manager presence in deprecated blocking_input.) +* :ghpull:`21806`: Backport PR #21663 on branch v3.5.x (Use standard subplot window in macosx backend) +* :ghpull:`21785`: Fix ConciseDateFormatter offset during zoom +* :ghpull:`21804`: Backport PR #21659 on branch v3.5.x (Fix PDF contents) +* :ghpull:`21791`: Refix check for manager presence in deprecated blocking_input. +* :ghpull:`21793`: Backport PR #21787 on branch v3.5.x (Fixes row/column mixup in GridSpec height_ratios documentation.) +* :ghpull:`21787`: Fixes row/column mixup in GridSpec height_ratios documentation. +* :ghpull:`21778`: Backport PR #21705 on branch v3.5.x (MNT: make print_figure kwarg wrapper support py311) +* :ghpull:`21779`: Backport PR #21751 on branch v3.5.x (FIX: manual colorbars and tight layout) +* :ghpull:`21777`: Backport PR #21758 on branch v3.5.x (FIX: Make sure a renderer gets attached to figure after draw) +* :ghpull:`21751`: FIX: manual colorbars and tight layout +* :ghpull:`21705`: MNT: make print_figure kwarg wrapper support py311 +* :ghpull:`21758`: FIX: Make sure a renderer gets attached to figure after draw +* :ghpull:`21775`: Backport PR #21771 on branch v3.5.x (DOC: fix missing ref) +* :ghpull:`21770`: Backport of PR #21631 on v3.5.x +* :ghpull:`21765`: Backport PR #21741 on branch v3.5.x (Reduce do_3d_projection deprecation warnings in external artists) +* :ghpull:`21764`: Backport PR #21762 on branch v3.5.x (FIX: align_x/ylabels) +* :ghpull:`21741`: Reduce do_3d_projection deprecation warnings in external artists +* :ghpull:`21762`: FIX: align_x/ylabels +* :ghpull:`21759`: Backport PR #21757 on branch v3.5.x (Fix doc typo.) +* :ghpull:`21704`: FIX: deprecation of render keyword to do_3d_projection +* :ghpull:`21730`: Backport PR #21727 on branch v3.5.x (Doc fix colormap inaccuracy) +* :ghpull:`21663`: Use standard subplot window in macosx backend +* :ghpull:`21725`: Backport PR #21681 on branch v3.5.x (Bind subplot_tool more closely to target figure.) +* :ghpull:`21665`: Include test notebooks in test package +* :ghpull:`21721`: Backport PR #21720 on branch v3.5.x (Fix compiler configuration priority for FreeType build) +* :ghpull:`21720`: Fix compiler configuration priority for FreeType build +* :ghpull:`21715`: Backport PR #21714 on branch v3.5.x (DOC: note renaming of config.cfg.template to mplconfig.cfg.template) +* :ghpull:`21706`: Backport PR #21703 on branch v3.5.x (Changed the link to the correct citing example) +* :ghpull:`21691`: Backport PR #21686 on branch v3.5.x (FIX: colorbar for horizontal contours) +* :ghpull:`21689`: Backport PR #21676 on branch v3.5.x (Fix boundary norm negative) +* :ghpull:`21686`: FIX: colorbar for horizontal contours +* :ghpull:`21681`: Bind subplot_tool more closely to target figure. +* :ghpull:`21676`: Fix boundary norm negative +* :ghpull:`21685`: Backport PR #21658 on branch v3.5.x (Validate that input to Poly3DCollection is a list of 2D array-like) +* :ghpull:`21684`: Backport PR #21662 on branch v3.5.x (FIX: put newline in matplotlibrc when setting default backend) +* :ghpull:`21658`: Validate that input to Poly3DCollection is a list of 2D array-like +* :ghpull:`21662`: FIX: put newline in matplotlibrc when setting default backend +* :ghpull:`21651`: Backport PR #21626 on branch v3.5.x (Added the definition of Deprecation and made Deprecation Process clearer) +* :ghpull:`21626`: Added the definition of Deprecation and made Deprecation Process clearer +* :ghpull:`21137`: Small cleanups to colorbar. + +Issues (29): + +* :ghissue:`21909`: [Bug]: Matplotlib is unable to apply the boundaries in the colorbar after updating to 3.5.0 +* :ghissue:`21654`: [Bug]: test_nbagg_01.ipynb not installed +* :ghissue:`21885`: [Bug]: test decorator breaks with new pytest importlib mode +* :ghissue:`21772`: [Bug]: cannot reset label of axis to center +* :ghissue:`21669`: [Bug]: Matplotlib 3.5 breaks unyt integration of error bars +* :ghissue:`21649`: [Bug]: Startpoints in streamplot fail on right and upper edges +* :ghissue:`21870`: [Bug]: Colormap + NoNorm only plots one color under ``matplotlib`` 3.5.0 +* :ghissue:`21882`: [Bug]: Colorbar does not work for negative values with contour/contourf +* :ghissue:`21803`: [Bug]: using ``set_offsets`` on scatter object raises TypeError +* :ghissue:`21839`: [Bug]: Top of plot clipped when using Subfigures without suptitle +* :ghissue:`21841`: [Bug]: Wrong tick labels and colorbar of discrete normalizer +* :ghissue:`21783`: [MNT]: wheel of 3.5.0 apears to depend on setuptools-scm which apears to be unintentional +* :ghissue:`21733`: [Bug]: Possible bug on arrows in annotation +* :ghissue:`21749`: [Bug]: Regression on ``tight_layout`` when manually adding axes for colorbars +* :ghissue:`19197`: Unexpected error after using Figure.canvas.draw on macosx backend +* :ghissue:`13968`: ``ax.get_xaxis().get_minorticklabels()`` always returns list of empty strings +* :ghissue:`7550`: Draw not caching with macosx backend +* :ghissue:`21740`: [Bug]: unavoidable ``DeprecationWarning`` when using ``Patch3D`` +* :ghissue:`15884`: DOC: Error in colormap manipulation tutorial +* :ghissue:`21648`: [Bug]: subplot parameter window appearing 1/4 size on macosx +* :ghissue:`21702`: [Doc]: Wrong link to the ready-made citation entry +* :ghissue:`21683`: [Bug]: add_lines broken for horizontal colorbars +* :ghissue:`21680`: [MNT]: macosx subplot parameters multiple windows +* :ghissue:`21679`: [MNT]: Close subplot_parameters window when main figure closes +* :ghissue:`21671`: [Bug]: 3.5.0 colorbar ValueError: minvalue must be less than or equal to maxvalue +* :ghissue:`21652`: [Bug]: ax.add_collection3d throws warning Mean of empty slice +* :ghissue:`21660`: [Bug]: mplsetup.cfg parsing issue +* :ghissue:`21668`: [Bug]: New plot directive error in 3.5.0 +* :ghissue:`21393`: [Doc]: describe deprecation process more explicitly diff --git a/doc/users/prev_whats_new/github_stats_3.5.2.rst b/doc/users/prev_whats_new/github_stats_3.5.2.rst new file mode 100644 index 000000000000..66f53d8e3672 --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.5.2.rst @@ -0,0 +1,335 @@ +.. _github-stats-3-5-2: + +GitHub statistics for 3.5.2 (May 02, 2022) +========================================== + +GitHub statistics for 2021/12/11 (tag: v3.5.1) - 2022/05/02 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 61 issues and merged 222 pull requests. +The full list can be seen `on GitHub `__ + +The following 30 authors contributed 319 commits. + +* Adeel Hassan +* Aitik Gupta +* Andrew Fennell +* andrzejnovak +* Antony Lee +* Clément Phan +* daniilS +* David Poznik +* David Stansby +* dependabot[bot] +* Edouard Berthe +* Elliott Sales de Andrade +* Greg Lucas +* Hassan Kibirige +* Jake VanderPlas +* Jay Stanley +* Jody Klymak +* MAKOMO +* Matthias Bussonnier +* Niyas Sait +* Oscar Gustafsson +* Pieter P +* Qijia Liu +* Quentin Peter +* Raphael Quast +* richardsheridan +* root +* Steffen Rehberg +* Thomas A Caswell +* Tim Hoffmann + +GitHub issues and pull requests: + +Pull Requests (222): + +* :ghpull:`22963`: Backport PR #22957 on branch v3.5.x (fix "is" comparison for np.array) +* :ghpull:`22951`: Backport PR #22946: FIX: Handle no-offsets in collection datalim +* :ghpull:`22957`: fix "is" comparison for np.array +* :ghpull:`22962`: Backport PR #22961 on branch v3.5.x (Raised macosx memory leak threshold) +* :ghpull:`22961`: Raised macosx memory leak threshold +* :ghpull:`22945`: FIX: Handle no-offsets in collection datalim +* :ghpull:`22946`: FIX: Handle no-offsets in collection datalim (alternative) +* :ghpull:`22944`: Backport PR #22907 on branch v3.5.x (Fix quad mesh cursor data) +* :ghpull:`22943`: Backport PR #22923 on branch v3.5.x (Fixed _upcast_err docstring and comments in _axes.py) +* :ghpull:`22907`: Fix quad mesh cursor data +* :ghpull:`22923`: Fixed _upcast_err docstring and comments in _axes.py +* :ghpull:`22876`: Backport PR #22560 on branch v3.5.x (Improve pandas/xarray/... conversion) +* :ghpull:`22942`: Backport PR #22933 on branch v3.5.x (Adjusted wording in pull request guidelines) +* :ghpull:`22941`: Backport PR #22898 on branch v3.5.x (Only set Tk scaling-on-map for Windows systems) +* :ghpull:`22935`: Backport PR #22002: Fix TkAgg memory leaks and test for memory growth regressions +* :ghpull:`22898`: Only set Tk scaling-on-map for Windows systems +* :ghpull:`22933`: Adjusted wording in pull request guidelines +* :ghpull:`22002`: Fix TkAgg memory leaks and test for memory growth regressions +* :ghpull:`22924`: Fix gtk4 incorrect import. +* :ghpull:`22922`: Backport PR #22904 on branch v3.5.x (Fixed typo in triage acknowledgment) +* :ghpull:`22904`: Fixed typo in triage acknowledgment +* :ghpull:`22890`: DOC: add ipykernel to list of optional dependencies +* :ghpull:`22878`: Backport PR #22871 on branch v3.5.x (Fix year offset not always being added) +* :ghpull:`22871`: Fix year offset not always being added +* :ghpull:`22844`: Backport PR #22313 on branch v3.5.x (Fix colorbar exponents) +* :ghpull:`22560`: Improve pandas/xarray/... conversion +* :ghpull:`22846`: Backport PR #22284 on branch v3.5.x (Specify font number for TTC font subsetting) +* :ghpull:`22284`: Specify font number for TTC font subsetting +* :ghpull:`22845`: Backport PR #22199 on branch v3.5.x (DOC: git:// is deprecated.) +* :ghpull:`22837`: Backport PR #22807 on branch v3.5.x (Replace quiver dpi callback with reinit-on-dpi-changed.) +* :ghpull:`22838`: Backport PR #22806 on branch v3.5.x (FIX: callback for subfigure uses parent) +* :ghpull:`22832`: Backport PR #22767 on branch v3.5.x (Fixed bug in find_nearest_contour) +* :ghpull:`22767`: Fixed bug in find_nearest_contour +* :ghpull:`22807`: Replace quiver dpi callback with reinit-on-dpi-changed. +* :ghpull:`22806`: FIX: callback for subfigure uses parent +* :ghpull:`22737`: Backport PR #22138: Fix clearing subfigures +* :ghpull:`22735`: MNT: prefer Figure.clear() as canonical over Figure.clf() +* :ghpull:`22783`: Backport PR #22732: FIX: maybe improve renderer dance +* :ghpull:`22748`: Backport PR #22628 on branch v3.5.x (Add RuntimeWarning guard around division-by-zero) +* :ghpull:`22732`: FIX: maybe improve renderer dance +* :ghpull:`22764`: Backport PR #22756 on branch v3.5.x (Use system distutils instead of the setuptools copy) +* :ghpull:`22780`: Backport PR #22766 on branch v3.5.x (FIX: account for constant deprecations in Pillow 9.1) +* :ghpull:`22781`: Backport PR #22776 on branch v3.5.x (Fix colorbar stealing from a single axes and with panchor=False.) +* :ghpull:`22782`: Backport PR #22774 on branch v3.5.x (Remove outdated doc for pie chart) +* :ghpull:`22774`: Remove outdated doc for pie chart +* :ghpull:`22776`: Fix colorbar stealing from a single axes and with panchor=False. +* :ghpull:`22766`: FIX: account for deprecations of constant in Pillow 9.1 +* :ghpull:`22756`: Use system distutils instead of the setuptools copy +* :ghpull:`22750`: Backport PR #22743: Fix configure_subplots with tool manager +* :ghpull:`22743`: Fix configure_subplots with tool manager +* :ghpull:`22628`: Add RuntimeWarning guard around division-by-zero +* :ghpull:`22736`: Backport PR #22719 on branch v3.5.x (Fix incorrect deprecation warning) +* :ghpull:`22719`: Fix incorrect deprecation warning +* :ghpull:`22138`: Fix clearing subfigures +* :ghpull:`22729`: Backport PR #22711 on branch v3.5.x (RangeSlider handle set_val bugfix) +* :ghpull:`22711`: RangeSlider handle set_val bugfix +* :ghpull:`22701`: Backport PR #22691 on branch v3.5.x (FIX: remove toggle on QuadMesh cursor data) +* :ghpull:`22723`: Backport PR #22716 on branch v3.5.x (DOC: set canonical) +* :ghpull:`22703`: Backport PR #22689 on branch v3.5.x (Fix path_effects to work on text with spaces only) +* :ghpull:`22689`: Fix path_effects to work on text with spaces only +* :ghpull:`22691`: FIX: remove toggle on QuadMesh cursor data +* :ghpull:`22696`: Backport PR #22693 on branch v3.5.x (Remove QuadMesh from mouseover set.) +* :ghpull:`22693`: Remove QuadMesh from mouseover set. +* :ghpull:`22647`: Backport PR #22429 on branch v3.5.x (Enable windows/arm64 platform) +* :ghpull:`22653`: Simplify FreeType version check to avoid packaging +* :ghpull:`22646`: Manual backport of pr 22635 on v3.5.x +* :ghpull:`22429`: Enable windows/arm64 platform +* :ghpull:`22635`: FIX: Handle inverted colorbar axes with extensions +* :ghpull:`22313`: Fix colorbar exponents +* :ghpull:`22619`: Backport PR #22611 on branch v3.5.x (FIX: Colorbars check for subplotspec attribute before using) +* :ghpull:`22618`: Backport PR #22617 on branch v3.5.x (Bump actions/checkout from 2 to 3) +* :ghpull:`22611`: FIX: Colorbars check for subplotspec attribute before using +* :ghpull:`22617`: Bump actions/checkout from 2 to 3 +* :ghpull:`22595`: Backport PR #22005: Further defer backend selection +* :ghpull:`22602`: Backport PR #22596 on branch v3.5.x (Fix backend in matplotlibrc if unset in mplsetup.cfg) +* :ghpull:`22596`: Fix backend in matplotlibrc if unset in mplsetup.cfg +* :ghpull:`22597`: Backport PR #22594 on branch v3.5.x (FIX: do not pass dashes to collections in errorbar) +* :ghpull:`22594`: FIX: do not pass dashes to collections in errorbar +* :ghpull:`22593`: Backport PR #22559 on branch v3.5.x (fix: fill stairs should have lw=0 instead of edgecolor="none") +* :ghpull:`22005`: Further defer backend selection +* :ghpull:`22559`: fix: fill stairs should have lw=0 instead of edgecolor="none" +* :ghpull:`22592`: Backport PR #22141 on branch v3.5.x (Fix check 1d) +* :ghpull:`22141`: Fix check 1d +* :ghpull:`22588`: Backport PR #22445 on branch v3.5.x (Fix loading tk on windows when current process has >1024 modules.) +* :ghpull:`22445`: Fix loading tk on windows when current process has >1024 modules. +* :ghpull:`22575`: Backport PR #22572 on branch v3.5.x (Fix issue with unhandled Done exception) +* :ghpull:`22578`: Backport PR #22038 on branch v3.5.x (DOC: Include alternatives to deprecations in the documentation) +* :ghpull:`22572`: Fix issue with unhandled Done exception +* :ghpull:`22557`: Backport PR #22549 on branch v3.5.x (Really fix wheel building on CI) +* :ghpull:`22549`: Really fix wheel building on CI +* :ghpull:`22548`: Backport PR #22540 on branch v3.5.x (Reorder text api docs.) +* :ghpull:`22540`: Reorder text api docs. +* :ghpull:`22542`: Backport PR #22534 on branch v3.5.x (Fix issue with manual clabel) +* :ghpull:`22534`: Fix issue with manual clabel +* :ghpull:`22501`: Backport PR #22499 on branch v3.5.x (FIX: make the show API on webagg consistent with others) +* :ghpull:`22499`: FIX: make the show API on webagg consistent with others +* :ghpull:`22500`: Backport PR #22496 on branch v3.5.x (Fix units in quick start example) +* :ghpull:`22496`: Fix units in quick start example +* :ghpull:`22493`: Backport PR #22483 on branch v3.5.x (Tweak arrow demo size.) +* :ghpull:`22492`: Backport PR #22476: FIX: Include (0, 0) offsets in scatter autoscaling +* :ghpull:`22483`: Tweak arrow demo size. +* :ghpull:`22476`: FIX: Include (0, 0) offsets in scatter autoscaling +* :ghpull:`22481`: Backport PR #22479 on branch v3.5.x (adds _enum qualifier for QColorDialog.ShowAlphaChannel. Closes #22471.) +* :ghpull:`22479`: adds _enum qualifier for QColorDialog.ShowAlphaChannel. Closes #22471. +* :ghpull:`22475`: Backport PR #22474 on branch v3.5.x (Clarify secondary_axis documentation) +* :ghpull:`22474`: Clarify secondary_axis documentation +* :ghpull:`22462`: Backport PR #22458 on branch v3.5.x (Fix Radar Chart Gridlines for Non-Circular Charts) +* :ghpull:`22456`: Backport PR #22375 on branch v3.5.x (Re-enable cibuildwheel on push) +* :ghpull:`22375`: Re-enable cibuildwheel on push +* :ghpull:`22443`: Backport PR #22442 on branch v3.5.x (CI: skip test to work around gs bug) +* :ghpull:`22442`: CI: skip test to work around gs bug +* :ghpull:`22441`: Backport PR #22434 on branch v3.5.x (DOC: imbalanced backticks.) +* :ghpull:`22436`: Backport PR #22431 on branch v3.5.x (Update Scipy intersphinx inventory link) +* :ghpull:`22438`: Backport PR #22430 on branch v3.5.x (fix method name in doc) +* :ghpull:`22434`: DOC: imbalanced backticks. +* :ghpull:`22426`: Backport PR #22398 on branch v3.5.x (Pin coverage to fix CI) +* :ghpull:`22428`: Backport PR #22368 on branch v3.5.x (Pin dependencies to fix CI) +* :ghpull:`22427`: Backport PR #22396 on branch v3.5.x (Clarify note in get_cmap()) +* :ghpull:`22396`: Clarify note in get_cmap() +* :ghpull:`22398`: Pin coverage to fix CI +* :ghpull:`22368`: Pin dependencies to fix CI +* :ghpull:`22358`: Backport PR #22349 on branch v3.5.x (Use latex as the program name for kpsewhich) +* :ghpull:`22349`: Use latex as the program name for kpsewhich +* :ghpull:`22348`: Backport PR #22346 on branch v3.5.x (Remove invalid ```` tag in ``animation.HTMLWriter``) +* :ghpull:`22346`: Remove invalid ```` tag in ``animation.HTMLWriter`` +* :ghpull:`22328`: Backport PR #22288 on branch v3.5.x (update documentation after #18966) +* :ghpull:`22288`: update documentation after #18966 +* :ghpull:`22325`: Backport PR #22283: Fixed ``repr`` for ``SecondaryAxis`` +* :ghpull:`22322`: Backport PR #22077 on branch v3.5.x (Fix keyboard event routing in Tk backend (fixes #13484, #14081, and #22028)) +* :ghpull:`22321`: Backport PR #22290 on branch v3.5.x (Respect ``position`` and ``group`` argument in Tk toolmanager add_toolitem) +* :ghpull:`22318`: Backport PR #22293 on branch v3.5.x (Modify example for x-axis tick labels at the top) +* :ghpull:`22319`: Backport PR #22279 on branch v3.5.x (Remove Axes sublists from docs) +* :ghpull:`22327`: Backport PR #22326 on branch v3.5.x (CI: ban coverage 6.3 that may be causing random hangs in fork test) +* :ghpull:`22326`: CI: ban coverage 6.3 that may be causing random hangs in fork test +* :ghpull:`22077`: Fix keyboard event routing in Tk backend (fixes #13484, #14081, and #22028) +* :ghpull:`22290`: Respect ``position`` and ``group`` argument in Tk toolmanager add_toolitem +* :ghpull:`22293`: Modify example for x-axis tick labels at the top +* :ghpull:`22311`: Backport PR #22285 on branch v3.5.x (Don't warn on grid removal deprecation if grid is hidden) +* :ghpull:`22310`: Backport PR #22294 on branch v3.5.x (Add set_cursor method to FigureCanvasTk) +* :ghpull:`22285`: Don't warn on grid removal deprecation if grid is hidden +* :ghpull:`22294`: Add set_cursor method to FigureCanvasTk +* :ghpull:`22309`: Backport PR #22301 on branch v3.5.x (FIX: repositioning axes labels: use get_window_extent instead for spines.) +* :ghpull:`22301`: FIX: repositioning axes labels: use get_window_extent instead for spines. +* :ghpull:`22307`: Backport PR #22306 on branch v3.5.x (FIX: ensure that used sub-packages are actually imported) +* :ghpull:`22306`: FIX: ensure that used sub-packages are actually imported +* :ghpull:`22283`: Fixed ``repr`` for ``SecondaryAxis`` +* :ghpull:`22275`: Backport PR #22254 on branch v3.5.x (Disable QuadMesh cursor data by default) +* :ghpull:`22254`: Disable QuadMesh cursor data by default +* :ghpull:`22269`: Backport PR #22265 on branch v3.5.x (Fix Qt enum access.) +* :ghpull:`22265`: Fix Qt enum access. +* :ghpull:`22259`: Backport PR #22256 on branch v3.5.x (Skip tests on the -doc branches) +* :ghpull:`22238`: Backport PR #22235 on branch v3.5.x (Run wheel builds on PRs when requested by a label) +* :ghpull:`22241`: Revert "Backport PR #22179 on branch v3.5.x (FIX: macosx check case-insensitive app name)" +* :ghpull:`22248`: Backport PR #22206 on branch v3.5.x (Improve formatting of "Anatomy of a figure") +* :ghpull:`22235`: Run wheel builds on PRs when requested by a label +* :ghpull:`22206`: Improve formatting of "Anatomy of a figure" +* :ghpull:`22220`: Backport PR #21833: Enforce backport conditions on v*-doc branches +* :ghpull:`22219`: Backport PR #22218 on branch v3.5.x (Fix typo in ``tutorials/intermediate/arranging_axes.py``) +* :ghpull:`22218`: Fix typo in ``tutorials/intermediate/arranging_axes.py`` +* :ghpull:`22217`: Backport PR #22209 on branch v3.5.x (DOC: Document default join style) +* :ghpull:`22209`: DOC: Document default join style +* :ghpull:`22214`: Backport PR #22208 on branch v3.5.x (Stop sorting artists in Figure Options dialog) +* :ghpull:`22215`: Backport PR #22177 on branch v3.5.x (Document ArtistList) +* :ghpull:`22177`: Document ArtistList +* :ghpull:`22208`: Stop sorting artists in Figure Options dialog +* :ghpull:`22199`: DOC: git:// is deprecated. +* :ghpull:`22210`: Backport PR #22202 on branch v3.5.x (PR: Fix merge of 18966) +* :ghpull:`22202`: PR: Fix merge of 18966 +* :ghpull:`22201`: Backport PR #22053 on branch v3.5.x (DOC: Document default cap styles) +* :ghpull:`22053`: DOC: Document default cap styles +* :ghpull:`22195`: Backport PR #22179 on branch v3.5.x (FIX: macosx check case-insensitive app name) +* :ghpull:`22192`: Backport PR #22190 on branch v3.5.x (DOC: Fix upstream URL for merge in CircleCI) +* :ghpull:`22188`: Backport PR #22187 on branch v3.5.x (Fix typo in ``axhline`` docstring) +* :ghpull:`22187`: Fix typo in ``axhline`` docstring +* :ghpull:`22185`: Backport PR #22184 on branch v3.5.x (Removed dev from 3.10-version) +* :ghpull:`22186`: Backport PR #21943 on branch v3.5.x (DOC: explain too many ticks) +* :ghpull:`21943`: DOC: explain too many ticks +* :ghpull:`22184`: Removed dev from 3.10-version +* :ghpull:`22168`: Backport PR #22144 on branch v3.5.x (Fix cl subgridspec) +* :ghpull:`22144`: Fix cl subgridspec +* :ghpull:`22155`: Backport PR #22082 on branch v3.5.x (Update both zoom/pan states on wx when triggering from keyboard.) +* :ghpull:`22082`: Update both zoom/pan states on wx when triggering from keyboard. +* :ghpull:`22153`: Backport PR #22147 on branch v3.5.x (Fix loading user-defined icons for Tk toolbar) +* :ghpull:`22152`: Backport PR #22135 on branch v3.5.x (Fix loading user-defined icons for Qt plot window) +* :ghpull:`22151`: Backport PR #22078 on branch v3.5.x (Prevent tooltips from overlapping buttons in NavigationToolbar2Tk (fixes issue mentioned in #22028)) +* :ghpull:`22135`: Fix loading user-defined icons for Qt plot window +* :ghpull:`22078`: Prevent tooltips from overlapping buttons in NavigationToolbar2Tk (fixes issue mentioned in #22028) +* :ghpull:`22147`: Fix loading user-defined icons for Tk toolbar +* :ghpull:`22136`: Backport PR #22132 on branch v3.5.x (TST: Increase fp tolerances for some images) +* :ghpull:`22132`: TST: Increase fp tolerances for some images +* :ghpull:`22121`: Backport PR #22116 on branch v3.5.x (FIX: there is no add_text method, fallback to add_artist) +* :ghpull:`22117`: Backport PR #21860 on branch v3.5.x (DOC: Update style sheet reference) +* :ghpull:`22116`: FIX: there is no add_text method, fallback to add_artist +* :ghpull:`22038`: DOC: Include alternatives to deprecations in the documentation +* :ghpull:`22074`: Backport PR #22066 on branch v3.5.x (FIX: Remove trailing zeros from offset significand) +* :ghpull:`22106`: Backport PR #22089: FIX: squash memory leak in colorbar +* :ghpull:`22089`: FIX: squash memory leak in colorbar +* :ghpull:`22101`: Backport PR #22099 on branch v3.5.x (CI: Disable numpy avx512 instructions) +* :ghpull:`22099`: CI: Disable numpy avx512 instructions +* :ghpull:`22095`: Backport PR #22083 on branch v3.5.x (Fix reference to Matplotlib FAQ in doc/index.rst) +* :ghpull:`22066`: FIX: Remove trailing zeros from offset significand +* :ghpull:`22072`: Backport PR #22071 on branch v3.5.x (Fix a small typo in docstring ("loation" --> "location")) +* :ghpull:`22071`: Fix a small typo in docstring ("loation" --> "location") +* :ghpull:`22070`: Backport PR #22069 on branch v3.5.x ([Doc] Fix typo in ``units.py`` documentation example) +* :ghpull:`22069`: [Doc] Fix typo in ``units.py`` documentation example +* :ghpull:`22067`: Backport PR #22064 on branch v3.5.x (DOC: Clarify y parameter in Axes.set_title) +* :ghpull:`22064`: DOC: Clarify y parameter in Axes.set_title +* :ghpull:`22049`: Backport PR #22048 on branch v3.5.x (Document how to prevent TeX from treating ``&``, ``#`` as special.) +* :ghpull:`22048`: Document how to prevent TeX from treating ``&``, ``#`` as special. +* :ghpull:`22047`: Backport PR #22044 on branch v3.5.x (Get correct source code link for decorated functions) +* :ghpull:`22044`: Get correct source code link for decorated functions +* :ghpull:`22024`: Backport PR #22009 on branch v3.5.x (FIX: Prevent set_alpha from changing color of legend patch) +* :ghpull:`22009`: FIX: Prevent set_alpha from changing color of legend patch +* :ghpull:`22019`: Backport PR #22018 on branch v3.5.x (BUG: fix handling of zero-dimensional arrays in cbook._reshape_2D) +* :ghpull:`22018`: BUG: fix handling of zero-dimensional arrays in cbook._reshape_2D +* :ghpull:`21996`: Backport PR #21990 on branch v3.5.x (Fix rubberbanding on wx+py3.10.) +* :ghpull:`21990`: Fix rubberbanding on wx+py3.10. +* :ghpull:`21987`: Backport PR #21862 on branch v3.5.x (DOC: Simplify markevery demo) +* :ghpull:`21969`: Backport PR #21948 on branch v3.5.x (Distinguish AbstractMovieWriter and MovieWriter in docs.) +* :ghpull:`21948`: Distinguish AbstractMovieWriter and MovieWriter in docs. +* :ghpull:`21953`: Backport PR #21946 on branch v3.5.x (DOC: fix interactive to not put Event Handling and Interactive Guide …) +* :ghpull:`21946`: DOC: fix interactive to not put Event Handling and Interactive Guide … + +Issues (61): + +* :ghissue:`22954`: [Doc]: v3.5.1 github stats are missing +* :ghissue:`22959`: [MNT]: macos-latest memory leak over threshold +* :ghissue:`22921`: [Bug]: Regression in animation from #22175 +* :ghissue:`22908`: [Bug]: QuadMesh get_cursor_data errors if no array is set +* :ghissue:`21901`: Suggested clarification of comments in errorbar helpers +* :ghissue:`22932`: [Doc]: small edits to the Pull request guidelines +* :ghissue:`22858`: [Bug]: FigureCanvasTkAgg call creates memory leak +* :ghissue:`20490`: Memory leaks on matplotlib 3.4.2 (and 3.4.0) +* :ghissue:`22900`: [Doc]: Typo in triage acknowledgment +* :ghissue:`22341`: [Bug]: GridSpec or related change between 3.4.3 and 3.5.1 +* :ghissue:`22472`: [Bug]: ConciseDateFormatter not showing year anywhere when plotting <12 months +* :ghissue:`22874`: [Bug]: Textbox doesn't accept input +* :ghissue:`21893`: [Bug]: ``backend_pdf`` gives ``TTLibError`` with ``pdf.fonttype : 42`` +* :ghissue:`22840`: [Bug]: Blank output EPS file when using latex and figure.autolayout = True +* :ghissue:`22762`: [Bug]: Issue with find_nearest_contour in contour.py +* :ghissue:`22823`: [Bug]: Changing Linestyle in plot window swaps some plotted lines +* :ghissue:`22804`: [Bug]: Quiver not working with subfigure? +* :ghissue:`22673`: [Bug]: tight_layout (version 3.5+) +* :ghissue:`21930`: [Bug]: EPS savefig messed up by 'figure.autolayout' rcParam on 3.5.0 +* :ghissue:`22753`: windows CI broken on azure +* :ghissue:`22088`: [Bug]: Tool Manager example broken +* :ghissue:`22624`: [Bug]: invalid value encountered with 'ortho' projection mode +* :ghissue:`22640`: [Bug]: Confusing deprecation warning when empty data passed to axis with category units +* :ghissue:`22137`: [Bug]: Cannot clear figure of subfigures +* :ghissue:`22706`: [Bug]: RangeSlider.set_val does not move the slider (only poly and value) +* :ghissue:`22727`: MAtplolib pan and zoom dead slow on new PC +* :ghissue:`22687`: [Bug]: Empty text or text with a newline at either end + path_effects crashes +* :ghissue:`22694`: Revert set_show_cursor_data +* :ghissue:`22520`: [Bug]: Slow lasso selector over QuadMesh collection +* :ghissue:`22648`: Add packaging to setup_requires? +* :ghissue:`22052`: [Bug]: invert_yaxis function cannot invert the "over value" in colorbar axes +* :ghissue:`22576`: [Bug]: ``inset_axes`` colorbar + ``tight_layout`` raises ``AttributeError`` +* :ghissue:`22590`: [Bug]: ValueError: Do not know how to convert "list" to dashes; when using axes errorbar. +* :ghissue:`21998`: [Bug]: Working with PyQt5, the different import order will make different result. +* :ghissue:`22330`: [Bug]: possible regression with pandas 1.4 with plt.plot when using a single column dataframe as the x argument +* :ghissue:`22125`: [Bug]: ``plt.plot`` thinks ``pandas.Series`` is 2-dimensional when nullable data type is used +* :ghissue:`22378`: [Bug]: TkAgg fails to find Tcl/Tk libraries in Windows for processes with a large number of modules loaded +* :ghissue:`22577`: [Bug]: Erroneous deprecation warning help message +* :ghissue:`21798`: [Bug]: Unhandled _get_renderer.Done exception in wxagg backend +* :ghissue:`22532`: [Issue]: Manually placing contour labels using ``clabel`` not working +* :ghissue:`22470`: [Bug]: Subsequent scatter plots work incorrectly +* :ghissue:`22471`: [Bug]: formlayout fails on PyQt6 due to the unqualified enum ShowAlphaChannel in class ColorButton +* :ghissue:`22473`: [Bug]: Secondary axis does not accept python builtins for transform +* :ghissue:`22384`: [Bug]: Curve styles gets mixed up when edited in the Curves Tab of Figure Options (Edit Axis) +* :ghissue:`22028`: [Bug]: mpl with py3.10.1 - Interactive figures - Constrain pan/zoom to x/y axis not work +* :ghissue:`13484`: Matplotlib keymap stop working after pressing tab +* :ghissue:`20130`: tk toolmanager add_toolitem fails to add tool to group other than the last one +* :ghissue:`21723`: [Bug]: Some styles trigger pcolormesh grid deprecation +* :ghissue:`22300`: [Bug]: Saving a fig with a colorbar using a ``TwoSlopeNorm`` sometimes results in 'posx and posy should be finite values' +* :ghissue:`22305`: [Bug]: Import Error in Matplotlib 3.5.1 +* :ghissue:`21917`: [Bug]: pcolormesh is not responsive in Matplotlib 3.5 +* :ghissue:`22094`: [Doc]: No documentation on ArtistList +* :ghissue:`21979`: [Doc]: Clarify default capstyle +* :ghissue:`22143`: [Bug]: ``constrained_layout`` merging similar subgrids +* :ghissue:`22131`: [Bug]: png icon image fails to load for manually defined tool buttons +* :ghissue:`22093`: [Bug]: AttributeError: 'AxesSubplot' object has no attribute 'add_text' +* :ghissue:`22085`: [Bug]: Memory leak with colorbar.make_axes +* :ghissue:`22065`: [Bug]: Additive offset with trailing zeros +* :ghissue:`15493`: common_texification misses & (ampersand) +* :ghissue:`22039`: [Doc]: [source] link for deprecated functions leads to _api/deprecation.py +* :ghissue:`22016`: [Bug]: matplotlib 3.3 changed how plt.hist handles iterables of zero-dimensional arrays. diff --git a/doc/users/prev_whats_new/github_stats_3.5.3.rst b/doc/users/prev_whats_new/github_stats_3.5.3.rst new file mode 100644 index 000000000000..bafd6d5c27eb --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.5.3.rst @@ -0,0 +1,127 @@ +.. _github-stats-3-5-3: + +GitHub statistics for 3.5.3 (Aug 10, 2022) +========================================== + +GitHub statistics for 2022/05/03 (tag: v3.5.2) - 2022/08/10 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 19 issues and merged 66 pull requests. +The full list can be seen `on GitHub `__ + +The following 20 authors contributed 99 commits. + +* Antony Lee +* Biswapriyo Nath +* David Gilbertson +* DWesl +* Elliott Sales de Andrade +* GavinZhang +* Greg Lucas +* Jody Klymak +* Kayran Schmidt +* Matthew Feickert +* Nickolaos Giannatos +* Oscar Gustafsson +* Ruth Comer +* SaumyaBhushan +* Scott Jones +* Scott Shambaugh +* tfpf +* Thomas A Caswell +* Tim Hoffmann +* wsykala + +GitHub issues and pull requests: + +Pull Requests (66): + +* :ghpull:`23591`: Backport PR #23549 on branch v3.5.x (Don't clip colorbar dividers) +* :ghpull:`23593`: STY: Fix whitespace error from new flake8 +* :ghpull:`23549`: Don't clip colorbar dividers +* :ghpull:`23528`: Backport PR #23523 on branch v3.5.x (TST: Update Quantity test class) +* :ghpull:`23523`: TST: Update Quantity test class +* :ghpull:`23508`: Add explicit registration of units in examples +* :ghpull:`23515`: Backport PR #23462: Fix AttributeError for pickle load of Figure class +* :ghpull:`23518`: Backport PR #23514 on branch v3.5.x (Fix doc build) +* :ghpull:`23517`: Backport PR #23511 on branch v3.5.x (supporting IBM i OS) +* :ghpull:`23511`: supporting IBM i OS +* :ghpull:`23462`: Fix AttributeError for pickle load of Figure class +* :ghpull:`23488`: Backport PR #23066 on branch v3.5.x (BLD: Define PyErr_SetFromWindowsErr on Cygwin.) +* :ghpull:`23066`: BLD: Define PyErr_SetFromWindowsErr on Cygwin. +* :ghpull:`23479`: Pin setuptools_scm on v3.5.x +* :ghpull:`22998`: Backport PR #22987 on branch v3.5.x (CI: bump test limit from tkagg on osx) +* :ghpull:`23478`: Backport PR #23476: FIX: reset to original DPI in getstate +* :ghpull:`23476`: FIX: reset to original DPI in getstate +* :ghpull:`23458`: Backport PR #23445 on branch v3.5.x (Compare thread native ids when checking whether running on main thread.) +* :ghpull:`23440`: Backport PR #23430 on branch v3.5.x (Fix divide by 0 runtime warning) +* :ghpull:`23430`: Fix divide by 0 runtime warning +* :ghpull:`23344`: Backport PR #23333: Fix errorbar handling of nan. +* :ghpull:`23333`: Fix errorbar handling of nan. +* :ghpull:`23338`: Backport PR #23278: Remove internal use of get/set dpi +* :ghpull:`23331`: Backport PR #22835 on branch v3.5.x (Fix BoundaryNorm cursor data output) +* :ghpull:`22835`: Fix BoundaryNorm cursor data output +* :ghpull:`23292`: Backport PR #23232 on branch v3.5.x (Fix passing stem markerfmt positionally when locs are not given) +* :ghpull:`23275`: Backport PR #23260 on branch v3.5.x (Fix Colorbar extend patches to have correct alpha) +* :ghpull:`23312`: Pin to an older pydata-sphinx-theme for v3.5.x +* :ghpull:`23278`: Remove internal use of get/set dpi +* :ghpull:`23232`: Fix passing stem markerfmt positionally when locs are not given +* :ghpull:`22865`: Fix issue with colorbar extend and drawedges +* :ghpull:`23260`: Fix Colorbar extend patches to have correct alpha +* :ghpull:`23245`: Backport PR #23144 on branch v3.5.x (Only import setuptools_scm when we are in a matplotlib git repo) +* :ghpull:`23144`: Only import setuptools_scm when we are in a matplotlib git repo +* :ghpull:`23242`: Backport PR #23203 on branch v3.5.x (Honour ``panchor`` keyword for colorbar on subplot) +* :ghpull:`23203`: Honour ``panchor`` keyword for colorbar on subplot +* :ghpull:`23228`: Backport PR #23209 on branch v3.5.x (Fix the vertical alignment of overunder symbols.) +* :ghpull:`23209`: Fix the vertical alignment of overunder symbols. +* :ghpull:`23184`: Backport PR #23174: Make sure SubFigure has _cachedRenderer +* :ghpull:`23194`: Backport PR #23095: Try to unbreak CI by xfailing OSX Tk tests +* :ghpull:`23113`: Backport PR #23057 and #23106 +* :ghpull:`23185`: Backport PR #23168 on branch v3.5.x (Corrected docstring for artist.Artist.set_agg_filter) +* :ghpull:`23168`: Corrected docstring for artist.Artist.set_agg_filter +* :ghpull:`23174`: Make sure SubFigure has _cachedRenderer +* :ghpull:`23110`: Tweak subprocess_run_helper. +* :ghpull:`23138`: Backport PR #23137 on branch v3.5.x (DOC fix typo) +* :ghpull:`23137`: DOC fix typo +* :ghpull:`23125`: Backport PR #23122 on branch v3.5.x (Remove redundant rcparam default) +* :ghpull:`23120`: Backport PR #23115 on branch v3.5.x (DOC fixed duplicate/wrong default) +* :ghpull:`23095`: Try to unbreak CI by xfailing OSX Tk tests +* :ghpull:`23106`: Reuse subprocess_run_helper in test_pylab_integration. +* :ghpull:`23112`: Backport PR #23111 on branch v3.5.x (Fix _g_sig_digits for value<0 and delta=0.) +* :ghpull:`23111`: Fix _g_sig_digits for value<0 and delta=0. +* :ghpull:`23057`: FIX: ensure switching the backend installs repl hook +* :ghpull:`23075`: Backport PR #23069 on branch v3.5.x (TST: forgive more failures on pyside2 / pyside6 cross imports) +* :ghpull:`23069`: TST: forgive more failures on pyside2 / pyside6 cross imports +* :ghpull:`22981`: Backport PR #22979 on branch v3.5.x (Skip additional backend tests on import error) +* :ghpull:`23064`: Backport PR #22975 on branch v3.5.x (MNT: fix __array__ to numpy) +* :ghpull:`22975`: MNT: fix __array__ to numpy +* :ghpull:`23058`: Backport PR #23051 on branch v3.5.x (Fix variable initialization due to jump bypassing it) +* :ghpull:`23051`: Fix variable initialization due to jump bypassing it +* :ghpull:`23010`: Backport PR #23000 on branch v3.5.x (Additional details on VS install on installation page) +* :ghpull:`22995`: Backport PR #22994 on branch v3.5.x (Docs: ignore >>> on code prompts on documentation prompts) +* :ghpull:`23001`: CI: Add trivial pre-commit.ci config to avoid CI failure +* :ghpull:`22987`: CI: bump test limit from tkagg on osx +* :ghpull:`22979`: Skip additional backend tests on import error + +Issues (19): + +* :ghissue:`22864`: [Bug]: Colorbar with drawedges=True and extend='both' does not draw edges at extremities +* :ghissue:`23382`: [TST] Upcoming dependency test failures +* :ghissue:`23470`: [Bug]: fig.canvas.mpl_connect in 3.5.2 not registering events in jupyter lab unless using widget pan or zoom controls +* :ghissue:`22997`: [Bug]: Cygwin build fails due to use of Windows-only functions in _tkagg.cpp +* :ghissue:`23471`: [Bug]: DPI of a figure is doubled after unpickling on M1 Mac +* :ghissue:`23050`: [Doc]: Docstring for artist.Artist.set_agg_filter is incorrect +* :ghissue:`23307`: [Bug]: PEX warns about missing ``setuptools`` from ``install_requires`` in matplotlib +* :ghissue:`23330`: [Bug]: Missing values cause exception in errorbar plot +* :ghissue:`21915`: [Bug]: scalar mappable format_cursor_data crashes on BoundarNorm +* :ghissue:`22970`: [Bug]: Colorbar extend patches do not have correct alpha +* :ghissue:`23114`: [Bug]: matplotlib __init__.py checks for .git folder 2 levels up, then errors due to setup tools_scm +* :ghissue:`23157`: [Bug]: colorbar ignores keyword panchor=False +* :ghissue:`23229`: [Bug]: matplotlib==3.5.2 breaks ipywidgets +* :ghissue:`18085`: vertical alignment of \sum depends on the presence of subscripts and superscripts +* :ghissue:`23173`: [Bug]: Crash when adding clabels to subfigures +* :ghissue:`23108`: [Bug]: Imshow with all negative values leads to math domain errors. +* :ghissue:`23042`: [Bug]: Figures fail to redraw with IPython +* :ghissue:`23004`: [Bug]: test failure of test_cross_Qt_imports in 3.5.2 +* :ghissue:`22973`: [Bug]: v3.5.2 causing plot to crash when plotting object with ``__array__`` method diff --git a/doc/users/prev_whats_new/github_stats_3.6.0.rst b/doc/users/prev_whats_new/github_stats_3.6.0.rst new file mode 100644 index 000000000000..aac0c0445fd3 --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.6.0.rst @@ -0,0 +1,1292 @@ +.. _github-stats-3-6-0: + +GitHub statistics for 3.6.0 (Sep 15, 2022) +========================================== + +GitHub statistics for 2021/11/16 (tag: v3.5.0) - 2022/09/15 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 202 issues and merged 894 pull requests. +The full list can be seen `on GitHub `__ + +The following 174 authors contributed 4425 commits. + +* Abhishek K M +* Adeel Hassan +* agra +* Aitik Gupta +* ambi7 +* Andras Deak +* Andres Martinez +* Andrew Fennell +* andrzejnovak +* Andrés Martínez +* Anna Mastori +* AnnaMastori +* Ante Sikic +* Antony Lee +* arndRemy +* Ben Root +* Biswapriyo Nath +* cavesdev +* Clément Phan +* Clément Walter +* code-review-doctor +* Connor Cozad +* Constantine Evans +* Croadden +* daniilS +* Danilo Palumbo +* David Gilbertson +* David Ketcheson +* David Matos +* David Poznik +* David Stansby +* Davide Sandonà +* dependabot[bot] +* dermasugita +* Diego Solano +* Dimitri Papadopoulos +* dj4t9n +* Dmitriy Fishman +* DWesl +* Edouard Berthe +* eindH +* Elliott Sales de Andrade +* Eric Firing +* Eric Larson +* Eric Prestat +* Federico Ariza +* Felix Nößler +* Fernando +* Gajendra Pal +* gajendra0180 +* GavinZhang +* Greg Lucas +* hannah +* Hansin Ahuja +* Harshal Prakash Patankar +* Hassan Kibirige +* Haziq Khurshid +* Henry +* henrybeUM +* Hood +* Hood Chatham +* Ian Hunt-Isaak +* Ian Thomas +* igurin-invn +* ikhebgeenaccount +* Isha Mehta +* Jake Bowhay +* Jake Li +* Jake Lishman +* Jake VanderPlas +* Jakub Klus +* James Tocknell +* Jan-Hendrik Müller +* Jay Joshi +* Jay Stanley +* jayjoshi112711 +* Jeff Beck +* Jody Klymak +* Joel Frederico +* Joseph Fox-Rabinovitz +* Josh Soref +* Jouni K. Seppänen +* Kayran Schmidt +* kdpenner +* Kian Eliasi +* Kinshuk Dua +* kislovskiy +* KIU Shueng Chuan +* kjain +* kolibril13 +* krassowski +* Krish-sysadmin +* Leeh Peter +* lgfunderburk +* Liam Toney +* Lucas Ricci +* Luke Davis +* luz paz +* mackopes +* MAKOMO +* MalikIdreesHasa +* Marcin Swaltek +* Mario +* Mario Sergio Valdés Tresanco +* martinRenou +* Matthew Feickert +* Matthias Bussonnier +* Mauricio Collares +* MeeseeksMachine +* melissawm +* Mr-Milk +* Navid C. Constantinou +* Nickolaos Giannatos +* Nicolas P. Rougier +* Niyas Sait +* noatamir +* ojeda-e +* Olivier Gauthé +* Oscar Gustafsson +* patquem +* Philipp Rohde +* Pieter Eendebak +* Pieter P +* Péter Leéh +* Qijia Liu +* Quentin Peter +* Raphael Quast +* rditlar9 +* Richard Penney +* richardsheridan +* Rike-Benjamin Schuppner +* Robert Cimrman +* Roberto Toro +* root +* Ruth Comer +* Ruth G. N +* Ruth Nainggolan +* Ryan May +* Rémi Achard +* SaumyaBhushan +* Scott Jones +* Scott Shambaugh +* selormtamakloe +* Simon Hoxbro +* skywateryang +* Stefanie Molin +* Steffen Rehberg +* stone +* Sven Eschlbeck +* sveneschlbeck +* takimata +* tfpf +* Thomas A Caswell +* Tim Hoffmann +* Tobias Megies +* Tomas Hrnciar +* Tomasz KuliÅ„ski +* trichter +* unknown +* Uwe Hubert +* vfdev-5 +* Vishal Chandratreya +* Vishal Pankaj Chandratreya +* Vishnu V K +* vk0812 +* Vlad Korolev +* Will Qian +* William Qian +* wqh17101 +* wsykala +* yaaun +* Yannic Schroeder +* yuanx749 +* 渡邉 美希 + +GitHub issues and pull requests: + +Pull Requests (894): + +* :ghpull:`23814`: Consolidate release notes for 3.6 +* :ghpull:`23899`: Backport PR #23885 on branch v3.6.x (DOC: Rearrange navbar-end elements) +* :ghpull:`23898`: Backport PR #23892 on branch v3.6.x (DOC: Fix docs for linestyles in contour) +* :ghpull:`23885`: DOC: Rearrange navbar-end elements +* :ghpull:`23894`: Backport PR #23881 on branch v3.6.x (Fix Pillow compatibility in example) +* :ghpull:`23897`: Backport PR #23887 on branch v3.6.x (Add missing label argument to barh docs) +* :ghpull:`23892`: DOC: Fix docs for linestyles in contour +* :ghpull:`23887`: Add missing label argument to barh docs +* :ghpull:`23893`: Backport PR #23886 on branch v3.6.x (CI: prefer (older) binaries over (newer) sdists) +* :ghpull:`23881`: Fix Pillow compatibility in example +* :ghpull:`23886`: CI: prefer (older) binaries over (newer) sdists +* :ghpull:`23880`: Backport PR #23862 on branch v3.6.x (Remove triggering of deprecation warning in AnchoredEllipse) +* :ghpull:`23862`: Remove triggering of deprecation warning in AnchoredEllipse +* :ghpull:`23879`: Backport PR #23864 on branch v3.6.x (Correct and improve documentation for anchored artists) +* :ghpull:`23877`: Backport PR #23841 on branch v3.6.x (clarified that hist computes histogram on unbinned data) +* :ghpull:`23872`: Backport PR #23871 on branch v3.6.x (DOC: Fix formatting of pick event demo example) +* :ghpull:`23841`: clarified that hist computes histogram on unbinned data +* :ghpull:`23864`: Correct and improve documentation for anchored artists +* :ghpull:`23871`: DOC: Fix formatting of pick event demo example +* :ghpull:`23869`: Backport PR #23867 on branch v3.6.x (DOC: fix deprecation warnings in examples) +* :ghpull:`23867`: DOC: fix deprecation warnings in examples +* :ghpull:`23858`: Backport PR #23855 on branch v3.6.x (DOC: fix deprecation warnings) +* :ghpull:`23859`: Backport PR #23844 on branch v3.6.x (Further improve dev setup instructions) +* :ghpull:`23844`: Further improve dev setup instructions +* :ghpull:`23855`: DOC: fix deprecation warnings +* :ghpull:`23854`: Backport PR #23852 on branch v3.6.x (Fix cross-compiling internal freetype) +* :ghpull:`23852`: Fix cross-compiling internal freetype +* :ghpull:`23853`: Backport PR #23830 on branch v3.6.x (Start testing on Python 3.11) +* :ghpull:`23830`: Start testing on Python 3.11 +* :ghpull:`23851`: Backport PR #23850 on branch v3.6.x (removed single word in documenting doc) +* :ghpull:`23850`: removed single word in documenting doc +* :ghpull:`23848`: Backport PR #23843 on branch v3.6.x (Clarify that pycairo>=1.14.0 is needed.) +* :ghpull:`23843`: Clarify that pycairo>=1.14.0 is needed. +* :ghpull:`23842`: Backport PR #23840 on branch v3.6.x (Remove documentation for axes_grid) +* :ghpull:`23838`: Backport PR #23834 on branch v3.6.x (Revert "Refactor handling of tick and ticklabel visibility in Axis.clear") +* :ghpull:`23840`: Remove documentation for axes_grid +* :ghpull:`23837`: Backport PR #23833 on branch v3.6.x (Remove search field from sidebar) +* :ghpull:`23836`: Backport PR #23823 on branch v3.6.x ([DOC] Improve dev setup description) +* :ghpull:`23834`: Revert "Refactor handling of tick and ticklabel visibility in Axis.clear" +* :ghpull:`23833`: Remove search field from sidebar +* :ghpull:`23823`: [DOC] Improve dev setup description +* :ghpull:`23822`: Backport PR #23813 on branch v3.6.x (Triplot duplicated label) +* :ghpull:`23813`: Triplot duplicated label +* :ghpull:`23811`: Backport PR #23805 on branch v3.6.x (sphinxext: Do not copy plot_directive.css's metadata) +* :ghpull:`23805`: sphinxext: Do not copy plot_directive.css's metadata +* :ghpull:`23800`: Backport PR #23785 on branch v3.6.x (FIX: ensure type stability for missing cmaps in ``set_cmap``) +* :ghpull:`23799`: Backport PR #23790 on branch v3.6.x (DOC: Add cache busting to all static assets) +* :ghpull:`23785`: FIX: ensure type stability for missing cmaps in ``set_cmap`` +* :ghpull:`23790`: DOC: Add cache busting to all static assets +* :ghpull:`23791`: Backport PR #23774 on branch v3.6.x (Correct rcParams-name in AutoDateFormatter doc-string) +* :ghpull:`23792`: Backport PR #23781 on branch v3.6.x (ci: Add plot types to sphinx-gallery artifacts) +* :ghpull:`23789`: Backport PR #23786 on branch v3.6.x (DOC: fontfallback works for most of the backends) +* :ghpull:`23788`: Backport PR #23784 on branch v3.6.x (DOC: Fix num2date docstring) +* :ghpull:`23786`: DOC: fontfallback works for most of the backends +* :ghpull:`23784`: DOC: Fix num2date docstring +* :ghpull:`23781`: ci: Add plot types to sphinx-gallery artifacts +* :ghpull:`23783`: Backport PR #23782 on branch v3.6.x (Remove ``Axes.cla`` from examples) +* :ghpull:`23782`: Remove ``Axes.cla`` from examples +* :ghpull:`23774`: Correct rcParams-name in AutoDateFormatter doc-string +* :ghpull:`23773`: Backport PR #23772 on branch v3.6.x (3d plots what's new cleanups) +* :ghpull:`23772`: 3d plots what's new cleanups +* :ghpull:`23765`: Backport PR #23762 on branch v3.6.x (FIX: legend handler warning too liberal) +* :ghpull:`23762`: FIX: legend handler warning too liberal +* :ghpull:`23759`: Backport PR #23686 on branch v3.6.x (Improve matplotlib.pyplot importtime by caching ArtistInspector) +* :ghpull:`23686`: Improve matplotlib.pyplot importtime by caching ArtistInspector +* :ghpull:`23756`: Backport PR #23569 on branch v3.6.x (Fix hidden xlabel bug in colorbar) +* :ghpull:`23755`: Backport PR #23742 on branch v3.6.x (FIX: unbreak ipympl) +* :ghpull:`23569`: Fix hidden xlabel bug in colorbar +* :ghpull:`23742`: FIX: unbreak ipympl +* :ghpull:`23752`: Backport PR #23750 on branch v3.6.x (Fix rcParams documentation) +* :ghpull:`23749`: Backport PR #23735 on branch v3.6.x (Correctly handle Axes subclasses that override cla) +* :ghpull:`23735`: Correctly handle Axes subclasses that override cla +* :ghpull:`23748`: Backport PR #23746 on branch v3.6.x (DOC: add numpydoc docstring + commentary to Axis.get_ticklocs) +* :ghpull:`23747`: Backport PR #23721 on branch v3.6.x (3d plot view angle documentation) +* :ghpull:`23746`: DOC: add numpydoc docstring + commentary to Axis.get_ticklocs +* :ghpull:`23721`: 3d plot view angle documentation +* :ghpull:`23744`: Backport PR #23740 on branch v3.6.x (Clarify error for colorbar with unparented mappable) +* :ghpull:`23741`: Backport PR #23674 on branch v3.6.x (Re-rename builtin seaborn styles to not include a dot.) +* :ghpull:`23740`: Clarify error for colorbar with unparented mappable +* :ghpull:`23674`: Re-rename builtin seaborn styles to not include a dot. +* :ghpull:`23738`: Backport PR #23639 on branch v3.6.x (Adding the new contributor meeting) +* :ghpull:`23739`: Backport PR #23712 on branch v3.6.x (FIX: do not try to help CPython with garbage collection) +* :ghpull:`23712`: FIX: do not try to help CPython with garbage collection +* :ghpull:`23639`: Adding the new contributor meeting +* :ghpull:`23732`: Backport PR #23729 on branch v3.6.x (Use cleaner recursion check in PyQt FigureCanvas' resizeEvent.) +* :ghpull:`23734`: Backport PR #23733 on branch v3.6.x (DOC: Update theme configuration for upcoming changes) +* :ghpull:`23733`: DOC: Update theme configuration for upcoming changes +* :ghpull:`23728`: Backport PR #23722 on branch v3.6.x (Restore deprecation class aliases in cbook) +* :ghpull:`23729`: Use cleaner recursion check in PyQt FigureCanvas' resizeEvent. +* :ghpull:`23726`: Backport PR #23711 on branch v3.6.x (Fix deprecation messages for vendoring unused things) +* :ghpull:`23722`: Restore deprecation class aliases in cbook +* :ghpull:`23727`: Backport PR #23724 on branch v3.6.x (Fix/harmonize spacing in dependencies.rst.) +* :ghpull:`23724`: Fix/harmonize spacing in dependencies.rst. +* :ghpull:`23711`: Fix deprecation messages for vendoring unused things +* :ghpull:`23715`: Backport PR #23708 on branch v3.6.x (Loosen up test_Normalize test) +* :ghpull:`23713`: Backport PR #23710 on branch v3.6.x (Fix cmap deprecations) +* :ghpull:`23708`: Loosen up test_Normalize test +* :ghpull:`23710`: Fix cmap deprecations +* :ghpull:`23696`: Backport PR #23695 on branch v3.6.x (Document polar handling of _interpolation_steps.) +* :ghpull:`23706`: Backport PR #23705 on branch v3.6.x (DOC: Added link to class under discussion) +* :ghpull:`23705`: DOC: Added link to class under discussion +* :ghpull:`23695`: Document polar handling of _interpolation_steps. +* :ghpull:`23668`: Api deprecate cmap functions +* :ghpull:`23049`: Add ``minor`` keyword argument to ``plt.x/yticks`` +* :ghpull:`23665`: Harmonize docstrings for boxstyle/connectionstyle/arrowstyle. +* :ghpull:`23636`: FIX: macosx flush_events should process all events +* :ghpull:`23555`: Uncamelcase offsetTrans in draw_path_collection. +* :ghpull:`23682`: Fix generated documentation for deprecated modules +* :ghpull:`23678`: Get rcParams from mpl +* :ghpull:`23571`: Simplify _bind_draw_path_function. +* :ghpull:`23673`: DOC: Highlight information about avoiding labels in legend +* :ghpull:`22506`: Replace MathtextBackend mechanism. +* :ghpull:`23340`: Set correct path for Arc +* :ghpull:`23562`: Fix issue with get_edgecolor and get_facecolor in 3D plots +* :ghpull:`23634`: make.bat: Don't override SPHINXOPTS/O from the environment +* :ghpull:`23675`: Deprecate helper functions in axis3d +* :ghpull:`23676`: MNT: Get rcParams from mpl +* :ghpull:`23677`: TST: Use article class when checking for pgf +* :ghpull:`23669`: CI: Azure update from ubuntu-18.04 to ubuntu-latest and ubuntu-20.04 +* :ghpull:`23670`: Add bar color demo. +* :ghpull:`23644`: Standardize edge-on axis locations when viewing primary 3d axis planes +* :ghpull:`23563`: Fix issue with drawing 3D lines where points are from nparray +* :ghpull:`23666`: MNT: Deprecate macosx prepare subplots tool +* :ghpull:`23572`: Deprecate ``get_grid_positions(..., raw=True)``. +* :ghpull:`23525`: Add functionality to label individual bars with Axes.bar() +* :ghpull:`23667`: Fix flake8 errors introduced by crossed PRs +* :ghpull:`23554`: MNT: Remove unused imports +* :ghpull:`23659`: Simplify/fix save_diff_image. +* :ghpull:`23663`: Small cleanups to _find_fonts_by_props. +* :ghpull:`23662`: Add tolerance to test failing on ppc64le +* :ghpull:`23623`: MNT: remove _gridspecs attribute on Figure classes +* :ghpull:`23654`: Reverts macosx change to ARC +* :ghpull:`23661`: Remove unused fontsize argument from private mathtext _get_info. +* :ghpull:`23655`: Merge branch v3.5.x into main +* :ghpull:`23658`: Increase tolerance on multi-font tests +* :ghpull:`23657`: Add eps to extension list in image triager +* :ghpull:`23656`: Fix broken link to MathML torture tests. +* :ghpull:`23649`: CI: Use anaconda-client v1.10.0 for upload of nightlies +* :ghpull:`23647`: Allow any color format to be used for axis3d.Axis.set_pane_color +* :ghpull:`23643`: Enable wheels for PyPy 3.8+ +* :ghpull:`23621`: DOC: update and extend fonts explanation +* :ghpull:`23612`: CI: try installing a different version of noto on OSX +* :ghpull:`23619`: add pikepdf and visual c++ dependency +* :ghpull:`23631`: Leave out ``barh`` from the basic plot types. +* :ghpull:`23637`: BLD: Add Python 3.11 builds to CI +* :ghpull:`23632`: Add discouraged admonitions +* :ghpull:`23620`: Doc update deps +* :ghpull:`23627`: Bump pypa/cibuildwheel from 2.8.1 to 2.9.0 +* :ghpull:`23628`: Change Title Case to Upper lower in templates +* :ghpull:`23206`: Change exception type for incorrect SVG date metadata +* :ghpull:`23387`: Remove setuptools_scm_git_archive dependency and add sdist test +* :ghpull:`23605`: Fix issues in examples, docs, and tutorials +* :ghpull:`23618`: [Doc]: Document the position parameter in apply_aspect() +* :ghpull:`23355`: Revert "Try to unbreak CI by xfailing OSX Tk tests" +* :ghpull:`23610`: TST: be more forgiving about IDing Noto +* :ghpull:`23609`: print version number when building docs +* :ghpull:`20832`: Implement multi-font embedding for PS Backend +* :ghpull:`20804`: Implement multi-font embedding for PDF Backend +* :ghpull:`23202`: MNT: Remove cached renderer from figure +* :ghpull:`23497`: Avoid gridspec in more examples +* :ghpull:`23602`: Editing "issues for new contributors" +* :ghpull:`23600`: DOC: view_init docstring for 3d axes primary view angles +* :ghpull:`23587`: BUG:datetime list starting with none +* :ghpull:`23559`: re-base of font fallback for pdf and eps output + SVG support +* :ghpull:`23557`: BLD: update the manylinux versions used +* :ghpull:`23596`: Minor cleanup of axes_grid1 +* :ghpull:`23594`: Expire deprecation on passing bytes to FT2Font.set_text +* :ghpull:`23435`: Add conda env to setup instructions +* :ghpull:`23574`: Move colorbar() doc to method itself. +* :ghpull:`23584`: Bump Ubuntu to 20.04 on GitHub Actions +* :ghpull:`23561`: Clean up code in tri +* :ghpull:`23582`: Cleanup axis3d.Axis.draw +* :ghpull:`23510`: Refactor Widget tests +* :ghpull:`20718`: Circle: Build docs in parallel. +* :ghpull:`22452`: ENH: add ability to remove layout engine +* :ghpull:`23516`: warning when scatter plot color settings discarded +* :ghpull:`23577`: apply_aspect cleanups +* :ghpull:`23575`: Cleanup parasite_simple example. +* :ghpull:`23567`: Remove noop setattr_cm. +* :ghpull:`23412`: Fix dash offset bug in Patch +* :ghpull:`21756`: MNT: Clean up some UTF strings and memory autorelease +* :ghpull:`23558`: MNT: Use UTF-8 string in macosx backend +* :ghpull:`23550`: Change exception types, improve argument checking, and cleanups in mpl_toolkits +* :ghpull:`23196`: Unify set_pickradius argument +* :ghpull:`20740`: Implement Font-Fallback in Matplotlib +* :ghpull:`22566`: Add rcparam for figure label size and weight +* :ghpull:`23551`: Remove transform arguments from _iter_collection +* :ghpull:`23444`: Deduplicate common parts in LatexManager.{__init__,_setup_latex_process} +* :ghpull:`23017`: [ENH] : Provide axis('equal') for Axes3D (replace PR #22705) +* :ghpull:`22950`: Simplify definition of mathtext symbols & correctly end tokens in mathtext parsing +* :ghpull:`23409`: Provide axis('equal') for Axes3D (replaces PR #23017) +* :ghpull:`23434`: Fix array-like linewidth for 3d scatter +* :ghpull:`23500`: Move the common implementation of Axes.set_x/y/zscale to Axis. +* :ghpull:`23533`: Add tests for sankey and minor fixes +* :ghpull:`23535`: Make margins error as claimed in doc-string +* :ghpull:`23546`: Simplify impl. of functions optionally used as context managers. +* :ghpull:`23494`: Fix various issues from SonarQube +* :ghpull:`23529`: Add workflow dispatch GitHub CI +* :ghpull:`23539`: Small improvements to WebAgg example +* :ghpull:`23541`: Change doc-build CI install order +* :ghpull:`23526`: DOC: make "family" less ambiguous in FontProperties docs +* :ghpull:`23537`: Move the deprecated RendererGTK{3,4}Cairo to a single place. +* :ghpull:`23140`: [Features] Allow setting legend title alignment +* :ghpull:`23538`: Fix imprecise docs re: backend dependencies. +* :ghpull:`23532`: Add test for RGBAxes +* :ghpull:`23453`: Add more tests for mplot3d +* :ghpull:`23501`: Let Axes.clear iterate over Axises. +* :ghpull:`23469`: Inline _init_axis_artists & _init_gridlines into clear. +* :ghpull:`23475`: Add markerfacealt to pass-through arguments for error bar lines +* :ghpull:`23527`: STY: fix whitespace on an assert +* :ghpull:`23495`: Fix sgskip'd examples +* :ghpull:`23404`: Restore matplotlib.__doc__ in Sphinx docs +* :ghpull:`23507`: Add hint when More than {max_open_warning} figures have been opened +* :ghpull:`23499`: Fix outdated comment re: event handlers in test_backends_interactive. +* :ghpull:`23498`: Fix direct instantiation of webagg_core managers. +* :ghpull:`23504`: Clarify formatting of the code-for-reproduction field in bug reports. +* :ghpull:`23489`: Add missing test data to install +* :ghpull:`23482`: Mathtext spaces must be independent of font style. +* :ghpull:`23486`: Bump pypa/cibuildwheel from 2.8.0 to 2.8.1 +* :ghpull:`23461`: Tweak Axes repr. +* :ghpull:`16931`: Make it easier to improve UI event metadata. +* :ghpull:`23468`: Display grid in floating axes example. +* :ghpull:`23467`: Remove old handling for factor=None in axisartist. +* :ghpull:`23443`: Try running the pgf backend off the article class. +* :ghpull:`23373`: Fix pan/zoom crashing when widget lock is unavailable +* :ghpull:`23466`: Update filename in example. +* :ghpull:`23464`: Deprecate macos close handler. +* :ghpull:`23463`: Deprecate Tick.label +* :ghpull:`23455`: Deprecate properties w_xaxis, w_yaxis, and w_zaxis +* :ghpull:`23448`: Tweak callbacks to generate pick events. +* :ghpull:`23233`: Default stem marker color follows the linecolor +* :ghpull:`23452`: Generalize Axes __repr__ to 3D +* :ghpull:`23445`: Compare thread native ids when checking whether running on main thread. +* :ghpull:`20752`: Set norms using scale names. +* :ghpull:`23438`: DOC: numpydoc-ify date Locator classes +* :ghpull:`23427`: Tweak pgf escapes. +* :ghpull:`23432`: Fixed typo in docs animation api +* :ghpull:`23420`: Clean up test_chunksize_fails() +* :ghpull:`23415`: Minor improvements to units_sample example +* :ghpull:`21339`: Added linear scaling test to Hexbin marginals +* :ghpull:`23414`: Bump pypa/cibuildwheel from 2.7.0 to 2.8.0 +* :ghpull:`23413`: Combine chunk size tests into one +* :ghpull:`23403`: Small cleanup to VertexSelector. +* :ghpull:`23291`: In the new/simplified backend API, don't customize draw_if_interactive. +* :ghpull:`23350`: Fixed SVG-as-text image comparison tests. +* :ghpull:`23406`: DOC: Fix calculation of bin centers in multi-histogram +* :ghpull:`23407`: TST: Add missing warning type to pytest.warns +* :ghpull:`23402`: Link 3D animation examples to one another. +* :ghpull:`23401`: Upload wheel artifacts from the correct directory +* :ghpull:`23374`: GOV: point CoC reports at CoC steering council subcomittee mailing list +* :ghpull:`23393`: Clean up formatting of custom cmap example +* :ghpull:`23146`: Update cibuildwheel +* :ghpull:`23368`: Add a helper to generate closed paths. +* :ghpull:`20220`: DOC: add mission statement +* :ghpull:`22364`: Tweak mathtext/tex docs. +* :ghpull:`23377`: Use tick_params more often over tick iteration +* :ghpull:`22820`: [Doc] consolidate ``rect`` documentation +* :ghpull:`23371`: Default animation.convert_args to ["-layers", "OptimizePlus"]. +* :ghpull:`23148`: DOC: change address to send security issues to +* :ghpull:`23365`: DOC: add new showcase example, replace gendered one +* :ghpull:`23033`: Fix issue with tex-encoding on non-Unicode platforms +* :ghpull:`23358`: Shorten/clarify definition of extension types. +* :ghpull:`23370`: Small cleanups to animation. +* :ghpull:`23364`: Rename/change signature of PyGlyph_new. +* :ghpull:`23363`: Simplify FigureCanvas multiple inheritance init by swapping bases order. +* :ghpull:`23366`: MNT: use devel version of theme +* :ghpull:`23357`: Fixed decimal points not appearing at end of Mathtext string. +* :ghpull:`23351`: DOC/MNT install docs with dev version of sphinx theme +* :ghpull:`23349`: CI: Remove old scipy-wheels-nightly uploads to ensure space +* :ghpull:`23348`: Support multi-figure MultiCursor; prepare improving its signature. +* :ghpull:`23360`: embedding_in_tk_sgskip.py: use root.destroy +* :ghpull:`23354`: MNT: Use list comprehension +* :ghpull:`23299`: FIX/API: do not reset backend key in rc_context +* :ghpull:`23191`: ENH: add width_ratios and height_ratios to subplots +* :ghpull:`23060`: MNT: Change objective C code to Automatic Reference Counting (ARC) +* :ghpull:`23347`: Simplify/improve check for pycairo in Gtk-based backends. +* :ghpull:`23316`: DOC: improve spines crosslinking +* :ghpull:`23100`: Remove custom backend_nbagg.show(), putting logic in manager show. +* :ghpull:`23342`: FIX: make sure addFont test removes the test font +* :ghpull:`23266`: negative_linestyles kwarg in contour.py +* :ghpull:`23332`: Validate Text linespacing on input. +* :ghpull:`23336`: Remove ineffective exclusion of Arcs without parent Axes. +* :ghpull:`23341`: MNT: Use '--pytest-test-first' option for naming clarity +* :ghpull:`23337`: Remove now inexistent "datapath" rcParam from style blacklist. +* :ghpull:`22004`: Make RendererCairo auto-infer surface size. +* :ghpull:`23208`: ENH: enable stripey lines +* :ghpull:`23288`: Correct URL area with rotated texts in PDFs +* :ghpull:`23197`: Add tests for pan +* :ghpull:`22167`: Deprecate selector ``visible`` attribute +* :ghpull:`23322`: Cleanup FontProperties examples. +* :ghpull:`23321`: Tweak examples capitalization/punctuation. +* :ghpull:`23270`: Fix handling of nonmath hyphens in mathtext. +* :ghpull:`23310`: Move Cursor demo from examples/misc to examples/event_handling +* :ghpull:`23313`: Drop CSS styles that are in mpl-sphinx-theme +* :ghpull:`23314`: Don't draw invisible 3D Axes +* :ghpull:`23302`: Deprecate stem(..., use_line_collection=False) +* :ghpull:`23309`: Remove front page examples +* :ghpull:`23282`: Backport PR #22865 on branch v3.5.x (Fix issue with colorbar extend and drawedges) +* :ghpull:`23231`: Add pytest-xvfb as test dependency +* :ghpull:`23318`: No need to return OrderedDict from _gen_axes_spines. +* :ghpull:`23295`: Replace re.sub by the faster str.translate. +* :ghpull:`23300`: Modify example of "Fig Axes Customize Simple" +* :ghpull:`23014`: Improve consistency in LogLocator and LogFormatter API +* :ghpull:`23286`: Refactor URL handling in PDF backend +* :ghpull:`23065`: Fix test_image_comparison_expect_rms +* :ghpull:`23294`: Simplify binary data handling in ps backend. +* :ghpull:`23284`: DOC: Switch to HTML5 and cleanup CSS +* :ghpull:`23276`: Add get/set methods for DPI in SubFigure +* :ghpull:`23207`: Update build environment and improve test +* :ghpull:`23213`: DEV: Add name-tests-test to pre-commit hooks +* :ghpull:`23289`: Properly make Name.hexify go through a deprecation cycle. +* :ghpull:`23177`: Deprecate positional passing of most Artist constructor parameters +* :ghpull:`23287`: Minor tweaks to pdf Name. +* :ghpull:`23285`: In mathtext, replace manual caching (via ``glyphd``) by lru_cache. +* :ghpull:`23034`: Correctly read the 'style' argument while processing 'genfrac'. +* :ghpull:`23247`: Support inverted parentheses in mathtext. +* :ghpull:`23190`: Deprecate unused methods in axis.py +* :ghpull:`23219`: MNT: Rename example files with 'test' in name +* :ghpull:`23277`: MNT: Remove dead code in SVG backend +* :ghpull:`23261`: Bump actions/setup-python from 3 to 4 +* :ghpull:`23264`: Changing environment.yml for it to work on Windows +* :ghpull:`23269`: MNT: Remove dead code in Colorbar +* :ghpull:`23262`: Simplify qt_compat, in particular post-removal of qt4 support. +* :ghpull:`23263`: Private helper to get requested backend without triggering resolution. +* :ghpull:`23243`: Fix spacing after mathtext operators with sub/superscripts +* :ghpull:`22839`: Fix spacing after mathtext operators with sub/superscripts +* :ghpull:`23256`: DOC: Add note about Inkscape install on Windows +* :ghpull:`23258`: DOC: remove Blue Book url +* :ghpull:`23255`: Add a helper to generate mathtext error strings. +* :ghpull:`23246`: Fix argument checking for set_interpolation_stage +* :ghpull:`22881`: Support not embedding glyphs in svg mathtests. +* :ghpull:`23198`: Rename ncol parameter in legend to ncols +* :ghpull:`23251`: Small simplifications to mathtext tests. +* :ghpull:`23249`: Don't allow ``r"$\left\\|\right.$"``, as in TeX. +* :ghpull:`23248`: Rename test markers +* :ghpull:`22507`: Remove *math* parameter of various mathtext internal APIs. +* :ghpull:`23192`: Add tests, improve error messages in axis/_base, and code cleanup +* :ghpull:`23241`: Fix invalid value in radio buttons example +* :ghpull:`23187`: Correct docs and use keyword arguments in _mathtext.py +* :ghpull:`23045`: MNT: Merge locally defined test marks +* :ghpull:`22289`: ENH: compressed layout +* :ghpull:`23237`: Expire BoxStyle._Base deprecation. +* :ghpull:`23225`: DOC: Fix version switcher links to documentation +* :ghpull:`23221`: DOC: recommend numpy random number generator class +* :ghpull:`23223`: Changed offset reference, add small doc +* :ghpull:`23215`: DOC: link the transforms tutorial from the module +* :ghpull:`23201`: Rework tricontour and tricontourf documentation +* :ghpull:`23013`: Add tests for date module +* :ghpull:`23188`: Mnt new default dates +* :ghpull:`22745`: MNT: Don't require renderer for window_extent and tightbbox +* :ghpull:`23077`: MNT: Remove keyword arguments to gca() +* :ghpull:`23182`: Simplify webagg blitting. +* :ghpull:`23181`: Init FigureCanvasAgg._lastKey in ``__init__``. +* :ghpull:`23175`: Point the version switcher to a name listed in switcher.json +* :ghpull:`22669`: Cleanup documentation generation for pyplot +* :ghpull:`22519`: fix markevery plot option with nans in data +* :ghpull:`21584`: Move towards having get_shared_{x,y}_axes return immutable views. +* :ghpull:`23170`: ENH: update ticks when requesting labels +* :ghpull:`23169`: DOC: Migrate to sphinx-design +* :ghpull:`23180`: Improve docstring of triplot() and PatchCollection +* :ghpull:`23153`: Restore accidentally removed pytest.ini and tests.py. +* :ghpull:`23166`: Deprecate passing most Legend arguments positionally +* :ghpull:`23165`: DOCS Fix a few typos +* :ghpull:`23167`: DOCS fix typo +* :ghpull:`23062`: Add stackplot to plot types listing +* :ghpull:`23161`: Added my (open access) book +* :ghpull:`23141`: Minor fix for astropy units support broken in earlier PR +* :ghpull:`23156`: No longer call draw_if_interactive in parasite_axes. +* :ghpull:`23150`: DOC fix typo +* :ghpull:`23149`: DOCS remove duplicate text +* :ghpull:`23145`: Fix format error in switcher.json +* :ghpull:`21755`: MNT: Clean up macosx backend set_message +* :ghpull:`23128`: DOCS Fix typos +* :ghpull:`23130`: Drop pytest warning config in nightly tests +* :ghpull:`23135`: Unpin coverage again +* :ghpull:`23133`: Make module deprecation messages consistent +* :ghpull:`23134`: Remove newline from start of deprecation warnings +* :ghpull:`22964`: Fix spelling errors +* :ghpull:`22929`: Handle NaN in bar labels and error bars +* :ghpull:`23093`: MNT: Removing 3.4 deprecations +* :ghpull:`23090`: Derive new_figure_manager from FigureCanvas.new_manager. +* :ghpull:`23099`: Remove unneeded cutout for webagg in show(). +* :ghpull:`23097`: Tweak check for IPython pylab mode. +* :ghpull:`23088`: Improve error for invalid format strings / misspelled data keys. +* :ghpull:`23092`: Ensure updated monkey-patching of sphinx-gallery EXAMPLE_HEADER +* :ghpull:`23087`: Fix width/height inversion in dviread debug helper. +* :ghpull:`23089`: Normalize tk load failures to ImportErrors. +* :ghpull:`23091`: Move test that fig.add_axes() needs parameters +* :ghpull:`23067`: more explicit in windows doc build instructions +* :ghpull:`23081`: MNT: Deprecate date_ticker_factory +* :ghpull:`23079`: MNT: Remove key_press and button_press from FigureManager +* :ghpull:`23076`: MNT: Remove positional argument handling in LineCollection +* :ghpull:`23078`: MNT: Remove deprecated axis.cla() +* :ghpull:`23054`: Slightly simplify tcl/tk load in extension. +* :ghpull:`23073`: MNT: Remove dummy_threading because threading is always available +* :ghpull:`22405`: DOC: put the gallery keywords in the meta tag +* :ghpull:`23071`: Fix installing contourpy on CI +* :ghpull:`23068`: Slight refactor of _c_internal_utils to linewrap it better. +* :ghpull:`23070`: Pathlibify autotools invocation in build. +* :ghpull:`22755`: Maybe run autogen as part of freetype install +* :ghpull:`23063`: doc: mathtext example: use axhspan() instead of fill_between() for backdrop rectangle shading +* :ghpull:`23055`: Cleanup Annotation.update_position. +* :ghpull:`22567`: Use contourpy for quad contour calculations +* :ghpull:`22801`: TST: fully parameterize test_lazy_linux_headless +* :ghpull:`22180`: ENH: Use rcParams savefig.directory on macosx backend +* :ghpull:`23048`: Add rrulewrapper to docs +* :ghpull:`23047`: Fix issue with hist and float16 data +* :ghpull:`23044`: Fix missing section header for nightly builds +* :ghpull:`23029`: Demonstrate both usetex and non-usetex in demo_text_path.py. +* :ghpull:`23038`: Factor out errorevery parsing for 2D and 3D errorbars. +* :ghpull:`23036`: Suppress traceback chaining for tex subprocess failures. +* :ghpull:`23037`: Suppress exception chaining in FontProperties. +* :ghpull:`23020`: Add test to close legend issue +* :ghpull:`23031`: Specify that style files are utf-8. +* :ghpull:`22991`: Enable ``plt.sca`` on subfigure's axes +* :ghpull:`23030`: DOC: Fix charset declaration in redirects +* :ghpull:`23022`: Fix some possible encoding issues for non-utf8 systems. +* :ghpull:`23023`: Bump docker/setup-qemu-action from 1 to 2 +* :ghpull:`23024`: DOC: do not suggest to sudo pip install Matplotlib +* :ghpull:`23018`: Fix typo in font family +* :ghpull:`22627`: ENH: rect for constrained_layout +* :ghpull:`22891`: Font example monospace +* :ghpull:`23006`: docs: add subplot-mosaic string compact notation +* :ghpull:`23009`: Fixed installation guide command typo +* :ghpull:`22926`: Fix RangeSlider for same init values #22686 +* :ghpull:`22989`: Merge v3.5.x back into main +* :ghpull:`22993`: STY: Fix typos in colormap +* :ghpull:`22777`: DEV: Add codespell to pre-commit hooks +* :ghpull:`22940`: Fixed dpi bug in rainbow text example +* :ghpull:`22298`: MNT: Remove cmap_d colormap access +* :ghpull:`22387`: Add a registry for color sequences +* :ghpull:`21594`: Document text alignment +* :ghpull:`22967`: TST: Add some tests for QuadMesh contains function +* :ghpull:`22936`: ENH: Add full-screen toggle to the macosx backend +* :ghpull:`22886`: MNT: remove mpl_toolkits.axes_grid +* :ghpull:`22952`: Make MarkerStyle immutable +* :ghpull:`22953`: MNT: Move set_cursor to the FigureCanvas +* :ghpull:`18854`: Standardize creation of FigureManager from a given FigureCanvas class. +* :ghpull:`22925`: Standardize creation of FigureManager from a given FigureCanvas class. +* :ghpull:`22875`: Remove Forward definitions where possible. +* :ghpull:`22928`: ENH: Add option to disable raising the window for macosx +* :ghpull:`22912`: DOC: Better doc of colors +* :ghpull:`22931`: BUG: Fix regression with ls=(0, ()) +* :ghpull:`22909`: FIX: skip sub directories when finding fonts on windows +* :ghpull:`22911`: Clarify docstring of [un]install_repl_displayhook() +* :ghpull:`22919`: CI: Add concurrency skips for GH Actions +* :ghpull:`22899`: Fix documentation markup issues +* :ghpull:`22906`: Clarify logic for repl displayhook. +* :ghpull:`22892`: Remove support for IPython<4. +* :ghpull:`22896`: Remove python-dateutil as test requirement +* :ghpull:`22885`: Deprecate two-layered backend_pdf.Op enum. +* :ghpull:`22883`: Tweak argument checking in tripcolor(). +* :ghpull:`22884`: Missing ``f`` prefix on f-strings fix +* :ghpull:`22877`: Small cleanups to mathtext. +* :ghpull:`21374`: Snap selectors +* :ghpull:`22824`: Remove some unnecessary extra boundaries for colorbars with extensions. +* :ghpull:`21448`: Use named groups in mathtext parser. +* :ghpull:`22609`: Improve usability of dviread.Text by third parties. +* :ghpull:`22809`: STY: Apply pre-commit hooks to codebase +* :ghpull:`22730`: Fix removed cross-references +* :ghpull:`22857`: Slightly simplify twin axes detection in MEP22 zoom. +* :ghpull:`22813`: MNT: Deprecate figure callbacks +* :ghpull:`22802`: MNT: make Axes.cla an alias for Axes.clear in all cases +* :ghpull:`22855`: Remove non-needed remove_text=False. +* :ghpull:`22854`: TST: Avoid floating point errors in asinh ticker +* :ghpull:`22850`: Simplify tick creation +* :ghpull:`22841`: Fix Tk error when updating toolbar checkbutton images +* :ghpull:`22707`: Proposed ENH: Allow user to turn off breaking of streamlines in streamplot (rebased) +* :ghpull:`22826`: Bump actions/upload-artifact from 2 to 3 +* :ghpull:`22825`: Bump codecov/codecov-action from 2 to 3 +* :ghpull:`22821`: Use bool for bool keyword arguments +* :ghpull:`22815`: Fix pickling of globally available, dynamically generated norm classes. +* :ghpull:`22702`: Doc tweak transform tutorial +* :ghpull:`22613`: DOC: Add links to explicit vs implicit API everywhere "OO" is used +* :ghpull:`22712`: Use repr in error messages +* :ghpull:`22794`: Fix ps export of colored hatches with no linewidth +* :ghpull:`22797`: Deprecate functions in backends +* :ghpull:`22608`: Axes.inset_axes: enable Axes subclass creation +* :ghpull:`22795`: Replace "marker simplification" by "marker subsampling" in docs. +* :ghpull:`22768`: Fix inkscape tests +* :ghpull:`22791`: Tweak _ConverterError reporting. +* :ghpull:`22447`: Improve bar_label annotation +* :ghpull:`22710`: Fix the error- TypeError: 'float' object is not iterable +* :ghpull:`22444`: Revert "CI: skip test to work around gs bug" +* :ghpull:`22785`: CI: Update weekly dependency test job +* :ghpull:`22784`: Fix 'misspelled' transform variable +* :ghpull:`22778`: Fix LaTeX formatting in examples +* :ghpull:`22779`: Improve mlab documentation (and example) +* :ghpull:`22759`: MNT: Skip existing wheels during nightly wheel upload +* :ghpull:`22751`: BLD: do not put an upper bound on pyparsing +* :ghpull:`22752`: DOC: Correct nightly wheels pip install command +* :ghpull:`22742`: Fix deprecation of backend_tools.ToolBase.destroy +* :ghpull:`22725`: Move towards making texmanager stateless. +* :ghpull:`22734`: Added clim support to tripcolor +* :ghpull:`22733`: CI: Add GHA workflow to upload nightly wheels +* :ghpull:`21637`: Also upload a subset of nightly wheels +* :ghpull:`22698`: Correct cross-references in documentation +* :ghpull:`22263`: DOC: condense version switcher +* :ghpull:`22361`: Revert datetime usetex ticklabels to use default tex font. +* :ghpull:`22721`: Small style fixes. +* :ghpull:`22356`: Cleanup tripcolor() +* :ghpull:`22360`: Let TeX handle multiline strings itself. +* :ghpull:`22418`: Deprecate auto-removal of overlapping Axes by plt.subplot{,2grid}. +* :ghpull:`22722`: Rename confusingly-named cm_fallback. +* :ghpull:`22697`: Deprecate in testing.decorators +* :ghpull:`22556`: Add text.parse_math rcParams +* :ghpull:`22163`: Change colour of Tk toolbar icons on dark backgrounds +* :ghpull:`22704`: Small simplification to textpath. +* :ghpull:`22498`: TST: increase coverage on tk tests +* :ghpull:`21425`: Make Axis3D constructor signature closer to the one of 2D axis. +* :ghpull:`22665`: Improve error message for incorrect color string +* :ghpull:`22685`: Rewrite plot format detection from sphinx build target +* :ghpull:`22670`: Update deprecated vmImage 'vs2017-win2016' in azure pipelines +* :ghpull:`22503`: Deprecate backend_qt.qApp. +* :ghpull:`22683`: Add missing space before : for parameters +* :ghpull:`22591`: Fix Path/str-discrepancy in FontManager.addpath and improve documentation +* :ghpull:`22680`: Bump actions/cache from 2 to 3 +* :ghpull:`22659`: Add description on quiver head parameters +* :ghpull:`22668`: Raise on missing closing quotes in matplotlibrc +* :ghpull:`22675`: Tweak colorbar_placement example. +* :ghpull:`22276`: Merge "Scatter Symbol" and "Scatter Custom Symbol" examples +* :ghpull:`22658`: Remove reference to now-deleted reminder note. +* :ghpull:`22652`: Update documentation example and fix See also +* :ghpull:`22587`: Refactor handling of tick and ticklabel visibility in Axis.clear() +* :ghpull:`22148`: MNT: Deprecate ``docstring`` +* :ghpull:`22170`: Add example to polygon selector docstring showing how to set vertices programmatically +* :ghpull:`22650`: Fix new leak in ft2font introduced in #22604 +* :ghpull:`22644`: FIX: Flush events after closing figures in macosx backend +* :ghpull:`22643`: Suppress exception chaining in colormap lookup. +* :ghpull:`22639`: ENH: MacOSX backend to use sRGB instead of GenericRGB colorspace +* :ghpull:`22509`: Simplifications to ToolManager.{add,remove}_tool. +* :ghpull:`22633`: DOC: remove space in directive. +* :ghpull:`22631`: Add space between individual transform components in svg output. +* :ghpull:`22523`: MNT: Use a context manager to change the norm in colorbar code +* :ghpull:`22615`: FIX: Change get_axis_map to axis_map now +* :ghpull:`22508`: Move tracking of autoscale status to Axis. +* :ghpull:`22547`: Small cleanups around TexManager usage. +* :ghpull:`22511`: Remove redundant rcParam-lookup in patches +* :ghpull:`22516`: Expire deprecations in backends +* :ghpull:`22612`: Updated grammar to reflect more common usage of output vs outputted in animation.py +* :ghpull:`22589`: Support quoted strings in matplotlibrc +* :ghpull:`22604`: MNT: Fix types in C-code to reduce warnings +* :ghpull:`22610`: Fix alternative suggestion in epoch2num() deprecation +* :ghpull:`22554`: Prepare for making create_dummy_axis not necessary. +* :ghpull:`22607`: ENH: Add dark/light mode theme to the buttons +* :ghpull:`21790`: FIX: Update blitting and drawing on the macosx backend +* :ghpull:`22175`: FIX: Update macosx animation handling +* :ghpull:`22569`: Require non-zero dash value +* :ghpull:`22544`: Correct paper sizes +* :ghpull:`20470`: Issues warnings for legend handles without handlers +* :ghpull:`22558`: MNT: Simplify imports +* :ghpull:`22580`: fix doc for annotation_clip parameter +* :ghpull:`22581`: DOC: fix various typos +* :ghpull:`22573`: Bump actions/setup-python from 2 to 3 +* :ghpull:`22568`: Rename qhull source to _qhull_wrapper.cpp. +* :ghpull:`22561`: FIX: Handle stopped animation figure resize +* :ghpull:`22562`: TST: Add a frame test for animations +* :ghpull:`22514`: Expire deprecations in cbook.deprecation +* :ghpull:`22555`: Use picklable callbacks for DraggableBase. +* :ghpull:`22552`: Tweak dependency checking in doc/conf.py. +* :ghpull:`22550`: Require sphinx>=3 & numpydoc>=1.0 for building docs. +* :ghpull:`22539`: Deprecate toplevel mpl.text.get_rotation; normalize rotations early. +* :ghpull:`22502`: Cleanup unused imports and variables in backends +* :ghpull:`20071`: Document, test, and simplify impl. of auto_adjustable_area. +* :ghpull:`22366`: Deprecation removal/updates in axes3d +* :ghpull:`22484`: Simplify the internal API to connect picklable callbacks. +* :ghpull:`22417`: Support passing rgbaFace as an array to agg's draw_path. +* :ghpull:`22412`: Turn _get_axis_map() into a property and remove _get_axis_list() +* :ghpull:`22486`: Expire deprecations in lines and patches +* :ghpull:`22512`: Increase coverage +* :ghpull:`22504`: Simplify FontProperties init. +* :ghpull:`22497`: Remove entries of MathTextParser._backend_mapping deprecated in 3.4. +* :ghpull:`22487`: Don't key MathTextParser cache off a mutable FontProperties. +* :ghpull:`22468`: Turn _mathtext.ship into a plain function. +* :ghpull:`22490`: Deprecate unused, untested Affine2D.identity(). +* :ghpull:`22491`: Linewrap setupext to 79 character lines. +* :ghpull:`22488`: Some more maintenance for mathtext internal implementation. +* :ghpull:`22485`: Change string representation of AxesImage +* :ghpull:`22240`: Add minimum macosx version +* :ghpull:`22480`: Remove _point_size_reduction. +* :ghpull:`22204`: Cleanup _mathtext internal API +* :ghpull:`22469`: Improve readability of mathtext internal structures. +* :ghpull:`22477`: Un-pyplot some examples which were already explicitly referencing axes. +* :ghpull:`22467`: Small cleanup to font handling in agg. +* :ghpull:`21178`: Add asinh axis scaling (*smooth* symmetric logscale) +* :ghpull:`22411`: Move cbook._define_aliases() to _api.define_aliases() +* :ghpull:`22465`: Deprecate unused AddList. +* :ghpull:`22451`: Clarify error message for bad keyword arguments. +* :ghpull:`21267`: Cleanup AnnotationBbox. +* :ghpull:`22464`: Small improvements related to radar_chart example. +* :ghpull:`22421`: Make most params to figure()/Figure() kwonly. +* :ghpull:`22457`: Copy arrowprops argument to FancyAnnotationBbox. +* :ghpull:`22454`: move ``_toolbar_2`` from webagg_core to webagg +* :ghpull:`22413`: Remove some trivial private getters/setters in axisartist +* :ghpull:`21634`: TST: Add future dependency tests as a weekly CI job +* :ghpull:`22079`: Share FigureManager class between gtk3 and gtk4. +* :ghpull:`22440`: Clarify warning about labels with leading underscores. +* :ghpull:`17488`: Make error message explicit in legend.py +* :ghpull:`22453`: Simplify impl. of polar limits setting API. +* :ghpull:`22449`: Small cleanup to quiver. +* :ghpull:`22415`: Make emit and auto args of set_{x,y,z}lim keyword only. +* :ghpull:`22422`: Deprecate backend_ps.convert_psfrags. +* :ghpull:`22194`: Drop support for Python 3.7 +* :ghpull:`22234`: Partial fix for grid alpha +* :ghpull:`22433`: Fix ambiguous link targets in docs. +* :ghpull:`22420`: Update plt.figure() docstring. +* :ghpull:`22388`: Make signature of Axes.annotate() more explicit. +* :ghpull:`22419`: Remove "Matplotlib version" from docs issue template +* :ghpull:`22423`: Avoid indiscriminate glob-remove in xpdf_distill. +* :ghpull:`22406`: [DOC]: Removed a redundant 'The' +* :ghpull:`21442`: Factor out common limits handling for x/y/z axes. +* :ghpull:`22397`: Axes capitalization in widgets and axes3d +* :ghpull:`22394`: Tweak Axes3D docstrings that refer to 2D plotting methods. +* :ghpull:`22383`: TST: fix doc build +* :ghpull:`21877`: DOC: attempt to explain the main different APIs +* :ghpull:`21238`: Raise when unknown signals are connected to CallbackRegistries. +* :ghpull:`22345`: MNT: make layout deprecations pending +* :ghpull:`21597`: FIX: Remove the deepcopy override from transforms +* :ghpull:`22370`: Replace tabs with spaces in C code. +* :ghpull:`22371`: Corrected a mistake in comments (Issue #22369) +* :ghpull:`21352`: Refactor hexbin(). +* :ghpull:`19214`: Improve autoscaling for high order Bezier curves +* :ghpull:`22268`: Deprecated is_decade and is_close_to_int +* :ghpull:`22359`: Slightly refactor TeX source generation. +* :ghpull:`22365`: Remove deprecated ``MovieWriter.cleanup`` +* :ghpull:`22363`: Properly capitalize "Unicode". +* :ghpull:`22025`: Deprecate various custom FigureFrameWx attributes/methods. +* :ghpull:`21391`: Reuse imsave()'s background-blending code in FigureCanvasAgg.print_jpeg. +* :ghpull:`22026`: Simplify wxframe deletion. +* :ghpull:`22351`: Fix "trailing" whitespace in C docstrings. +* :ghpull:`22342`: Docstrings for _qhull. +* :ghpull:`21836`: Slightly shorten ft2font init. +* :ghpull:`21962`: Privatize various internal APIs of backend_pgf. +* :ghpull:`22114`: Rewrite AxesStack independently of cbook.Stack. +* :ghpull:`22332`: Let TransformedPatchPath inherit most functionality from TransformedPath. +* :ghpull:`22292`: Cleanup Axis._translate_tick_kw +* :ghpull:`22339`: wx.App() should be init'ed in new_figure_manager_given_figure +* :ghpull:`22315`: More standardization of floating point slop in mpl_toolkits. +* :ghpull:`22337`: DOC: More cleanup axes -> Axes +* :ghpull:`22323`: Replace sole use of maxdict by lru_cache. +* :ghpull:`22229`: FIX: make safe to add / remove artists during ArtistList iteration +* :ghpull:`22196`: ``dates`` classes and functions support ``tz`` both as string and ``tzinfo`` +* :ghpull:`22161`: Add box when setting ``PolygonSelector.verts`` +* :ghpull:`19368`: Raise warning and downsample if data given to _image.resample is too large +* :ghpull:`22250`: Unify toolbar init across backends. +* :ghpull:`22304`: Added tests for ContourSet.legend_elements +* :ghpull:`21583`: Add pre-commit config and dev instructions +* :ghpull:`21547`: Custom cap widths in box and whisker plots in bxp() and boxplot() +* :ghpull:`20887`: Implement a consistent behavior in TkAgg backend for bad blit bbox +* :ghpull:`22317`: Rename outdated seaborn styles. +* :ghpull:`22271`: Rework/fix Text layout cache. +* :ghpull:`22097`: In mpl_toolkits, use the same floating point slop as for standard ticks. +* :ghpull:`22295`: Display bad format string in error message. +* :ghpull:`22287`: Removed unused code and variables +* :ghpull:`22244`: MNT: colorbar locators properties +* :ghpull:`22270`: Expanded documentation of Axis.set_ticks as per discussion in issue #22262 +* :ghpull:`22280`: Simplify FontProperties.copy(). +* :ghpull:`22174`: Give the Tk toolbar buttons a flat look +* :ghpull:`22046`: Add the ability to change the focal length of the camera for 3D plots +* :ghpull:`22251`: Colorbar docstring reorg +* :ghpull:`21933`: MNT: privatize colorbar attr +* :ghpull:`22258`: DOC: fix version switcher +* :ghpull:`22261`: DOC: fix switcher json +* :ghpull:`22154`: Add some tests for minspan{x,y} in RectangleSelector +* :ghpull:`22246`: DOC: add dropdown +* :ghpull:`22133`: Deprecated ``afm``, ``fontconfig_pattern``, and ``type1font`` +* :ghpull:`22249`: DOC: More capitalization of Axes +* :ghpull:`22021`: Ensure that all toolbar (old/new) subclasses can be init'ed consistently +* :ghpull:`22213`: Improve ft2font error reporting. +* :ghpull:`22245`: Deprecate cleared kwarg to get_renderer. +* :ghpull:`22239`: Fix typos +* :ghpull:`22216`: turn off the grid after creating colorbar axes +* :ghpull:`22055`: FIX: Return value instead of enum in get_capstyle/_joinstyle +* :ghpull:`22228`: Remove some unnecessary getattrs. +* :ghpull:`20426`: ENH: Layout engine +* :ghpull:`22224`: Trivial doc fix to annotations tutorial. +* :ghpull:`21894`: Jointly track x and y in PolygonSelector. +* :ghpull:`22205`: Bump minimum NumPy to 1.19 +* :ghpull:`22203`: Factor out underline-thickness lookups in mathtext. +* :ghpull:`22189`: DOC: Add hatch API to reference +* :ghpull:`22084`: Clean up 3d plot box_aspect zooming +* :ghpull:`22098`: Expire axes_grid1/axisartist deprecations. +* :ghpull:`22013`: Use standard toolbar in wx. +* :ghpull:`22160`: Removed unused variables etc. +* :ghpull:`22179`: FIX: macosx check case-insensitive app name +* :ghpull:`22157`: Improved coverage of mathtext and removed unused code +* :ghpull:`21781`: Use a fixture to get widget testing axes +* :ghpull:`22140`: Ensure log formatters use Unicode minus +* :ghpull:`21342`: Fix drawing animated artists changed in selector callback +* :ghpull:`22134`: Deprecated ``tight_bbox`` and ``tight_layout`` modules +* :ghpull:`21965`: Switch transOffset to offset_transform. +* :ghpull:`22145`: Make Tk windows use the same icon as other backends +* :ghpull:`22107`: Expire mathttext-related deprecations +* :ghpull:`22139`: FIX: width/height were reversed in macosx rectangle creation +* :ghpull:`22123`: Deprecate accepting arbitrary parameters in some get_window_extent() methods +* :ghpull:`22122`: Hint at draw_without_rendering() in Text.get_window_extent +* :ghpull:`22120`: Drop dependency on scipy in the docs. +* :ghpull:`22063`: FIX: Autoposition title when yaxis has offset +* :ghpull:`22119`: Micro-optimize skew(). +* :ghpull:`22109`: Remove unnecessary null checks in macosx.m, and some more maintenance +* :ghpull:`21977`: Add corner coordinate helper methods to Ellipse/Rectangle +* :ghpull:`21830`: Add option of bounding box for PolygonSelector +* :ghpull:`22115`: Turn _localaxes into a plain list. +* :ghpull:`22108`: Micro-optimize rotation transform. +* :ghpull:`22043`: Cleanup differential equations examples. +* :ghpull:`22080`: Simple style(ish) fixes. +* :ghpull:`22110`: Right-aligned status text in backends +* :ghpull:`21873`: DOC: Update and consolidate Custom Tick Formatter for Time Series example +* :ghpull:`22112`: Fix a small typo +* :ghpull:`20117`: Very soft-deprecate AxesDivider.new_{horizontal,vertical}. +* :ghpull:`22034`: Update lines_with_ticks_demo.py +* :ghpull:`22102`: DOC: rename usage tutorial to quick_start +* :ghpull:`19228`: Validate text rotation in setter +* :ghpull:`22081`: Expire colorbar-related deprecations. +* :ghpull:`22008`: Added color keyword argument to math_to_image +* :ghpull:`22058`: Remove exprired mplot3d deprecations for 3.6 +* :ghpull:`22073`: DOC: Add new tutorial to external resources. +* :ghpull:`22054`: MNT: Set CapStyle member names automatically +* :ghpull:`22061`: De-duplicate mplot3D API docs +* :ghpull:`22075`: Remove unnecessary ``.figure`` qualifier in docs. +* :ghpull:`22051`: Make required_interactive_framework required on FigureCanvas. +* :ghpull:`22050`: Deprecate the noop, unused FigureCanvasBase.resize. +* :ghpull:`22030`: Add explanatory comments to "broken" horizontal bar plot example +* :ghpull:`22001`: Fix: [Bug]: triplot with 'ls' argument yields TypeError #21995 +* :ghpull:`22045`: Fill in missing Axes3D box_aspect argument docstring +* :ghpull:`22042`: Keep FontEntry helpers private. +* :ghpull:`21042`: Make rcParams.copy() return a new RcParams instance. +* :ghpull:`22032`: flipy only affects the drawing of texts, not of images. +* :ghpull:`21993`: Added docstring to rrulewrapper class +* :ghpull:`21935`: Significantly improve tight layout performance for cartopy axes +* :ghpull:`22000`: Some gtk cleanups. +* :ghpull:`21983`: Simplify canvas class control in FigureFrameWx. +* :ghpull:`21985`: Slightly tighten the _get_layout_cache_key API. +* :ghpull:`22020`: Simplify wx _print_image. +* :ghpull:`22010`: Fix syntax highlighting in contrib guide. +* :ghpull:`22003`: Initialize RendererCairo.{width,height} in constructor. +* :ghpull:`21992`: Use _make_classic_style_pseudo_toolbar more. +* :ghpull:`21916`: Fix picklability of make_norm_from_scale norms. +* :ghpull:`21981`: FigureCanvasCairo can init RendererCairo; kill RendererCairo subclasses. +* :ghpull:`21986`: InvLogTransform should only return masked arrays for masked inputs. +* :ghpull:`21991`: PEP8ify wx callback names. +* :ghpull:`21975`: DOC: remove experimental tag from CL +* :ghpull:`21989`: Autoinfer norm bounds. +* :ghpull:`21980`: Removed loaded modules logging +* :ghpull:`21982`: Deprecate duplicated FigureManagerGTK{3,4}Agg classes. +* :ghpull:`21963`: Clarify current behavior of draw_path_collection. +* :ghpull:`21974`: Reword inset axes example. +* :ghpull:`21835`: Small improvements to interactive examples +* :ghpull:`21050`: Store dash_pattern as single attribute, not two. +* :ghpull:`21557`: Fix transparency when exporting to png via pgf backend. +* :ghpull:`21904`: Added _repr_html_ for fonts +* :ghpull:`21696`: Use cycling iterators in RendererBase. +* :ghpull:`21955`: Refactor common parts of ImageMagick{,File}Writer. +* :ghpull:`21952`: Clarify coordinates for RectangleSelector properties +* :ghpull:`21964`: Fix some more missing references. +* :ghpull:`21516`: Make _request_autoscale_view more generalizable to 3D. +* :ghpull:`21947`: Slightly cleanup RendererBase docs. +* :ghpull:`21961`: Privatize various internal APIs of backend_pgf. +* :ghpull:`21956`: Remove tests for avconv animation writers. +* :ghpull:`21954`: DOC: Move Animation and MovieWriter inheritance diagrams ... +* :ghpull:`21780`: Add a click_and_move widget test helper +* :ghpull:`21941`: Merge branch v3.5.x into main +* :ghpull:`21936`: Small ``__getstate__`` cleanups. +* :ghpull:`21939`: Update comment re: register_at_fork. +* :ghpull:`21910`: Fold _rgbacache into _imcache. +* :ghpull:`21921`: Clean up RectangleSelector move code +* :ghpull:`21925`: Drop labelling from PR welcome action +* :ghpull:`14930`: Set Dock icon on the macosx backend +* :ghpull:`21920`: Improve square state calculation in RectangleSelector +* :ghpull:`21919`: Fix use_data_coordinates docstring +* :ghpull:`21881`: Add a PolygonSelector.verts setter +* :ghpull:`20839`: Fix centre and square state and add rotation for rectangle selector +* :ghpull:`21874`: DOC: Add Date Tick Locators and Formatters example +* :ghpull:`21799`: Added get_font_names() to fontManager +* :ghpull:`21871`: DOC: Code from markevery_prop_cycle moved to test. +* :ghpull:`21395`: Expire _check_savefig_extra_args-related deprecations. +* :ghpull:`21867`: Remove unused bbox arg to _convert_agg_to_wx_bitmap. +* :ghpull:`21868`: Use partialmethod for better signatures in backend_ps. +* :ghpull:`21520`: Shorten some inset_locator docstrings. +* :ghpull:`21737`: Update the "Rotating a 3D plot" gallery example to show all 3 rotation axes +* :ghpull:`21851`: Re-order a widget test function +* :ghpull:`10762`: Normalization of elevation and azimuth angles for surface plots +* :ghpull:`21426`: Add ability to roll the camera in 3D plots +* :ghpull:`21822`: Replace NSDictionary by switch-case. +* :ghpull:`21512`: MNT: Add modifier key press handling to macosx backend +* :ghpull:`21784`: Set macOS icon when using Qt backend +* :ghpull:`21748`: Shorten PyObjectType defs in macosx.m. +* :ghpull:`21809`: MNT: Turn all macosx warnings into errors while building +* :ghpull:`21792`: Fix missing return value in closeButtonPressed. +* :ghpull:`21767`: Inherit many macos backend docstrings. +* :ghpull:`21766`: Don't hide build log on GHA. +* :ghpull:`21728`: Factor out some macosx gil handling for py-method calls from callbacks. +* :ghpull:`21754`: Update gitattributes so that objc diffs are correctly contextualized. +* :ghpull:`21752`: Add a helper for directly output pdf streams. +* :ghpull:`21750`: Don't sort pdf dicts. +* :ghpull:`21745`: DOC: Clarify Coords Report Example +* :ghpull:`21746`: Fix/add docstring signatures to many C++ methods. +* :ghpull:`21631`: DOC: change gridspec tutorial to arranging_axes tutorial +* :ghpull:`21318`: FIX: better error message for shared axes and axis('equal') +* :ghpull:`21519`: mark_inset should manually unstale axes limits before drawing itself. +* :ghpull:`21724`: Fix copyright date with SOURCE_DATE_EPOCH set +* :ghpull:`21398`: FIX: logic of title repositioning +* :ghpull:`21717`: Simplify macosx toolbar init. +* :ghpull:`21690`: Whitespace/braces/#defines cleanup to macosx. +* :ghpull:`21695`: Use _api.check_shape more. +* :ghpull:`21698`: Small code cleanups and style fixes. +* :ghpull:`21529`: Delay-load keymaps in toolmanager. +* :ghpull:`21525`: Fix support for clim in scatter. +* :ghpull:`21697`: Drop non-significant zeros from ps output. +* :ghpull:`21692`: CI: Remove CI test runs from forks of matplotlib +* :ghpull:`21591`: Make ToolFullScreen a Tool, not a ToolToggle. +* :ghpull:`21677`: Simplify test for negative xerr/yerr. +* :ghpull:`21657`: Replace some image_comparisons by return-value-tests/check_figures_e… +* :ghpull:`21664`: Merge 3.5.x into main +* :ghpull:`21490`: Make Line2D copy its inputs +* :ghpull:`21639`: Skip some uses of packaging's PEP440 version for non-Python versions. +* :ghpull:`21604`: Fix centre square rectangle selector part 1 +* :ghpull:`21593`: Check for images added-and-modified in a same PR +* :ghpull:`20750`: Shorten issue templates +* :ghpull:`21590`: Make gtk3 full_screen_toggle more robust against external changes. +* :ghpull:`21582`: Organize checklist in PR template +* :ghpull:`21580`: Rename/remove _lastCursor, as needed. +* :ghpull:`21567`: Removed the range parameter from the validate_whiskers function's err… +* :ghpull:`21565`: Further remove remnants of offset_position. +* :ghpull:`21542`: [ENH]: Use new style format strings for colorbar ticks +* :ghpull:`21564`: Skip invisible artists when doing 3d projection. +* :ghpull:`21558`: Various small fixes for streamplot(). +* :ghpull:`21544`: Return minorticks as array, not as list. +* :ghpull:`21546`: Added links to the mosaic docs in figure and pyplot module docstrings +* :ghpull:`21545`: Turn mouseover into a mpl-style getset_property. +* :ghpull:`21537`: Remove unnecessary False arg when constructing wx.App. +* :ghpull:`21536`: Reword margins docstrings, and fix bounds on zmargin values. +* :ghpull:`21535`: typo-correction-on-line-185 +* :ghpull:`21534`: Do not use space in directive calling. +* :ghpull:`21494`: Adding tutorial links for blitting in widgets.py +* :ghpull:`21407`: Stash exceptions when FT2Font closes the underlying stream. +* :ghpull:`21431`: set_ticks([single_tick]) should also expand view limits. +* :ghpull:`21444`: Make pipong example self-contained. +* :ghpull:`21392`: Add label about workflow to new contributor PRs +* :ghpull:`21440`: Install sphinx-panels along with development setup +* :ghpull:`21434`: Remove coords_flat variable +* :ghpull:`21415`: Move gui_support.macosx option to packages section. +* :ghpull:`21412`: Privatize some SVG internal APIs. +* :ghpull:`21401`: Uncamelcase some internal variables in axis.py; rename _get_tick_bboxes. +* :ghpull:`21417`: Use Bbox.unit() more. +* :ghpull:`20253`: Simplify parameter handling in FloatingAxesBase. +* :ghpull:`21379`: Simplify filename tracking in FT2Font. +* :ghpull:`21278`: Clear findfont cache when calling addfont(). +* :ghpull:`21400`: Use bbox.{size,bounds,width,height,p0,...} where appropriate. +* :ghpull:`21408`: Reword annotations tutorial section titles. +* :ghpull:`21371`: Rename default branch +* :ghpull:`21389`: Log pixel coordinates in event_handling coords_demo example on terminal/console +* :ghpull:`21376`: Factor common parts of saving to different formats using pillow. +* :ghpull:`21377`: Enable tests for text path based markers +* :ghpull:`21283`: Demonstrate inset_axes in scatter_hist example. +* :ghpull:`21356`: Raise an exception when find_tex_file fails to find a file. +* :ghpull:`21362`: Simplify wording of allowed errorbar() error values +* :ghpull:`21274`: ENH: Add support to save images in WebP format +* :ghpull:`21289`: Simplify _init_legend_box. +* :ghpull:`21256`: Make image_comparison work even without the autoclose fixture. +* :ghpull:`21343`: Fix type1font docstring markup/punctuation. +* :ghpull:`21341`: Fix trivial docstring typo. +* :ghpull:`21301`: Simplify ``Colormap.__call__`` a bit. +* :ghpull:`21280`: Make ``Path.__deepcopy__`` interact better with subclasses, e.g. TextPath. +* :ghpull:`21266`: Fix #21101 Add validator to errorbar method +* :ghpull:`20921`: Fix problem with (deep)copy of TextPath +* :ghpull:`20914`: 19195 rotated markers +* :ghpull:`21276`: Add language about not assigning issues +* :ghpull:`20715`: Improve Type-1 font parsing +* :ghpull:`21218`: Parametrize/simplify test_missing_psfont. +* :ghpull:`21213`: Compress comments in make_image. +* :ghpull:`21187`: Deprecate error_msg_foo helpers. +* :ghpull:`21190`: Deprecate mlab.stride_windows. +* :ghpull:`21152`: Rename ``**kw`` to ``**kwargs``. +* :ghpull:`21087`: Move colormap examples from userdemo to images_contours_and_fields. +* :ghpull:`21074`: Deprecate MarkerStyle(None). +* :ghpull:`20990`: Explicit registration of canvas-specific tool subclasses. +* :ghpull:`21049`: Simplify setting Legend attributes +* :ghpull:`21056`: Deprecate support for no-args MarkerStyle(). +* :ghpull:`21059`: Remove dummy test command from setup.py +* :ghpull:`21015`: Prepare for rcParams.copy() returning a new RcParams instance in the future +* :ghpull:`21021`: Factor out for_layout_only backcompat support in get_tightlayout. +* :ghpull:`21023`: Inline ToolManager._trigger_tool to its sole call site. +* :ghpull:`21005`: Test the rcParams deprecation machinery. +* :ghpull:`21010`: Avoid TransformedBbox where unneeded. +* :ghpull:`21019`: Reword custom_ticker1 example. +* :ghpull:`20995`: Deprecate some backend_gtk3 helper globals. +* :ghpull:`21004`: Remove now-unused rcParams _deprecated entries. +* :ghpull:`20986`: Make HandlerLine2D{,Compound} inherit constructors from HandlerNpoints. +* :ghpull:`20974`: Rename symbol_name to glyph_name where appropriate. +* :ghpull:`20961`: Small cleanups to math_to_image. +* :ghpull:`20957`: legend_handler_map cleanups. +* :ghpull:`20955`: Remove unused HostAxes._get_legend_handles. +* :ghpull:`20851`: Try to install the Noto Sans CJK font + +Issues (202): + +* :ghissue:`23827`: backend_gtk3agg.py calls set_device_scale +* :ghissue:`23560`: [Doc]: mpl_toolkits.axes_grid still mentioned as maintained +* :ghissue:`23794`: [Doc]: Version switcher broken in devdocs +* :ghissue:`23806`: [Bug]: possible regression in axis ticks handling in matplotlib 3.6.0rc2 +* :ghissue:`22965`: [Bug]: triplot duplicates label legend +* :ghissue:`23807`: streamplot raises ValueError when the input is zeros +* :ghissue:`23761`: [Bug]: False positive legend handler warnings in 3.6.0.rc1 +* :ghissue:`23398`: [Bug]: Newer versions of matplotlib ignore xlabel on colorbar axis +* :ghissue:`23699`: [Bug]: Bug with toolbar instantiation in notebook +* :ghissue:`23745`: [Doc]: Minor rcParams/matplotlibrc doc issues +* :ghissue:`23717`: [Bug]: AxesSubplot.get_yticks not returning the actual printed ticks +* :ghissue:`21508`: [Doc]: Create diagram to show rotation directions for 3D plots +* :ghissue:`23709`: [Bug]: colorbar with unattached mappables can't steal space +* :ghissue:`23701`: [Bug]: plt.figure(), plt.close() leaks memory +* :ghissue:`22409`: [Bug]: AttributeError: 'QResizeEvent' object has no attribute 'pos' +* :ghissue:`19609`: DeprecationWarning when changing color maps +* :ghissue:`23716`: MatplotlibDeprecationWarning removal hard-breaks seaborn in 3.6rc1 +* :ghissue:`23719`: [Bug]: register_cmap deprecation message seems wrong +* :ghissue:`23707`: test_Normalize fails on aarch64/ppc64le/s390x +* :ghissue:`21107`: [MNT]: Should plt.xticks() get a minor keyword argument +* :ghissue:`23679`: [Doc]: Deprecated modules not in docs +* :ghissue:`19550`: Arc and pathpatch_2d_to_3d plots full ellipse +* :ghissue:`23329`: [Bug]: ``plt.autoscale()`` fails for partial ``Arc`` +* :ghissue:`11266`: Arc patch ignoring theta1/theta2 when added to Axes via PatchCollection +* :ghissue:`4067`: 'Poly3DCollection' object has no attribute '_facecolors2d' +* :ghissue:`23622`: [MNT]: make.bat not parsing sphinxopt +* :ghissue:`23459`: [Bug]: 'Line3D' object has no attribute '_verts3d' +* :ghissue:`23653`: [Bug]: macosx subplot tool causes segfault when window closed +* :ghissue:`23660`: [Bug]: Test test_figure.py::test_subfigure_ss[png] FAILED on ppc64le +* :ghissue:`23645`: [MNT]: Python 3.11 manylinux wheels +* :ghissue:`23650`: TTF fonts loaded from file are not embedded/displayed properly when saved to pdf +* :ghissue:`23583`: [Doc]: Document the position parameter in apply_aspect() +* :ghissue:`23386`: setuptools_scm-git-archive is obsolete +* :ghissue:`23220`: [Doc]: Clarify ``offset`` parameter in linestyle +* :ghissue:`22746`: [Doc]: Document that rcParams['font.family'] can be a list +* :ghissue:`8187`: Axes doesn't have ````legends```` attribute? +* :ghissue:`23580`: [Bug]: TypeError when plotting against list of datetime.date where 0th element of list is None +* :ghissue:`15514`: Relevant methods are only documented in base classes and thus not easily discoverable +* :ghissue:`21611`: DOC: Add conda environment instructions to developers guide +* :ghissue:`23487`: [Bug]: scatter plot color settings discarded unless c given +* :ghissue:`22977`: [Bug]: offset dash linestyle has no effect in patch objects +* :ghissue:`18883`: Matplotlib would not try to apply all the font in font list to draw all characters in the given string. +* :ghissue:`22570`: [ENH]: Provide ``axis('equal')`` for ``Axes3D``. +* :ghissue:`23433`: [Bug]: array-like linewidth raises an error for scatter3D +* :ghissue:`12388`: Legend Title Left Alignment +* :ghissue:`23375`: [Bug]: markerfacecoloralt not supported when drawing errorbars +* :ghissue:`17973`: DOC: matplotlib.__doc__ not included in online docs ? +* :ghissue:`23474`: [Bug]: ``\,`` and ``\mathrm{\,}`` are not identical in Mathtext when using CM and STIX +* :ghissue:`8715`: event handlers have different signatures across backends +* :ghissue:`18271`: PGF uses the minimal document class +* :ghissue:`23324`: [Bug]: Exception not handled in widgetlock() +* :ghissue:`15710`: doc for type of tz parameter is inconsistent throughout dates.py +* :ghissue:`21165`: Hexbin marginals need a test for linear scaling +* :ghissue:`23105`: [MNT]: Deprecate per-backend customization of draw_if_interactive +* :ghissue:`23147`: [Bug]: with setuptools>=60, cannot find msbuild +* :ghissue:`23379`: [Bug]: Offset notation on y-axis can overlap with a long title +* :ghissue:`22819`: [Doc]: Make rect argument consistent in the docstrings +* :ghissue:`23172`: [Bug]: Calling matplotlib.pyplot.show() outside of matplotlib.pyplot.rc_context no longer works +* :ghissue:`23019`: [Bug]: ``UnicodeDecodeError`` when using some special and accented characters in TeX +* :ghissue:`23334`: [Doc]: Tk embedding example crashes Spyder +* :ghissue:`23298`: [Bug]: get_backend() clears figures from Gcf.figs if they were created under rc_context +* :ghissue:`21942`: [ENH]: add width/height_ratios to subplots and friends +* :ghissue:`23028`: [ENH]: contour kwarg for negative_linestyle +* :ghissue:`19223`: Certain non-hashable parameters to text() give cryptic error messages +* :ghissue:`18351`: Add the ability to plot striped lines +* :ghissue:`23205`: [Bug]: URL-area not rotated in PDFs +* :ghissue:`23268`: [Bug]: hyphen renders different length depending on presence of MathText +* :ghissue:`23308`: [Bug]: set_visible() not working for 3d projection +* :ghissue:`23296`: Set_color method for line2d object in latest document not work +* :ghissue:`22992`: [Bug]: test_image_comparison_expect_rms nondeterministic failure +* :ghissue:`23008`: [ENH]: Use ``\genfrac`` in display style? +* :ghissue:`23214`: [MNT]: Rename examples with "test" in the name +* :ghissue:`17852`: Thin space missing after mathtext operators +* :ghissue:`12078`: Inconsistency in keyword-arguments ncol/ncols, nrow/nrows +* :ghissue:`23239`: [Doc]: steps is not implemented in line styles. +* :ghissue:`23151`: [MNT]: default date limits... +* :ghissue:`9462`: Misaligned bottoms of subplots for png output with bbox_inches='tight' +* :ghissue:`21369`: [Bug]: ax.invert_xaxis() and ax.invert_yaxis() both flip the X axis +* :ghissue:`20797`: ``macosx`` cursors break with images +* :ghissue:`23084`: [TST] Upcoming dependency test failures +* :ghissue:`22910`: [Bug]: bar_label fails with nan errorbar values +* :ghissue:`23074`: [Bug]: matplotlib crashes if ``_tkinter`` doesn't have ``__file__`` +* :ghissue:`23083`: [Bug]: Confusing error messages +* :ghissue:`22391`: [Doc]: Remove "keywords" line at the bottom of all examples +* :ghissue:`20202`: Daylocator causes frozen computer when used with FuncAnimation +* :ghissue:`22529`: Replace C++ quad contouring code with use of ContourPy +* :ghissue:`21710`: [ENH]: macosx backend does not respect rcParams["savefig.directory"] +* :ghissue:`21880`: [Doc]: rrulewrapper not included in API docs +* :ghissue:`22622`: [Bug]: Gaps and overlapping areas between bins when using float16 +* :ghissue:`23043`: [TST] Upcoming dependency test failures +* :ghissue:`17960`: Line2D object markers are lost when retrieved from legend.get_lines() when linestyle='None' +* :ghissue:`23026`: [MNT]: Require that matplotlibrc/style files use utf-8 (or have an encoding cookie) +* :ghissue:`22947`: [Bug]: Can't use ``plt.sca()`` on axes created using subfigures +* :ghissue:`22623`: [ENH]: support rect with constrained_layout ("layout only to part of the figure") +* :ghissue:`22917`: "ab;cd" missing in subplot_mosaic tutorial +* :ghissue:`22686`: [Bug]: can not give init value for RangeSlider widget +* :ghissue:`22740`: [MNT]: Add codespell to pre-commit hooks +* :ghissue:`22893`: rainbow text example is broken +* :ghissue:`21571`: [Doc]: Clarify text positioning +* :ghissue:`22092`: [Bug]: Configure subplots dialog freezes for TkAgg with toolmanager +* :ghissue:`22760`: [Bug]: Macosx legend picker doesn't work anymore +* :ghissue:`16369`: Call to input blocks slider input on osx with the default agg 'MacOSX'. It works fine on when TkAgg is used. +* :ghissue:`22915`: [Bug]: figure.raise_window rcParam does not work on MacOSX backend +* :ghissue:`22930`: [Bug]: Regression in dashes due to #22569 +* :ghissue:`22859`: [Bug]: findSystemFonts should not look in subdirectories of C:\Windows\Fonts\ +* :ghissue:`22882`: Missing ``f`` prefix on f-strings +* :ghissue:`22738`: [MNT]: make Axes.cla an alias for Axes.clear in all cases +* :ghissue:`22708`: [TST] Upcoming dependency test failures +* :ghissue:`8388`: Proposed ENH: Allow user to turn off breaking of streamlines in streamplot +* :ghissue:`20755`: [Bug]: make_norm_from_scale should create picklable classes even when used in-line. +* :ghissue:`18249`: Expand the explanation of the Object-Oriented interface +* :ghissue:`22792`: [Bug]: .eps greyscale hatching of patches when lw=0 +* :ghissue:`22630`: [ENH]: enable passing of projection keyword to Axes.inset_axes +* :ghissue:`22414`: [Bug]: bar_label overlaps bars when y-axis is inverted +* :ghissue:`22726`: [Bug]: tripcolor ignores clim +* :ghissue:`21635`: [ENH]: Add a nightly wheel build +* :ghissue:`9994`: document where nightly wheels are published +* :ghissue:`22350`: [Bug]: text.usetex Vs. DateFormatter +* :ghissue:`4976`: missing imshow() subplots when using tight_layout() +* :ghissue:`22150`: [ENH]: Tool icons are hardly visible in Tk when using a dark theme +* :ghissue:`22662`: Leave color parameter empty should be fine[ENH]: +* :ghissue:`22671`: [Doc]: plot_format adaption invalidates sphinx cache +* :ghissue:`22582`: [Bug]: FontManager.addfont doesn't accept pathlib.Path of TTF font +* :ghissue:`22657`: [ENH]: vector map +* :ghissue:`16181`: The great API cleanup +* :ghissue:`22636`: [Bug]: Infinite loop when there is single double quote in matplotlibrc +* :ghissue:`22266`: [Doc]: Improve examples in documentation +* :ghissue:`11861`: Figure does not close until script finishes execution +* :ghissue:`19288`: Escape # character in matplotlibrc +* :ghissue:`22579`: [Bug]: Replacement for epoch2num behaves differently (does not accept arrays) +* :ghissue:`22605`: [Bug]: Tool contrast low with dark theme on macosx backend +* :ghissue:`17642`: bring osx backend flush_events to feature parity with other backend +* :ghissue:`19268`: Drawing the canvas does not populate ticklabels on MacOSX backend +* :ghissue:`17445`: MacOSX does not render frames in which new artists are added when blitting +* :ghissue:`10980`: Current versions cannot reproduce rotate_axes_3d_demo.py +* :ghissue:`18451`: MacOSX backend fails with animation in certain scripts +* :ghissue:`22603`: [MNT]: Replace str(n)cpy etc with safe versions (C++) +* :ghissue:`19121`: Handle and label not created for Text with label +* :ghissue:`22563`: [Doc]: annotation_clip=None not correctly documented +* :ghissue:`12528`: Empty axes on draw after blitted animation finishes +* :ghissue:`20991`: [Bug]: Error when using path effect with a PolyCollection +* :ghissue:`19563`: path_effects kwarg triggers exception on 3D scatterplot +* :ghissue:`8650`: System Error in backend_agg. (with a fix!) +* :ghissue:`20294`: ``AxesImage.__str__`` is wrong if the image does not span the full Axes. +* :ghissue:`18066`: Document minimum supported OSX version for macos backend +* :ghissue:`17018`: Add documentation about transparency of frame +* :ghissue:`22403`: [MNT]: Confusing prompt in docs issue template +* :ghissue:`8839`: mpl_connect silently does nothing when passed an invalid event type string +* :ghissue:`22343`: [MNT]: Delay (or make pending) the deprecation of set_constrained_layout/set_tight_layout +* :ghissue:`21554`: [Bug]: ``ValueError`` upon deepcopy of a ``Figure`` object +* :ghissue:`22369`: [Doc]: Incorrect comment in example code for creating adjacent subplots +* :ghissue:`19174`: connectionstyle arc3 with high rad value pushes up data interval of x-axis and y-axis. +* :ghissue:`8351`: seaborn styles make "+", "x" markers invisible; proposed workaround for shipped styles +* :ghissue:`22278`: Deprecate/remove maxdict +* :ghissue:`19276`: imshow with very large arrays not working as expected +* :ghissue:`22035`: [ENH]: Specify a custom focal length / FOV for the 3d camera +* :ghissue:`22264`: [Bug]: new constrained_layout causes axes to go invisible(?) +* :ghissue:`21774`: [MNT]: Improvements to widget tests +* :ghissue:`18722`: Consider removing AFM+mathtext support +* :ghissue:`21540`: [Bug]: cm fontset in log scale does not use Unicode minus +* :ghissue:`22062`: [Bug]: Autopositioned title overlaps with offset text +* :ghissue:`22093`: [Bug]: AttributeError: 'AxesSubplot' object has no attribute 'add_text' +* :ghissue:`22012`: [Bug]: Mouseover coordinate/value text should be right aligned +* :ghissue:`21995`: [Bug]: triplot with 'ls' argument yields TypeError +* :ghissue:`20249`: MatplotlibDeprecationWarning when updating rcparams +* :ghissue:`15781`: MatplotlibDeprecationWarning examples.directory is deprecated +* :ghissue:`13118`: No MatplotlibDeprecationWarning for default rcParams +* :ghissue:`21978`: Remove logging debug of loaded modules +* :ghissue:`11738`: pgf backend doesn't make background transparent +* :ghissue:`18039`: Add ``_repr_html_`` for fonts +* :ghissue:`21970`: [Bug]: tight layout breaks with toolbar.push_current() +* :ghissue:`14850`: No icon showing up with macosx backend +* :ghissue:`17283`: Create Date Formatter/Locator Reference +* :ghissue:`21761`: [Doc]: add how to know available fonts... +* :ghissue:`21863`: [Doc]: Remove example "prop_cycle property markevery in rcParams" +* :ghissue:`10241`: Axes3D.view_init elevation issue between 270 and 360 degrees +* :ghissue:`14453`: add third angle to view_init() +* :ghissue:`20486`: Modifier key press events not recognized on MacOSX backend +* :ghissue:`9837`: MacOS: Key modifiers deprecated +* :ghissue:`11416`: RuntimeError: adjustable='datalim' is not allowed when both axes are shared. +* :ghissue:`17711`: inset_locator.mark_inset() misplaces box connectors +* :ghissue:`20854`: [Doc]: Incorrect copyright start year at the bottom of devdocs page +* :ghissue:`21394`: [Bug]: Subplot title does not obey padding +* :ghissue:`20998`: [Bug]: ToolManager does not respect rcParams["keymap."] set after import time +* :ghissue:`7075`: Superscripts in axis label cut when saving .eps with bbox_inches="tight" +* :ghissue:`21514`: [Doc]: Error message of validate_whiskers is not updated +* :ghissue:`21532`: [Doc]: subplot_mosaic docstring should link to the tutorial +* :ghissue:`16550`: Docs: performance discussion of tight_layout +* :ghissue:`21378`: [ENH]: use new style format strings for colorbar ticks +* :ghissue:`19323`: Streamplot color mapping fails on (near-)empty array. +* :ghissue:`19559`: Axes.get_xticks() returns a numpy array but Axes.get_xticks(minor=True) returns a plain list +* :ghissue:`21526`: [Doc]: Little Typo on Introductory Tutorial +* :ghissue:`19195`: Rotate Markers in functions like plot, scatter, etcetera +* :ghissue:`21364`: [Bug]: double free when FT2Font constructor is interrupted by KeyboardInterrupt +* :ghissue:`16581`: Can't not refresh new font in running interpreter +* :ghissue:`21162`: [ENH]: saving images in webp format +* :ghissue:`18168`: The example of the testing decorator does not work. +* :ghissue:`20943`: [Bug]: Deepcopy of TextPath fails +* :ghissue:`21101`: [Bug]: Errorbars separated from markers with negative errors +* :ghissue:`17986`: MEP22 per-backend tool registration +* :ghissue:`4938`: Feature request: add option to disable mathtext parsing +* :ghissue:`11435`: plt.subplot eats my subplots diff --git a/doc/users/prev_whats_new/github_stats_3.6.1.rst b/doc/users/prev_whats_new/github_stats_3.6.1.rst new file mode 100644 index 000000000000..d47dc28fa076 --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.6.1.rst @@ -0,0 +1,143 @@ +.. _github-stats-3-6-1: + +GitHub statistics for 3.6.1 (Oct 08, 2022) +========================================== + +GitHub statistics for 2022/09/16 (tag: v3.6.0) - 2022/10/08 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 22 issues and merged 80 pull requests. +The full list can be seen `on GitHub `__ + +The following 19 authors contributed 129 commits. + +* Antony Lee +* baharev +* David Stansby +* dependabot[bot] +* Eli Rykoff +* Elliott Sales de Andrade +* erykoff +* Greg Lucas +* hannah +* Ian Hunt-Isaak +* Jody Klymak +* melissawm +* Oscar Gustafsson +* Ruth Comer +* slackline +* Steffen Rehberg +* Thomas A Caswell +* Tim Hoffmann +* مهدي شينون (Mehdi Chinoune) + +GitHub issues and pull requests: + +Pull Requests (80): + +* :ghpull:`24124`: Backport PR #24111 on branch v3.6.x (FIX: add missing method to ColormapRegistry) +* :ghpull:`24111`: FIX: add missing method to ColormapRegistry +* :ghpull:`24117`: Backport PR #24113 on branch v3.6.x (Add exception class to pytest.warns calls) +* :ghpull:`24116`: Backport PR #24115 on branch v3.6.x (Fix mask lookup in fill_between for NumPy 1.24+) +* :ghpull:`24113`: Add exception class to pytest.warns calls +* :ghpull:`24115`: Fix mask lookup in fill_between for NumPy 1.24+ +* :ghpull:`24112`: Backport PR #24109 on branch v3.6.x (DOC: add API change note for colorbar deprecation) +* :ghpull:`24109`: DOC: add API change note for colorbar deprecation +* :ghpull:`24107`: Backport PR #24088 on branch v3.6.x (MNT: make orphaned colorbar deprecate versus raise) +* :ghpull:`24088`: MNT: make orphaned colorbar deprecate versus raise +* :ghpull:`24103`: Backport PR #23684 on branch v3.6.x (Fix rectangle and hatches for colorbar) +* :ghpull:`23684`: Fix rectangle and hatches for colorbar +* :ghpull:`24087`: Backport PR #24084 on branch v3.6.x (Revert argument checking for label_mode) +* :ghpull:`24084`: Revert argument checking for label_mode +* :ghpull:`24078`: Backport PR #24047 on branch v3.6.x (Revert #22360: Let TeX handle multiline strings itself) +* :ghpull:`24047`: Revert #22360: Let TeX handle multiline strings itself +* :ghpull:`24077`: Backport PR #24054 on branch v3.6.x ( DOC: Move OO-examples from pyplot section) +* :ghpull:`24054`: DOC: Move OO-examples from pyplot section +* :ghpull:`24072`: Backport PR #24069 on branch v3.6.x (Clarification of marker size in scatter) +* :ghpull:`24073`: Backport PR #24070 on branch v3.6.x (DOC: colorbar may steal from array of axes) +* :ghpull:`24070`: DOC: colorbar may steal from array of axes +* :ghpull:`24069`: Clarification of marker size in scatter +* :ghpull:`24059`: Backport PR #23638 on branch v3.6.x (FIX: correctly handle generic font families in svg text-as-text mode) +* :ghpull:`23638`: FIX: correctly handle generic font families in svg text-as-text mode +* :ghpull:`24048`: Backport PR #24045 on branch v3.6.x (Fix _FigureManagerGTK.resize on GTK4) +* :ghpull:`24055`: Backport PR #24046 on branch v3.6.x (Ignore 'CFMessagePort: bootstrap_register' messages) +* :ghpull:`24046`: Ignore 'CFMessagePort: bootstrap_register' messages +* :ghpull:`24051`: Backport PR #24037 on branch v3.6.x ([DOC]: make spanselector example codeblock continuous) +* :ghpull:`24037`: [DOC]: make spanselector example codeblock continuous +* :ghpull:`24045`: Fix _FigureManagerGTK.resize on GTK4 +* :ghpull:`24043`: Backport PR #24041 on branch v3.6.x (DOC: Fix incorrect redirect) +* :ghpull:`24030`: Backport PR #24019 on branch v3.6.x (Don't require FigureCanvas on backend module more) +* :ghpull:`24040`: Backport PR #24018 on branch v3.6.x (When comparing eps images, run ghostscript with -dEPSCrop.) +* :ghpull:`24018`: When comparing eps images, run ghostscript with -dEPSCrop. +* :ghpull:`24033`: Backport PR #24032 on branch v3.6.x (Reword SpanSelector example.) +* :ghpull:`24029`: Backport PR #24026 on branch v3.6.x (Don't modify Axes property cycle in stackplot) +* :ghpull:`23994`: Backport PR #23964 on branch v3.6.x (Fix issue with empty line in ps backend) +* :ghpull:`24019`: Don't require FigureCanvas on backend module more +* :ghpull:`24026`: Don't modify Axes property cycle in stackplot +* :ghpull:`24027`: Backport PR #23904 on branch v3.6.x (added a reversing section to colormap reference) +* :ghpull:`24017`: Backport PR #24014 on branch v3.6.x (Bump pypa/cibuildwheel from 2.10.1 to 2.10.2) +* :ghpull:`24014`: Bump pypa/cibuildwheel from 2.10.1 to 2.10.2 +* :ghpull:`24007`: Backport PR #24004 on branch v3.6.x (Increase consistency in tutorials and examples) +* :ghpull:`23964`: Fix issue with empty line in ps backend +* :ghpull:`23904`: added a reversing section to colormap reference +* :ghpull:`23990`: Backport PR #23978 on branch v3.6.x (DOC: Suppress IPython output in examples and tutorials where not needed) +* :ghpull:`23978`: DOC: Suppress IPython output in examples and tutorials where not needed +* :ghpull:`23916`: Backport PR #23912 on branch v3.6.x (FIX: only expect FigureCanvas on backend module if using new style) +* :ghpull:`23989`: Backport PR #23944 on branch v3.6.x (FIX: ValueError when hexbin is run with empty arrays and log scaling.) +* :ghpull:`23944`: FIX: ValueError when hexbin is run with empty arrays and log scaling. +* :ghpull:`23988`: Backport PR #23987 on branch v3.6.x (FIX: do not set constrained layout on false-y values) +* :ghpull:`23987`: FIX: do not set constrained layout on false-y values +* :ghpull:`23982`: Backport PR #23980 on branch v3.6.x (DOC: Move Quick Start Tutorial to first position) +* :ghpull:`23979`: Backport PR #23975 on branch v3.6.x (Reword docstring of reset_position.) +* :ghpull:`23975`: Reword docstring of reset_position. +* :ghpull:`23966`: Backport PR #23930 on branch v3.6.x (Fix edge color, links, wording; closes matplotlib/matplotlib#23895) +* :ghpull:`23971`: Backport PR #23906 on branch v3.6.x (Edit mplot3d examples for correctness and consistency) +* :ghpull:`23906`: Edit mplot3d examples for correctness and consistency +* :ghpull:`23963`: Backport PR #23957 on branch v3.6.x (Bump pypa/cibuildwheel from 2.9.0 to 2.10.1) +* :ghpull:`23930`: Fix edge color, links, wording; closes matplotlib/matplotlib#23895 +* :ghpull:`23910`: FIX: do not append None to stream in ps +* :ghpull:`23957`: Bump pypa/cibuildwheel from 2.9.0 to 2.10.1 +* :ghpull:`23960`: Backport PR #23947 on branch v3.6.x (Fix building on MINGW) +* :ghpull:`23942`: DOC: fix versions in v3.6.x doc switcher +* :ghpull:`23961`: Backport PR #23958 on branch v3.6.x (DOC: Remove Adding Animations section) +* :ghpull:`23958`: DOC: Remove Adding Animations section +* :ghpull:`23947`: Fix building on MINGW +* :ghpull:`23945`: Backport PR #23941 on branch v3.6.x (consistent notation for minor/patch branches) +* :ghpull:`23956`: Backport PR #23751 on branch v3.6.x (FIX: show bars when the first location is nan) +* :ghpull:`23751`: FIX: show bars when the first location is nan +* :ghpull:`23938`: Backport PR #23919 on branch v3.6.x (DOC: remove dead "Show Source" links) +* :ghpull:`23952`: Backport PR #23951 on branch v3.6.x (DOC: Make animation continuous) +* :ghpull:`23949`: DOC: Display "dev" instead of "devdocs" in the version switcher +* :ghpull:`23940`: Fix typos in github_stats.rst +* :ghpull:`23936`: Backport PR #23935 on branch v3.6.x (DOC: fix versions is doc switcher) +* :ghpull:`23933`: Backport PR #23932 on branch v3.6.x (DOC: Fix formatting in image tutorial) +* :ghpull:`23932`: DOC: Fix formatting in image tutorial +* :ghpull:`23926`: Backport PR #23925 on branch v3.6.x (FIX: use process_event in dpi changes on macosx backend) +* :ghpull:`23925`: FIX: use process_event in dpi changes on macosx backend +* :ghpull:`23912`: FIX: only expect FigureCanvas on backend module if using new style + +Issues (22): + +* :ghissue:`23981`: [ENH]: Default ``matplotlib.colormaps[None]`` to call ``matplotlib.colormaps[matplotlib.rcParams['image.cmap']]``? +* :ghissue:`24106`: [Bug]: fill_between gives IndexError with numpy 1.24.0.dev +* :ghissue:`24053`: Cartopy axes_grid_basic example broken by Matplotlib 3.6 +* :ghissue:`23977`: [Bug]: Eqnarray in AnchoredText results in misplaced text (new in v3.6.0) +* :ghissue:`23973`: [Bug]: ValueError: Unable to determine Axes to steal space for Colorbar. +* :ghissue:`23456`: [Bug]: Horizontal colorbars drawn incorrectly with hatches +* :ghissue:`15922`: Pyplot gallery section is mostly OO examples +* :ghissue:`23700`: [Doc]: scatter points +* :ghissue:`23492`: [Bug]: svg backend does not use configured generic family lists +* :ghissue:`22528`: [Bug]: problem with font property in text elements of svg figures +* :ghissue:`23911`: [Bug]: 3.6.0 doesn't interact well with pycharm throwing "backend_interagg" exception +* :ghissue:`24024`: stackplot should not change Axes cycler +* :ghissue:`23954`: [Bug]: Text label with empty line causes a "TypeError: cannot unpack non-iterable NoneType object" in PostScript backend +* :ghissue:`23922`: [Bug]: Refactor of hexbin for 3.6.0 crashes with empty arrays and log scaling +* :ghissue:`23986`: [Bug]: Constrained layout UserWarning even when False +* :ghissue:`23895`: [Bug]: 3D surface is not plotted for the contour3d_3 example in the gallery +* :ghissue:`23955`: [Doc]: Adding animations to Youtube channel +* :ghissue:`23943`: [Bug]: Couldn't build matplotlib 3.6.0 with both Clang-15 and GCC-12 +* :ghissue:`23687`: [Bug]: barplot does not show anything when x or bottom start and end with NaN +* :ghissue:`23876`: [Doc]: Missing source files +* :ghissue:`23909`: [Doc]: add animation examples to show animated subplots +* :ghissue:`23921`: [Bug]: resize_event deprecation warnings when creating figure on macOS with version 3.6.0 diff --git a/doc/users/prev_whats_new/github_stats_3.6.2.rst b/doc/users/prev_whats_new/github_stats_3.6.2.rst new file mode 100644 index 000000000000..f633448aeaf1 --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.6.2.rst @@ -0,0 +1,151 @@ +.. _github-stats-3-6-2: + +GitHub statistics for 3.6.2 (Nov 02, 2022) +========================================== + +GitHub statistics for 2022/10/08 (tag: v3.6.1) - 2022/11/02 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 21 issues and merged 86 pull requests. +The full list can be seen `on GitHub `__ + +The following 22 authors contributed 27 commits. + +* Antony Lee +* Carsten Schnober +* dependabot[bot] +* Elliott Sales de Andrade +* hannah +* j1642 +* Jaco Verster +* jacoverster +* Jae-Joon Lee +* Jeffrey Aaron Paul +* jeffreypaul15 +* Jody Klymak +* Kostya Farber +* Kyle Sunden +* Martok +* Muhammad Abdur Rakib +* Oscar Gustafsson +* Pavel Grunt +* Ruth Comer +* Thomas A Caswell +* Tiger Nie +* Tim Hoffmann + +GitHub issues and pull requests: + +Pull Requests (86): + +* :ghpull:`24341`: Backport PR #24301 on branch v3.6.x (Restore get_renderer function in deprecated tight_layout) +* :ghpull:`24301`: Restore get_renderer function in deprecated tight_layout +* :ghpull:`24337`: Backport PR #24238 on branch v3.6.x (Update example and docstring to encourage the use of functools.partial in FuncAnimation) +* :ghpull:`24336`: Backport PR #24335 on branch v3.6.x (Fix missing word in ImageMagickWriter docstring.) +* :ghpull:`20358`: Updates example and docstring to encourage the use of functools.partial in FuncAnimation +* :ghpull:`24238`: Update example and docstring to encourage the use of functools.partial in FuncAnimation +* :ghpull:`24335`: Fix missing word in ImageMagickWriter docstring. +* :ghpull:`24330`: Backport PR #24282 on branch v3.6.x (Fix some minor docstring typos) +* :ghpull:`24323`: Backport PR #24320 on branch v3.6.x (DOC: add warning note to imsave) +* :ghpull:`24282`: Fix some minor docstring typos +* :ghpull:`24327`: Backport PR #24310 on branch v3.6.x (show axes changing in animate decay example) +* :ghpull:`24310`: show axes changing in animate decay example +* :ghpull:`24324`: Backport PR #24259 on branch v3.6.x (Move empty hexbin fix to make_norm_from_scale.) +* :ghpull:`24325`: Backport PR #24095 on branch v3.6.x (nb/webagg: Move mouse events to outer canvas div) +* :ghpull:`24326`: Backport PR #24318 on branch v3.6.x (Bump pypa/cibuildwheel from 2.11.1 to 2.11.2) +* :ghpull:`24318`: Bump pypa/cibuildwheel from 2.11.1 to 2.11.2 +* :ghpull:`24095`: nb/webagg: Move mouse events to outer canvas div +* :ghpull:`24259`: Move empty hexbin fix to make_norm_from_scale. +* :ghpull:`24320`: DOC: add warning note to imsave +* :ghpull:`24297`: Backport PR #24294 on branch v3.6.x (Run test if fontconfig is present) +* :ghpull:`24294`: Run test if fontconfig is present +* :ghpull:`24286`: Backport PR #24284 on branch v3.6.x (Remove comment about cmap from voxels docstring) +* :ghpull:`24284`: Remove comment about cmap from voxels docstring +* :ghpull:`24280`: Backport PR #24145 on branch v3.6.x (Updated Angles on Bracket arrow styles example to make angles clear #23176) +* :ghpull:`24145`: Updated Angles on Bracket arrow styles example to make angles clear #23176 +* :ghpull:`24270`: Backport PR #24265 on branch v3.6.x (Restore (and warn on) seaborn styles in style.library) +* :ghpull:`24271`: Backport PR #24266 on branch v3.6.x (TST: Increase fp tolerance on more tests for new NumPy) +* :ghpull:`24266`: TST: Increase fp tolerance on more tests for new NumPy +* :ghpull:`24265`: Restore (and warn on) seaborn styles in style.library +* :ghpull:`24267`: Backport PR #24261 on branch v3.6.x (Fix pie chart in demo_agg_filter.py) +* :ghpull:`24261`: Fix pie chart in demo_agg_filter.py +* :ghpull:`24258`: Backport PR #24108 on branch v3.6.x (Add 3D plots to plot_types doc page) +* :ghpull:`24108`: Add 3D plots to plot_types doc page +* :ghpull:`24255`: Backport PR #24250 on branch v3.6.x (Fix key reporting in pick events) +* :ghpull:`24250`: Fix key reporting in pick events +* :ghpull:`24237`: Backport PR #24197 on branch v3.6.x (Properly set and inherit backend_version.) +* :ghpull:`24197`: Properly set and inherit backend_version. +* :ghpull:`24234`: Backport PR #23607 on branch v3.6.x (DOC: document that appearance is part of our stable API) +* :ghpull:`24233`: Backport PR #23985 on branch v3.6.x (Improve rubberband rendering in wx and tk) +* :ghpull:`24232`: Backport PR #24096 on branch v3.6.x ([DOC]: Add simple animation scatter plot to the example documentation) +* :ghpull:`24231`: Backport PR #24009 on branch v3.6.x (Fix evaluating colormaps on non-numpy arrays) +* :ghpull:`24230`: Backport PR #24229 on branch v3.6.x (FIX: do not mutate dictionaries passed in by user) +* :ghpull:`23607`: DOC: document that appearance is part of our stable API +* :ghpull:`23985`: Improve rubberband rendering in wx and tk +* :ghpull:`24096`: [DOC]: Add simple animation scatter plot to the example documentation +* :ghpull:`24009`: Fix evaluating colormaps on non-numpy arrays +* :ghpull:`24229`: FIX: do not mutate dictionaries passed in by user +* :ghpull:`24223`: Backport PR #24184 on branch v3.6.x (Add tests for ToolManager) +* :ghpull:`24219`: Backport PR #23995 on branch v3.6.x (DOC: Lowercase some parameter names) +* :ghpull:`23995`: DOC: Lowercase some parameter names +* :ghpull:`24184`: Add tests for ToolManager +* :ghpull:`24211`: Backport PR #24202 on branch v3.6.x (Bump pypa/cibuildwheel from 2.10.2 to 2.11.1) +* :ghpull:`24214`: Backport PR #24169 on branch v3.6.x ([DOC]: added parent link for ``FuncAnimation`` and ``ArtistAnimation``) +* :ghpull:`24169`: [DOC]: add parent link for ``FuncAnimation`` and ``ArtistAnimation`` +* :ghpull:`24202`: Bump pypa/cibuildwheel from 2.10.2 to 2.11.1 +* :ghpull:`24206`: Backport PR #24081 on branch v3.6.x (TST: force test with shared test image to run in serial) +* :ghpull:`24181`: Backport PR #24177 on branch v3.6.x (Don't simplify paths used for autoscaling) +* :ghpull:`24200`: Backport PR #24193 on branch v3.6.x (DOC: Explain gridsize in hexbin()) +* :ghpull:`24201`: Backport PR #24194 on branch v3.6.x (DOC: Improve plot_directive documentation) +* :ghpull:`24194`: DOC: Improve plot_directive documentation +* :ghpull:`24193`: DOC: Explain gridsize in hexbin() +* :ghpull:`24192`: Backport PR #24187 on branch v3.6.x (DOC: Fix toc structure in explain/interactive) +* :ghpull:`24186`: Backport PR #24157 on branch v3.6.x (test only PR milestoning guidance) +* :ghpull:`24187`: DOC: Fix toc structure in explain/interactive +* :ghpull:`24190`: DOC: fix markup +* :ghpull:`24157`: test only PR milestoning guidance +* :ghpull:`24183`: Backport PR #24178 on branch v3.6.x (Fall back to Python-level Thread for GUI warning) +* :ghpull:`24180`: Backport PR #24173 on branch v3.6.x (TST: convert nose-style tests) +* :ghpull:`24178`: Fall back to Python-level Thread for GUI warning +* :ghpull:`24177`: Don't simplify paths used for autoscaling +* :ghpull:`24173`: TST: convert nose-style tests +* :ghpull:`24174`: Backport PR #24171 on branch v3.6.x (Fix example where wrong variable was used) +* :ghpull:`24176`: Backport PR #24167 on branch v3.6.x (FIX: turn off layout engine tightbbox) +* :ghpull:`24167`: FIX: turn off layout engine tightbbox +* :ghpull:`24171`: Fix example where wrong variable was used +* :ghpull:`24172`: Backport PR #24158 on branch v3.6.x (Fix Qt with PySide6 6.4.0) +* :ghpull:`24158`: Fix Qt with PySide6 6.4.0 +* :ghpull:`24165`: Backport PR #24164 on branch v3.6.x (Fix argument order in hist() docstring.) +* :ghpull:`24164`: Fix argument order in hist() docstring. +* :ghpull:`24151`: Backport PR #24149 on branch v3.6.x (FIX: handle input to ax.bar that is all nan) +* :ghpull:`24149`: FIX: handle input to ax.bar that is all nan +* :ghpull:`24146`: Backport PR #24137 on branch v3.6.x (Add note about blitting and zorder in animations) +* :ghpull:`24137`: Add note about blitting and zorder in animations +* :ghpull:`24134`: Backport PR #24130 on branch v3.6.x (DOC: align contour parameter doc with implementation) +* :ghpull:`24130`: DOC: align contour parameter doc with implementation +* :ghpull:`24081`: TST: force test with shared test image to run in serial + +Issues (21): + +* :ghissue:`20326`: FuncAnimation Named Arguments +* :ghissue:`24332`: [Bug]: backend bug in matplotlib==3.6.1 with python3.11 and PySide6==6.4.0.1 +* :ghissue:`24296`: [Doc]: Axes limits not updated in animate decay +* :ghissue:`24089`: [Bug]: Resizing does not work in WebAgg backend in Safari +* :ghissue:`3657`: matplotlib.pyplot.imsave colormaps some grayscale images before saving them +* :ghissue:`24060`: [TST] Upcoming dependency test failures +* :ghissue:`24264`: [Bug]: Setting matplotlib.pyplot.style.library['seaborn-colorblind'] result in key error on matplotlib v3.6.1 +* :ghissue:`23900`: [Doc]: Adding some 3D plots to plot gallery +* :ghissue:`24199`: [Bug]: pick events do not forward mouseevent-key on Linux +* :ghissue:`23969`: [ENH]: Make rubber band more visible +* :ghissue:`23132`: [Bug]: call cmap object on torch.tensor will output first element all 0 +* :ghissue:`21349`: [Bug]: Hexbin gridsize interpreted differently for x and y +* :ghissue:`22905`: [Doc]: Duplicated toc entries +* :ghissue:`24094`: [Bug]: macOS: PyPy 3.8 (v7.3.9) threading get_native_id Broken +* :ghissue:`24097`: [Bug]: ax.hist density not auto-scaled when using histtype='step' +* :ghissue:`24148`: remove nose-style test classes +* :ghissue:`24133`: [Bug]: Incorrect crop after constrained layout with equal aspect ratio and bbox_inches = tight +* :ghissue:`24155`: [Bug]: TypeError: int() argument must be a string, a bytes-like object or a number, not 'KeyboardModifier' +* :ghissue:`24127`: [Bug]: ax.bar raises for all-nan data on matplotlib 3.6.1 +* :ghissue:`2959`: artists zorder is ignored during animations +* :ghissue:`24121`: [Doc]: Contour functions: auto-generated levels diff --git a/doc/users/prev_whats_new/github_stats_3.6.3.rst b/doc/users/prev_whats_new/github_stats_3.6.3.rst new file mode 100644 index 000000000000..b1d17a791c87 --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.6.3.rst @@ -0,0 +1,165 @@ +.. _github-stats-3-6-3: + +GitHub statistics for 3.6.3 (Jan 11, 2023) +========================================== + +GitHub statistics for 2022/11/02 (tag: v3.6.2) - 2023/01/11 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 16 issues and merged 107 pull requests. +The full list can be seen `on GitHub `__ + +The following 20 authors contributed 198 commits. + +* Antony Lee +* Chahak Mehta +* David Stansby +* Elliott Sales de Andrade +* Eric Larson +* hannah +* iofall +* Jody Klymak +* Kaidong Hu +* Kyle Sunden +* matt statham +* Matthias Bussonnier +* Muhammad Abdur Rakib +* Oscar Gustafsson +* ramvikrams +* Ruth Comer +* Steffen Rehberg +* Thomas A Caswell +* Tim Hoffmann +* yuanx749 + +GitHub issues and pull requests: + +Pull Requests (107): + +* :ghpull:`24939`: Backport PR #23390 on branch v3.6.x (FIX: colorbar contour with log norm should default to log locator and formatter...) +* :ghpull:`24936`: Backport PR #24927 on branch v3.6.x (DOC: Remove space after directive name, before double-colon) +* :ghpull:`23390`: FIX: colorbar contour with log norm should default to log locator and formatter... +* :ghpull:`24932`: Backport PR #24783 on branch v3.6.x (inset locator fix with tests added) +* :ghpull:`24783`: inset locator fix with tests added +* :ghpull:`24927`: DOC: Remove space after directive name, before double-colon +* :ghpull:`24881`: Backport PR #24880 on branch v3.6.x (Minor cleanups to named colors example.) +* :ghpull:`24876`: Backport PR #24873 on branch v3.6.x (Copy-edit fonts docs.) +* :ghpull:`24857`: Backport PR #24856 on branch v3.6.x (fix typo) +* :ghpull:`24852`: Backport PR #24843 on branch v3.6.x (Show that fill_between and span_where provide similar functionalities.) +* :ghpull:`24808`: Backport PR #24807 on branch v3.6.x (Axes.stem docstring document orientation as literals) +* :ghpull:`24807`: Axes.stem docstring document orientation as literals +* :ghpull:`24791`: Backport PR #24785 on branch v3.6.x (Fix random generation of single floats) +* :ghpull:`24777`: Backport PR #24772 on branch v3.6.x (Fix Left ventricle bullseye example) +* :ghpull:`24775`: Backport PR #24774 on branch v3.6.x (DOC: fix strip_chart example with numpy 1.24) +* :ghpull:`24765`: Backport PR #24764 on branch v3.6.x (DOC: ``subplot_mosaic`` tutorial - clarify ratios keywords used directly) +* :ghpull:`24739`: Backport PR #24732 on branch v3.6.x (Use masked stack to preserve mask info) +* :ghpull:`24738`: Backport PR #24735 on branch v3.6.x (Correct note about aspect) +* :ghpull:`24732`: Use masked stack to preserve mask info +* :ghpull:`24735`: Correct note about aspect +* :ghpull:`24729`: Backport PR #24715 on branch v3.6.x (Add note that users do not instantiate Axes directly) +* :ghpull:`24715`: Add note that users do not instantiate Axes directly +* :ghpull:`24721`: Backport PR #24607 on branch v3.6.x (DOC: tweak wording on Figure.show warning) +* :ghpull:`24607`: DOC: tweak wording on Figure.show warning +* :ghpull:`24694`: Backport PR #24692 on branch v3.6.x (Avoid rgba8888->argb32 conversion if qt can do it for us.) +* :ghpull:`24692`: Avoid rgba8888->argb32 conversion if qt can do it for us. +* :ghpull:`24684`: Backport PR #24654: Don't manually invalidate cached lines in _update_transScale +* :ghpull:`24687`: Backport PR #24003 on branch v3.6.x (Fix wording and links lifecycle tutorial) +* :ghpull:`24685`: Backport PR #23974 on branch v3.6.x (Fix repeated word typos) +* :ghpull:`24680`: Backport PR #24677 on branch v3.6.x (FIX: do not replace the Axes._children list object) +* :ghpull:`24677`: FIX: do not replace the Axes._children list object +* :ghpull:`24659`: Backport PR #24657 on branch v3.6.x (BUG: Fix bug with mutable input modification) +* :ghpull:`24657`: BUG: Fix bug with mutable input modification +* :ghpull:`24654`: Don't manually invalidate cached lines in _update_transScale. +* :ghpull:`24650`: Backport PR #24645 on branch v3.6.x (Removed 'above' wording from Input hook integration docs (#24632)) +* :ghpull:`24647`: Backport PR #24643 on branch v3.6.x (DOC: annotation coords are not floats) +* :ghpull:`24643`: DOC: annotation coords are not floats +* :ghpull:`24625`: Backport PR #24606: FIX: do not use deprecated API in gtk4 backend +* :ghpull:`24633`: Backport PR #24592 on branch v3.6.x (DOC: Don't try to link paths that are on a different drive) +* :ghpull:`24592`: DOC: Don't try to link paths that are on a different drive +* :ghpull:`24628`: Backport PR #24584 on branch v3.6.x (DOC: add "See Also: draw_idle" reference to pyplot.draw) +* :ghpull:`24584`: DOC: add "See Also: draw_idle" reference to pyplot.draw +* :ghpull:`24601`: Backport PR #24600 on branch v3.6.x (Fix: Gracefully fail the string validator for tuple inputs) +* :ghpull:`24609`: Backport PR #24595 on branch v3.6.x (ci: Stop building wheels on AppVeyor) +* :ghpull:`24616`: Backport PR #24397 on branch v3.6.x (Simplify appveyor to only use conda) +* :ghpull:`24615`: Backport PR #24598 on branch v3.6.x (Check for errors/warnings on failed doc-builds) +* :ghpull:`24606`: FIX: do not use deprecated API in gtk4 backend +* :ghpull:`24612`: Backport PR #23868 on branch v3.6.x (Show errors and warnings in doc CI after build.) +* :ghpull:`24595`: ci: Stop building wheels on AppVeyor +* :ghpull:`24600`: Fix: Gracefully fail the string validator for tuple inputs +* :ghpull:`24593`: Backport PR #24580 on branch v3.6.x (Update the polar transform information in doc #24499) +* :ghpull:`24587`: Backport PR #24579: Add explicit permissions to GitHub Actions +* :ghpull:`24579`: Add explicit permissions to GitHub Actions +* :ghpull:`24561`: Backport PR #24540 on branch v3.6.x (DOC: add note about enabling c++11 support for old gcc) +* :ghpull:`24559`: Backport PR #24299 on branch v3.6.x (Rework style sheet reference example to cycle props) +* :ghpull:`24551`: Backport PR #24548 on branch v3.6.x (DOC: improved the doc for layout_engine.py) +* :ghpull:`24548`: DOC: improved the doc for layout_engine.py +* :ghpull:`24535`: Backport PR #24514 on branch v3.6.x (Fix potential issue in contour) +* :ghpull:`24534`: Backport PR #24521 on branch v3.6.x (Doc: improve spelling and grammar) +* :ghpull:`24533`: Backport PR #24517 on branch v3.6.x (DOC: improve grammar and consistency) +* :ghpull:`24532`: Backport PR #24520 on branch v3.6.x (Doc: Fix grammar and spelling) +* :ghpull:`24514`: Fix potential issue in contour +* :ghpull:`24521`: Doc: improve spelling and grammar +* :ghpull:`24517`: DOC: improve grammar and consistency +* :ghpull:`24520`: Doc: Fix grammar and spelling +* :ghpull:`24515`: Backport PR #24512 on branch v3.6.x (Tweak markup in toolkits tutorials.) +* :ghpull:`24503`: Backport PR #24502 on branch v3.6.x (Remove link from demo_floating_axes title.) +* :ghpull:`24505`: Backport PR #24482 on branch v3.6.x (Use relative frame path in HTMLWriter) +* :ghpull:`24506`: Backport of PR#24488 (Update for pydata-sphinx-theme 0.12.0) +* :ghpull:`24482`: Use relative frame path in HTMLWriter +* :ghpull:`24496`: Backport PR #24495 on branch v3.6.x (Update adding of google analytics key for docs) +* :ghpull:`24495`: Update adding of google analytics key for docs +* :ghpull:`24488`: Update for pydata-sphinx-theme 0.12.0 +* :ghpull:`24485`: Backport PR #24481 on branch v3.6.x (Fix floating-point drift in oscilloscope example) +* :ghpull:`24475`: DOC: Fix examples gallery layout issues +* :ghpull:`24478`: Backport PR #24444 on branch v3.6.x (DOC: AnnotationBbox keyword descriptions) +* :ghpull:`24444`: DOC: AnnotationBbox keyword descriptions +* :ghpull:`24468`: Backport PR #24429 on branch v3.6.x (DOC: Clarify transparency in colors) +* :ghpull:`24466`: Backport PR #24460 on branch v3.6.x (Define autoscale() based on autoscale_None().) +* :ghpull:`24460`: Define autoscale() based on autoscale_None(). +* :ghpull:`24463`: Backport PR #24459 on branch v3.6.x (removed unused variable and fixed text in doc) +* :ghpull:`24459`: removed unused variable and fixed text in doc +* :ghpull:`24458`: Backport PR #24434 on branch v3.6.x (Fix pyplot.figlegend docstring) +* :ghpull:`24434`: Fix pyplot.figlegend docstring +* :ghpull:`24456`: Backport PR #24402 on branch v3.6.x (DOC: Fix title formats in backend api docs) +* :ghpull:`24438`: Backport PR #24435 on branch v3.6.x (Minor improvements to LogLocator docstring) +* :ghpull:`24435`: Minor improvements to LogLocator docstring +* :ghpull:`24426`: Backport PR #24422 on branch v3.6.x (Make QT_API a link in the qt embedding example.) +* :ghpull:`24411`: Backport PR #24407 on branch v3.6.x (Reword "Reordering is not commutative" phrase in tutorial.) +* :ghpull:`24400`: Backport PR #24399 on branch v3.6.x (Fix docstring of Figure.subfigures.) +* :ghpull:`24399`: Fix docstring of Figure.subfigures. +* :ghpull:`24391`: Backport PR #24380 on branch v3.6.x (DOC: Remove the example "Pythonic Matplotlib") +* :ghpull:`24384`: Backport PR #24377 on branch v3.6.x (DOC: Cleanup Spine placement example) +* :ghpull:`24381`: Backport PR #24366 on branch v3.6.x (DOC: Improve Image Slices Viewer example) +* :ghpull:`24382`: Backport PR #24378 on branch v3.6.x (DOC: Cleanup spines usage in examples) +* :ghpull:`24378`: DOC: Cleanup spines usage in examples +* :ghpull:`24366`: DOC: Improve Image Slices Viewer example +* :ghpull:`24370`: Backport PR #24368 on branch v3.6.x (DOC: Install dev dependencies before building matplotlib) +* :ghpull:`24368`: DOC: Install dev dependencies before building matplotlib +* :ghpull:`24365`: Backport PR #24363 on branch v3.6.x (DOC: Fix syntax of suggestion) +* :ghpull:`24358`: Backport PR #24354 on branch v3.6.x (DOC: clarify rc_context resets all rcParams changes) +* :ghpull:`24354`: DOC: clarify rc_context resets all rcParams changes +* :ghpull:`24353`: Backport PR #24343 on branch v3.6.x (Emit "axes not compatible with tight_layout" in a single place.) +* :ghpull:`24343`: Emit "axes not compatible with tight_layout" in a single place. +* :ghpull:`24346`: Backport PR #24344 on branch v3.6.x (Add test for colorbar extend alpha) +* :ghpull:`24344`: Add test for colorbar extend alpha +* :ghpull:`23974`: Fix repeated word typos + +Issues (16): + +* :ghissue:`23389`: [Bug]: Colorbar with log scales wrong format +* :ghissue:`24589`: [Bug]: inset_locator is broken when used with subfigures +* :ghissue:`10160`: Low resolution (dpi problem) with Qt5 backend on new iMac Pro Retina +* :ghissue:`24545`: [Bug]: ``matplotlib.pyplot.scatter`` does not respect mask rules with ``datetime`` +* :ghissue:`24639`: [Bug]: The Axes3D does not work as expected. +* :ghissue:`22169`: [Doc]: figure.show works beyond what is documented +* :ghissue:`23968`: [Bug]: Zoom rubber band lags in larger window +* :ghissue:`24574`: [Bug]: Extension error (sphinx.ext.linkcode) while building docs +* :ghissue:`24602`: ``close_event`` deprecated warning. +* :ghissue:`24518`: [Doc]: ``layout_engine`` description +* :ghissue:`23581`: [BUG]: frame paths relative to the html file when saving an animation to html +* :ghissue:`23976`: [Doc]: Examples Gallery Layout changed to one or two columns +* :ghissue:`24390`: [Doc]: alpha setting for annotation ``TextArea`` +* :ghissue:`24433`: [Doc]: figlegend examples call ``fig.figlegend`` instead of ``plt.figlegend`` or ``fig.legend`` +* :ghissue:`24360`: [ENH]: imshow support for multiple slice image volume +* :ghissue:`24359`: [Bug]: Documentation not so clear that a C/C++-compiler is required to install from source diff --git a/doc/users/prev_whats_new/github_stats_3.7.0.rst b/doc/users/prev_whats_new/github_stats_3.7.0.rst new file mode 100644 index 000000000000..754c4c1fc059 --- /dev/null +++ b/doc/users/prev_whats_new/github_stats_3.7.0.rst @@ -0,0 +1,681 @@ +.. _github-stats-3-7-0: + +GitHub statistics for 3.7.0 (Feb 13, 2023) +========================================== + +GitHub statistics for 2022/09/16 (tag: v3.6.0) - 2023/02/13 + +These lists are automatically generated, and may be incomplete or contain duplicates. + +We closed 120 issues and merged 427 pull requests. +The full list can be seen `on GitHub `__ + +The following 112 authors contributed 1962 commits. + +* Abhijnan Bajpai +* Adrien F. Vincent +* Ahoy Ahoy +* Akshit Tyagi +* Ali Meshkat +* Almar Klein +* Andrés Martínez +* Ante Sikic +* Antony Lee +* Augustin LAVILLE +* baharev +* cargobuild +* Carsten Schnober +* Chahak Mehta +* Charisma Kausar +* David Stansby +* dependabot[bot] +* DerWeh +* Eero Vaher +* Elliott Sales de Andrade +* Eric Larson +* Eric Prestat +* erykoff +* EunHo Lee +* Felix Goudreault +* Greg Lucas +* hannah +* Ian Hunt-Isaak +* Ian Thomas +* intellizEHL +* iofall +* j1642 +* jacoverster +* Jae-Joon Lee +* Jakub Klus +* James Braza +* Jay Stanley +* Jef Myers +* jeffreypaul15 +* Jefro +* Jody Klymak +* John Paul Jepko +* Joseph Fox-Rabinovitz +* Joshua Barrass +* Julian Chen +* Junaid Khan +* Justin Tracey +* Kaidong Hu +* Kanza +* Karan +* Kian Eliasi +* kolibril13 +* Kostya Farber +* Krutarth Patel +* Kyle Sunden +* Leo Singer +* Lucas Ricci +* luke +* Marc Van den Bossche +* Martok +* Marvvxi +* Matthew Feickert +* Mauricio Collares +* MeeseeksMachine +* melissawm +* Mikhail Ryazanov +* Muhammad Abdur Rakib +* noatamir +* NRaudseps +* Olivier Castany +* Oscar Gustafsson +* parthpankajtiwary +* Paul Seyfert +* Pavel Grunt +* Pieter Eendebak +* PIotr Strzelczyk +* Pratim Ugale +* pre-commit-ci[bot] +* ramvikrams +* richardsheridan +* Ruth Comer +* Ryan May +* saranti +* Scott Shambaugh +* Shabnam Sadegh +* Shawn Zhong +* Simon Waldherr +* Skhaki18 +* slackline +* Snipeur060 +* Sourajita Dewasi +* SourajitaDewasi +* Stefanie Molin +* Steffen Rehberg +* Sven Eschlbeck +* sveneschlbeck +* takimata +* tfpf +* Thomas A Caswell +* Tiger Nie +* Tim Hoffmann +* Tom +* Tortar +* tsumli +* tybeller +* vdbma +* Vishal Pankaj Chandratreya +* vivekvedant +* whyvra +* yuanx749 +* zhizheng1 +* مهدي شينون (Mehdi Chinoune) + +GitHub issues and pull requests: + +Pull Requests (427): + +* :ghpull:`25201`: Backport PR #25196 on branch v3.7.x (Add deprecation for setting data with non sequence type in ``Line2D``) +* :ghpull:`25196`: Add deprecation for setting data with non sequence type in ``Line2D`` +* :ghpull:`25197`: Backport PR #25193 on branch v3.7.x (Fix displacement of colorbar for eps with bbox_inches='tight') +* :ghpull:`25193`: Fix displacement of colorbar for eps with bbox_inches='tight' +* :ghpull:`24781`: DOC: restore SHA to footer +* :ghpull:`25188`: Backport PR #25085 on branch v3.7.x (FIX: only try to update blit caches if the canvas we expect) +* :ghpull:`25170`: Backport PR #25097 on branch v3.7.x (fix FigureCanvasTkAgg memory leak via weakrefs) +* :ghpull:`25186`: Backport PR #24893 on branch v3.7.x (STY: make allowed line length 9 longer to 88 from 79) +* :ghpull:`25185`: Backport PR #25183 on branch v3.7.x (FIX: do not use deprecated API internally) +* :ghpull:`25184`: Backport PR #25174 on branch v3.7.x (Accept LA icons for the toolbar) +* :ghpull:`25085`: FIX: only try to update blit caches if the canvas we expect +* :ghpull:`25183`: FIX: do not use deprecated API internally +* :ghpull:`25182`: Backport PR #25052 on branch v3.7.x (Support both Bbox and list for bbox to table/Table) +* :ghpull:`25174`: Accept LA icons for the toolbar +* :ghpull:`25052`: Support both Bbox and list for bbox to table/Table +* :ghpull:`25095`: Backport PR #23442 on branch v3.7.x (Remove need to detect math mode in pgf strings) +* :ghpull:`25097`: fix FigureCanvasTkAgg memory leak via weakrefs +* :ghpull:`25167`: Backport PR #25122 on branch v3.7.x (FIX: scaling factor for window with negative value) +* :ghpull:`25122`: FIX: scaling factor for window with negative value +* :ghpull:`25161`: Backport PR #25158 on branch v3.7.x (Disconnect SubplotTool destroyer callback on tool_fig close) +* :ghpull:`25160`: Backport PR #25129 on branch v3.7.x (Undeprecate Cursor event handlers) +* :ghpull:`25158`: Disconnect SubplotTool destroyer callback on tool_fig close +* :ghpull:`25129`: Undeprecate Cursor event handlers +* :ghpull:`25154`: Backport PR #25151 on branch v3.7.x (Increase timeout to GitHub API) +* :ghpull:`25151`: Increase timeout to GitHub API +* :ghpull:`25136`: Backport PR #25126 on branch v3.7.x (FIX: fully invalidate TransformWrapper parents before swapping) +* :ghpull:`25132`: Backport PR #24993 on branch v3.7.x ([DOC] GitHub spelling and links) +* :ghpull:`25126`: FIX: fully invalidate TransformWrapper parents before swapping +* :ghpull:`24993`: [DOC] GitHub spelling and links +* :ghpull:`25118`: Backport PR #25113 on branch v3.7.x (Fix outdated comment re: _update_label_position.) +* :ghpull:`25113`: Fix outdated comment re: _update_label_position. +* :ghpull:`25111`: Backport PR #25110 on branch v3.7.x (Stop recommending ``ncol`` in legend examples) +* :ghpull:`25110`: Stop recommending ``ncol`` in legend examples +* :ghpull:`25106`: Fix cursor_demo wrt. Line2D.set_x/ydata not accepting scalars anymore. +* :ghpull:`25103`: Backport PR #25098 on branch v3.7.x (Correctly pass valinit as keyword in SliderTool.) +* :ghpull:`25098`: Correctly pass valinit as keyword in SliderTool. +* :ghpull:`23442`: Remove need to detect math mode in pgf strings +* :ghpull:`25093`: Backport PR #25092 on branch v3.7.x (Fix distribution of test data) +* :ghpull:`24893`: STY: make allowed line length 9 longer to 88 from 79 +* :ghpull:`25092`: Fix distribution of test data +* :ghpull:`25089`: Backport PR #25088 on branch v3.7.x (DOC: Fix broken cross-reference when building PDF) +* :ghpull:`25088`: DOC: Fix broken cross-reference when building PDF +* :ghpull:`25083`: Backport PR #25074 on branch v3.7.x (Revert "Use system distutils instead of the setuptools copy") +* :ghpull:`25082`: Backport PR #25079 on branch v3.7.x (FIX: Only send one update signal when autoscaling norms) +* :ghpull:`25084`: DOC: Fix typos in GitHub stats +* :ghpull:`25074`: Revert "Use system distutils instead of the setuptools copy" +* :ghpull:`25079`: FIX: Only send one update signal when autoscaling norms +* :ghpull:`25072`: Merge v3.6.x into v3.7.x +* :ghpull:`25071`: Backport PR #25039 on branch v3.7.x (Updated WebAgg JS to check and send request over wss if using HTTPS) +* :ghpull:`25039`: Updated WebAgg JS to check and send request over wss if using HTTPS +* :ghpull:`25070`: Backport PR #25058 on branch v3.7.x (fix for pcolormesh doesn't allow shading = 'flat' in the option) +* :ghpull:`25058`: fix for pcolormesh doesn't allow shading = 'flat' in the option +* :ghpull:`25067`: Backport PR #25054 on branch v3.7.x (Remove note that mathtext.fontset = "custom" is unsupported.) +* :ghpull:`25066`: Backport PR #24999 on branch v3.7.x (DOC: figure explanation) +* :ghpull:`25054`: Remove note that mathtext.fontset = "custom" is unsupported. +* :ghpull:`25065`: Backport PR #24838 on branch v3.7.x (Add styling support to Check and Radio buttons ) +* :ghpull:`24999`: DOC: figure explanation +* :ghpull:`24838`: Add styling support to Check and Radio buttons +* :ghpull:`25056`: Backport PR #25055 on branch v3.7.x (Reword awkward sentence in FAQ.) +* :ghpull:`25055`: Reword awkward sentence in FAQ. +* :ghpull:`25049`: Backport PR #25047 on branch v3.7.x (Remove dead code from deprecated-and-removed block) +* :ghpull:`25047`: Remove dead code from deprecated-and-removed block +* :ghpull:`25037`: Backport PR #25018 on branch v3.7.x (Simplify "artist reference" example.) +* :ghpull:`25018`: Simplify "artist reference" example. +* :ghpull:`25034`: Backport PR #24812 on branch v3.7.x ([Doc] expanded basic pie example) +* :ghpull:`24812`: [Doc] expanded basic pie example +* :ghpull:`25029`: Backport PR #25019 on branch v3.7.x (Tweak titles pyplot examples.) +* :ghpull:`25019`: Tweak titles pyplot examples. +* :ghpull:`25026`: Backport PR #25017 on branch v3.7.x (Capitalize headings in example Gallery) +* :ghpull:`25017`: Capitalize headings in example Gallery +* :ghpull:`25010`: Backport PR #24989 on branch v3.7.x (Suppress pyparsing warning) +* :ghpull:`25008`: Backport PR #25004 on branch v3.7.x (Bump pypa/cibuildwheel from 2.11.4 to 2.12.0) +* :ghpull:`24989`: Suppress pyparsing warning +* :ghpull:`25004`: Bump pypa/cibuildwheel from 2.11.4 to 2.12.0 +* :ghpull:`25001`: Backport PR #25000 on branch v3.7.x (Update matplotlibrc urls) +* :ghpull:`25000`: Update matplotlibrc urls +* :ghpull:`24977`: Backport PR #24970 on branch v3.7.x (FIX: Handle uint8 indices properly for colormap lookups) +* :ghpull:`24970`: FIX: Handle uint8 indices properly for colormap lookups +* :ghpull:`24975`: Backport PR #24971 on branch v3.7.x (FIX: adjust_bbox should not modify layout engine) +* :ghpull:`24974`: Backport PR #24973 on branch v3.7.x (MNT: Fix double % signs in matplotlibrc) +* :ghpull:`24966`: Backport PR #24965 on branch v3.7.x (Remove additional deprecations from 3.5) +* :ghpull:`24971`: FIX: adjust_bbox should not modify layout engine +* :ghpull:`24973`: MNT: Fix double % signs in matplotlibrc +* :ghpull:`24965`: Remove additional deprecations from 3.5 +* :ghpull:`24963`: Backport PR #24912 on branch v3.7.x (Remove contour warning for "no-valid-levels".) +* :ghpull:`24962`: Backport PR #24957 on branch v3.7.x (DOC: Enable Opensearch) +* :ghpull:`24961`: Backport PR #24948 on branch v3.7.x (Remove remaining deprecations from 3.5) +* :ghpull:`24959`: Backport PR #24254 on branch v3.7.x (Expire deprecations in widgets and keyword only arguments for Selectors) +* :ghpull:`24912`: Remove contour warning for "no-valid-levels". +* :ghpull:`24960`: Backport PR #24825 on branch v3.7.x (Allow non-default scales on polar axes) +* :ghpull:`24957`: DOC: Enable Opensearch +* :ghpull:`24948`: Remove remaining deprecations from 3.5 +* :ghpull:`24825`: Allow non-default scales on polar axes +* :ghpull:`24254`: Expire deprecations in widgets and keyword only arguments for Selectors +* :ghpull:`24956`: Backport PR #24955 on branch v3.7.x (Cleanup bullseye plot example.) +* :ghpull:`24955`: Cleanup bullseye plot example. +* :ghpull:`24949`: Backport PR #24918 on branch v3.7.x (DOC: animation faster) +* :ghpull:`24947`: Auto backport of pr 24897 on v3.7.x +* :ghpull:`24945`: Backport PR #24940 on branch v3.7.x ([MNT] specify which gallery sections come last) +* :ghpull:`24918`: DOC: animation faster +* :ghpull:`24917`: Backport PR #24897: DOC: Add ref for every under examples/animation +* :ghpull:`24940`: [MNT] specify which gallery sections come last +* :ghpull:`24941`: Backport PR #24655 on branch v3.7.x (Update font_manager to only use registry on Win) +* :ghpull:`24655`: Update font_manager to only use registry on Win +* :ghpull:`24937`: Backport PR #24470 on branch v3.7.x ([ENH] hatch keyword for pie + some pie documentation) +* :ghpull:`24938`: Backport PR #23390 on branch v3.7.x (FIX: colorbar contour with log norm should default to log locator and formatter...) +* :ghpull:`24935`: Backport PR #24934 on branch v3.7.x (Swap ipython directives for code-block directives) +* :ghpull:`24470`: [ENH] hatch keyword for pie + some pie documentation +* :ghpull:`24933`: Backport PR #24924 on branch v3.7.x (Fix toggling layout engines) +* :ghpull:`24934`: Swap ipython directives for code-block directives +* :ghpull:`24931`: Backport PR #24783 on branch v3.7.x (inset locator fix with tests added) +* :ghpull:`24924`: Fix toggling layout engines +* :ghpull:`24928`: Backport PR #24927 on branch v3.7.x (DOC: Remove space after directive name, before double-colon) +* :ghpull:`24926`: Backport PR #24925 on branch v3.7.x (DOC: Improve documentation for set_loglevel) +* :ghpull:`24925`: DOC: Improve documentation for set_loglevel +* :ghpull:`24922`: Backport PR #24921 on branch v3.7.x (Pin sphinx != 6.1.2) +* :ghpull:`24921`: Pin sphinx != 6.1.2 +* :ghpull:`24911`: Backport PR #24904 on branch v3.7.x (Deprecate AxisArtistHelpers with inconsistent loc/nth_coord.) +* :ghpull:`24897`: DOC: Add ref for every under examples/animation +* :ghpull:`24904`: Deprecate AxisArtistHelpers with inconsistent loc/nth_coord. +* :ghpull:`22314`: Add a helper to generate xy coordinates for AxisArtistHelper. +* :ghpull:`24841`: changed method in animation tutorial table of methods +* :ghpull:`24902`: Remove provisional note from pyplot.subplot_mosaic +* :ghpull:`24891`: DOC: mark mosaic as no longer provisional +* :ghpull:`24889`: Harmonize exceptions for unknown keyword arguments. +* :ghpull:`24085`: Set facecolor of FilledArrow axisline style and fix tight layout +* :ghpull:`19743`: ENH: allow fig.legend outside axes... +* :ghpull:`24887`: [MNT] Bump NumPy to 1.20 +* :ghpull:`24896`: changed contribute docs link to writing docs +* :ghpull:`24894`: DOC: explain clipbox a bit better +* :ghpull:`24864`: Deprecate BrokenBarHCollection. +* :ghpull:`24869`: Skip displaying pan/zoom navigate mode in toolbar. +* :ghpull:`24892`: FIX: error in formatting in error string in redirect extension +* :ghpull:`24895`: add new & improved doc notices to what's new +* :ghpull:`24888`: update install instructions for conda +* :ghpull:`24886`: CI: rotate the circleci deploy key +* :ghpull:`24879`: Document "." as a filled marker. +* :ghpull:`24870`: Better default bool contour levels. +* :ghpull:`24786`: Increase a few test tolerances on some arches +* :ghpull:`24863`: Add parameter doc to PolarTransform +* :ghpull:`24845`: Fix toggling of MultiCursor.{horizOn,vertOn} +* :ghpull:`24862`: Fix argument checking in ``Axes3D.quiver`` +* :ghpull:`24868`: [pre-commit.ci] pre-commit autoupdate +* :ghpull:`24840`: Simplify/robustify segment-point distance calculation. +* :ghpull:`24850`: Improve PolarAffine docstring +* :ghpull:`24851`: Variable rename t > theta +* :ghpull:`24763`: Allow polar scales where zero is not in valid interval +* :ghpull:`24846`: Promote pending cm deprecations to full deprecations +* :ghpull:`24848`: ``Collection.set_linestyle``: remove redundant string handling +* :ghpull:`24839`: Move geo/polar projections to their own pages +* :ghpull:`24727`: Handle argument "facecolors=None" correctly in plot_surface() +* :ghpull:`24847`: Avoid extra copy initializing empty Affine2D +* :ghpull:`24837`: DOC: Replace .format by f-strings in examples +* :ghpull:`24604`: Enh/extend mosaic kwargs +* :ghpull:`24131`: Deprecate attributes and expire deprecation in animation +* :ghpull:`23457`: Add blitting support to button widgets +* :ghpull:`24832`: [MNT] Improve variable naming in bar +* :ghpull:`24829`: Simplify shape-checking in QuadMesh.set_array. +* :ghpull:`24835`: Delay nightly wheel builds by 2 hours +* :ghpull:`24831`: [Doc] Fix ndarray-links for arguments +* :ghpull:`24824`: Fix incorrect method in doc +* :ghpull:`24826`: space in version added for reverse in legend +* :ghpull:`24819`: Bump pypa/cibuildwheel from 2.11.3 to 2.11.4 +* :ghpull:`24811`: removed casting handles to list in legend +* :ghpull:`24759`: Reverse legend +* :ghpull:`24465`: Reparametrize offsetbox calculations in terms of bboxes. +* :ghpull:`22316`: Arbitrary figure customization hooks. +* :ghpull:`22329`: Enforce that Line data modifications are sequences +* :ghpull:`24730`: Data access API for rcParams +* :ghpull:`24699`: Implement nested four-level TeX cache +* :ghpull:`24752`: DOC: Make event handling table scrollable +* :ghpull:`24637`: Fixes #20044 pass AnnotationBbox to renderer +* :ghpull:`24810`: Don't modify dictionary input to widgets +* :ghpull:`24769`: Improve matplotlib.axes documentation +* :ghpull:`24806`: Deprecate 'x' argument for widgets.TextBox.begin_typing +* :ghpull:`24293`: Handle rasterization start & stop only from Artist +* :ghpull:`24768`: Fix/zorder rasterization +* :ghpull:`24474`: Use scatter for check boxes and set facecolors correctly in check boxes and radio buttons +* :ghpull:`24262`: Fix issue with space allocated for single tick that should not be there +* :ghpull:`24780`: Update environment.yml +* :ghpull:`23576`: Soft deprecate the textpath module (import from text instead) +* :ghpull:`24750`: Fix deprecations of \*Cursor widget event handlers +* :ghpull:`24757`: Allow using masked in ``set_offsets`` +* :ghpull:`21661`: Fix plot directive with func calls +* :ghpull:`24803`: Correct type in docstring of zorder for streamplot and LineCollection +* :ghpull:`24801`: Correct docstring of RangeSlider.on_changed +* :ghpull:`24802`: Correct docstring of CheckButtons.get_status +* :ghpull:`24758`: MNT: Simplify code related to masked arrays +* :ghpull:`24756`: DOC: Simplify some table markup +* :ghpull:`24795`: DOC: Fix duplicate redirect +* :ghpull:`24782`: DOC: update typos and grammar errors +* :ghpull:`24794`: Update README.md +* :ghpull:`24071`: Deprecate undefined label_mode to Grid +* :ghpull:`24724`: Run delvewheel on Windows for wheels +* :ghpull:`24538`: [Doc] Document legend_handles and legend_handlers +* :ghpull:`24751`: DOC: Update Artist inheritance diagram +* :ghpull:`24761`: Don't set the never-used Line2D._contains in set_picker. +* :ghpull:`24760`: Remove unused dicts from backend_cairo. +* :ghpull:`24736`: DOC: simplify CheckButton example +* :ghpull:`22700`: MAINT: Move docstring of ``LogLocator`` to class +* :ghpull:`19763`: Remove visibility changes in draw for \*Cursor widgets +* :ghpull:`23473`: Separately track modifier keys for mouse events. +* :ghpull:`24748`: DOC: remove research notice +* :ghpull:`24734`: Support masked dates +* :ghpull:`24737`: MNT: make fig.colorbar(..., ax=INPUT) even more forgiving +* :ghpull:`24120`: don't try to start a new event loop in WebAgg when in an ipykernel +* :ghpull:`24362`: Allow bool-like values for sharex/sharey +* :ghpull:`24740`: Minor redundancy cleanup of code which sets 3D aspect 3D +* :ghpull:`22273`: Improve inheritance diagrams +* :ghpull:`24668`: Add test for remaining axis options +* :ghpull:`9598`: ENH: rely on non-rectangular patch paths rather than bboxes for legend auto-placing (fix #9580) +* :ghpull:`22920`: Mnt deprecate mlab +* :ghpull:`24408`: Fix: restore make_axes to accept a tuple of axes +* :ghpull:`24731`: DOC: Post warnings as reviews on PRs +* :ghpull:`24652`: Offsetbox default arguments +* :ghpull:`24720`: FIX: be more forgiving in default draw wrapper +* :ghpull:`24719`: Remove quotes from EngFormatter.format_eng example +* :ghpull:`24718`: Remove refresh function from polar ThetaLocator +* :ghpull:`24710`: Drop support for Qt<5.10. +* :ghpull:`24509`: Factor out & improve accuracy of derivatives calculations in axisartist. +* :ghpull:`19591`: reverse order in which stackplot elements are added to axes +* :ghpull:`24367`: STY: Update macosx zoom rect styling +* :ghpull:`24706`: Bump pypa/cibuildwheel from 2.11.2 to 2.11.3 +* :ghpull:`24705`: Cleanup a few examples. +* :ghpull:`21096`: FIX: improve symlog ticker +* :ghpull:`24498`: DOC: Update multiple category bar chart examples +* :ghpull:`24688`: Deprecate quiver_doc and barbs_doc class members +* :ghpull:`24526`: [Doc] Fix spelling and grammar in tutorials +* :ghpull:`24675`: TST: set style in mpl_toolkits to ease later transition +* :ghpull:`24484`: Artist's draw method prevents rasterization by default +* :ghpull:`24667`: Test scroll zoom bbox update +* :ghpull:`24662`: Doc/git force +* :ghpull:`24664`: Deprecate offsetbox.bbox_artist +* :ghpull:`24670`: Tiny capitalization fix. +* :ghpull:`24596`: ENH: Add ellipse class for annotation box styles +* :ghpull:`24249`: Add legend tests for 3D plots +* :ghpull:`24627`: MNT: when clearing an Axes via clear/cla fully detach children +* :ghpull:`24653`: Directly call _long_axis()._set_axes_scale in Colorbar. +* :ghpull:`24640`: Small TransformWrapper cleanups. +* :ghpull:`24528`: BUG: Warn when an existing layout manager changes to tight layout +* :ghpull:`24635`: Remove unneeded _update_transScale calls in _init_axis. +* :ghpull:`24641`: Fix that font files never pass the test on Win +* :ghpull:`24522`: Use pybind11 for tri module +* :ghpull:`24603`: Shorten the definition of sawtooth boxstyle. +* :ghpull:`24630`: Improve error message for gridspec when the index is not an integer. +* :ghpull:`24634`: Init axes._children early enough to avoid need for some getattr calls. +* :ghpull:`24629`: Doc/gitwash redirects +* :ghpull:`24624`: Expire FancyBboxPatch deprecations. +* :ghpull:`24619`: ENH: Allow RGB(A) arrays for pcolormesh +* :ghpull:`23588`: Refactoring gitwash +* :ghpull:`21549`: Unifying the Figure getter/setter interface to match its constructor +* :ghpull:`24582`: Shorten demo_axes_grid example. +* :ghpull:`24577`: Fold _set_ticklabels into set_ticklabels. +* :ghpull:`24581`: Simplify implementation of _is_sorted. +* :ghpull:`24575`: Use std::isnan and fix compiler warning +* :ghpull:`24570`: FIX: VPacker and HPacker bottom/top alignment +* :ghpull:`23812`: Ci add codeql +* :ghpull:`24556`: Fix incorrect window_extent of AxesImage +* :ghpull:`24566`: Improve argument checking for set_xticks(). +* :ghpull:`24544`: DOC: Add links to supported file formats in animations tutorial +* :ghpull:`24511`: Add test for mutating input arrays #8990 +* :ghpull:`24558`: In mplot3d, fix a doc typo and autogen zaxis_inverted. +* :ghpull:`24555`: ENH: Add warning for SymLogScale when values in linear scale range +* :ghpull:`23417`: Consistently set label on axis with units +* :ghpull:`24542`: DOC: Clarify supported animation formats in animation tutorial +* :ghpull:`23685`: Add mathtext support for ``\middle`` and correct rendering of ``\|`` +* :ghpull:`24539`: Fix misnamed api changes entry. +* :ghpull:`23692`: Add ``Axes.get_tick_params()`` method. +* :ghpull:`24132`: CenteredNorm changes +* :ghpull:`24529`: Transform ParasiteAxesBase._update_viewlim into standard callback. +* :ghpull:`24304`: Simplify some patches path definitions. +* :ghpull:`24431`: FIX: Support passing one alpha per event sequence to eventplot() +* :ghpull:`24527`: Fix testing of whether backends use the new pyplot_show API. +* :ghpull:`24537`: Fix triage tool due to test reorganization +* :ghpull:`21831`: FIX: pre-composite animation frames to white background +* :ghpull:`24205`: Plot directive: delegate file handling to Sphinx +* :ghpull:`24274`: Animation Tutorial +* :ghpull:`24519`: MNT: remove unused arguments to private methods and minor doc fixes +* :ghpull:`24525`: [Doc] Fix spelling and grammar in examples +* :ghpull:`24523`: [Doc] fix more spelling and grammar +* :ghpull:`24218`: Document what pyplot expects from a backend. +* :ghpull:`24513`: Modernize a bit floating_axes tests. +* :ghpull:`24491`: Make Path3DCollection store indexed offset, and only apply z-ordered offset during draw +* :ghpull:`24500`: DOC: Removed matplotlib from mission statement title +* :ghpull:`24490`: DOC: Remove text rotation example +* :ghpull:`24487`: Update tests to run with 3.11 (not rc) +* :ghpull:`24439`: Remove custom polar behaviour in LogLocator +* :ghpull:`24461`: Shorten and explain more calculations in axes_divider. +* :ghpull:`24472`: [DOC] removed flake8 from PR template +* :ghpull:`24467`: [DOC] swapped params in fig_compare_error msg +* :ghpull:`24455`: Draw RadioButtons using scatter to ensure circular buttons. +* :ghpull:`24462`: Don't pass unused xdescent to _get_packed_offsets. +* :ghpull:`24446`: Remove axis() manual argument parsing. +* :ghpull:`24334`: ENH: Check labels arg when kwargs passed in Axis.set_ticks() +* :ghpull:`24430`: MNT: Issue a warning instead of logging if RGB(A) passed to scatter(..., c) +* :ghpull:`24397`: Simplify appveyor to only use conda +* :ghpull:`24447`: Factor out error generation for function calls with wrong nargs. +* :ghpull:`24441`: DOC: Fix example for what's new imshow so it isn't cut off or crowded. +* :ghpull:`24443`: Add valid values to ``get_*axis_transform`` docstring +* :ghpull:`24440`: DOC: Fix colorbar what's new entry so it isn't cut off. +* :ghpull:`23787`: Use pybind11 for C/C++ extensions +* :ghpull:`24247`: Split toolkit tests into their toolkits +* :ghpull:`24432`: DOC: Fix What's New entry for bar_label() formatting. +* :ghpull:`23101`: Move show() to somewhere naturally inheritable / document what pyplot expects from a backend. +* :ghpull:`24215`: Add :shows-source-link: option to Sphinx plot directive +* :ghpull:`24423`: Tighten the Qt binding selection docs. +* :ghpull:`24403`: Use ``repr`` in error message Addresses #21959 +* :ghpull:`24415`: made f2tfont error message explicit that it needs path to file +* :ghpull:`24329`: Kill FontconfigPatternParser. +* :ghpull:`23267`: Add location keyword argument to Colorbar +* :ghpull:`24375`: DOC: Group pyplot plotting commands +* :ghpull:`24307`: DOC: Organize Axes3D methods into sections +* :ghpull:`22230`: FIX: add support for imshow extent to have units +* :ghpull:`24252`: Change default rotation mode for 3D labels to 'anchor' +* :ghpull:`24356`: Expire QuadMesh old signature deprecation +* :ghpull:`24355`: Expire unused positional parameters in canvas subclasses +* :ghpull:`24257`: Load style files from third-party packages. +* :ghpull:`24279`: Cleanup BboxImage example. +* :ghpull:`24342`: Use HTML5 for webagg files +* :ghpull:`24339`: DOC: Minor cleanup in "Writing documentation" +* :ghpull:`24338`: DOC: Group pyplot commands by category +* :ghpull:`24314`: Minor improvements to Annotations Tutorial +* :ghpull:`23914`: Add shading of Poly3DCollection +* :ghpull:`24322`: GOV: change security reporting to use tidelift +* :ghpull:`24305`: Unify logic of ConnectionStyle._Base.{_clip,_shrink}. +* :ghpull:`24303`: Simplify generate_fontconfig_pattern. +* :ghpull:`24319`: Bump mamba-org/provision-with-micromamba from 13 to 14 +* :ghpull:`24239`: Fix mathtext rendering of ``\|`` and sizing of ``|`` and ``\|`` +* :ghpull:`23606`: added offset section & restructured annotations tutorial +* :ghpull:`24125`: Expire miscellaneous deprecations from 3.5 +* :ghpull:`24306`: Remove unnecessary/replaceable explicit str calls. +* :ghpull:`24295`: Remove unnecessary np.{,as}array / astype calls. +* :ghpull:`24302`: MNT: Remove redundant int after round +* :ghpull:`24290`: Cleanup Barbs._find_tails. +* :ghpull:`24298`: List all the places to update when adding a dependency. +* :ghpull:`24289`: Cleanup image_zcoord example. +* :ghpull:`23865`: Add test and example for VBoxDivider +* :ghpull:`24287`: Simplifying glyph stream logic in ps backend +* :ghpull:`24291`: Rely on builtin round() instead of manual rounding. +* :ghpull:`24062`: Replaced std::random_shuffle with std::shuffle in tri +* :ghpull:`24278`: Use oldest-supported-numpy for build +* :ghpull:`24161`: Versioning directives policy +* :ghpull:`24013`: Deprecate matplotlib.tri.* submodules +* :ghpull:`24031`: Add rcParams for 3D pane color +* :ghpull:`24220`: Simplify and tighten parse_fontconfig_pattern. +* :ghpull:`24251`: Expire deprecation for ``auto_add_to_figure=True`` in ``Axes3D`` +* :ghpull:`24160`: sample versioning directives, empty + description +* :ghpull:`24253`: Expire deprecation of grid argument name +* :ghpull:`14471`: FIX: don't close figures if switch_backend is a no-op +* :ghpull:`24240`: Deprecate unit_cube-related methods in Axes3D +* :ghpull:`24244`: Clarify that z must be finite for tricountour(f) +* :ghpull:`23536`: Improve mpl_toolkit documentation +* :ghpull:`24243`: Improve documentation for ticker +* :ghpull:`24189`: Do not pass gridspec_kw to inner layouts in subplot_mosaic +* :ghpull:`24242`: Add information about environment variables in matplotlib.__doc__ +* :ghpull:`24241`: Small animation docs/style fixes. +* :ghpull:`24236`: DOC: Mark SubplotBase removals in code style +* :ghpull:`24141`: Set figure options dynamically +* :ghpull:`23796`: Remove useless semicolons in "Introductory / Basic Usage" tutorial +* :ghpull:`23573`: Merge SubplotBase into AxesBase. +* :ghpull:`23931`: Raise ValueError on negative number inputs for set_aspect +* :ghpull:`24065`: Fixed the positioning of cursor in Textbox: no approximation +* :ghpull:`24122`: Add textcolor to legend based on labelcolor string +* :ghpull:`24182`: MNT: Remove redundant method, fix signature and add doc-string to ``draw_tex`` +* :ghpull:`24224`: Deprecate Julian date-related functions and constant +* :ghpull:`24196`: MNT: Update pre-commit hooks +* :ghpull:`24221`: Deprecate BufferRegion.to_string{,_argb}. +* :ghpull:`23683`: Simplify/add pyparsing error messages on mathtext/fontconfig errors. +* :ghpull:`24210`: Small cleanups to axislines docs. +* :ghpull:`24213`: Cleanup make_compound_path_from_poly doc, example. +* :ghpull:`24208`: Deprecate backend_webagg.ServerThread. +* :ghpull:`24207`: Recommend multiple_yaxis_with_spines over parasite axes. +* :ghpull:`24156`: Automatically update rebase label +* :ghpull:`24198`: Deprecate unused backend_ps.{PsBackendHelper,ps_backend_helper}. +* :ghpull:`24129`: Expire cursor-related deprecations +* :ghpull:`24179`: MNT: Refactor ``Renderer.get_text_width_height_descent`` +* :ghpull:`24191`: BLD: be more cautious about checking editable mode +* :ghpull:`24000`: Generalize validation that pyplot commands are documented +* :ghpull:`24144`: Deprecate some label-related attributes on ContourLabeler. +* :ghpull:`24162`: windows doc build parity +* :ghpull:`24102`: Simplest pyproject.toml containing build-system only +* :ghpull:`24091`: MNT: Clean up code in SecondaryAxis +* :ghpull:`24140`: Replace ClabelText by set_transform_rotates_text. +* :ghpull:`24143`: Add QuadContourSet.remove. +* :ghpull:`24138`: [DOC] Fix some documentation typos +* :ghpull:`24128`: Expire deprecations in dates and ticker +* :ghpull:`23907`: Inherit OffsetBox.get_window_extent. +* :ghpull:`23449`: Add pan and zoom toolbar handling to 3D Axes (Replaces PR#22614) +* :ghpull:`24126`: Bump version when invalid hatches error +* :ghpull:`23874`: Expire parameter renaming and deletion and attribute privatization from 3.5 +* :ghpull:`23592`: Polar errcaps +* :ghpull:`24083`: Enable interactive figure resizing for webagg and nbagg backends +* :ghpull:`24110`: test readme rendering +* :ghpull:`24067`: README.rst to README.md +* :ghpull:`23702`: Get Mathtext ``\times`` symbol from ``cmsy10`` when using ``cmr10``. +* :ghpull:`24066`: Simplify svg font expansion logic. +* :ghpull:`23730`: [DOC]: Add grid to style sheets +* :ghpull:`24020`: [DOC]: adding a grid to the style sheet reference. +* :ghpull:`23579`: Remove direct manipulation of HostAxes.parasites by end users. +* :ghpull:`23553`: Add tests for ImageGrid +* :ghpull:`23918`: Merge v3.6.x branch to main +* :ghpull:`23902`: Add test and improve examples for mpl_toolkits +* :ghpull:`23950`: DOC: Don't import doctest because we're not using it +* :ghpull:`21006`: Rotate errorbar caps in polar plots +* :ghpull:`23870`: Implement Sphinx-Gallery's ``make html-noplot`` +* :ghpull:`23905`: made explicit that install link is install docs in readme +* :ghpull:`23824`: Deprecate draw_gouraud_triangle +* :ghpull:`23913`: Add draggable as param to Legend init +* :ghpull:`23896`: Inline AnchoredOffsetBox._update_offset_func. +* :ghpull:`23889`: Update image tutorial. +* :ghpull:`23861`: Move axes_grid tests to axes_grid1 +* :ghpull:`23254`: Add PathCollection test for ps backend +* :ghpull:`23542`: Add even more mplot3d tests +* :ghpull:`23698`: Fix bug in ``Axes.bar_label(label_type='center')`` for non-linear scales. +* :ghpull:`23767`: DEV: add flake8-force plugin +* :ghpull:`23835`: Fix version switcher links +* :ghpull:`23832`: Improve skip message for image comparison tests +* :ghpull:`23690`: Add new-style string formatting option and callable option to ``fmt`` in ``Axes.bar_label()``. +* :ghpull:`23804`: Fix TexManager's support for ``openin_any = p`` +* :ghpull:`23737`: Update grammar +* :ghpull:`23552`: Provide ``adjustable='box'`` to 3D axes aspect ratio setting +* :ghpull:`23769`: Bump mamba-org/provision-with-micromamba from 12 to 13 +* :ghpull:`23590`: Changing bar examples to tea and coffee +* :ghpull:`21253`: Fix: axis, ticks are set to defaults fontsize after ax.clear() +* :ghpull:`21968`: Changed fstring to make error clearer +* :ghpull:`22614`: ENH: Add pan and zoom toolbar handling to 3D Axes +* :ghpull:`21562`: Add a test for Hexbin Linear + +Issues (120): + +* :ghissue:`25176`: [Bug]: Colorbar is displaced when saving as .eps with bbox_inches='tight' +* :ghissue:`25075`: [Bug]: Widget blitting broken when saving as PDF +* :ghissue:`25181`: unavoidable warnings in nbagg on ``plt.close`` +* :ghissue:`25134`: [Doc]: pyplot.boxplot whisker length wrong docs +* :ghissue:`24395`: Any resizing of the plot after plt.show results in an error when closing the window +* :ghissue:`25107`: [Doc]: annotated_cursor example seems broken +* :ghissue:`25124`: [Bug]: ax.plot(x,y) disappears after changing y_scale +* :ghissue:`8278`: FuncAnimation with generator defaults to arbitrary save_count=100 +* :ghissue:`22765`: Document distutil vs setuptools issues or fix usage +* :ghissue:`25077`: [Bug]: Setting norm with existing colorbar fails with 3.6.3 +* :ghissue:`23999`: [Bug]: Annotation misplaced when rasterizing and saving as PDF +* :ghissue:`25040`: [Bug]: Request to insecure websocket endpoint is blocked by browser +* :ghissue:`24678`: [Bug]: pcolormesh doesn't allow shading = 'flat' in the option +* :ghissue:`15388`: matplotlib.collections.QuadMesh.set_array() input arg format is weird and undocumented +* :ghissue:`23779`: [ENH]: control the size of the tex cache +* :ghissue:`24583`: [ENH]: provide public API for styling radio buttons +* :ghissue:`21895`: [Bug]: slow rendering of multiple axes (time scales as 2nd power of label count) +* :ghissue:`4781`: Add API to register styles +* :ghissue:`24972`: [MNT]: UserWarning from pyparsing upon immediate import +* :ghissue:`24865`: [Bug]: NumPy 1.24 deprecation warnings +* :ghissue:`24954`: [Bug]: compressed layout setting can be forgotten on second save +* :ghissue:`23778`: [ENH]: Allow override of contour level autoscaling +* :ghissue:`20203`: contour edge case with all data below levels and a surrounding field of zeros +* :ghissue:`12803`: pcolormesh in log polar coordinates +* :ghissue:`24383`: log scale and polar broken +* :ghissue:`22847`: [Bug]: Cannot toggle set_tight_layout +* :ghissue:`23646`: [Bug]: matplotlib.set_loglevel() adds a console handler +* :ghissue:`24673`: [Doc]: animation examples show controls; source does not reproduce them +* :ghissue:`7617`: set_ylabel does not work as expected with SubplotZero +* :ghissue:`13023`: constrained_layout support for figure.legend +* :ghissue:`15973`: span_where fails with timeseries on the x-axis +* :ghissue:`24867`: [Bug]: controlling text on toolbar in wx +* :ghissue:`24421`: [Doc]: change to install from conda forge +* :ghissue:`24890`: [Bug]: Clipping mask can shift in PDF and SVG file outputs when Bbox is adjusted +* :ghissue:`23849`: [Bug]: The point marker is not actually unfilled +* :ghissue:`24321`: [ENH]: Auto-detect bool arrays passed to contour()? +* :ghissue:`24842`: axes3d.quiver() fails when providing args to Line3DCollection +* :ghissue:`24093`: [Bug]: CenteredNorm gets stuck in infinite recursion when given all zeros +* :ghissue:`24571`: [ENH]: gridspec_mosaic +* :ghissue:`24815`: [TST] Upcoming dependency test failures +* :ghissue:`24712`: [ENH]: Reverse legend +* :ghissue:`22308`: [Bug] set_3d_properties type error in Matplotlib 3.5.1 +* :ghissue:`24741`: [Doc]: tables in "notes" cut off content +* :ghissue:`20044`: AnnotationBbox gid not passed to renderer +* :ghissue:`24762`: [Doc]: Development workflow doc has lots of typos and clunky sentences +* :ghissue:`24235`: [Bug]: pcolormesh(rasterized=True) conflicts with set_rasterization_zorder() +* :ghissue:`24471`: [Bug]: CheckBoxes should be square, not rectangular +* :ghissue:`18804`: bugged pads on savefig +* :ghissue:`20656`: Sphinx extension plot_directive not able to detect function +* :ghissue:`24704`: [Bug]: ImportError: DLL load failed on Windows +* :ghissue:`20639`: document Legend.legendHandles +* :ghissue:`19633`: Multicursor disappears when not moving on nbagg with useblit=False + burns CPU +* :ghissue:`24717`: Update Research Notice on README.md +* :ghissue:`22754`: [Bug]: It is recommended for you to run autogen before configuring freetype +* :ghissue:`24349`: [Bug]: sharex and sharey don't accept 0 and 1 as bool values +* :ghissue:`20577`: Using ``legend(labelcolor="markerfacecolor")`` with a scatter plot throws an error +* :ghissue:`24424`: [Doc]: Inheritance diagrams +* :ghissue:`9580`: Broken legend auto-position with step*-type histograms +* :ghissue:`22176`: [MNT]: Write a bot to post doc build issues +* :ghissue:`24623`: [Bug]: ``offsetbox`` classes have optional arguments that are really not optional +* :ghissue:`24693`: [MNT]: Update minver policy re: GUI toolkits +* :ghissue:`23566`: [ENH]: Z-axis/3D support for Figure options +* :ghissue:`23777`: [ENH] Interactive Zoom Rectangle Color Review for MACOSX backend +* :ghissue:`24676`: [Doc]: quiver_doc etc leads to documentation of the documentation string +* :ghissue:`24568`: [ENH]: Ellipse annotation +* :ghissue:`6982`: cla(), clf() should unset the ``.axes`` and ``.figure`` attributes of deparented artists +* :ghissue:`11227`: fig.set_dpi() does not set the dpi correctly +* :ghissue:`24418`: [ENH]: rgp or rgba option for pyplot pcolormesh and/or pcolor +* :ghissue:`22236`: [Bug]: integer colours for pcolorfast / quadmesh +* :ghissue:`4277`: RGB not supported in pcolormesh +* :ghissue:`23155`: [ENH]: do_3d_projection could restore original verts order after draw() finishes +* :ghissue:`24386`: [Bug]: ``align`` in ``HPacker`` is reversed +* :ghissue:`23803`: Static code analysis +* :ghissue:`8990`: Surprising behaviour of mutating input arrays to Axes.plot vs Axes3D.plot +* :ghissue:`24550`: [ENH]: Warn when a SymLogScale receives values that are all in the linear regime +* :ghissue:`23416`: [Bug]: Inconsistent y-axis unit label with plot/scatter +* :ghissue:`23603`: [MNT]: Only a subset of attributes set via ``Axes.tick_params()`` are accessible via public methods and attributes +* :ghissue:`13858`: matplotlib.sphinxext.plot_directive generates incorrect links when using dirhtml builder +* :ghissue:`19376`: eventplot: allow a list of alpha channels as in the case with colors +* :ghissue:`24508`: [Bug]: Re-organization of mpl_toolkits tests broke tools/triage_tests.py +* :ghissue:`19040`: v3.3.0 Regression, Animation draws artists multiple times. +* :ghissue:`12324`: DOC: Write a unified backend doc +* :ghissue:`24464`: Issue with legend labelcolor='linecolor' for errorbar plots +* :ghissue:`24273`: [ENH]: Axes.set_xticks/Axis.set_ticks only validates kwargs if ticklabels are set, but they should +* :ghissue:`24454`: [Bug]: "import matplotlib.pyplot" gives ModuleNotFoundError +* :ghissue:`24394`: [TST]: Appveyor Qt tests failing +* :ghissue:`21959`: [ENH]: Use ``repr`` instead of ``str`` in the error message +* :ghissue:`22676`: [ENH]: Colorbar should support location kwarg that sets both orientation and ticklocation +* :ghissue:`23901`: [Doc]: add summary table to Axes3D similar to Axes +* :ghissue:`22105`: [Bug]: imshow extents can't have units? +* :ghissue:`21878`: [MNT]: make axis labels of 3d axis anchor-rotate +* :ghissue:`17978`: Document how to distribute style files in python packages +* :ghissue:`23965`: Simplify glyph stream logic in ps backend +* :ghissue:`19509`: Adding lightsource when plotting Poly3DCollection +* :ghissue:`17523`: Unclear if no gallery argument for doc builds works +* :ghissue:`23250`: [Bug]: Incorrect mathtext rendering of ``r"$\|$"`` with default (dejavu) math fontfamily +* :ghissue:`24010`: c++17 removed random_shuffle +* :ghissue:`20424`: function shadowing their own definition modules +* :ghissue:`20781`: Make the pane color in 3d plots configurable +* :ghissue:`14426`: Existing FigureCanvasQT objects destroyed by call to plt.figure +* :ghissue:`5908`: Unclear Documentation ticker class +* :ghissue:`24099`: [Bug]: Error using width_ratios with nested mosaic in subplot_mosaic() +* :ghissue:`6893`: List environment variables in matplotlib.__doc__ +* :ghissue:`11445`: The axes module structure +* :ghissue:`23847`: [Bug]: set_aspect with negative argument leads to infinite loop +* :ghissue:`24136`: [Doc]: document ``julian2num`` and ``num2julian``? +* :ghissue:`5332`: QuadContourSet lacks remove method +* :ghissue:`110`: pan and zoom are broken for mplot3d +* :ghissue:`441`: Polar plot error bars don't rotate with angle +* :ghissue:`24064`: Convert readme.rst to readme.md +* :ghissue:`10029`: \times in minor ticklabels not recognized due to \mathdefault +* :ghissue:`24080`: verify quoting method in svg backend for font names +* :ghissue:`23601`: [Doc]: add gridlines to style sheet reference +* :ghissue:`24075`: [ENH]: Resizing the figure with webagg backend by dragging the corner +* :ghissue:`23352`: [Doc]: bar examples should probably not have "score by ... gender" in them... +* :ghissue:`23819`: [MNT]: Make draw_gouraud_triangle optional +* :ghissue:`9181`: legend draggable as keyword +* :ghissue:`23688`: [Bug]: ``Axes.bar_label()`` on log scale does not center the label. +* :ghissue:`23689`: [ENH]: Add f-string formatting to labels in ``Axes.bar_label()`` +* :ghissue:`23718`: [Bug]: Installing from source fails during Freetype compilation with spaces in working directory filepath diff --git a/doc/users/prev_whats_new/whats_new_0.98.4.rst b/doc/users/prev_whats_new/whats_new_0.98.4.rst index 96f8768a99d8..88d376cf79bf 100644 --- a/doc/users/prev_whats_new/whats_new_0.98.4.rst +++ b/doc/users/prev_whats_new/whats_new_0.98.4.rst @@ -1,7 +1,7 @@ .. _whats-new-0-98-4: -New in matplotlib 0.98.4 -======================== +What's new in Matplotlib 0.98.4 +=============================== .. contents:: Table of Contents :depth: 2 @@ -30,12 +30,18 @@ multiple columns and rows, as well as fancy box drawing. See :func:`~matplotlib.pyplot.legend` and :class:`matplotlib.legend.Legend`. -.. figure:: ../../gallery/pyplots/images/sphx_glr_whats_new_98_4_legend_001.png - :target: ../../gallery/pyplots/whats_new_98_4_legend.html - :align: center - :scale: 50 +.. plot:: + + ax = plt.subplot() + t1 = np.arange(0.0, 1.0, 0.01) + for n in [1, 2, 3, 4]: + plt.plot(t1, t1**n, label=f"n={n}") + + leg = plt.legend(loc='best', ncol=2, mode="expand", shadow=True, fancybox=True) + leg.get_frame().set_alpha(0.5) + + plt.show() - What's New 98 4 Legend .. _fancy-annotations: @@ -145,12 +151,20 @@ can pass an *x* array and a *ylower* and *yupper* array to fill between, and an optional *where* argument which is a logical mask where you want to do the filling. -.. figure:: ../../gallery/pyplots/images/sphx_glr_whats_new_98_4_fill_between_001.png - :target: ../../gallery/pyplots/whats_new_98_4_fill_between.html - :align: center - :scale: 50 +.. plot:: + + x = np.arange(-5, 5, 0.01) + y1 = -5*x*x + x + 10 + y2 = 5*x*x + x + + fig, ax = plt.subplots() + ax.plot(x, y1, x, y2, color='black') + ax.fill_between(x, y1, y2, where=(y2 > y1), facecolor='yellow', alpha=0.5) + ax.fill_between(x, y1, y2, where=(y2 <= y1), facecolor='red', alpha=0.5) + ax.set_title('Fill Between') + + plt.show() - What's New 98 4 Fill Between Lots more --------- diff --git a/doc/users/prev_whats_new/whats_new_0.99.rst b/doc/users/prev_whats_new/whats_new_0.99.rst index 2a74e39e6b55..c2d761a25031 100644 --- a/doc/users/prev_whats_new/whats_new_0.99.rst +++ b/doc/users/prev_whats_new/whats_new_0.99.rst @@ -1,7 +1,7 @@ .. _whats-new-0-99: -New in matplotlib 0.99 -====================== +What's new in Matplotlib 0.99 (Aug 29, 2009) +============================================ .. contents:: Table of Contents :depth: 2 @@ -22,19 +22,29 @@ working with paths and transformations: :doc:`/tutorials/advanced/path_tutorial` mplot3d -------- - Reinier Heeres has ported John Porter's mplot3d over to the new matplotlib transformations framework, and it is now available as a toolkit mpl_toolkits.mplot3d (which now comes standard with all mpl installs). See :ref:`mplot3d-examples-index` and -:ref:`toolkit_mplot3d-tutorial` +:doc:`/tutorials/toolkits/mplot3d`. + +.. plot:: + + from matplotlib import cm + from mpl_toolkits.mplot3d import Axes3D + + X = np.arange(-5, 5, 0.25) + Y = np.arange(-5, 5, 0.25) + X, Y = np.meshgrid(X, Y) + R = np.sqrt(X**2 + Y**2) + Z = np.sin(R) -.. figure:: ../../gallery/pyplots/images/sphx_glr_whats_new_99_mplot3d_001.png - :target: ../../gallery/pyplots/whats_new_99_mplot3d.html - :align: center - :scale: 50 + fig = plt.figure() + ax = Axes3D(fig, auto_add_to_figure=False) + fig.add_axes(ax) + ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.viridis) - What's New 99 Mplot3d + plt.show() .. _whats-new-axes-grid: @@ -48,12 +58,49 @@ new mpl installs. See :ref:`axes_grid1-examples-index`, :ref:`axisartist-examples-index`, :ref:`axes_grid1_users-guide-index` and :ref:`axisartist_users-guide-index` -.. figure:: ../../gallery/pyplots/images/sphx_glr_whats_new_99_axes_grid_001.png - :target: ../../gallery/pyplots/whats_new_99_axes_grid.html - :align: center - :scale: 50 +.. plot:: + + from mpl_toolkits.axes_grid1.axes_rgb import RGBAxes + + + def get_demo_image(): + # prepare image + delta = 0.5 + + extent = (-3, 4, -4, 3) + x = np.arange(-3.0, 4.001, delta) + y = np.arange(-4.0, 3.001, delta) + X, Y = np.meshgrid(x, y) + Z1 = np.exp(-X**2 - Y**2) + Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) + Z = (Z1 - Z2) * 2 + + return Z, extent + + + def get_rgb(): + Z, extent = get_demo_image() + + Z[Z < 0] = 0. + Z = Z / Z.max() + + R = Z[:13, :13] + G = Z[2:, 2:] + B = Z[:13, 2:] + + return R, G, B - What's New 99 Axes Grid + + fig = plt.figure() + ax = RGBAxes(fig, [0.1, 0.1, 0.8, 0.8]) + + r, g, b = get_rgb() + ax.imshow_rgb(r, g, b, origin="lower") + + ax.RGB.set_xlim(0., 9.5) + ax.RGB.set_ylim(0.9, 10.6) + + plt.show() .. _whats-new-spine: @@ -65,12 +112,50 @@ that denote the data limits -- in various arbitrary locations. No longer are your axis lines constrained to be a simple rectangle around the figure -- you can turn on or off left, bottom, right and top, as well as "detach" the spine to offset it away from the data. See -:doc:`/gallery/ticks_and_spines/spine_placement_demo` and +:doc:`/gallery/spines/spine_placement_demo` and :class:`matplotlib.spines.Spine`. -.. figure:: ../../gallery/pyplots/images/sphx_glr_whats_new_99_spines_001.png - :target: ../../gallery/pyplots/whats_new_99_spines.html - :align: center - :scale: 50 +.. plot:: + + def adjust_spines(ax, spines): + for loc, spine in ax.spines.items(): + if loc in spines: + spine.set_position(('outward', 10)) # outward by 10 points + else: + spine.set_color('none') # don't draw spine + + # turn off ticks where there is no spine + if 'left' in spines: + ax.yaxis.set_ticks_position('left') + else: + # no yaxis ticks + ax.yaxis.set_ticks([]) + + if 'bottom' in spines: + ax.xaxis.set_ticks_position('bottom') + else: + # no xaxis ticks + ax.xaxis.set_ticks([]) + + fig = plt.figure() + + x = np.linspace(0, 2*np.pi, 100) + y = 2*np.sin(x) + + ax = fig.add_subplot(2, 2, 1) + ax.plot(x, y) + adjust_spines(ax, ['left']) + + ax = fig.add_subplot(2, 2, 2) + ax.plot(x, y) + adjust_spines(ax, []) + + ax = fig.add_subplot(2, 2, 3) + ax.plot(x, y) + adjust_spines(ax, ['left', 'bottom']) + + ax = fig.add_subplot(2, 2, 4) + ax.plot(x, y) + adjust_spines(ax, ['bottom']) - What's New 99 Spines + plt.show() 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 cafa8917d518..ab902977cb1e 100644 --- a/doc/users/prev_whats_new/whats_new_1.0.rst +++ b/doc/users/prev_whats_new/whats_new_1.0.rst @@ -1,7 +1,7 @@ .. _whats-new-1-0: -New in matplotlib 1.0 -===================== +What's new in Matplotlib 1.0 (Jul 06, 2010) +=========================================== .. contents:: Table of Contents :depth: 2 @@ -12,7 +12,7 @@ HTML5/Canvas backend -------------------- Simon Ratcliffe and Ludwig Schwardt have released an `HTML5/Canvas -`__ backend for matplotlib. The +`__ backend for matplotlib. The backend is almost feature complete, and they have done a lot of work comparing their html5 rendered images with our core renderer Agg. The backend features client/server interactive navigation of matplotlib @@ -23,15 +23,14 @@ Sophisticated subplot grid layout Jae-Joon Lee has written :mod:`~matplotlib.gridspec`, a new module for doing complex subplot layouts, featuring row and column spans and -more. See :doc:`/tutorials/intermediate/gridspec` for a tutorial overview. +more. See :doc:`/tutorials/intermediate/arranging_axes` for a tutorial +overview. .. figure:: ../../gallery/userdemo/images/sphx_glr_demo_gridspec01_001.png :target: ../../gallery/userdemo/demo_gridspec01.html :align: center :scale: 50 - Demo Gridspec01 - Easy pythonic subplots ----------------------- @@ -44,9 +43,9 @@ indexing (starts with 0). e.g.:: fig, axarr = plt.subplots(2, 2) axarr[0,0].plot([1,2,3]) # upper, left -See :doc:`/gallery/subplots_axes_and_figures/subplot_demo` for several code examples. +See :doc:`/gallery/subplots_axes_and_figures/subplot` for several code examples. -Contour fixes and and triplot +Contour fixes and triplot ----------------------------- Ian Thomas has fixed a long-standing bug that has vexed our most @@ -63,8 +62,6 @@ plotting unstructured triangular grids. :align: center :scale: 50 - Triplot Demo - multiple calls to show supported -------------------------------- @@ -92,12 +89,29 @@ supporting mixing of 2D and 3D graphs in the same figure, and/or multiple 3D graphs in a single figure, using the "projection" keyword argument to add_axes or add_subplot. Thanks Ben Root. -.. figure:: ../../gallery/pyplots/images/sphx_glr_whats_new_1_subplot3d_001.png - :target: ../../gallery/pyplots/whats_new_1_subplot3d.html - :align: center - :scale: 50 +.. plot:: + + from mpl_toolkits.mplot3d.axes3d import get_test_data + + fig = plt.figure() + + X = np.arange(-5, 5, 0.25) + Y = np.arange(-5, 5, 0.25) + X, Y = np.meshgrid(X, Y) + R = np.sqrt(X**2 + Y**2) + Z = np.sin(R) + ax = fig.add_subplot(1, 2, 1, projection='3d') + surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='viridis', + linewidth=0, antialiased=False) + ax.set_zlim3d(-1.01, 1.01) + + fig.colorbar(surf, shrink=0.5, aspect=5) + + X, Y, Z = get_test_data(0.05) + ax = fig.add_subplot(1, 2, 2, projection='3d') + ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) - What's New 1 Subplot3d + plt.show() tick_params ----------- diff --git a/doc/users/prev_whats_new/whats_new_1.1.rst b/doc/users/prev_whats_new/whats_new_1.1.rst index f4c9cd42d31d..901351466fac 100644 --- a/doc/users/prev_whats_new/whats_new_1.1.rst +++ b/doc/users/prev_whats_new/whats_new_1.1.rst @@ -1,7 +1,7 @@ .. _whats-new-1-1: -New in matplotlib 1.1 -===================== +What's new in Matplotlib 1.1 (Nov 02, 2011) +=========================================== .. contents:: Table of Contents :depth: 2 @@ -26,9 +26,6 @@ Kevin Davies has extended Yannick Copin's original Sankey example into a module :align: center :scale: 50 - Sankey Rankine - - Animation --------- @@ -42,7 +39,7 @@ pendulum ` which uses .. raw:: html - + This should be considered as a beta release of the framework; please try it and provide feedback. @@ -54,7 +51,7 @@ Tight Layout A frequent issue raised by users of matplotlib is the lack of a layout engine to nicely space out elements of the plots. While matplotlib still adheres to the philosophy of giving users complete control over the placement -of plot elements, Jae-Joon Lee created the :mod:`~matplotlib.tight_layout` +of plot elements, Jae-Joon Lee created the ``matplotlib.tight_layout`` module and introduced a new command :func:`~matplotlib.pyplot.tight_layout` to address the most common layout issues. @@ -125,8 +122,6 @@ examples. :align: center :scale: 50 - Legend Demo4 - mplot3d ------- @@ -138,7 +133,7 @@ as 2D plotting, Ben Root has made several improvements to the improved to bring the class towards feature-parity with regular Axes objects -* Documentation for :ref:`toolkit_mplot3d-tutorial` was significantly expanded +* Documentation for :doc:`/tutorials/toolkits/mplot3d` was significantly expanded * Axis labels and orientation improved @@ -151,8 +146,6 @@ as 2D plotting, Ben Root has made several improvements to the :align: center :scale: 50 - Offset - * :meth:`~mpl_toolkits.mplot3d.axes3d.Axes3D.contourf` gains *zdir* and *offset* kwargs. You can now do this: @@ -161,8 +154,6 @@ as 2D plotting, Ben Root has made several improvements to the :align: center :scale: 50 - Contourf3d 2 - Numerix support removed ----------------------- diff --git a/doc/users/prev_whats_new/whats_new_1.2.2.rst b/doc/users/prev_whats_new/whats_new_1.2.2.rst index 51c43403d22c..ab81018925cd 100644 --- a/doc/users/prev_whats_new/whats_new_1.2.2.rst +++ b/doc/users/prev_whats_new/whats_new_1.2.2.rst @@ -1,7 +1,7 @@ .. _whats-new-1-2-2: -New in matplotlib 1.2.2 -======================= +What's new in Matplotlib 1.2.2 +============================== .. contents:: Table of Contents :depth: 2 diff --git a/doc/users/prev_whats_new/whats_new_1.2.rst b/doc/users/prev_whats_new/whats_new_1.2.rst index fc6039314b9f..3bfa20e671be 100644 --- a/doc/users/prev_whats_new/whats_new_1.2.rst +++ b/doc/users/prev_whats_new/whats_new_1.2.rst @@ -1,8 +1,8 @@ .. _whats-new-1-2: -New in matplotlib 1.2 -===================== +What's new in Matplotlib 1.2 (Nov 9, 2012) +========================================== .. contents:: Table of Contents :depth: 2 @@ -67,8 +67,6 @@ Damon McDougall added a new plotting method for the :align: center :scale: 50 - Trisurf3d - Control the lengths of colorbar extensions ------------------------------------------ @@ -133,9 +131,6 @@ median and confidence interval. :align: center :scale: 50 - Boxplot Demo3 - - New RC parameter functionality ------------------------------ @@ -168,9 +163,6 @@ local intensity of the vector field. :align: center :scale: 50 - Plot Streamplot - - New hist functionality ---------------------- @@ -187,7 +179,7 @@ Updated shipped dependencies The following dependencies that ship with matplotlib and are optionally installed alongside it have been updated: -- `pytz `_ 2012d +- `pytz `_ 2012d - `dateutil `_ 1.5 on Python 2.x, and 2.1 on Python 3.x @@ -204,8 +196,6 @@ a triangulation. :align: center :scale: 50 - Tripcolor Demo - Hatching patterns in filled contour plots, with legends ------------------------------------------------------- @@ -218,8 +208,6 @@ to use a legend to identify contoured ranges. :align: center :scale: 50 - Contourf Hatching - Known issues in the matplotlib 1.2 release ------------------------------------------ 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 ddc35661dc74..855235069917 100644 --- a/doc/users/prev_whats_new/whats_new_1.3.rst +++ b/doc/users/prev_whats_new/whats_new_1.3.rst @@ -1,7 +1,7 @@ .. _whats-new-1-3: -New in matplotlib 1.3 -===================== +What's new in Matplotlib 1.3 (Aug 01, 2013) +=========================================== .. contents:: Table of Contents :depth: 2 @@ -96,22 +96,18 @@ to modify each artist's sketch parameters individually with :align: center :scale: 50 - xkcd - Updated Axes3D.contour methods ------------------------------ Damon McDougall updated the :meth:`~mpl_toolkits.mplot3d.axes3d.Axes3D.tricontour` and :meth:`~mpl_toolkits.mplot3d.axes3d.Axes3D.tricontourf` methods to allow 3D -contour plots on abitrary unstructured user-specified triangulations. +contour plots on arbitrary unstructured user-specified triangulations. .. figure:: ../../gallery/mplot3d/images/sphx_glr_tricontour3d_001.png :target: ../../gallery/mplot3d/tricontour3d.html :align: center :scale: 50 - Tricontour3d - New eventplot plot type ``````````````````````` Todd Jennings added a :func:`~matplotlib.pyplot.eventplot` function to @@ -122,8 +118,6 @@ create multiple rows or columns of identical line segments :align: center :scale: 50 - Eventplot Demo - As part of this feature, there is a new :class:`~matplotlib.collections.EventCollection` class that allows for plotting and manipulating rows or columns of identical line segments. @@ -146,8 +140,6 @@ added (:class:`~matplotlib.tri.TriAnalyzer`). :align: center :scale: 50 - Tricontour Smooth User - Baselines for stackplot ``````````````````````` Till Stensitzki added non-zero baselines to @@ -159,8 +151,6 @@ weighted. :align: center :scale: 50 - Stackplot Demo2 - Rectangular colorbar extensions ``````````````````````````````` Andrew Dawson added a new keyword argument *extendrect* to @@ -204,8 +194,6 @@ Thanks to Jae-Joon Lee, path effects now also work on plot lines. :align: center :scale: 50 - Patheffect Demo - Easier creation of colormap and normalizer for levels with colors ````````````````````````````````````````````````````````````````` Phil Elson added the :func:`matplotlib.colors.from_levels_and_colors` @@ -299,7 +287,7 @@ hard-coded to default to 0, default value of both rcParam values is 0. Changes to font rcParams ```````````````````````` -The `font.*` rcParams now affect only text objects created after the +The ``font.*`` rcParams now affect only text objects created after the rcParam has been set, and will not retroactively affect already existing text objects. This brings their behavior in line with most other rcParams. @@ -389,7 +377,7 @@ information. XDG base directory support `````````````````````````` On Linux, matplotlib now uses the `XDG base directory specification -`_ to +`_ to find the :file:`matplotlibrc` configuration file. :file:`matplotlibrc` should now be kept in :file:`~/.config/matplotlib`, rather than :file:`~/.matplotlib`. If your configuration is found in the old location, it will still be used, but 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 e9df55993cc2..febcb93047b9 100644 --- a/doc/users/prev_whats_new/whats_new_1.4.rst +++ b/doc/users/prev_whats_new/whats_new_1.4.rst @@ -1,8 +1,8 @@ .. _whats-new-1-4: -New in matplotlib 1.4 -===================== +What's new in Matplotlib 1.4 (Aug 25, 2014) +=========================================== Thomas A. Caswell served as the release manager for the 1.4 release. @@ -20,7 +20,7 @@ New colormap ------------ In heatmaps, a green-to-red spectrum is often used to indicate intensity of activity, but this can be problematic for the red/green colorblind. A new, -colorblind-friendly colormap is now available at :class:`matplotlib.cm.Wistia`. +colorblind-friendly colormap is now available at ``matplotlib.cm.Wistia``. This colormap maintains the red/green symbolism while achieving deuteranopic legibility through brightness variations. See `here `__ @@ -142,7 +142,7 @@ Added the kwarg 'which' to `.Axes.get_xticklabels`, `.Axes.get_yticklabels` and `.Axis.get_ticklabels`. 'which' can be 'major', 'minor', or 'both' select which ticks to return, like -:func:`~matplotlib.Axis.set_ticks_position`. If 'which' is `None` then the old +`~.XAxis.set_ticks_position`. If 'which' is `None` then the old behaviour (controlled by the bool *minor*). Separate horizontal/vertical axes padding support in ImageGrid @@ -165,8 +165,6 @@ specifically the Skew-T used in meteorology. :align: center :scale: 50 - Skewt - Support for specifying properties of wedge and text in pie charts. `````````````````````````````````````````````````````````````````` Added the kwargs 'wedgeprops' and 'textprops' to `~.Axes.pie` @@ -209,9 +207,9 @@ to their liking. This feature was implemented for a software engineering course at the University of Toronto, Scarborough, run in Winter 2014 by Anya Tafliovich. -More `markevery` options to show only a subset of markers +More *markevery* options to show only a subset of markers ````````````````````````````````````````````````````````` -Rohan Walker extended the `markevery` property in +Rohan Walker extended the *markevery* property in :class:`~matplotlib.lines.Line2D`. You can now specify a subset of markers to show with an int, slice object, numpy fancy indexing, or float. Using a float shows markers at approximately equal display-coordinate-distances along the @@ -220,7 +218,7 @@ line. Added size related functions to specialized `.Collection`\s ``````````````````````````````````````````````````````````` -Added the `get_size` and `set_size` functions to control the size of +Added the ``get_size`` and ``set_size`` functions to control the size of elements of specialized collections ( :class:`~matplotlib.collections.AsteriskPolygonCollection` :class:`~matplotlib.collections.BrokenBarHCollection` @@ -234,7 +232,7 @@ elements of specialized collections ( Fixed the mouse coordinates giving the wrong theta value in Polar graph ``````````````````````````````````````````````````````````````````````` Added code to -:func:`~matplotlib.InvertedPolarTransform.transform_non_affine` +`~.polar.InvertedPolarTransform.transform_non_affine` to ensure that the calculated theta value was between the range of 0 and 2 * pi since the problem was that the value can become negative after applying the direction and rotation to the theta calculation. @@ -244,7 +242,7 @@ Simple quiver plot for mplot3d toolkit A team of students in an *Engineering Large Software Systems* course, taught by Prof. Anya Tafliovich at the University of Toronto, implemented a simple version of a quiver plot in 3D space for the mplot3d toolkit as one of their -term project. This feature is documented in :func:`~mpl_toolkits.mplot3d.Axes3D.quiver`. +term project. This feature is documented in `~.Axes3D.quiver`. The team members are: Ryan Steve D'Souza, Victor B, xbtsw, Yang Wang, David, Caradec Bisesar and Vlad Vassilovski. @@ -253,8 +251,6 @@ Caradec Bisesar and Vlad Vassilovski. :align: center :scale: 50 - Quiver3d - polar-plot r-tick locations ``````````````````````````` Added the ability to control the angular position of the r-tick labels 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 97e4f81fd306..b94355b97b93 100644 --- a/doc/users/prev_whats_new/whats_new_1.5.rst +++ b/doc/users/prev_whats_new/whats_new_1.5.rst @@ -1,7 +1,7 @@ .. _whats-new-1-5: -New in matplotlib 1.5 -===================== +What's new in Matplotlib 1.5 (Oct 29, 2015) +=========================================== .. contents:: Table of Contents :depth: 2 @@ -36,7 +36,7 @@ that the draw command is deferred and only called once. The upshot of this is that for interactive backends (including ``%matplotlib notebook``) in interactive mode (with ``plt.ion()``) -.. code-block :: python +.. code-block:: python import matplotlib.pyplot as plt fig, ax = plt.subplots() @@ -109,9 +109,6 @@ on two or more property cycles. :align: center :scale: 50 - Color Cycle - - New Colormaps ------------- @@ -316,9 +313,6 @@ specified, the default value is taken from rcParams. :align: center :scale: 50 - Contour Corner Mask - - Mostly unified linestyles for `.Line2D`, `.Patch` and `.Collection` ``````````````````````````````````````````````````````````````````` @@ -346,7 +340,7 @@ Added a :mod:`.legend_handler` for :class:`~matplotlib.collections.PolyCollectio Support for alternate pivots in mplot3d quiver plot ``````````````````````````````````````````````````` -Added a :code:`pivot` kwarg to :func:`~mpl_toolkits.mplot3d.Axes3D.quiver` +Added a :code:`pivot` kwarg to `~.Axes3D.quiver` that controls the pivot point around which the quiver line rotates. This also determines the placement of the arrow head along the quiver line. @@ -368,7 +362,7 @@ Add step kwargs to fill_between Added ``step`` kwarg to `.Axes.fill_between` to allow to fill between lines drawn using the 'step' draw style. The values of ``step`` match -those of the ``where`` kwarg of `.Axes.step`. The asymmetry of of the +those of the ``where`` kwarg of `.Axes.step`. The asymmetry of the kwargs names is not ideal, but `.Axes.fill_between` already has a ``where`` kwarg. @@ -379,9 +373,6 @@ This is particularly useful for plotting pre-binned histograms. :align: center :scale: 50 - Filled Step - - Square Plot ``````````` @@ -494,8 +485,7 @@ backends. DateFormatter strftime `````````````````````` -:class:`~matplotlib.dates.DateFormatter`\ 's -:meth:`~matplotlib.dates.DateFormatter.__call__` method will format +:class:`~matplotlib.dates.DateFormatter`\ 's ``__call__`` method will format a :class:`datetime.datetime` object with the format string passed to the formatter's constructor. This method accepts datetimes with years before 1900, unlike :meth:`datetime.datetime.strftime`. @@ -609,12 +599,12 @@ that comes as replacement for `.NavigationToolbar2` with the figures. Before we had the `.NavigationToolbar2` with its own tools like ``zoom/pan/home/save/...`` and also we had the shortcuts like ``yscale/grid/quit/....``. `.ToolManager` relocate all those actions as -`Tools` (located in `~matplotlib.backend_tools`), and defines a way to +Tools (located in `~matplotlib.backend_tools`), and defines a way to access/trigger/reconfigure them. -The `Toolbars` are replaced for `ToolContainers` that are just GUI -interfaces to `trigger` the tools. But don't worry the default -backends include a `ToolContainer` called `toolbar` +The Toolbars are replaced by `.ToolContainerBase`\s that are just GUI +interfaces to trigger the tools. But don't worry the default +backends include a `.ToolContainerBase` called ``toolbar`` .. note:: @@ -726,7 +716,7 @@ default, performing no display. ``html5`` converts the animation to an h264 encoded video, which is embedded directly in the notebook. Users not wishing to use the ``_repr_html_`` display hook can also manually -call the `to_html5_video` method to get the HTML and display using +call the `.to_html5_video` method to get the HTML and display using IPython's ``HTML`` display class:: from IPython.display import HTML diff --git a/doc/users/prev_whats_new/whats_new_2.0.0.rst b/doc/users/prev_whats_new/whats_new_2.0.0.rst index 7a6bac78e97a..0f5edb7c0e3f 100644 --- a/doc/users/prev_whats_new/whats_new_2.0.0.rst +++ b/doc/users/prev_whats_new/whats_new_2.0.0.rst @@ -1,11 +1,11 @@ .. _whats-new-2-0-0: -New in matplotlib 2.0 -===================== +What's new in Matplotlib 2.0 (Jan 17, 2017) +=========================================== .. note:: - matplotlib 2.0 supports Python 2.7, and 3.4+ + Matplotlib 2.0 supports Python 2.7, and 3.4+ @@ -17,7 +17,7 @@ The major changes in v2.0 are related to overhauling the default styles. .. toctree:: :maxdepth: 2 - ../dflt_style_changes + dflt_style_changes Improved color conversion API and RGBA support @@ -117,10 +117,11 @@ choropleths, labeled image features, etc. -Axis offset label now responds to `labelcolor` +Axis offset label now responds to *labelcolor* ---------------------------------------------- -Axis offset labels are now colored the same as axis tick markers when `labelcolor` is altered. +Axis offset labels are now colored the same as axis tick markers when +*labelcolor* is altered. Improved offset text choice --------------------------- diff --git a/doc/users/prev_whats_new/whats_new_2.1.0.rst b/doc/users/prev_whats_new/whats_new_2.1.0.rst index d426be2834cf..a66e2e10f3a2 100644 --- a/doc/users/prev_whats_new/whats_new_2.1.0.rst +++ b/doc/users/prev_whats_new/whats_new_2.1.0.rst @@ -1,7 +1,7 @@ .. _whats-new-2-1-0: -New in Matplotlib 2.1.0 -======================= +What's new in Matplotlib 2.1.0 (Oct 7, 2017) +============================================ Documentation +++++++++++++ @@ -391,11 +391,11 @@ time and can enhance the visibility of the flow pattern in some use cases. -`Axis.set_tick_params` now responds to ``rotation`` +``Axis.set_tick_params`` now responds to *rotation* --------------------------------------------------- Bulk setting of tick label rotation is now possible via -:func:`~matplotlib.axes.Axes.tick_params` using the ``rotation`` +:func:`~matplotlib.axes.Axes.tick_params` using the *rotation* keyword. :: @@ -406,7 +406,7 @@ keyword. Ticklabels are turned off instead of being invisible ---------------------------------------------------- -Internally, the `.Tick`'s :func:`~matplotlib.axis.Tick.label1On` attribute +Internally, the `.Tick`'s ``matplotlib.axis.Tick.label1On`` attribute is now used to hide tick labels instead of setting the visibility on the tick label objects. This improves overall performance and fixes some issues. 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 05affccc7a6f..77b9056048f4 100644 --- a/doc/users/prev_whats_new/whats_new_2.2.rst +++ b/doc/users/prev_whats_new/whats_new_2.2.rst @@ -1,7 +1,7 @@ .. _whats-new-2-2-0: -New in Matplotlib 2.2 -===================== +What's new in Matplotlib 2.2 (Mar 06, 2018) +=========================================== Constrained Layout Manager -------------------------- @@ -60,7 +60,7 @@ New ``figure`` kwarg for ``GridSpec`` In order to facilitate ``constrained_layout``, ``GridSpec`` now accepts a ``figure`` keyword. This is backwards compatible, in that not supplying this will simply cause ``constrained_layout`` to not operate on the subplots -orgainzed by this ``GridSpec`` instance. Routines that use ``GridSpec`` (e.g. +organized by this ``GridSpec`` instance. Routines that use ``GridSpec`` (e.g. ``fig.subplots``) have been modified to pass the figure to ``GridSpec``. diff --git a/doc/users/prev_whats_new/whats_new_3.0.rst b/doc/users/prev_whats_new/whats_new_3.0.rst index 38247aee606b..683a7b5c702d 100644 --- a/doc/users/prev_whats_new/whats_new_3.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.0.rst @@ -1,7 +1,7 @@ .. _whats-new-3-0-0: -New in Matplotlib 3.0 -===================== +What's new in Matplotlib 3.0 (Sep 18, 2018) +=========================================== Improved default backend selection ---------------------------------- @@ -60,11 +60,11 @@ frame. Padding and separation parameters can be adjusted. Add ``minorticks_on()/off()`` methods for colorbar -------------------------------------------------- -A new method :meth:`.colorbar.Colobar.minorticks_on` has been added to +A new method ``ColorbarBase.minorticks_on`` has been added to correctly display minor ticks on a colorbar. This method doesn't allow the minor ticks to extend into the regions beyond vmin and vmax when the *extend* keyword argument (used while creating the colorbar) is set to 'both', 'max' or -'min'. A complementary method :meth:`.colorbar.Colobar.minorticks_off` has +'min'. A complementary method ``ColorbarBase.minorticks_off`` has also been added to remove the minor ticks on the colorbar. @@ -90,7 +90,7 @@ behaviour has been removed. Now if the file name exists on disk, the user is prompted whether or not to overwrite it. This eliminates guesswork, and allows intentional overwriting, especially when the figure name has been -manually set using `.figure.Figure.canvas.set_window_title()`. +manually set using ``figure.canvas.set_window_title()``. Legend now has a *title_fontsize* keyword argument (and rcParam) @@ -108,9 +108,8 @@ Support for axes.prop_cycle property *markevery* in rcParams ------------------------------------------------------------ The Matplotlib ``rcParams`` settings object now supports configuration -of the attribute :rc:`axes.prop_cycle` with cyclers using the `markevery` -Line2D object property. An example of this feature is provided at -:doc:`/gallery/lines_bars_and_markers/markevery_prop_cycle`. +of the attribute :rc:`axes.prop_cycle` with cyclers using the *markevery* +Line2D object property. Multi-page PDF support for pgf backend -------------------------------------- @@ -142,10 +141,10 @@ independent on the axes size or units. To revert to the previous behaviour set the axes' aspect ratio to automatic by using ``ax.set_aspect("auto")`` or ``plt.axis("auto")``. -Add ``ax.get_gridspec`` to `.SubplotBase` ------------------------------------------ +Add ``ax.get_gridspec`` to ``SubplotBase`` +------------------------------------------ -New method `.SubplotBase.get_gridspec` is added so that users can +New method ``SubplotBase.get_gridspec`` is added so that users can easily get the gridspec that went into making an axes: .. code:: diff --git a/doc/users/prev_whats_new/whats_new_3.1.0.rst b/doc/users/prev_whats_new/whats_new_3.1.0.rst index 4501243fee8a..8821f8e59257 100644 --- a/doc/users/prev_whats_new/whats_new_3.1.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.1.0.rst @@ -1,6 +1,7 @@ +.. _whats-new-3-1-0: -What's new in Matplotlib 3.1 -============================ +What's new in Matplotlib 3.1 (May 18, 2019) +=========================================== For a list of all of the issues and pull requests since the last revision, see the :ref:`github-stats`. @@ -168,7 +169,7 @@ Return type of ArtistInspector.get_aliases changed was used to simulate a set in earlier versions of Python. It has now been replaced by a set, i.e. ``{fullname: {alias1, alias2, ...}}``. -This value is also stored in `.ArtistInspector.aliasd`, which has likewise +This value is also stored in ``ArtistInspector.aliasd``, which has likewise changed. @@ -332,7 +333,7 @@ were previously displayed as ``1e+04``. MouseEvent button attribute is now an IntEnum ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -The :attr:`button` attribute of `~.MouseEvent` instances can take the values +The ``button`` attribute of `~.MouseEvent` instances can take the values None, 1 (left button), 2 (middle button), 3 (right button), "up" (scroll), and "down" (scroll). For better legibility, the 1, 2, and 3 values are now represented using the `enum.IntEnum` class `matplotlib.backend_bases.MouseButton`, diff --git a/doc/users/prev_whats_new/whats_new_3.2.0.rst b/doc/users/prev_whats_new/whats_new_3.2.0.rst index efa099d01a23..12d7fab3af90 100644 --- a/doc/users/prev_whats_new/whats_new_3.2.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.2.0.rst @@ -1,6 +1,7 @@ +.. _whats-new-3-2-0: -What's new in Matplotlib 3.2 -============================ +What's new in Matplotlib 3.2 (Mar 04, 2020) +=========================================== For a list of all of the issues and pull requests since the last revision, see the :ref:`github-stats`. diff --git a/doc/users/prev_whats_new/whats_new_3.3.0.rst b/doc/users/prev_whats_new/whats_new_3.3.0.rst index 04ca9d923f76..58959627246a 100644 --- a/doc/users/prev_whats_new/whats_new_3.3.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.3.0.rst @@ -1,6 +1,8 @@ -============================== -What's new in Matplotlib 3.3.0 -============================== +.. _whats-new-3-3-0: + +============================================= +What's new in Matplotlib 3.3.0 (Jul 16, 2020) +============================================= For a list of all of the issues and pull requests since the last revision, see the :ref:`github-stats`. @@ -48,7 +50,7 @@ or as a string (with single-character Axes labels): ha='center', va='center', fontsize=36, color='darkgrey') -See :doc:`/tutorials/provisional/mosaic` for more details and examples. +See :doc:`/gallery/subplots_axes_and_figures/mosaic` for more details and examples. ``GridSpec.subplots()`` ----------------------- @@ -285,7 +287,7 @@ Align labels to Axes edges -------------------------- `~.axes.Axes.set_xlabel`, `~.axes.Axes.set_ylabel` and -`.ColorbarBase.set_label` support a parameter ``loc`` for simplified +``ColorbarBase.set_label`` support a parameter ``loc`` for simplified positioning. For the xlabel, the supported values are 'left', 'center', or 'right'. For the ylabel, the supported values are 'bottom', 'center', or 'top'. @@ -371,8 +373,8 @@ set with the new rcParameter :rc:`axes.titley`. .. plot:: fig, axs = plt.subplots(1, 2, constrained_layout=True, figsize=(5, 2)) - axs[0].set_title('y=0.7\n$\sum_{j_n} x_j$', y=0.7) - axs[1].set_title('y=None\n$\sum_{j_n} x_j$') + axs[0].set_title('y=0.7\n$\\sum_{j_n} x_j$', y=0.7) + axs[1].set_title('y=None\n$\\sum_{j_n} x_j$') plt.show() Offset text is now set to the top when using ``axis.tick_top()`` diff --git a/doc/users/prev_whats_new/whats_new_3.4.0.rst b/doc/users/prev_whats_new/whats_new_3.4.0.rst index cb8220bb5f0b..85a2f8a80975 100644 --- a/doc/users/prev_whats_new/whats_new_3.4.0.rst +++ b/doc/users/prev_whats_new/whats_new_3.4.0.rst @@ -1,6 +1,8 @@ -============================== -What's new in Matplotlib 3.4.0 -============================== +.. _whats-new-3-4-0: + +============================================= +What's new in Matplotlib 3.4.0 (Mar 26, 2021) +============================================= For a list of all of the issues and pull requests since the last revision, see the :ref:`github-stats`. @@ -164,7 +166,7 @@ New automatic labeling for bar charts A new `.Axes.bar_label` method has been added for auto-labeling bar charts. .. figure:: /gallery/lines_bars_and_markers/images/sphx_glr_bar_label_demo_001.png - :target: /gallery/lines_bars_and_markers/bar_label_demo.html + :target: ../../gallery/lines_bars_and_markers/bar_label_demo.html Example of the new automatic labeling. @@ -394,7 +396,7 @@ style. This can be used to, e.g., distinguish the valid and invalid sides of the constraint boundaries in the solution space of optimizations. .. figure:: /gallery/misc/images/sphx_glr_tickedstroke_demo_002.png - :target: /gallery/misc/tickedstroke_demo.html + :target: ../../gallery/misc/tickedstroke_demo.html Colors and colormaps @@ -700,7 +702,7 @@ The new `.Text` parameter ``transform_rotates_text`` now sets whether rotations of the transform affect the text direction. .. figure:: /gallery/text_labels_and_annotations/images/sphx_glr_text_rotation_relative_to_line_001.png - :target: /gallery/text_labels_and_annotations/text_rotation_relative_to_line.html + :target: ../../gallery/text_labels_and_annotations/text_rotation_relative_to_line.html Example of the new *transform_rotates_text* parameter @@ -724,7 +726,7 @@ for each individual text element in a plot. If no parameter is set, the global value :rc:`mathtext.fontset` will be used. .. figure:: /gallery/text_labels_and_annotations/images/sphx_glr_mathtext_fontfamily_example_001.png - :target: /gallery/text_labels_and_annotations/mathtext_fontfamily_example.html + :target: ../../gallery/text_labels_and_annotations/mathtext_fontfamily_example.html ``TextArea``/``AnchoredText`` support *horizontalalignment* ----------------------------------------------------------- @@ -858,13 +860,13 @@ its entirety, supporting features such as custom styling for error lines and cap marks, control over errorbar spacing, upper and lower limit marks. .. figure:: /gallery/mplot3d/images/sphx_glr_errorbar3d_001.png - :target: /gallery/mplot3d/errorbar3d.html + :target: ../../gallery/mplot3d/errorbar3d.html Stem plots in 3D Axes --------------------- Stem plots are now supported on 3D Axes. Much like 2D stems, -`~.axes3d.Axes3D.stem3D` supports plotting the stems in various orientations: +`~.axes3d.Axes3D.stem` supports plotting the stems in various orientations: .. plot:: diff --git a/doc/users/prev_whats_new/whats_new_3.5.0.rst b/doc/users/prev_whats_new/whats_new_3.5.0.rst new file mode 100644 index 000000000000..5e69ab6be926 --- /dev/null +++ b/doc/users/prev_whats_new/whats_new_3.5.0.rst @@ -0,0 +1,681 @@ +============================================= +What's new in Matplotlib 3.5.0 (Nov 15, 2021) +============================================= + +For a list of all of the issues and pull requests since the last revision, see +the :ref:`github-stats`. + +.. contents:: Table of Contents + :depth: 4 + +.. toctree:: + :maxdepth: 4 + +Figure and Axes creation / management +===================================== + +``subplot_mosaic`` supports simple Axes sharing +----------------------------------------------- + +`.Figure.subplot_mosaic`, `.pyplot.subplot_mosaic` support *simple* Axes +sharing (i.e., only `True`/`False` may be passed to *sharex*/*sharey*). When +`True`, tick label visibility and Axis units will be shared. + +.. plot:: + :include-source: + + mosaic = [ + ['A', [['B', 'C'], + ['D', 'E']]], + ['F', 'G'], + ] + fig = plt.figure(constrained_layout=True) + ax_dict = fig.subplot_mosaic(mosaic, sharex=True, sharey=True) + # All Axes use these scales after this call. + ax_dict['A'].set(xscale='log', yscale='logit') + +Figure now has ``draw_without_rendering`` method +------------------------------------------------ + +Some aspects of a figure are only determined at draw-time, such as the exact +position of text artists or deferred computation like automatic data limits. +If you need these values, you can use ``figure.canvas.draw()`` to force a full +draw. However, this has side effects, sometimes requires an open file, and is +doing more work than is needed. + +The new `.Figure.draw_without_rendering` method runs all the updates that +``draw()`` does, but skips rendering the figure. It's thus more efficient if +you need the updated values to configure further aspects of the figure. + +Figure ``__init__`` passes keyword arguments through to set +----------------------------------------------------------- + +Similar to many other sub-classes of `~.Artist`, the `~.FigureBase`, +`~.SubFigure`, and `~.Figure` classes will now pass any additional keyword +arguments to `~.Artist.set` to allow properties of the newly created object to +be set at initialization time. For example:: + + from matplotlib.figure import Figure + fig = Figure(label='my figure') + +Plotting methods +================ + +Add ``Annulus`` patch +--------------------- + +`.Annulus` is a new class for drawing elliptical annuli. + +.. plot:: + + import matplotlib.pyplot as plt + from matplotlib.patches import Annulus + + fig, ax = plt.subplots() + cir = Annulus((0.5, 0.5), 0.2, 0.05, fc='g') # circular annulus + ell = Annulus((0.5, 0.5), (0.5, 0.3), 0.1, 45, # elliptical + fc='m', ec='b', alpha=0.5, hatch='xxx') + ax.add_patch(cir) + ax.add_patch(ell) + ax.set_aspect('equal') + +``set_data`` method for ``FancyArrow`` patch +-------------------------------------------- + +`.FancyArrow`, the patch returned by ``ax.arrow``, now has a ``set_data`` +method that allows modifying the arrow after creation, e.g., for animation. + +New arrow styles in ``ArrowStyle`` and ``ConnectionPatch`` +---------------------------------------------------------- + +The new *arrow* parameter to `.ArrowStyle` substitutes the use of the +*beginarrow* and *endarrow* parameters in the creation of arrows. It receives +arrows strings like ``'<-'``, ``']-[``' and ``']->``' instead of individual +booleans. + +Two new styles ``']->'`` and ``'<-['`` are also added via this mechanism. +`.ConnectionPatch`, which accepts arrow styles though its *arrowstyle* +parameter, also accepts these new styles. + +.. plot:: + + import matplotlib.patches as mpatches + + fig, ax = plt.subplots(figsize=(4, 4)) + + ax.plot([0.75, 0.75], [0.25, 0.75], 'ok') + ax.set(xlim=(0, 1), ylim=(0, 1), title='New ArrowStyle options') + + ax.annotate(']->', (0.75, 0.25), (0.25, 0.25), + arrowprops=dict( + arrowstyle=']->', connectionstyle="arc3,rad=-0.05", + shrinkA=5, shrinkB=5, + ), + bbox=dict(boxstyle='square', fc='w'), size='large') + + ax.annotate('<-[', (0.75, 0.75), (0.25, 0.75), + arrowprops=dict( + arrowstyle='<-[', connectionstyle="arc3,rad=-0.05", + shrinkA=5, shrinkB=5, + ), + bbox=dict(boxstyle='square', fc='w'), size='large') + +Setting collection offset transform after initialization +-------------------------------------------------------- + +The added `.collections.Collection.set_offset_transform` may be used to set the +offset transform after initialization. This can be helpful when creating a +`.collections.Collection` outside an Axes object, and later adding it with +`.Axes.add_collection()` and setting the offset transform to ``Axes.transData``. + +Colors and colormaps +==================== + +Colormap registry (experimental) +-------------------------------- + +Colormaps are now managed via `matplotlib.colormaps` (or `.pyplot.colormaps`), +which is a `.ColormapRegistry`. While we are confident that the API is final, +we formally mark it as experimental for 3.5 because we want to keep the option +to still modify the API for 3.6 should the need arise. + +Colormaps can be obtained using item access:: + + import matplotlib.pyplot as plt + cmap = plt.colormaps['viridis'] + +To register new colormaps use:: + + plt.colormaps.register(my_colormap) + +We recommend to use the new API instead of the `~.cm.get_cmap` and +`~.cm.register_cmap` functions for new code. `matplotlib.cm.get_cmap` and +`matplotlib.cm.register_cmap` will eventually be deprecated and removed. +Within `.pyplot`, ``plt.get_cmap()`` and ``plt.register_cmap()`` will continue +to be supported for backward compatibility. + +Image interpolation now possible at RGBA stage +---------------------------------------------- + +Images in Matplotlib created via `~.axes.Axes.imshow` are resampled to match +the resolution of the current canvas. It is useful to apply an auto-aliasing +filter when downsampling to reduce Moiré effects. By default, interpolation is +done on the data, a norm applied, and then the colormapping performed. + +However, it is often desirable for the anti-aliasing interpolation to happen +in RGBA space, where the colors are interpolated rather than the data. This +usually leads to colors outside the colormap, but visually blends adjacent +colors, and is what browsers and other image processing software do. + +A new keyword argument *interpolation_stage* is provided for +`~.axes.Axes.imshow` to set the stage at which the anti-aliasing interpolation +happens. The default is the current behaviour of "data", with the alternative +being "rgba" for the newly-available behavior. + +.. figure:: /gallery/images_contours_and_fields/images/sphx_glr_image_antialiasing_001.png + :target: ../../gallery/images_contours_and_fields/image_antialiasing.html + + Example of the interpolation stage options. + +For more details see the discussion of the new keyword argument in +:doc:`/gallery/images_contours_and_fields/image_antialiasing`. + +``imshow`` supports half-float arrays +------------------------------------- + +The `~.axes.Axes.imshow` method now supports half-float arrays, i.e., NumPy +arrays with dtype ``np.float16``. + +A callback registry has been added to Normalize objects +------------------------------------------------------- + +`.colors.Normalize` objects now have a callback registry, ``callbacks``, that +can be connected to by other objects to be notified when the norm is updated. +The callback emits the key ``changed`` when the norm is modified. +`.cm.ScalarMappable` is now a listener and will register a change when the +norm's vmin, vmax or other attributes are changed. + +Titles, ticks, and labels +========================= + +Settings tick positions and labels simultaneously in ``set_ticks`` +------------------------------------------------------------------ + +`.Axis.set_ticks` (and the corresponding `.Axes.set_xticks` / +`.Axes.set_yticks`) has a new parameter *labels* allowing to set tick positions +and labels simultaneously. + +Previously, setting tick labels was done using `.Axis.set_ticklabels` (or +the corresponding `.Axes.set_xticklabels` / `.Axes.set_yticklabels`); this +usually only makes sense if tick positions were previously fixed with +`~.Axis.set_ticks`:: + + ax.set_xticks([1, 2, 3]) + ax.set_xticklabels(['a', 'b', 'c']) + +The combined functionality is now available in `~.Axis.set_ticks`:: + + ax.set_xticks([1, 2, 3], ['a', 'b', 'c']) + +The use of `.Axis.set_ticklabels` is discouraged, but it will stay available +for backward compatibility. + +Note: This addition makes the API of `~.Axis.set_ticks` also more similar to +`.pyplot.xticks` / `.pyplot.yticks`, which already had the additional *labels* +parameter. + +Fonts and Text +============== + +Triple and quadruple dot mathtext accents +----------------------------------------- + +In addition to single and double dot accents, mathtext now supports triple and +quadruple dot accents. + +.. plot:: + :include-source: + + fig = plt.figure(figsize=(3, 1)) + fig.text(0.5, 0.5, r'$\dot{a} \ddot{b} \dddot{c} \ddddot{d}$', fontsize=40, + horizontalalignment='center', verticalalignment='center') + +Font properties of legend title are configurable +------------------------------------------------ + +Title's font properties can be set via the *title_fontproperties* keyword +argument, for example: + +.. plot:: + + fig, ax = plt.subplots(figsize=(4, 3)) + ax.plot(range(10), label='point') + ax.legend(title='Points', + title_fontproperties={'family': 'serif', 'size': 20}) + +``Text`` and ``TextBox`` added *parse_math* option +-------------------------------------------------- + +`.Text` and `.TextBox` objects now allow a *parse_math* keyword-only argument +which controls whether math should be parsed from the displayed string. If +*True*, the string will be parsed as a math text object. If *False*, the string +will be considered a literal and no parsing will occur. + +Text can be positioned inside TextBox widget +-------------------------------------------- + +A new parameter called *textalignment* can be used to control for the position +of the text inside the Axes of the `.TextBox` widget. + +.. plot:: + + from matplotlib import pyplot as plt + from matplotlib.widgets import TextBox + + fig = plt.figure(figsize=(4, 3)) + for i, alignment in enumerate(['left', 'center', 'right']): + box_input = fig.add_axes([0.1, 0.7 - i*0.3, 0.8, 0.2]) + text_box = TextBox(ax=box_input, initial=f'{alignment} alignment', + label='', textalignment=alignment) + +Simplifying the font setting for usetex mode +-------------------------------------------- + +Now the :rc:`font.family` accepts some font names as value for a more +user-friendly setup. + +.. code-block:: + + plt.rcParams.update({ + "text.usetex": True, + "font.family": "Helvetica" + }) + +Type 42 subsetting is now enabled for PDF/PS backends +----------------------------------------------------- + +`~matplotlib.backends.backend_pdf` and `~matplotlib.backends.backend_ps` now +use a unified Type 42 font subsetting interface, with the help of `fontTools +`_ + +Set :rc:`pdf.fonttype` or :rc:`ps.fonttype` to ``42`` to trigger this +workflow:: + + # for PDF backend + plt.rcParams['pdf.fonttype'] = 42 + + # for PS backend + plt.rcParams['ps.fonttype'] = 42 + + fig, ax = plt.subplots() + ax.text(0.4, 0.5, 'subsetted document is smaller in size!') + + fig.savefig("document.pdf") + fig.savefig("document.ps") + +rcParams improvements +===================== + +Allow setting default legend labelcolor globally +------------------------------------------------ + +A new :rc:`legend.labelcolor` sets the default *labelcolor* argument for +`.Figure.legend`. The special values 'linecolor', 'markerfacecolor' (or +'mfc'), or 'markeredgecolor' (or 'mec') will cause the legend text to match the +corresponding color of marker. + +.. plot:: + + plt.rcParams['legend.labelcolor'] = 'linecolor' + + # Make some fake data. + a = np.arange(0, 3, .02) + c = np.exp(a) + d = c[::-1] + + fig, ax = plt.subplots() + ax.plot(a, c, 'g--', label='Model length') + ax.plot(a, d, 'r:', label='Data length') + + ax.legend() + + plt.show() + +3D Axes improvements +==================== + +Axes3D now allows manual control of draw order +---------------------------------------------- + +The `~mpl_toolkits.mplot3d.axes3d.Axes3D` class now has *computed_zorder* +parameter. When set to False, Artists are drawn using their ``zorder`` +attribute. + +.. plot:: + + import matplotlib.patches as mpatches + from mpl_toolkits.mplot3d import art3d + + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(6.4, 3), + subplot_kw=dict(projection='3d')) + + ax1.set_title('computed_zorder = True (default)') + ax2.set_title('computed_zorder = False') + ax2.computed_zorder = False + + corners = ((0, 0, 0), (0, 5, 0), (5, 5, 0), (5, 0, 0)) + for ax in (ax1, ax2): + tri = art3d.Poly3DCollection([corners], + facecolors='white', + edgecolors='black', + zorder=1) + ax.add_collection3d(tri) + line, = ax.plot((2, 2), (2, 2), (0, 4), c='red', zorder=2, + label='zorder=2') + points = ax.scatter((3, 3), (1, 3), (1, 3), c='red', zorder=10, + label='zorder=10') + + ax.set_xlim((0, 5)) + ax.set_ylim((0, 5)) + ax.set_zlim((0, 2.5)) + + plane = mpatches.Patch(facecolor='white', edgecolor='black', + label='zorder=1') + fig.legend(handles=[plane, line, points], loc='lower center') + +Allow changing the vertical axis in 3d plots +---------------------------------------------- + +`~mpl_toolkits.mplot3d.axes3d.Axes3D.view_init` now has the parameter +*vertical_axis* which allows switching which axis is aligned vertically. + +.. plot:: + + Nphi, Nr = 18, 8 + phi = np.linspace(0, np.pi, Nphi) + r = np.arange(Nr) + phi = np.tile(phi, Nr).flatten() + r = np.repeat(r, Nphi).flatten() + + x = r * np.sin(phi) + y = r * np.cos(phi) + z = Nr - r + + fig, axs = plt.subplots(1, 3, figsize=(7, 3), + subplot_kw=dict(projection='3d'), + gridspec_kw=dict(wspace=0.4, left=0.08, right=0.98, + bottom=0, top=1)) + for vert_a, ax in zip(['z', 'y', 'x'], axs): + pc = ax.scatter(x, y, z, c=z) + ax.view_init(azim=30, elev=30, vertical_axis=vert_a) + ax.set(xlabel='x', ylabel='y', zlabel='z', + title=f'vertical_axis={vert_a!r}') + +``plot_surface`` supports masked arrays and NaNs +------------------------------------------------ + +`.axes3d.Axes3D.plot_surface` supports masked arrays and NaNs, and will now +hide quads that contain masked or NaN points. The behaviour is similar to +`.Axes.contour` with ``corner_mask=True``. + +.. plot:: + + import matplotlib + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots(figsize=(6, 6), subplot_kw={'projection': '3d'}, + constrained_layout=True) + + x, y = np.mgrid[1:10:1, 1:10:1] + z = x ** 3 + y ** 3 - 500 + z = np.ma.masked_array(z, z < 0) + + ax.plot_surface(x, y, z, rstride=1, cstride=1, linewidth=0, cmap='inferno') + ax.view_init(35, -90) + +3D plotting methods support *data* keyword argument +--------------------------------------------------- + +To match all 2D plotting methods, the 3D Axes now support the *data* keyword +argument. This allows passing arguments indirectly from a DataFrame-like +structure. :: + + data = { # A labelled data set, or e.g., Pandas DataFrame. + 'x': ..., + 'y': ..., + 'z': ..., + 'width': ..., + 'depth': ..., + 'top': ..., + } + + fig, ax = plt.subplots(subplot_kw={'projection': '3d') + ax.bar3d('x', 'y', 'z', 'width', 'depth', 'top', data=data) + +Interactive tool improvements +============================= + +Colorbars now have pan and zoom functionality +--------------------------------------------- + +Interactive plots with colorbars can now be zoomed and panned on the colorbar +axis. This adjusts the *vmin* and *vmax* of the ``ScalarMappable`` associated +with the colorbar. This is currently only enabled for continuous norms. Norms +used with contourf and categoricals, such as ``BoundaryNorm`` and ``NoNorm``, +have the interactive capability disabled by default. ``cb.ax.set_navigate()`` +can be used to set whether a colorbar axes is interactive or not. + +Updated the appearance of Slider widgets +---------------------------------------- + +The appearance of `~.Slider` and `~.RangeSlider` widgets were updated and given +new styling parameters for the added handles. + +.. plot:: + + import matplotlib.pyplot as plt + from matplotlib.widgets import Slider + + plt.figure(figsize=(4, 2)) + ax_old = plt.axes([0.2, 0.65, 0.65, 0.1]) + ax_new = plt.axes([0.2, 0.25, 0.65, 0.1]) + Slider(ax_new, "New", 0, 1) + + ax = ax_old + valmin = 0 + valinit = 0.5 + ax.set_xlim([0, 1]) + ax_old.axvspan(valmin, valinit, 0, 1) + ax.axvline(valinit, 0, 1, color="r", lw=1) + ax.set_xticks([]) + ax.set_yticks([]) + ax.text( + -0.02, + 0.5, + "Old", + transform=ax.transAxes, + verticalalignment="center", + horizontalalignment="right", + ) + + ax.text( + 1.02, + 0.5, + "0.5", + transform=ax.transAxes, + verticalalignment="center", + horizontalalignment="left", + ) + +Removing points on a PolygonSelector +------------------------------------ + +After completing a `~matplotlib.widgets.PolygonSelector`, individual points can +now be removed by right-clicking on them. + +Dragging selectors +------------------ + +The `~matplotlib.widgets.SpanSelector`, `~matplotlib.widgets.RectangleSelector` +and `~matplotlib.widgets.EllipseSelector` have a new keyword argument, +*drag_from_anywhere*, which when set to `True` allows you to click and drag +from anywhere inside the selector to move it. Previously it was only possible +to move it by either activating the move modifier button, or clicking on the +central handle. + +The size of the `~matplotlib.widgets.SpanSelector` can now be changed using the +edge handles. + +Clearing selectors +------------------ + +The selectors (`~.widgets.EllipseSelector`, `~.widgets.LassoSelector`, +`~.widgets.PolygonSelector`, `~.widgets.RectangleSelector`, and +`~.widgets.SpanSelector`) have a new method *clear*, which will clear the +current selection and get the selector ready to make a new selection. This is +equivalent to pressing the *escape* key. + +Setting artist properties of selectors +-------------------------------------- + +The artist properties of the `~.widgets.EllipseSelector`, +`~.widgets.LassoSelector`, `~.widgets.PolygonSelector`, +`~.widgets.RectangleSelector` and `~.widgets.SpanSelector` selectors can be +changed using the ``set_props`` and ``set_handle_props`` methods. + +Ignore events outside selection +------------------------------- + +The `~.widgets.EllipseSelector`, `~.widgets.RectangleSelector` and +`~.widgets.SpanSelector` selectors have a new keyword argument, +*ignore_event_outside*, which when set to `True` will ignore events outside of +the current selection. The handles or the new dragging functionality can instead +be used to change the selection. + +``CallbackRegistry`` objects gain a method to temporarily block signals +----------------------------------------------------------------------- + +The context manager `~matplotlib.cbook.CallbackRegistry.blocked` can be used +to block callback signals from being processed by the ``CallbackRegistry``. +The optional keyword, *signal*, can be used to block a specific signal +from being processed and let all other signals pass. + +.. code-block:: + + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + ax.imshow([[0, 1], [2, 3]]) + + # Block all interactivity through the canvas callbacks + with fig.canvas.callbacks.blocked(): + plt.show() + + fig, ax = plt.subplots() + ax.imshow([[0, 1], [2, 3]]) + + # Only block key press events + with fig.canvas.callbacks.blocked(signal="key_press_event"): + plt.show() + +Directional sizing cursors +-------------------------- + +Canvases now support setting directional sizing cursors, i.e., horizontal and +vertical double arrows. These are used in e.g., selector widgets. Try the +:doc:`/gallery/widgets/mouse_cursor` example to see the cursor in your desired +backend. + +Sphinx extensions +================= + +More configuration of ``mathmpl`` sphinx extension +-------------------------------------------------- + +The `matplotlib.sphinxext.mathmpl` sphinx extension supports two new +configuration options that may be specified in your ``conf.py``: + +- ``mathmpl_fontsize`` (float), which sets the font size of the math text in + points; +- ``mathmpl_srcset`` (list of str), which provides a list of sizes to support + `responsive resolution images + `__ + The list should contain additional x-descriptors (``'1.5x'``, ``'2x'``, etc.) + to generate (1x is the default and always included.) + +Backend-specific improvements +============================= + +GTK backend +----------- + +A backend supporting GTK4_ has been added. Both Agg and Cairo renderers are +supported. The GTK4 backends may be selected as GTK4Agg or GTK4Cairo. + +.. _GTK4: https://www.gtk.org/ + +Qt backends +----------- + +Support for Qt6 (using either PyQt6_ or PySide6_) has been added, with either +the Agg or Cairo renderers. Simultaneously, support for Qt4 has been dropped. +Both Qt6 and Qt5 are supported by a combined backend (QtAgg or QtCairo), and +the loaded version is determined by modules already imported, the +:envvar:`QT_API` environment variable, and available packages. See +:ref:`QT_bindings` for details. The versioned Qt5 backend names (Qt5Agg or +Qt5Cairo) remain supported for backwards compatibility. + +.. _PyQt6: https://www.riverbankcomputing.com/static/Docs/PyQt6/ +.. _PySide6: https://doc.qt.io/qtforpython/ + +HiDPI support in Cairo-based, GTK, and Tk backends +-------------------------------------------------- + +The GTK3 backends now support HiDPI fully, including mixed monitor cases (on +Wayland only). The newly added GTK4 backends also support HiDPI. + +The TkAgg backend now supports HiDPI **on Windows only**, including mixed +monitor cases. + +All Cairo-based backends correctly support HiDPI as well as their Agg +counterparts did (i.e., if the toolkit supports HiDPI, then the \*Cairo backend +will now support it, but not otherwise.) + +Qt figure options editor improvements +------------------------------------- + +The figure options editor in the Qt backend now also supports editing the left +and right titles (plus the existing centre title). Editing Axis limits is +better supported when using a date converter. The ``symlog`` option is now +available in Axis scaling options. All entries with the same label are now +shown in the Curves tab. + +WX backends support mouse navigation buttons +-------------------------------------------- + +The WX backends now support navigating through view states using the mouse +forward/backward buttons, as in other backends. + +WebAgg uses asyncio instead of Tornado +-------------------------------------- + +The WebAgg backend defaults to using `asyncio` over Tornado for timer support. +This allows using the WebAgg backend in JupyterLite. + +Version information +=================== + +We switched to the `release-branch-semver`_ version scheme of setuptools-scm. +This only affects the version information for development builds. Their version +number now describes the targeted release, i.e. 3.5.0.dev820+g6768ef8c4c is 820 +commits after the previous release and is scheduled to be officially released +as 3.5.0 later. + +In addition to the string ``__version__``, there is now a namedtuple +``__version_info__`` as well, which is modelled after `sys.version_info`_. Its +primary use is safely comparing version information, e.g. ``if +__version_info__ >= (3, 4, 2)``. + +.. _release-branch-semver: https://github.com/pypa/setuptools_scm#version-number-construction +.. _sys.version_info: https://docs.python.org/3/library/sys.html#sys.version_info diff --git a/doc/users/prev_whats_new/whats_new_3.5.2.rst b/doc/users/prev_whats_new/whats_new_3.5.2.rst new file mode 100644 index 000000000000..85b1c38862eb --- /dev/null +++ b/doc/users/prev_whats_new/whats_new_3.5.2.rst @@ -0,0 +1,20 @@ +============================================= +What's new in Matplotlib 3.5.2 (May 02, 2022) +============================================= + +For a list of all of the issues and pull requests since the last revision, see +the :ref:`github-stats`. + +.. contents:: Table of Contents + :depth: 4 + +.. toctree:: + :maxdepth: 4 + +Windows on ARM support +---------------------- + +Preliminary support for Windows on arm64 target has been added; this requires +FreeType 2.11 or above. + +No binary wheels are available yet but it may be built from source. diff --git a/doc/users/prev_whats_new/whats_new_3.6.0.rst b/doc/users/prev_whats_new/whats_new_3.6.0.rst new file mode 100644 index 000000000000..79706376a870 --- /dev/null +++ b/doc/users/prev_whats_new/whats_new_3.6.0.rst @@ -0,0 +1,890 @@ +============================================= +What's new in Matplotlib 3.6.0 (Sep 15, 2022) +============================================= + +For a list of all of the issues and pull requests since the last revision, see +the :ref:`github-stats`. + +.. contents:: Table of Contents + :depth: 4 + +.. toctree:: + :maxdepth: 4 + +Figure and Axes creation / management +===================================== +``subplots``, ``subplot_mosaic`` accept *height_ratios* and *width_ratios* arguments +------------------------------------------------------------------------------------ + +The relative width and height of columns and rows in `~.Figure.subplots` and +`~.Figure.subplot_mosaic` can be controlled by passing *height_ratios* and +*width_ratios* keyword arguments to the methods: + +.. plot:: + :alt: A figure with three subplots in three rows and one column. The height of the subplot in the first row is three times than the subplots in the 2nd and 3rd row. + :include-source: true + + fig = plt.figure() + axs = fig.subplots(3, 1, sharex=True, height_ratios=[3, 1, 1]) + +Previously, this required passing the ratios in *gridspec_kw* arguments:: + + fig = plt.figure() + axs = fig.subplots(3, 1, sharex=True, + gridspec_kw=dict(height_ratios=[3, 1, 1])) + +Constrained layout is no longer considered experimental +------------------------------------------------------- + +The constrained layout engine and API is no longer considered experimental. +Arbitrary changes to behaviour and API are no longer permitted without a +deprecation period. + +New ``layout_engine`` module +---------------------------- + +Matplotlib ships with ``tight_layout`` and ``constrained_layout`` layout +engines. A new `.layout_engine` module is provided to allow downstream +libraries to write their own layout engines and `~.figure.Figure` objects can +now take a `.LayoutEngine` subclass as an argument to the *layout* parameter. + +Compressed layout for fixed-aspect ratio Axes +--------------------------------------------- + +Simple arrangements of Axes with fixed aspect ratios can now be packed together +with ``fig, axs = plt.subplots(2, 3, layout='compressed')``. + +With ``layout='tight'`` or ``'constrained'``, Axes with a fixed aspect ratio +can leave large gaps between each other: + +.. plot:: + :alt: A figure labelled "fixed-aspect plots, layout=constrained". Figure has subplots displayed in 2 rows and 2 columns; Subplots have large gaps between each other. + + fig, axs = plt.subplots(2, 2, figsize=(5, 3), + sharex=True, sharey=True, layout="constrained") + for ax in axs.flat: + ax.imshow([[0, 1], [2, 3]]) + fig.suptitle("fixed-aspect plots, layout='constrained'") + +Using the ``layout='compressed'`` layout reduces the space between the Axes, +and adds the extra space to the outer margins: + +.. plot:: + :alt: Four identical two by two heatmaps, each cell a different color: purple, blue, yellow, green going clockwise from upper left corner. The four heatmaps are laid out in a two by two grid with minimum white space between the heatmaps. + + fig, axs = plt.subplots(2, 2, figsize=(5, 3), + sharex=True, sharey=True, layout='compressed') + for ax in axs.flat: + ax.imshow([[0, 1], [2, 3]]) + fig.suptitle("fixed-aspect plots, layout='compressed'") + +See :ref:`compressed_layout` for further details. + +Layout engines may now be removed +--------------------------------- + +The layout engine on a Figure may now be removed by calling +`.Figure.set_layout_engine` with ``'none'``. This may be useful after computing +layout in order to reduce computations, e.g., for subsequent animation loops. + +A different layout engine may be set afterwards, so long as it is compatible +with the previous layout engine. + +``Axes.inset_axes`` flexibility +------------------------------- + +`matplotlib.axes.Axes.inset_axes` now accepts the *projection*, *polar* and +*axes_class* keyword arguments, so that subclasses of `matplotlib.axes.Axes` +may be returned. + +.. plot:: + :alt: Plot of a straight line y=x, with a small inset axes in the lower right corner that shows a circle with radial grid lines and a line plotted in polar coordinates. + :include-source: true + + fig, ax = plt.subplots() + + ax.plot([0, 2], [1, 2]) + + polar_ax = ax.inset_axes([0.75, 0.25, 0.2, 0.2], projection='polar') + polar_ax.plot([0, 2], [1, 2]) + +WebP is now a supported output format +------------------------------------- + +Figures may now be saved in WebP format by using the ``.webp`` file extension, +or passing ``format='webp'`` to `~.Figure.savefig`. This relies on `Pillow +`_ support for WebP. + +Garbage collection is no longer run on figure close +--------------------------------------------------- + +Matplotlib has a large number of circular references (between Figure and +Manager, between Axes and Figure, Axes and Artist, Figure and Canvas, etc.) so +when the user drops their last reference to a Figure (and clears it from +pyplot's state), the objects will not immediately be deleted. + +To account for this we have long (since before 2004) had a `gc.collect` (of the +lowest two generations only) in the closing code in order to promptly clean up +after ourselves. However this is both not doing what we want (as most of our +objects will actually survive) and due to clearing out the first generation +opened us up to having unbounded memory usage. + +In cases with a very tight loop between creating the figure and destroying it +(e.g. ``plt.figure(); plt.close()``) the first generation will never grow large +enough for Python to consider running the collection on the higher generations. +This will lead to unbounded memory usage as the long-lived objects are never +re-considered to look for reference cycles and hence are never deleted. + +We now no longer do any garbage collection when a figure is closed, and rely on +Python automatically deciding to run garbage collection periodically. If you +have strict memory requirements, you can call `gc.collect` yourself but this +may have performance impacts in a tight computation loop. + +Plotting methods +================ + +Striped lines (experimental) +---------------------------- + +The new *gapcolor* parameter to `~.Axes.plot` enables the creation of striped +lines. + +.. plot:: + :alt: Plot of x**3 where the line is an orange-blue striped line, achieved using the keywords linestyle='--', color='orange', gapcolor='blue' + :include-source: true + + x = np.linspace(1., 3., 10) + y = x**3 + + fig, ax = plt.subplots() + ax.plot(x, y, linestyle='--', color='orange', gapcolor='blue', + linewidth=3, label='a striped line') + ax.legend() + +Custom cap widths in box and whisker plots in ``bxp`` and ``boxplot`` +--------------------------------------------------------------------- + +The new *capwidths* parameter to `~.Axes.bxp` and `~.Axes.boxplot` allows +controlling the widths of the caps in box and whisker plots. + +.. plot:: + :alt: A box plot with capwidths 0.01 and 0.2 + :include-source: true + + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + capwidths = [0.01, 0.2] + + fig, ax = plt.subplots() + ax.boxplot([x, x], notch=True, capwidths=capwidths) + ax.set_title(f'{capwidths=}') + +Easier labelling of bars in bar plot +------------------------------------ + +The *label* argument of `~.Axes.bar` and `~.Axes.barh` can now be passed a list +of labels for the bars. The list must be the same length as *x* and labels the +individual bars. Repeated labels are not de-duplicated and will cause repeated +label entries, so this is best used when bars also differ in style (e.g., by +passing a list to *color*, as below.) + +.. plot:: + :alt: Bar chart: blue bar height 10, orange bar height 20, green bar height 15 legend with blue box labeled a, orange box labeled b, and green box labeled c + :include-source: true + + x = ["a", "b", "c"] + y = [10, 20, 15] + color = ['C0', 'C1', 'C2'] + + fig, ax = plt.subplots() + ax.bar(x, y, color=color, label=x) + ax.legend() + +New style format string for colorbar ticks +------------------------------------------ + +The *format* argument of `~.Figure.colorbar` (and other colorbar methods) now +accepts ``{}``-style format strings. + +.. code-block:: python + + fig, ax = plt.subplots() + im = ax.imshow(z) + fig.colorbar(im, format='{x:.2e}') # Instead of '%.2e' + +Linestyles for negative contours may be set individually +-------------------------------------------------------- + +The line style of negative contours may be set by passing the +*negative_linestyles* argument to `.Axes.contour`. Previously, this style could +only be set globally via :rc:`contour.negative_linestyles`. + +.. plot:: + :alt: Two contour plots, each showing two positive and two negative contours. The positive contours are shown in solid black lines in both plots. In one plot the negative contours are shown in dashed lines, which is the current styling. In the other plot they're shown in dotted lines, which is one of the new options. + :include-source: true + + delta = 0.025 + x = np.arange(-3.0, 3.0, delta) + y = np.arange(-2.0, 2.0, delta) + X, Y = np.meshgrid(x, y) + Z1 = np.exp(-X**2 - Y**2) + Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) + Z = (Z1 - Z2) * 2 + + fig, axs = plt.subplots(1, 2) + + CS = axs[0].contour(X, Y, Z, 6, colors='k') + axs[0].clabel(CS, fontsize=9, inline=True) + axs[0].set_title('Default negative contours') + + CS = axs[1].contour(X, Y, Z, 6, colors='k', negative_linestyles='dotted') + axs[1].clabel(CS, fontsize=9, inline=True) + axs[1].set_title('Dotted negative contours') + +Improved quad contour calculations via ContourPy +------------------------------------------------ + +The contouring functions `~.axes.Axes.contour` and `~.axes.Axes.contourf` have +a new keyword argument *algorithm* to control which algorithm is used to +calculate the contours. There is a choice of four algorithms to use, and the +default is to use ``algorithm='mpl2014'`` which is the same algorithm that +Matplotlib has been using since 2014. + +Previously Matplotlib shipped its own C++ code for calculating the contours of +quad grids. Now the external library `ContourPy +`_ is used instead. + +Other possible values of the *algorithm* keyword argument at this time are +``'mpl2005'``, ``'serial'`` and ``'threaded'``; see the `ContourPy +documentation `_ for further details. + +.. note:: + + Contour lines and polygons produced by ``algorithm='mpl2014'`` will be the + same as those produced before this change to within floating-point + tolerance. The exception is for duplicate points, i.e. contours containing + adjacent (x, y) points that are identical; previously the duplicate points + were removed, now they are kept. Contours affected by this will produce the + same visual output, but there will be a greater number of points in the + contours. + + The locations of contour labels obtained by using `~.axes.Axes.clabel` may + also be different. + +``errorbar`` supports *markerfacecoloralt* +------------------------------------------ + +The *markerfacecoloralt* parameter is now passed to the line plotter from +`.Axes.errorbar`. The documentation now accurately lists which properties are +passed to `.Line2D`, rather than claiming that all keyword arguments are passed +on. + +.. plot:: + :alt: Graph with error bar showing ±0.2 error on the x-axis, and ±0.4 error on the y-axis. Error bar marker is a circle radius 20. Error bar face color is blue. + :include-source: true + + x = np.arange(0.1, 4, 0.5) + y = np.exp(-x) + + fig, ax = plt.subplots() + ax.errorbar(x, y, xerr=0.2, yerr=0.4, + linestyle=':', color='darkgrey', + marker='o', markersize=20, fillstyle='left', + markerfacecolor='tab:blue', markerfacecoloralt='tab:orange', + markeredgecolor='tab:brown', markeredgewidth=2) + +``streamplot`` can disable streamline breaks +-------------------------------------------- + +It is now possible to specify that streamplots have continuous, unbroken +streamlines. Previously streamlines would end to limit the number of lines +within a single grid cell. See the difference between the plots below: + +.. plot:: + :alt: A figure with two streamplots. First streamplot has broken streamlines. Second streamplot has continuous streamlines. + + w = 3 + Y, X = np.mgrid[-w:w:100j, -w:w:100j] + U = -1 - X**2 + Y + V = 1 + X - Y**2 + speed = np.sqrt(U**2 + V**2) + + fig, (ax0, ax1) = plt.subplots(1, 2, sharex=True) + + ax0.streamplot(X, Y, U, V, broken_streamlines=True) + ax0.set_title('broken_streamlines=True') + + ax1.streamplot(X, Y, U, V, broken_streamlines=False) + ax1.set_title('broken_streamlines=False') + +New axis scale ``asinh`` (experimental) +--------------------------------------- + +The new ``asinh`` axis scale offers an alternative to ``symlog`` that smoothly +transitions between the quasi-linear and asymptotically logarithmic regions of +the scale. This is based on an arcsinh transformation that allows plotting both +positive and negative values that span many orders of magnitude. + +.. plot:: + :alt: Figure with 2 subplots. Subplot on the left uses symlog scale on the y axis. The transition at -2 is not smooth. Subplot on the right use asinh scale. The transition at -2 is smooth. + + fig, (ax0, ax1) = plt.subplots(1, 2, sharex=True) + x = np.linspace(-3, 6, 100) + + ax0.plot(x, x) + ax0.set_yscale('symlog') + ax0.grid() + ax0.set_title('symlog') + + ax1.plot(x, x) + ax1.set_yscale('asinh') + ax1.grid() + ax1.set_title(r'$sinh^{-1}$') + + for p in (-2, 2): + for ax in (ax0, ax1): + c = plt.Circle((p, p), radius=0.5, fill=False, + color='red', alpha=0.8, lw=3) + ax.add_patch(c) + +``stairs(..., fill=True)`` hides patch edge by setting linewidth +---------------------------------------------------------------- + +``stairs(..., fill=True)`` would previously hide Patch edges by setting +``edgecolor="none"``. Consequently, calling ``set_color()`` on the Patch later +would make the Patch appear larger. + +Now, by using ``linewidth=0``, this apparent size change is prevented. Likewise +calling ``stairs(..., fill=True, linewidth=3)`` will behave more transparently. + +Fix the dash offset of the Patch class +-------------------------------------- + +Formerly, when setting the line style on a `.Patch` object using a dash tuple, +the offset was ignored. Now the offset is applied to the Patch as expected and +it can be used as it is used with `.Line2D` objects. + +Rectangle patch rotation point +------------------------------ + +The rotation point of the `~matplotlib.patches.Rectangle` can now be set to +'xy', 'center' or a 2-tuple of numbers using the *rotation_point* argument. + +.. plot:: + :alt: Blue square that isn't rotated. Green square rotated 45 degrees relative to center. Orange square rotated 45 degrees relative to lower right corner. Red square rotated 45 degrees relative to point in upper right quadrant. + + fig, ax = plt.subplots() + + rect = plt.Rectangle((0, 0), 1, 1, facecolor='none', edgecolor='C0') + ax.add_patch(rect) + ax.annotate('Unrotated', (1, 0), color='C0', + horizontalalignment='right', verticalalignment='top', + xytext=(0, -3), textcoords='offset points') + + for rotation_point, color in zip(['xy', 'center', (0.75, 0.25)], + ['C1', 'C2', 'C3']): + ax.add_patch( + plt.Rectangle((0, 0), 1, 1, facecolor='none', edgecolor=color, + angle=45, rotation_point=rotation_point)) + + if rotation_point == 'center': + point = 0.5, 0.5 + elif rotation_point == 'xy': + point = 0, 0 + else: + point = rotation_point + ax.plot(point[:1], point[1:], color=color, marker='o') + + label = f'{rotation_point}' + if label == 'xy': + label += ' (default)' + ax.annotate(label, point, color=color, + xytext=(3, 3), textcoords='offset points') + + ax.set_aspect(1) + ax.set_title('rotation_point options') + +Colors and colormaps +==================== + +Color sequence registry +----------------------- + +The color sequence registry, `.ColorSequenceRegistry`, contains sequences +(i.e., simple lists) of colors that are known to Matplotlib by name. This will +not normally be used directly, but through the universal instance at +`matplotlib.color_sequences`. + +Colormap method for creating a different lookup table size +---------------------------------------------------------- + +The new method `.Colormap.resampled` creates a new `.Colormap` instance +with the specified lookup table size. This is a replacement for manipulating +the lookup table size via ``get_cmap``. + +Use:: + + get_cmap(name).resampled(N) + +instead of:: + + get_cmap(name, lut=N) + +Setting norms with strings +-------------------------- + +Norms can now be set (e.g. on images) using the string name of the +corresponding scale, e.g. ``imshow(array, norm="log")``. Note that in that +case, it is permissible to also pass *vmin* and *vmax*, as a new Norm instance +will be created under the hood. + +Titles, ticks, and labels +========================= + +``plt.xticks`` and ``plt.yticks`` support *minor* keyword argument +------------------------------------------------------------------ + +It is now possible to set or get minor ticks using `.pyplot.xticks` and +`.pyplot.yticks` by setting ``minor=True``. + +.. plot:: + :alt: Plot showing a line from 1,2 to 3.5,-0.5. X axis showing the 1, 2 and 3 minor ticks on the x axis as One, Zwei, Trois. + :include-source: true + + plt.figure() + plt.plot([1, 2, 3, 3.5], [2, 1, 0, -0.5]) + plt.xticks([1, 2, 3], ["One", "Zwei", "Trois"]) + plt.xticks([np.sqrt(2), 2.5, np.pi], + [r"$\sqrt{2}$", r"$\frac{5}{2}$", r"$\pi$"], minor=True) + +Legends +======= + +Legend can control alignment of title and handles +------------------------------------------------- + +`.Legend` now supports controlling the alignment of the title and handles via +the keyword argument *alignment*. You can also use `.Legend.set_alignment` to +control the alignment on existing Legends. + +.. plot:: + :alt: Figure with 3 subplots. All the subplots are titled test. The three subplots have legends titled alignment='left', alignment='center', alignment='right'. The legend texts are respectively aligned left, center and right. + :include-source: true + + fig, axs = plt.subplots(3, 1) + for i, alignment in enumerate(['left', 'center', 'right']): + axs[i].plot(range(10), label='test') + axs[i].legend(title=f'{alignment=}', alignment=alignment) + +*ncol* keyword argument to ``legend`` renamed to *ncols* +-------------------------------------------------------- + +The *ncol* keyword argument to `~.Axes.legend` for controlling the number of +columns is renamed to *ncols* for consistency with the *ncols* and *nrows* +keywords of `~.Figure.subplots` and `~.GridSpec`. *ncol* remains supported for +backwards compatibility, but is discouraged. + +Markers +======= + +``marker`` can now be set to the string "none" +---------------------------------------------- + +The string "none" means *no-marker*, consistent with other APIs which support +the lowercase version. Using "none" is recommended over using "None", to avoid +confusion with the None object. + +Customization of ``MarkerStyle`` join and cap style +--------------------------------------------------- + +New `.MarkerStyle` parameters allow control of join style and cap style, and +for the user to supply a transformation to be applied to the marker (e.g. a +rotation). + +.. plot:: + :alt: Three rows of markers, columns are blue, green, and purple. First row is y-shaped markers with different capstyles: butt, end is squared off at endpoint; projecting, end is squared off at short distance from endpoint; round, end is rounded. Second row is star-shaped markers with different join styles: miter, star points are sharp triangles; round, star points are rounded; bevel, star points are beveled. Last row shows stars rotated at different angles: small star rotated 0 degrees - top point vertical; medium star rotated 45 degrees - top point tilted right; large star rotated 90 degrees - top point tilted left. + :include-source: true + + from matplotlib.markers import CapStyle, JoinStyle, MarkerStyle + from matplotlib.transforms import Affine2D + + fig, axs = plt.subplots(3, 1, layout='constrained') + for ax in axs: + ax.axis('off') + ax.set_xlim(-0.5, 2.5) + + axs[0].set_title('Cap styles', fontsize=14) + for col, cap in enumerate(CapStyle): + axs[0].plot(col, 0, markersize=32, markeredgewidth=8, + marker=MarkerStyle('1', capstyle=cap)) + # Show the marker edge for comparison with the cap. + axs[0].plot(col, 0, markersize=32, markeredgewidth=1, + markerfacecolor='none', markeredgecolor='lightgrey', + marker=MarkerStyle('1')) + axs[0].annotate(cap.name, (col, 0), + xytext=(20, -5), textcoords='offset points') + + axs[1].set_title('Join styles', fontsize=14) + for col, join in enumerate(JoinStyle): + axs[1].plot(col, 0, markersize=32, markeredgewidth=8, + marker=MarkerStyle('*', joinstyle=join)) + # Show the marker edge for comparison with the join. + axs[1].plot(col, 0, markersize=32, markeredgewidth=1, + markerfacecolor='none', markeredgecolor='lightgrey', + marker=MarkerStyle('*')) + axs[1].annotate(join.name, (col, 0), + xytext=(20, -5), textcoords='offset points') + + axs[2].set_title('Arbitrary transforms', fontsize=14) + for col, (size, rot) in enumerate(zip([2, 5, 7], [0, 45, 90])): + t = Affine2D().rotate_deg(rot).scale(size) + axs[2].plot(col, 0, marker=MarkerStyle('*', transform=t)) + +Fonts and Text +============== + +Font fallback +------------- + +It is now possible to specify a list of fonts families and Matplotlib will try +them in order to locate a required glyph. + +.. plot:: + :caption: Demonstration of mixed English and Chinese text with font fallback. + :alt: The phrase "There are 几个汉字 in between!" rendered in various fonts. + :include-source: True + + plt.rcParams["font.size"] = 20 + fig = plt.figure(figsize=(4.75, 1.85)) + + text = "There are 几个汉字 in between!" + fig.text(0.05, 0.85, text, family=["WenQuanYi Zen Hei"]) + fig.text(0.05, 0.65, text, family=["Noto Sans CJK JP"]) + fig.text(0.05, 0.45, text, family=["DejaVu Sans", "Noto Sans CJK JP"]) + fig.text(0.05, 0.25, text, family=["DejaVu Sans", "WenQuanYi Zen Hei"]) + +This currently works with the Agg (and all of the GUI embeddings), svg, pdf, +ps, and inline backends. + +List of available font names +---------------------------- + +The list of available fonts are now easily accessible. To get a list of the +available font names in Matplotlib use: + +.. code-block:: python + + from matplotlib import font_manager + font_manager.get_font_names() + +``math_to_image`` now has a *color* keyword argument +---------------------------------------------------- + +To easily support external libraries that rely on the MathText rendering of +Matplotlib to generate equation images, a *color* keyword argument was added to +`~matplotlib.mathtext.math_to_image`. + +.. code-block:: python + + from matplotlib import mathtext + mathtext.math_to_image('$x^2$', 'filename.png', color='Maroon') + +Active URL area rotates with link text +-------------------------------------- + +When link text is rotated in a figure, the active URL area will now include the +rotated link area. Previously, the active area remained in the original, +non-rotated, position. + +rcParams improvements +===================== + +Allow setting figure label size and weight globally and separately from title +----------------------------------------------------------------------------- + +For figure labels, ``Figure.supxlabel`` and ``Figure.supylabel``, the size and +weight can be set separately from the figure title using :rc:`figure.labelsize` +and :rc:`figure.labelweight`. + +.. plot:: + :alt: A figure with 4 plots organised in 2 rows and 2 columns. The title of the figure is suptitle in bold and 64 points. The x axis is labelled supxlabel, and y axis is labelled subylabel. Both labels are 32 points and bold. + :include-source: true + + # Original (previously combined with below) rcParams: + plt.rcParams['figure.titlesize'] = 64 + plt.rcParams['figure.titleweight'] = 'bold' + + # New rcParams: + plt.rcParams['figure.labelsize'] = 32 + plt.rcParams['figure.labelweight'] = 'bold' + + fig, axs = plt.subplots(2, 2, layout='constrained') + for ax in axs.flat: + ax.set(xlabel='xlabel', ylabel='ylabel') + + fig.suptitle('suptitle') + fig.supxlabel('supxlabel') + fig.supylabel('supylabel') + +Note that if you have changed :rc:`figure.titlesize` or +:rc:`figure.titleweight`, you must now also change the introduced parameters +for a result consistent with past behaviour. + +Mathtext parsing can be disabled globally +----------------------------------------- + +The :rc:`text.parse_math` setting may be used to disable parsing of mathtext in +all `.Text` objects (most notably from the `.Axes.text` method). + +Double-quoted strings in matplotlibrc +------------------------------------- + +You can now use double-quotes around strings. This allows using the '#' +character in strings. Without quotes, '#' is interpreted as start of a comment. +In particular, you can now define hex-colors: + +.. code-block:: none + + grid.color: "#b0b0b0" + +3D Axes improvements +==================== + +Standardized views for primary plane viewing angles +--------------------------------------------------- + +When viewing a 3D plot in one of the primary view planes (i.e., perpendicular +to the XY, XZ, or YZ planes), the Axis will be displayed in a standard +location. For further information on 3D views, see +:ref:`toolkit_mplot3d-view-angles` and :doc:`/gallery/mplot3d/view_planes_3d`. + +Custom focal length for 3D camera +--------------------------------- + +The 3D Axes can now better mimic real-world cameras by specifying the focal +length of the virtual camera. The default focal length of 1 corresponds to a +Field of View (FOV) of 90°, and is backwards-compatible with existing 3D plots. +An increased focal length between 1 and infinity "flattens" the image, while a +decreased focal length between 1 and 0 exaggerates the perspective and gives +the image more apparent depth. + +The focal length can be calculated from a desired FOV via the equation: + +.. mathmpl:: + + focal\_length = 1/\tan(FOV/2) + +.. plot:: + :alt: A figure showing 3 basic 3D Wireframe plots. From left to right, the plots use focal length of 0.2, 1 and infinity. Focal length between 0.2 and 1 produce plot with depth while focal length between 1 and infinity show relatively flattened image. + :include-source: true + + from mpl_toolkits.mplot3d import axes3d + + X, Y, Z = axes3d.get_test_data(0.05) + + fig, axs = plt.subplots(1, 3, figsize=(7, 4), + subplot_kw={'projection': '3d'}) + + for ax, focal_length in zip(axs, [0.2, 1, np.inf]): + ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) + ax.set_proj_type('persp', focal_length=focal_length) + ax.set_title(f"{focal_length=}") + +3D plots gained a 3rd "roll" viewing angle +------------------------------------------ + +3D plots can now be viewed from any orientation with the addition of a 3rd roll +angle, which rotates the plot about the viewing axis. Interactive rotation +using the mouse still only controls elevation and azimuth, meaning that this +feature is relevant to users who create more complex camera angles +programmatically. The default roll angle of 0 is backwards-compatible with +existing 3D plots. + +.. plot:: + :alt: View of a wireframe of a 3D contour that is somewhat a thickened s shape. Elevation and azimuth are 0 degrees so the shape is viewed straight on, but tilted because the roll is 30 degrees. + :include-source: true + + from mpl_toolkits.mplot3d import axes3d + + X, Y, Z = axes3d.get_test_data(0.05) + + fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) + + ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) + ax.view_init(elev=0, azim=0, roll=30) + ax.set_title('elev=0, azim=0, roll=30') + +Equal aspect ratio for 3D plots +------------------------------- + +Users can set the aspect ratio for the X, Y, Z axes of a 3D plot to be 'equal', +'equalxy', 'equalxz', or 'equalyz' rather than the default of 'auto'. + +.. plot:: + :alt: Five plots, each showing a different aspect option for a rectangle that has height 4, depth 1, and width 1. auto: none of the dimensions have equal aspect, depth and width form a rectangular and height appears shrunken in proportion. equal: all the dimensions have equal aspect. equalxy: width and depth equal, height not so looks shrunken in proportion. equalyz: depth and height equal, width not so elongated. equalxz: width and height equal, depth not so elongated. + :include-source: true + + from itertools import combinations, product + + aspects = [ + ['auto', 'equal', '.'], + ['equalxy', 'equalyz', 'equalxz'], + ] + fig, axs = plt.subplot_mosaic(aspects, figsize=(7, 6), + subplot_kw={'projection': '3d'}) + + # Draw rectangular cuboid with side lengths [1, 1, 5] + r = [0, 1] + scale = np.array([1, 1, 5]) + pts = combinations(np.array(list(product(r, r, r))), 2) + for start, end in pts: + if np.sum(np.abs(start - end)) == r[1] - r[0]: + for ax in axs.values(): + ax.plot3D(*zip(start*scale, end*scale), color='C0') + + # Set the aspect ratios + for aspect, ax in axs.items(): + ax.set_box_aspect((3, 4, 5)) + ax.set_aspect(aspect) + ax.set_title(f'set_aspect({aspect!r})') + +Interactive tool improvements +============================= + +Rotation, aspect ratio correction and add/remove state +------------------------------------------------------ + +The `.RectangleSelector` and `.EllipseSelector` can now be rotated +interactively between -45° and 45°. The range limits are currently dictated by +the implementation. The rotation is enabled or disabled by striking the *r* key +('r' is the default key mapped to 'rotate' in *state_modifier_keys*) or by +calling ``selector.add_state('rotate')``. + +The aspect ratio of the axes can now be taken into account when using the +"square" state. This is enabled by specifying ``use_data_coordinates='True'`` +when the selector is initialized. + +In addition to changing selector state interactively using the modifier keys +defined in *state_modifier_keys*, the selector state can now be changed +programmatically using the *add_state* and *remove_state* methods. + +.. code-block:: python + + from matplotlib.widgets import RectangleSelector + + values = np.arange(0, 100) + + fig = plt.figure() + ax = fig.add_subplot() + ax.plot(values, values) + + selector = RectangleSelector(ax, print, interactive=True, + drag_from_anywhere=True, + use_data_coordinates=True) + selector.add_state('rotate') # alternatively press 'r' key + # rotate the selector interactively + + selector.remove_state('rotate') # alternatively press 'r' key + + selector.add_state('square') + +``MultiCursor`` now supports Axes split over multiple figures +------------------------------------------------------------- + +Previously, `.MultiCursor` only worked if all target Axes belonged to the same +figure. + +As a consequence of this change, the first argument to the `.MultiCursor` +constructor has become unused (it was previously the joint canvas of all Axes, +but the canvases are now directly inferred from the list of Axes). + +``PolygonSelector`` bounding boxes +---------------------------------- + +`.PolygonSelector` now has a *draw_bounding_box* argument, which when set to +`True` will draw a bounding box around the polygon once it is complete. The +bounding box can be resized and moved, allowing the points of the polygon to be +easily resized. + +Setting ``PolygonSelector`` vertices +------------------------------------ + +The vertices of `.PolygonSelector` can now be set programmatically by using the +`.PolygonSelector.verts` property. Setting the vertices this way will reset the +selector, and create a new complete selector with the supplied vertices. + +``SpanSelector`` widget can now be snapped to specified values +-------------------------------------------------------------- + +The `.SpanSelector` widget can now be snapped to values specified by the +*snap_values* argument. + +More toolbar icons are styled for dark themes +--------------------------------------------- + +On the macOS and Tk backends, toolbar icons will now be inverted when using a +dark theme. + +Platform-specific changes +========================= + +Wx backend uses standard toolbar +-------------------------------- + +Instead of a custom sizer, the toolbar is set on Wx windows as a standard +toolbar. + +Improvements to macosx backend +------------------------------ + +Modifier keys handled more consistently +....................................... + +The macosx backend now handles modifier keys in a manner more consistent with +other backends. See the table in :ref:`event-connections` for further +information. + +``savefig.directory`` rcParam support +..................................... + +The macosx backend will now obey the :rc:`savefig.directory` setting. If set to +a non-empty string, then the save dialog will default to this directory, and +preserve subsequent save directories as they are changed. + +``figure.raise_window`` rcParam support +....................................... + +The macosx backend will now obey the :rc:`figure.raise_window` setting. If set +to False, figure windows will not be raised to the top on update. + +Full-screen toggle support +.......................... + +As supported on other backends, the macosx backend now supports toggling +fullscreen view. By default, this view can be toggled by pressing the :kbd:`f` +key. + +Improved animation and blitting support +....................................... + +The macosx backend has been improved to fix blitting, animation frames with new +artists, and to reduce unnecessary draw calls. + +macOS application icon applied on Qt backend +-------------------------------------------- + +When using the Qt-based backends on macOS, the application icon will now be +set, as is done on other backends/platforms. + +New minimum macOS version +------------------------- + +The macosx backend now requires macOS >= 10.12. + +Windows on ARM support +---------------------- + +Preliminary support for Windows on arm64 target has been added. This support +requires FreeType 2.11 or above. + +No binary wheels are available yet but it may be built from source. diff --git a/doc/users/prev_whats_new/whats_new_3.7.0.rst b/doc/users/prev_whats_new/whats_new_3.7.0.rst new file mode 100644 index 000000000000..96ac42be38f2 --- /dev/null +++ b/doc/users/prev_whats_new/whats_new_3.7.0.rst @@ -0,0 +1,451 @@ +============================================= +What's new in Matplotlib 3.7.0 (Feb 13, 2023) +============================================= + +For a list of all of the issues and pull requests since the last revision, see +the :ref:`github-stats`. + +.. contents:: Table of Contents + :depth: 4 + +.. toctree:: + :maxdepth: 4 + +Plotting and Annotation improvements +==================================== + + +``hatch`` parameter for pie +--------------------------- + +`~matplotlib.axes.Axes.pie` now accepts a *hatch* keyword that takes as input +a hatch or list of hatches: + +.. plot:: + :include-source: true + :alt: Two pie charts, identified as ax1 and ax2, both have a small blue slice, a medium orange slice, and a large green slice. ax1 has a dot hatching on the small slice, a small open circle hatching on the medium slice, and a large open circle hatching on the large slice. ax2 has the same large open circle with a dot hatch on every slice. + + fig, (ax1, ax2) = plt.subplots(ncols=2) + x = [10, 30, 60] + + ax1.pie(x, hatch=['.', 'o', 'O']) + ax2.pie(x, hatch='.O') + + ax1.set_title("hatch=['.', 'o', 'O']") + ax2.set_title("hatch='.O'") + + +Polar plot errors drawn in polar coordinates +-------------------------------------------- +Caps and error lines are now drawn with respect to polar coordinates, +when plotting errorbars on polar plots. + +.. figure:: /gallery/pie_and_polar_charts/images/sphx_glr_polar_error_caps_001.png + :target: ../../gallery/pie_and_polar_charts/polar_error_caps.html + + + +Additional format string options in `~matplotlib.axes.Axes.bar_label` +--------------------------------------------------------------------- + +The ``fmt`` argument of `~matplotlib.axes.Axes.bar_label` now accepts +{}-style format strings: + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + + fruit_names = ['Coffee', 'Salted Caramel', 'Pistachio'] + fruit_counts = [4000, 2000, 7000] + + fig, ax = plt.subplots() + bar_container = ax.bar(fruit_names, fruit_counts) + ax.set(ylabel='pints sold', title='Gelato sales by flavor', ylim=(0, 8000)) + ax.bar_label(bar_container, fmt='{:,.0f}') + +It also accepts callables: + +.. plot:: + :include-source: true + + animal_names = ['Lion', 'Gazelle', 'Cheetah'] + mph_speed = [50, 60, 75] + + fig, ax = plt.subplots() + bar_container = ax.bar(animal_names, mph_speed) + ax.set(ylabel='speed in MPH', title='Running speeds', ylim=(0, 80)) + ax.bar_label( + bar_container, fmt=lambda x: '{:.1f} km/h'.format(x * 1.61) + ) + + + +``ellipse`` boxstyle option for annotations +------------------------------------------- + +The ``'ellipse'`` option for boxstyle can now be used to create annotations +with an elliptical outline. It can be used as a closed curve shape for +longer texts instead of the ``'circle'`` boxstyle which can get quite big. + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + fig, ax = plt.subplots(figsize=(5, 5)) + t = ax.text(0.5, 0.5, "elliptical box", + ha="center", size=15, + bbox=dict(boxstyle="ellipse,pad=0.3")) + + +The *extent* of ``imshow`` can now be expressed with units +---------------------------------------------------------- +The *extent* parameter of `~.axes.Axes.imshow` and `~.AxesImage.set_extent` +can now be expressed with units. + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + import numpy as np + + fig, ax = plt.subplots(layout='constrained') + date_first = np.datetime64('2020-01-01', 'D') + date_last = np.datetime64('2020-01-11', 'D') + + arr = [[i+j for i in range(10)] for j in range(10)] + + ax.imshow(arr, origin='lower', extent=[0, 10, date_first, date_last]) + + plt.show() + +Reversed order of legend entries +-------------------------------- +The order of legend entries can now be reversed by passing ``reverse=True`` to +`~.Axes.legend`. + + +``pcolormesh`` accepts RGB(A) colors +------------------------------------ + +The `~.Axes.pcolormesh` method can now handle explicit colors +specified with RGB(A) values. To specify colors, the array must be 3D +with a shape of ``(M, N, [3, 4])``. + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + import numpy as np + + colors = np.linspace(0, 1, 90).reshape((5, 6, 3)) + plt.pcolormesh(colors) + plt.show() + + + + +View current appearance settings for ticks, tick labels, and gridlines +---------------------------------------------------------------------- + +The new `~matplotlib.axis.Axis.get_tick_params` method can be used to +retrieve the appearance settings that will be applied to any +additional ticks, tick labels, and gridlines added to the plot: + +.. code-block:: pycon + + >>> import matplotlib.pyplot as plt + + >>> fig, ax = plt.subplots() + >>> ax.yaxis.set_tick_params(labelsize=30, labelcolor='red', + ... direction='out', which='major') + >>> ax.yaxis.get_tick_params(which='major') + {'direction': 'out', + 'left': True, + 'right': False, + 'labelleft': True, + 'labelright': False, + 'gridOn': False, + 'labelsize': 30, + 'labelcolor': 'red'} + >>> ax.yaxis.get_tick_params(which='minor') + {'left': True, + 'right': False, + 'labelleft': True, + 'labelright': False, + 'gridOn': False} + + + +Style files can be imported from third-party packages +----------------------------------------------------- + +Third-party packages can now distribute style files that are globally available +as follows. Assume that a package is importable as ``import mypackage``, with +a ``mypackage/__init__.py`` module. Then a ``mypackage/presentation.mplstyle`` +style sheet can be used as ``plt.style.use("mypackage.presentation")``. + +The implementation does not actually import ``mypackage``, making this process +safe against possible import-time side effects. Subpackages (e.g. +``dotted.package.name``) are also supported. + + +Improvements to 3D Plotting +=========================== + + +3D plot pan and zoom buttons +---------------------------- + +The pan and zoom buttons in the toolbar of 3D plots are now enabled. +Unselect both to rotate the plot. When the zoom button is pressed, +zoom in by using the left mouse button to draw a bounding box, and +out by using the right mouse button to draw the box. When zooming a +3D plot, the current view aspect ratios are kept fixed. + + +*adjustable* keyword argument for setting equal aspect ratios in 3D +------------------------------------------------------------------- + +While setting equal aspect ratios for 3D plots, users can choose to modify +either the data limits or the bounding box in parity with 2D Axes. + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + import numpy as np + from itertools import combinations, product + + aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz') + fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'}, + figsize=(12, 6)) + + # Draw rectangular cuboid with side lengths [4, 3, 5] + r = [0, 1] + scale = np.array([4, 3, 5]) + pts = combinations(np.array(list(product(r, r, r))), 2) + for start, end in pts: + if np.sum(np.abs(start - end)) == r[1] - r[0]: + for ax in axs: + ax.plot3D(*zip(start*scale, end*scale), color='C0') + + # Set the aspect ratios + for i, ax in enumerate(axs): + ax.set_aspect(aspects[i], adjustable='datalim') + # Alternatively: ax.set_aspect(aspects[i], adjustable='box') + # which will change the box aspect ratio instead of axis data limits. + ax.set_title(f"set_aspect('{aspects[i]}')") + + plt.show() + + +``Poly3DCollection`` supports shading +------------------------------------- + +It is now possible to shade a `.Poly3DCollection`. This is useful if the +polygons are obtained from e.g. a 3D model. + +.. plot:: + :include-source: true + + import numpy as np + import matplotlib.pyplot as plt + from mpl_toolkits.mplot3d.art3d import Poly3DCollection + + # Define 3D shape + block = np.array([ + [[1, 1, 0], + [1, 0, 0], + [0, 1, 0]], + [[1, 1, 0], + [1, 1, 1], + [1, 0, 0]], + [[1, 1, 0], + [1, 1, 1], + [0, 1, 0]], + [[1, 0, 0], + [1, 1, 1], + [0, 1, 0]] + ]) + + ax = plt.subplot(projection='3d') + pc = Poly3DCollection(block, facecolors='b', shade=True) + ax.add_collection(pc) + plt.show() + + + +rcParam for 3D pane color +------------------------- + +The rcParams :rc:`axes3d.xaxis.panecolor`, :rc:`axes3d.yaxis.panecolor`, +:rc:`axes3d.zaxis.panecolor` can be used to change the color of the background +panes in 3D plots. Note that it is often beneficial to give them slightly +different shades to obtain a "3D effect" and to make them slightly transparent +(alpha < 1). + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + with plt.rc_context({'axes3d.xaxis.panecolor': (0.9, 0.0, 0.0, 0.5), + 'axes3d.yaxis.panecolor': (0.7, 0.0, 0.0, 0.5), + 'axes3d.zaxis.panecolor': (0.8, 0.0, 0.0, 0.5)}): + fig = plt.figure() + fig.add_subplot(projection='3d') + + + + +Figure and Axes Layout +====================== + +``colorbar`` now has a *location* keyword argument +-------------------------------------------------- + +The ``colorbar`` method now supports a *location* keyword argument to more +easily position the color bar. This is useful when providing your own inset +axes using the *cax* keyword argument and behaves similar to the case where +axes are not provided (where the *location* keyword is passed through). +*orientation* and *ticklocation* are no longer required as they are +determined by *location*. *ticklocation* can still be provided if the +automatic setting is not preferred. (*orientation* can also be provided but +must be compatible with the *location*.) + +An example is: + +.. plot:: + :include-source: true + + import matplotlib.pyplot as plt + import numpy as np + rng = np.random.default_rng(19680801) + imdata = rng.random((10, 10)) + fig, ax = plt.subplots(layout='constrained') + im = ax.imshow(imdata) + fig.colorbar(im, cax=ax.inset_axes([0, 1.05, 1, 0.05]), + location='top') + + + +Figure legends can be placed outside figures using constrained_layout +--------------------------------------------------------------------- +Constrained layout will make space for Figure legends if they are specified +by a *loc* keyword argument that starts with the string "outside". The +codes are unique from axes codes, in that "outside upper right" will +make room at the top of the figure for the legend, whereas +"outside right upper" will make room on the right-hand side of the figure. +See :doc:`/tutorials/intermediate/legend_guide` for details. + + +Per-subplot keyword arguments in ``subplot_mosaic`` +---------------------------------------------------- + +It is now possible to pass keyword arguments through to Axes creation in each +specific call to ``add_subplot`` in `.Figure.subplot_mosaic` and +`.pyplot.subplot_mosaic` : + +.. plot:: + :include-source: true + + fig, axd = plt.subplot_mosaic( + "AB;CD", + per_subplot_kw={ + "A": {"projection": "polar"}, + ("C", "D"): {"xscale": "log"}, + "B": {"projection": "3d"}, + }, + ) + + +This is particularly useful for creating mosaics with mixed projections, but +any keyword arguments can be passed through. + + +``subplot_mosaic`` no longer provisional +---------------------------------------- + +The API on `.Figure.subplot_mosaic` and `.pyplot.subplot_mosaic` are now +considered stable and will change under Matplotlib's normal deprecation +process. + + +Widget Improvements +=================== + + +Custom styling of button widgets +-------------------------------- + +Additional custom styling of button widgets may be achieved via the +*label_props* and *radio_props* arguments to `.RadioButtons`; and the +*label_props*, *frame_props*, and *check_props* arguments to `.CheckButtons`. + +.. plot:: + :include-source: true + + from matplotlib.widgets import CheckButtons, RadioButtons + + fig, ax = plt.subplots(nrows=2, ncols=2, figsize=(5, 2), width_ratios=[1, 2]) + default_rb = RadioButtons(ax[0, 0], ['Apples', 'Oranges']) + styled_rb = RadioButtons(ax[0, 1], ['Apples', 'Oranges'], + label_props={'color': ['red', 'orange'], + 'fontsize': [16, 20]}, + radio_props={'edgecolor': ['red', 'orange'], + 'facecolor': ['mistyrose', 'peachpuff']}) + + default_cb = CheckButtons(ax[1, 0], ['Apples', 'Oranges'], + actives=[True, True]) + styled_cb = CheckButtons(ax[1, 1], ['Apples', 'Oranges'], + actives=[True, True], + label_props={'color': ['red', 'orange'], + 'fontsize': [16, 20]}, + frame_props={'edgecolor': ['red', 'orange'], + 'facecolor': ['mistyrose', 'peachpuff']}, + check_props={'color': ['darkred', 'darkorange']}) + + ax[0, 0].set_title('Default') + ax[0, 1].set_title('Stylized') + + +Blitting in Button widgets +-------------------------- + +The `.Button`, `.CheckButtons`, and `.RadioButtons` widgets now support +blitting for faster rendering, on backends that support it, by passing +``useblit=True`` to the constructor. Blitting is enabled by default on +supported backends. + + +Other Improvements +================== + + +Source links can be shown or hidden for each Sphinx plot directive +------------------------------------------------------------------ +The :doc:`Sphinx plot directive ` +(``.. plot::``) now supports a ``:show-source-link:`` option to show or hide +the link to the source code for each plot. The default is set using the +``plot_html_show_source_link`` variable in :file:`conf.py` (which +defaults to True). + + + +Figure hooks +------------ + +The new :rc:`figure.hooks` provides a mechanism to register +arbitrary customizations on pyplot figures; it is a list of +"dotted.module.name:dotted.callable.name" strings specifying functions +that are called on each figure created by `.pyplot.figure`; these +functions can e.g. attach callbacks or modify the toolbar. See +:doc:`/gallery/user_interfaces/mplcvd` for an example of toolbar customization. + + +New & Improved Narrative Documentation +====================================== +* Brand new :doc:`Animations ` tutorial. +* New grouped and stacked `bar chart <../../gallery/index.html#lines_bars_and_markers>`_ examples. +* New section for new contributors and reorganized git instructions in the :ref:`contributing guide`. +* Restructured :doc:`/tutorials/text/annotations` tutorial. diff --git a/doc/users/project/citing.rst b/doc/users/project/citing.rst new file mode 100644 index 000000000000..6c01fb81a350 --- /dev/null +++ b/doc/users/project/citing.rst @@ -0,0 +1,180 @@ +.. redirect-from:: /citing + +Citing Matplotlib +================= + +If Matplotlib contributes to a project that leads to a scientific publication, +please acknowledge this fact by citing `J. D. Hunter, "Matplotlib: A 2D +Graphics Environment", Computing in Science & Engineering, vol. 9, no. 3, +pp. 90-95, 2007 `_. + +.. literalinclude:: ../../../CITATION.bib + :language: bibtex + +.. container:: sphx-glr-download + + :download:`Download BibTeX bibliography file: CITATION.bib <../../../CITATION.bib>` + +DOIs +---- + +The following DOI represents *all* Matplotlib versions. Please select a more +specific DOI from the list below, referring to the version used for your publication. + + .. image:: https://zenodo.org/badge/DOI/10.5281/zenodo.592536.svg + :target: https://doi.org/10.5281/zenodo.592536 + +By version +~~~~~~~~~~ +.. START OF AUTOGENERATED + + +v3.7.0 + .. image:: ../../_static/zenodo_cache/7637593.svg + :target: https://doi.org/10.5281/zenodo.7637593 +v3.6.3 + .. image:: ../../_static/zenodo_cache/7527665.svg + :target: https://doi.org/10.5281/zenodo.7527665 +v3.6.2 + .. image:: ../../_static/zenodo_cache/7275322.svg + :target: https://doi.org/10.5281/zenodo.7275322 +v3.6.1 + .. image:: ../../_static/zenodo_cache/7162185.svg + :target: https://doi.org/10.5281/zenodo.7162185 +v3.6.0 + .. image:: ../../_static/zenodo_cache/7084615.svg + :target: https://doi.org/10.5281/zenodo.7084615 +v3.5.3 + .. image:: ../../_static/zenodo_cache/6982547.svg + :target: https://doi.org/10.5281/zenodo.6982547 +v3.5.2 + .. image:: ../../_static/zenodo_cache/6513224.svg + :target: https://doi.org/10.5281/zenodo.6513224 +v3.5.1 + .. image:: ../../_static/zenodo_cache/5773480.svg + :target: https://doi.org/10.5281/zenodo.5773480 +v3.5.0 + .. image:: ../../_static/zenodo_cache/5706396.svg + :target: https://doi.org/10.5281/zenodo.5706396 +v3.4.3 + .. image:: ../../_static/zenodo_cache/5194481.svg + :target: https://doi.org/10.5281/zenodo.5194481 +v3.4.2 + .. image:: ../../_static/zenodo_cache/4743323.svg + :target: https://doi.org/10.5281/zenodo.4743323 +v3.4.1 + .. image:: ../../_static/zenodo_cache/4649959.svg + :target: https://doi.org/10.5281/zenodo.4649959 +v3.4.0 + .. image:: ../../_static/zenodo_cache/4638398.svg + :target: https://doi.org/10.5281/zenodo.4638398 +v3.3.4 + .. image:: ../../_static/zenodo_cache/4475376.svg + :target: https://doi.org/10.5281/zenodo.4475376 +v3.3.3 + .. image:: ../../_static/zenodo_cache/4268928.svg + :target: https://doi.org/10.5281/zenodo.4268928 +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 +v3.3.0 + .. image:: ../../_static/zenodo_cache/3948793.svg + :target: https://doi.org/10.5281/zenodo.3948793 +v3.2.2 + .. image:: ../../_static/zenodo_cache/3898017.svg + :target: https://doi.org/10.5281/zenodo.3898017 +v3.2.1 + .. image:: ../../_static/zenodo_cache/3714460.svg + :target: https://doi.org/10.5281/zenodo.3714460 +v3.2.0 + .. image:: ../../_static/zenodo_cache/3695547.svg + :target: https://doi.org/10.5281/zenodo.3695547 +v3.1.3 + .. image:: ../../_static/zenodo_cache/3633844.svg + :target: https://doi.org/10.5281/zenodo.3633844 +v3.1.2 + .. image:: ../../_static/zenodo_cache/3563226.svg + :target: https://doi.org/10.5281/zenodo.3563226 +v3.1.1 + .. image:: ../../_static/zenodo_cache/3264781.svg + :target: https://doi.org/10.5281/zenodo.3264781 +v3.1.0 + .. image:: ../../_static/zenodo_cache/2893252.svg + :target: https://doi.org/10.5281/zenodo.2893252 +v3.0.3 + .. image:: ../../_static/zenodo_cache/2577644.svg + :target: https://doi.org/10.5281/zenodo.2577644 +v3.0.2 + .. image:: ../../_static/zenodo_cache/1482099.svg + :target: https://doi.org/10.5281/zenodo.1482099 +v3.0.1 + .. image:: ../../_static/zenodo_cache/1482098.svg + :target: https://doi.org/10.5281/zenodo.1482098 +v2.2.5 + .. image:: ../../_static/zenodo_cache/3633833.svg + :target: https://doi.org/10.5281/zenodo.3633833 +v3.0.0 + .. image:: ../../_static/zenodo_cache/1420605.svg + :target: https://doi.org/10.5281/zenodo.1420605 +v2.2.4 + .. image:: ../../_static/zenodo_cache/2669103.svg + :target: https://doi.org/10.5281/zenodo.2669103 +v2.2.3 + .. image:: ../../_static/zenodo_cache/1343133.svg + :target: https://doi.org/10.5281/zenodo.1343133 +v2.2.2 + .. image:: ../../_static/zenodo_cache/1202077.svg + :target: https://doi.org/10.5281/zenodo.1202077 +v2.2.1 + .. image:: ../../_static/zenodo_cache/1202050.svg + :target: https://doi.org/10.5281/zenodo.1202050 +v2.2.0 + .. image:: ../../_static/zenodo_cache/1189358.svg + :target: https://doi.org/10.5281/zenodo.1189358 +v2.1.2 + .. image:: ../../_static/zenodo_cache/1154287.svg + :target: https://doi.org/10.5281/zenodo.1154287 +v2.1.1 + .. image:: ../../_static/zenodo_cache/1098480.svg + :target: https://doi.org/10.5281/zenodo.1098480 +v2.1.0 + .. image:: ../../_static/zenodo_cache/1004650.svg + :target: https://doi.org/10.5281/zenodo.1004650 +v2.0.2 + .. image:: ../../_static/zenodo_cache/573577.svg + :target: https://doi.org/10.5281/zenodo.573577 +v2.0.1 + .. image:: ../../_static/zenodo_cache/570311.svg + :target: https://doi.org/10.5281/zenodo.570311 +v2.0.0 + .. image:: ../../_static/zenodo_cache/248351.svg + :target: https://doi.org/10.5281/zenodo.248351 +v1.5.3 + .. image:: ../../_static/zenodo_cache/61948.svg + :target: https://doi.org/10.5281/zenodo.61948 +v1.5.2 + .. image:: ../../_static/zenodo_cache/56926.svg + :target: https://doi.org/10.5281/zenodo.56926 +v1.5.1 + .. image:: ../../_static/zenodo_cache/44579.svg + :target: https://doi.org/10.5281/zenodo.44579 +v1.5.0 + .. image:: ../../_static/zenodo_cache/32914.svg + :target: https://doi.org/10.5281/zenodo.32914 +v1.4.3 + .. image:: ../../_static/zenodo_cache/15423.svg + :target: https://doi.org/10.5281/zenodo.15423 +v1.4.2 + .. image:: ../../_static/zenodo_cache/12400.svg + :target: https://doi.org/10.5281/zenodo.12400 +v1.4.1 + .. image:: ../../_static/zenodo_cache/12287.svg + :target: https://doi.org/10.5281/zenodo.12287 +v1.4.0 + .. image:: ../../_static/zenodo_cache/11451.svg + :target: https://doi.org/10.5281/zenodo.11451 + +.. END OF AUTOGENERATED diff --git a/doc/users/credits.rst b/doc/users/project/credits.rst similarity index 99% rename from doc/users/credits.rst rename to doc/users/project/credits.rst index 0c0feed44218..c23d6ac11298 100644 --- a/doc/users/credits.rst +++ b/doc/users/project/credits.rst @@ -1,5 +1,7 @@ .. Note: This file is auto-generated using generate_credits.py +.. redirect-from:: /users/credits + .. _credits: ******* @@ -11,7 +13,7 @@ Matplotlib was written by John D. Hunter, with contributions from an ever-increasing number of users and developers. The current lead developer is Thomas A. Caswell, who is assisted by many `active developers `_. -Please also see our instructions on :doc:`/citing`. +Please also see our instructions on :doc:`/users/project/citing`. The following is a list of contributors extracted from the git revision control history of the project: diff --git a/doc/users/history.rst b/doc/users/project/history.rst similarity index 95% rename from doc/users/history.rst rename to doc/users/project/history.rst index 5294382b951e..3291d7e43f8b 100644 --- a/doc/users/history.rst +++ b/doc/users/project/history.rst @@ -1,3 +1,7 @@ +.. redirect-from:: /users/history + +.. _project_history: + History ======= @@ -9,7 +13,7 @@ History Matplotlib is a library for making 2D plots of arrays in `Python `_. Although it has its origins in emulating the MATLAB graphics commands, it is -independent of MATLAB, and can be used in a Pythonic, object oriented +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 code to provide good performance even for large arrays. @@ -72,17 +76,17 @@ nothing about output. The *backends* are device-dependent drawing devices, aka renderers, that transform the frontend representation to hardcopy or a display device (:ref:`what-is-a-backend`). Example backends: PS creates `PostScript® -`_ hardcopy, SVG +`_ hardcopy, SVG creates `Scalable Vector Graphics `_ hardcopy, Agg creates PNG output using the high quality `Anti-Grain -Geometry `_ +Geometry `_ library that ships with Matplotlib, GTK embeds Matplotlib in a `Gtk+ `_ application, GTKAgg uses the Anti-Grain renderer to create a figure and embed it in a Gtk+ application, and so on for `PDF -`_, `WxWidgets +`_, `WxWidgets `_, `Tkinter -`_, etc. +`_, etc. Matplotlib is used by many people in many different contexts. Some people want to automatically generate PostScript files to send diff --git a/doc/users/project/index.rst b/doc/users/project/index.rst new file mode 100644 index 000000000000..27f60d166abb --- /dev/null +++ b/doc/users/project/index.rst @@ -0,0 +1,13 @@ +.. redirect-from:: /users/backmatter + +Project information +------------------- + +.. toctree:: + :maxdepth: 2 + + mission.rst + history.rst + citing.rst + license.rst + credits.rst diff --git a/doc/users/project/license.rst b/doc/users/project/license.rst new file mode 100644 index 000000000000..a55927d9f2c5 --- /dev/null +++ b/doc/users/project/license.rst @@ -0,0 +1,50 @@ +.. _license: + +.. redirect-from:: /users/license + +******* +License +******* + +Matplotlib only uses BSD compatible code, and its license is based on +the `PSF `_ license. See the Open +Source Initiative `licenses page +`_ for details on individual +licenses. Non-BSD compatible licenses (e.g., LGPL) are acceptable in +matplotlib toolkits. For a discussion of the motivations behind the +licencing choice, see :ref:`license-discussion`. + +Copyright policy +================ + +John Hunter began Matplotlib around 2003. Since shortly before his +passing in 2012, Michael Droettboom has been the lead maintainer of +Matplotlib, but, as has always been the case, Matplotlib is the work +of many. + +Prior to July of 2013, and the 1.3.0 release, the copyright of the +source code was held by John Hunter. As of July 2013, and the 1.3.0 +release, matplotlib has moved to a shared copyright model. + +Matplotlib uses a shared copyright model. Each contributor maintains +copyright over their contributions to Matplotlib. But, it is important to +note that these contributions are typically only changes to the +repositories. Thus, the Matplotlib source code, in its entirety, is not +the copyright of any single person or institution. Instead, it is the +collective copyright of the entire Matplotlib Development Team. If +individual contributors want to maintain a record of what +changes/contributions they have specific copyright on, they should +indicate their copyright in the commit message of the change, when +they commit the change to one of the matplotlib repositories. + +The Matplotlib Development Team is the set of all contributors to the +matplotlib project. A full list can be obtained from the git version +control logs. + +.. _license-agreement: + +License agreement +================= + +.. literalinclude:: ../../../LICENSE/LICENSE + :language: none diff --git a/doc/users/project/mission.rst b/doc/users/project/mission.rst new file mode 100644 index 000000000000..431da04e4891 --- /dev/null +++ b/doc/users/project/mission.rst @@ -0,0 +1,21 @@ +Mission Statement +================= + +The Matplotlib developer community develops, maintains, and supports Matplotlib +and its extensions to provide data visualization tools for the Scientific +Python Ecosystem. + +Adapting the requirements :ref:`laid out by John Hunter ` +Matplotlib should: + +* Support users of the Scientific Python ecosystem; +* Facilitate interactive data exploration; +* Produce high-quality raster and vector format outputs suitable for publication; +* Provide a simple graphical user interface and support embedding in applications; +* Be understandable and extensible by people familiar with data processing in Python; +* Make common plots easy, and novel or complex visualizations possible. + +We believe that a diverse developer community creates the best software, and we +welcome anyone who shares our mission, and our values described in the `code of +conduct +`__. diff --git a/doc/users/release_notes.rst b/doc/users/release_notes.rst new file mode 100644 index 000000000000..55264842ecd4 --- /dev/null +++ b/doc/users/release_notes.rst @@ -0,0 +1,236 @@ +.. redirect-from:: /api/api_changes_old +.. redirect-from:: /users/whats_new_old + + +============= +Release notes +============= + +.. include from another document so that it's easy to exclude this for releases +.. .. include:: release_notes_next.rst + + +Version 3.7 +=========== +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_3.7.0.rst + ../api/prev_api_changes/api_changes_3.7.0.rst + github_stats.rst + prev_whats_new/github_stats_3.7.0.rst + + +Version 3.6 +=========== +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_3.6.0.rst + ../api/prev_api_changes/api_changes_3.6.1.rst + ../api/prev_api_changes/api_changes_3.6.0.rst + prev_whats_new/github_stats_3.6.3.rst + prev_whats_new/github_stats_3.6.2.rst + prev_whats_new/github_stats_3.6.1.rst + prev_whats_new/github_stats_3.6.0.rst + +Version 3.5 +=========== +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_3.5.2.rst + prev_whats_new/whats_new_3.5.0.rst + ../api/prev_api_changes/api_changes_3.5.3.rst + ../api/prev_api_changes/api_changes_3.5.2.rst + ../api/prev_api_changes/api_changes_3.5.0.rst + prev_whats_new/github_stats_3.5.3.rst + prev_whats_new/github_stats_3.5.2.rst + prev_whats_new/github_stats_3.5.1.rst + prev_whats_new/github_stats_3.5.0.rst + +Version 3.4 +=========== +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_3.4.0.rst + ../api/prev_api_changes/api_changes_3.4.2.rst + ../api/prev_api_changes/api_changes_3.4.0.rst + prev_whats_new/github_stats_3.4.1.rst + prev_whats_new/github_stats_3.4.0.rst + +Past versions +============= + +Version 3.3 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_3.3.0.rst + ../api/prev_api_changes/api_changes_3.3.1.rst + ../api/prev_api_changes/api_changes_3.3.0.rst + prev_whats_new/github_stats_3.3.4.rst + prev_whats_new/github_stats_3.3.3.rst + prev_whats_new/github_stats_3.3.2.rst + prev_whats_new/github_stats_3.3.1.rst + prev_whats_new/github_stats_3.3.0.rst + +Version 3.2 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_3.2.0.rst + ../api/prev_api_changes/api_changes_3.2.0.rst + prev_whats_new/github_stats_3.2.2.rst + prev_whats_new/github_stats_3.2.1.rst + prev_whats_new/github_stats_3.2.0.rst + +Version 3.1 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_3.1.0.rst + ../api/prev_api_changes/api_changes_3.1.1.rst + ../api/prev_api_changes/api_changes_3.1.0.rst + prev_whats_new/github_stats_3.1.3.rst + prev_whats_new/github_stats_3.1.2.rst + prev_whats_new/github_stats_3.1.1.rst + prev_whats_new/github_stats_3.1.0.rst + +Version 3.0 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_3.0.rst + ../api/prev_api_changes/api_changes_3.0.1.rst + ../api/prev_api_changes/api_changes_3.0.0.rst + prev_whats_new/github_stats_3.0.3.rst + prev_whats_new/github_stats_3.0.2.rst + prev_whats_new/github_stats_3.0.1.rst + prev_whats_new/github_stats_3.0.0.rst + +Version 2.2 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_2.2.rst + ../api/prev_api_changes/api_changes_2.2.0.rst + +Version 2.1 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_2.1.0.rst + ../api/prev_api_changes/api_changes_2.1.2.rst + ../api/prev_api_changes/api_changes_2.1.1.rst + ../api/prev_api_changes/api_changes_2.1.0.rst + +Version 2.0 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_2.0.0.rst + ../api/prev_api_changes/api_changes_2.0.1.rst + ../api/prev_api_changes/api_changes_2.0.0.rst + +Version 1.5 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_1.5.rst + ../api/prev_api_changes/api_changes_1.5.3.rst + ../api/prev_api_changes/api_changes_1.5.2.rst + ../api/prev_api_changes/api_changes_1.5.0.rst + +Version 1.4 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_1.4.rst + ../api/prev_api_changes/api_changes_1.4.x.rst + +Version 1.3 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_1.3.rst + ../api/prev_api_changes/api_changes_1.3.x.rst + +Version 1.2 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_1.2.2.rst + prev_whats_new/whats_new_1.2.rst + ../api/prev_api_changes/api_changes_1.2.x.rst + +Version 1.1 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_1.1.rst + ../api/prev_api_changes/api_changes_1.1.x.rst + +Version 1.0 +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/whats_new_1.0.rst + +Version 0.x +~~~~~~~~~~~ +.. toctree:: + :maxdepth: 1 + + prev_whats_new/changelog.rst + prev_whats_new/whats_new_0.99.rst + ../api/prev_api_changes/api_changes_0.99.x.rst + ../api/prev_api_changes/api_changes_0.99.rst + prev_whats_new/whats_new_0.98.4.rst + ../api/prev_api_changes/api_changes_0.98.x.rst + ../api/prev_api_changes/api_changes_0.98.1.rst + ../api/prev_api_changes/api_changes_0.98.0.rst + ../api/prev_api_changes/api_changes_0.91.2.rst + ../api/prev_api_changes/api_changes_0.91.0.rst + ../api/prev_api_changes/api_changes_0.90.1.rst + ../api/prev_api_changes/api_changes_0.90.0.rst + + ../api/prev_api_changes/api_changes_0.87.7.rst + ../api/prev_api_changes/api_changes_0.86.rst + ../api/prev_api_changes/api_changes_0.85.rst + ../api/prev_api_changes/api_changes_0.84.rst + ../api/prev_api_changes/api_changes_0.83.rst + ../api/prev_api_changes/api_changes_0.82.rst + ../api/prev_api_changes/api_changes_0.81.rst + ../api/prev_api_changes/api_changes_0.80.rst + + ../api/prev_api_changes/api_changes_0.73.rst + ../api/prev_api_changes/api_changes_0.72.rst + ../api/prev_api_changes/api_changes_0.71.rst + ../api/prev_api_changes/api_changes_0.70.rst + + ../api/prev_api_changes/api_changes_0.65.1.rst + ../api/prev_api_changes/api_changes_0.65.rst + ../api/prev_api_changes/api_changes_0.63.rst + ../api/prev_api_changes/api_changes_0.61.rst + ../api/prev_api_changes/api_changes_0.60.rst + + ../api/prev_api_changes/api_changes_0.54.3.rst + ../api/prev_api_changes/api_changes_0.54.rst + ../api/prev_api_changes/api_changes_0.50.rst + ../api/prev_api_changes/api_changes_0.42.rst + ../api/prev_api_changes/api_changes_0.40.rst diff --git a/doc/users/release_notes_next.rst b/doc/users/release_notes_next.rst new file mode 100644 index 000000000000..6813f61c5f90 --- /dev/null +++ b/doc/users/release_notes_next.rst @@ -0,0 +1,10 @@ +:orphan: + +Next version +============ +.. toctree:: + :maxdepth: 1 + + next_whats_new + ../api/next_api_changes + github_stats diff --git a/doc/users/resources/index.rst b/doc/users/resources/index.rst new file mode 100644 index 000000000000..a2c4ccd4a7fc --- /dev/null +++ b/doc/users/resources/index.rst @@ -0,0 +1,87 @@ +.. _resources-index: + +.. redirect-from:: /resources/index + +****************** +External resources +****************** + + +============================ +Books, chapters and articles +============================ + +* `Scientific Visualization: Python + Matplotlib (2021) + `_ + by Nicolas P. Rougier + +* `Mastering matplotlib + `_ + by Duncan M. McGreggor + +* `Interactive Applications Using Matplotlib + `_ + by Benjamin Root + +* `Matplotlib for Python Developers + `_ + by Sandro Tosi + +* `Matplotlib chapter `_ + by John Hunter and Michael Droettboom in The Architecture of Open Source + Applications + +* `Ten Simple Rules for Better Figures + `_ + by Nicolas P. Rougier, Michael Droettboom and Philip E. Bourne + +* `Learning Scientific Programming with Python chapter 7 + `_ + by Christian Hill + +* `Hands-On Data Analysis with Pandas, chapters 5 and 6 + `_ + by Stefanie Molin + +====== +Videos +====== + +* `Plotting with matplotlib `_ + by Mike Müller + +* `Introduction to NumPy and Matplotlib + `_ by Eric Jones + +* `Anatomy of Matplotlib + `_ + by Benjamin Root + +* `Data Visualization Basics with Python (O'Reilly) + `_ + by Randal S. Olson +* `Matplotlib Introduction + `_ + by codebasics +* `Matplotlib + `_ + by Derek Banas + +========= +Tutorials +========= + + +* `The Python Graph Gallery `_ + by Yan Holtz + +* `Matplotlib tutorial `_ + by Nicolas P. Rougier + +* `Anatomy of Matplotlib - IPython Notebooks + `_ + by Benjamin Root + +* `Beyond the Basics: Data Visualization in Python + `_ + by Stefanie Molin diff --git a/doc/users/whats_new.rst b/doc/users/whats_new.rst deleted file mode 100644 index 776f477ce4d4..000000000000 --- a/doc/users/whats_new.rst +++ /dev/null @@ -1,29 +0,0 @@ -.. _whats-new: - -=========== -What's new? -=========== - -.. ifconfig:: releaselevel == 'dev' - - .. note:: - - The list below is a table of contents of individual files from the - 'next_whats_new' folder. - - When a release is made - - - All the files in 'next_whats_new/' should be moved to a single file in - 'prev_whats_new/'. - - The include directive below should be changed to point to the new file - created in the previous step. - - - .. toctree:: - :glob: - :maxdepth: 1 - - next_whats_new/* - -.. Be sure to update the version in `exclude_patterns` in conf.py. -.. include:: prev_whats_new/whats_new_3.4.0.rst diff --git a/doc/users/whats_new_old.rst b/doc/users/whats_new_old.rst deleted file mode 100644 index d24992a1b242..000000000000 --- a/doc/users/whats_new_old.rst +++ /dev/null @@ -1,12 +0,0 @@ - -=================== -Previous What's New -=================== - -.. toctree:: - :glob: - :maxdepth: 1 - :reversed: - - prev_whats_new/changelog - prev_whats_new/whats_new_* diff --git a/environment.yml b/environment.yml new file mode 100644 index 000000000000..22cf6796ac5e --- /dev/null +++ b/environment.yml @@ -0,0 +1,63 @@ +# To set up a development environment using conda run: +# +# conda env create -f environment.yml +# conda activate mpl-dev +# pip install -e . +# +name: mpl-dev +channels: + - conda-forge +dependencies: + # runtime dependencies + - cairocffi + - contourpy>=1.0.1 + - cycler>=0.10.0 + - fonttools>=4.22.0 + - importlib-resources>=3.2.0 + - kiwisolver>=1.0.1 + - numpy>=1.20 + - pillow>=6.2 + - pybind11>=2.6.0 + - pygobject + - pyparsing + - pyqt + - python-dateutil>=2.1 + - setuptools + - setuptools_scm + - wxpython + # building documentation + - colorspacious + - graphviz + - ipython + - ipywidgets + - numpydoc>=0.8 + - packaging + - pydata-sphinx-theme + - sphinx>=1.8.1,!=2.0.0 + - sphinx-copybutton + - sphinx-gallery>=0.10 + - sphinx-design + - pip + - pip: + - mpl-sphinx-theme + - sphinxcontrib-svg2pdfconverter + - pikepdf + # testing + - coverage + - flake8>=3.8 + - flake8-docstrings>=1.4.0 + - gtk4 + - ipykernel + - nbconvert[execute]!=6.0.0,!=6.0.1 + - nbformat!=5.0.0,!=5.0.1 + - pandas!=0.25.0 + - psutil + - pre-commit + - pydocstyle>=5.1.0 + - pytest!=4.6.0,!=5.4.0 + - pytest-cov + - pytest-rerunfailures + - pytest-timeout + - pytest-xdist + - tornado + - pytz diff --git a/examples/README b/examples/README deleted file mode 100644 index 746f2e334664..000000000000 --- a/examples/README +++ /dev/null @@ -1,42 +0,0 @@ -Matplotlib examples -=================== - -There are a variety of ways to use Matplotlib, and most of them are -illustrated in the examples in this directory. - -Probably the most common way people use Matplotlib is with the -procedural interface, which follows the MATLAB/IDL/Mathematica approach -of using simple procedures like "plot" or "title" to modify the current -figure. These examples are included in the "pylab_examples" directory. -If you want to write more robust scripts, e.g., for production use or in -a web application server, you will probably want to use the Matplotlib -API for full control. These examples are found in the "api" directory. -Below is a brief description of the different directories found here: - - * animation - Dynamic plots, see the documentation at - http://matplotlib.org/api/animation_api.html - - * axes_grid1 - Examples related to the axes_grid1 toolkit. - - * axisartist - Examples related to the axisartist toolkit. - - * event_handling - How to interact with your figure, mouse presses, - key presses, object picking, etc. - - * misc - Miscellaneous examples. Some demos for loading and working - with record arrays. - - * mplot3d - 3D examples. - - * pylab_examples - The interface to Matplotlib similar to MATLAB. - - * tests - Tests used by Matplotlib developers to check functionality. - (These tests are still sometimes useful, but mostly developers should - use the pytest tests which perform automatic image comparison.) - - * units - Working with unit data and custom types in Matplotlib. - - * user_interfaces - Using Matplotlib in a GUI application, e.g., - Tkinter, wxPython, PyGObject, PyQt widgets. - - * widgets - Examples using interactive widgets. diff --git a/examples/README.txt b/examples/README.txt index f2331df7246a..8e41fb83d300 100644 --- a/examples/README.txt +++ b/examples/README.txt @@ -2,13 +2,13 @@ .. _gallery: -======= -Gallery -======= +======== +Examples +======== -This gallery contains examples of the many things you can do with -Matplotlib. Click on any image to see the full image and source code. +This page contains example plots. Click on any image to see the full image +and source code. -For longer tutorials, see our `tutorials page <../tutorials/index.html>`_. -You can also find `external resources <../resources/index.html>`_ and -a `FAQ <../faq/index.html>`_ in our `user guide <../contents.html>`_. +For longer tutorials, see our :ref:`tutorials page `. +You can also find :ref:`external resources ` and +a :ref:`FAQ ` in our :ref:`user guide `. diff --git a/examples/animation/animate_decay.py b/examples/animation/animate_decay.py index 88f25e2d6aaa..cfd95e4e5c29 100644 --- a/examples/animation/animate_decay.py +++ b/examples/animation/animate_decay.py @@ -4,8 +4,11 @@ ===== This example showcases: + - using a generator to drive an animation, - changing axes limits during an animation. + +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import itertools @@ -23,7 +26,7 @@ def data_gen(): def init(): ax.set_ylim(-1.1, 1.1) - ax.set_xlim(0, 10) + ax.set_xlim(0, 1) del xdata[:] del ydata[:] line.set_data(xdata, ydata) @@ -49,5 +52,7 @@ def run(data): return line, -ani = animation.FuncAnimation(fig, run, data_gen, interval=10, init_func=init) +# Only save last 100 frames, but run forever +ani = animation.FuncAnimation(fig, run, data_gen, interval=100, init_func=init, + save_count=100) plt.show() diff --git a/examples/animation/animated_histogram.py b/examples/animation/animated_histogram.py index 737d4c9a3833..3b242c4ce4cc 100644 --- a/examples/animation/animated_histogram.py +++ b/examples/animation/animated_histogram.py @@ -45,6 +45,8 @@ def animate(frame_number): # ``prepare_animation`` will define ``animate`` function working with supplied # `.BarContainer`, all this is used to setup `.FuncAnimation`. +# Output generated via `matplotlib.animation.Animation.to_jshtml`. + fig, ax = plt.subplots() _, _, bar_container = ax.hist(data, HIST_BINS, lw=1, ec="yellow", fc="green", alpha=0.5) diff --git a/examples/animation/animation_demo.py b/examples/animation/animation_demo.py index beab2801d731..d2de7c43e7b5 100644 --- a/examples/animation/animation_demo.py +++ b/examples/animation/animation_demo.py @@ -10,6 +10,8 @@ examples that use it. Note that calling `time.sleep` instead of `~.pyplot.pause` would *not* work. + +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import matplotlib.pyplot as plt @@ -20,9 +22,9 @@ fig, ax = plt.subplots() -for i in range(len(data)): - ax.cla() - ax.imshow(data[i]) - ax.set_title("frame {}".format(i)) +for i, img in enumerate(data): + ax.clear() + ax.imshow(img) + ax.set_title(f"frame {i}") # Note that using time.sleep does *not* work here! plt.pause(0.1) diff --git a/examples/animation/bayes_update.py b/examples/animation/bayes_update.py index d1c43c71fa31..1b56b74f656a 100644 --- a/examples/animation/bayes_update.py +++ b/examples/animation/bayes_update.py @@ -7,6 +7,8 @@ new data arrives. The vertical line represents the theoretical value to which the plotted distribution should converge. + +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import math @@ -47,7 +49,7 @@ def __call__(self, i): return self.line, # Choose success based on exceed a threshold with a uniform pick - if np.random.rand(1,) < self.prob: + if np.random.rand() < self.prob: self.success += 1 y = beta_pdf(self.x, self.success + 1, (i - self.success) + 1) self.line.set_data(self.x, y) diff --git a/examples/animation/double_pendulum.py b/examples/animation/double_pendulum.py index d497c1e24d83..8f055be79286 100644 --- a/examples/animation/double_pendulum.py +++ b/examples/animation/double_pendulum.py @@ -7,12 +7,13 @@ Double pendulum formula translated from the C code at http://www.physics.usyd.edu.au/~wheat/dpend_html/solve_dpend.c + +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ from numpy import sin, cos import numpy as np import matplotlib.pyplot as plt -import scipy.integrate as integrate import matplotlib.animation as animation from collections import deque @@ -22,13 +23,13 @@ L = L1 + L2 # maximal length of the combined pendulum M1 = 1.0 # mass of pendulum 1 in kg M2 = 1.0 # mass of pendulum 2 in kg -t_stop = 5 # how many seconds to simulate +t_stop = 2.5 # how many seconds to simulate history_len = 500 # how many trajectory points to display -def derivs(state, t): - +def derivs(t, state): dydx = np.zeros_like(state) + dydx[0] = state[1] delta = state[2] - state[0] @@ -51,7 +52,7 @@ def derivs(state, t): return dydx # create a time array from 0..t_stop sampled at 0.02 second steps -dt = 0.02 +dt = 0.01 t = np.arange(0, t_stop, dt) # th1 and th2 are the initial angles (degrees) @@ -64,8 +65,15 @@ def derivs(state, t): # initial state state = np.radians([th1, w1, th2, w2]) -# integrate your ODE using scipy.integrate. -y = integrate.odeint(derivs, state, t) +# integrate the ODE using Euler's method +y = np.empty((len(t), 4)) +y[0] = state +for i in range(1, len(t)): + y[i] = y[i - 1] + derivs(t[i - 1], y[i - 1]) * dt + +# A more accurate estimate could be obtained e.g. using scipy: +# +# y = scipy.integrate.solve_ivp(derivs, t[[0, -1]], state, t_eval=t).y.T x1 = L1*sin(y[:, 0]) y1 = -L1*cos(y[:, 0]) @@ -79,7 +87,7 @@ def derivs(state, t): ax.grid() line, = ax.plot([], [], 'o-', lw=2) -trace, = ax.plot([], [], ',-', lw=1) +trace, = ax.plot([], [], '.-', lw=1, ms=2) time_template = 'time = %.1fs' time_text = ax.text(0.05, 0.9, '', transform=ax.transAxes) history_x, history_y = deque(maxlen=history_len), deque(maxlen=history_len) diff --git a/examples/animation/dynamic_image.py b/examples/animation/dynamic_image.py index 5543da039639..d74e8bc8c1a9 100644 --- a/examples/animation/dynamic_image.py +++ b/examples/animation/dynamic_image.py @@ -3,6 +3,7 @@ Animated image using a precomputed list of images ================================================= +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np @@ -23,8 +24,8 @@ def f(x, y): # each frame ims = [] for i in range(60): - x += np.pi / 15. - y += np.pi / 20. + x += np.pi / 15 + y += np.pi / 30 im = ax.imshow(f(x, y), animated=True) if i == 0: ax.imshow(f(x, y)) # show an initial one first diff --git a/examples/animation/frame_grabbing_sgskip.py b/examples/animation/frame_grabbing_sgskip.py index e74576249aa0..3a3d5d39c04d 100644 --- a/examples/animation/frame_grabbing_sgskip.py +++ b/examples/animation/frame_grabbing_sgskip.py @@ -6,6 +6,8 @@ Use a MovieWriter directly to grab individual frames and write them to a file. This avoids any event loop integration, and thus works even with the Agg backend. This is not recommended for use in an interactive setting. + +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np diff --git a/examples/animation/multiple_axes.py b/examples/animation/multiple_axes.py new file mode 100644 index 000000000000..f3c246d151ac --- /dev/null +++ b/examples/animation/multiple_axes.py @@ -0,0 +1,81 @@ +""" +======================= +Multiple axes animation +======================= + +This example showcases: + +- how animation across multiple subplots works, +- using a figure artist in the animation. + +Output generated via `matplotlib.animation.Animation.to_jshtml`. +""" + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation +from matplotlib.patches import ConnectionPatch + +fig, (axl, axr) = plt.subplots( + ncols=2, + sharey=True, + figsize=(6, 2), + gridspec_kw=dict(width_ratios=[1, 3], wspace=0), +) +axl.set_aspect(1) +axr.set_box_aspect(1 / 3) +axr.yaxis.set_visible(False) +axr.xaxis.set_ticks([0, np.pi, 2 * np.pi], ["0", r"$\pi$", r"$2\pi$"]) + +# draw circle with initial point in left Axes +x = np.linspace(0, 2 * np.pi, 50) +axl.plot(np.cos(x), np.sin(x), "k", lw=0.3) +point, = axl.plot(0, 0, "o") + +# draw full curve to set view limits in right Axes +sine, = axr.plot(x, np.sin(x)) + +# draw connecting line between both graphs +con = ConnectionPatch( + (1, 0), + (0, 0), + "data", + "data", + axesA=axl, + axesB=axr, + color="C0", + ls="dotted", +) +fig.add_artist(con) + + +def animate(i): + x = np.linspace(0, i, int(i * 25 / np.pi)) + sine.set_data(x, np.sin(x)) + x, y = np.cos(i), np.sin(i) + point.set_data([x], [y]) + con.xy1 = x, y + con.xy2 = i, y + return point, sine, con + + +ani = animation.FuncAnimation( + fig, + animate, + interval=50, + blit=False, # blitting can't be used with Figure artists + frames=x, + repeat_delay=100, +) + +plt.show() + +############################################################################# +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.patches.ConnectionPatch` +# - `matplotlib.animation.FuncAnimation` diff --git a/examples/animation/pause_resume.py b/examples/animation/pause_resume.py index ed20197f6167..4d6f281a7e0f 100644 --- a/examples/animation/pause_resume.py +++ b/examples/animation/pause_resume.py @@ -4,8 +4,19 @@ ================================= This example showcases: + - using the Animation.pause() method to pause an animation. - using the Animation.resume() method to resume an animation. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. + +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import matplotlib.pyplot as plt diff --git a/examples/animation/rain.py b/examples/animation/rain.py index 270a6a0787af..e5c2a467dbc0 100644 --- a/examples/animation/rain.py +++ b/examples/animation/rain.py @@ -7,6 +7,8 @@ of 50 scatter points. Author: Nicolas P. Rougier + +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np @@ -67,5 +69,5 @@ def update(frame_number): # Construct the animation, using the update function as the animation director. -animation = FuncAnimation(fig, update, interval=10) +animation = FuncAnimation(fig, update, interval=10, save_count=100) plt.show() diff --git a/examples/animation/random_walk.py b/examples/animation/random_walk.py index 2488f98b144c..83cd530f6b42 100644 --- a/examples/animation/random_walk.py +++ b/examples/animation/random_walk.py @@ -3,6 +3,7 @@ Animated 3D random walk ======================= +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np @@ -13,62 +14,40 @@ np.random.seed(19680801) -def gen_rand_line(length, dims=2): - """ - Create a line using a random walk algorithm. +def random_walk(num_steps, max_step=0.05): + """Return a 3D random walk as (num_steps, 3) array.""" + start_pos = np.random.random(3) + steps = np.random.uniform(-max_step, max_step, size=(num_steps, 3)) + walk = start_pos + np.cumsum(steps, axis=0) + return walk - Parameters - ---------- - length : int - The number of points of the line. - dims : int - The number of dimensions of the line. - """ - line_data = np.empty((dims, length)) - line_data[:, 0] = np.random.rand(dims) - for index in range(1, length): - # scaling the random numbers by 0.1 so - # movement is small compared to position. - # subtraction by 0.5 is to change the range to [-0.5, 0.5] - # to allow a line to move backwards. - step = (np.random.rand(dims) - 0.5) * 0.1 - line_data[:, index] = line_data[:, index - 1] + step - return line_data - -def update_lines(num, data_lines, lines): - for line, data in zip(lines, data_lines): +def update_lines(num, walks, lines): + for line, walk in zip(lines, walks): # NOTE: there is no .set_data() for 3 dim data... - line.set_data(data[0:2, :num]) - line.set_3d_properties(data[2, :num]) + line.set_data(walk[:num, :2].T) + line.set_3d_properties(walk[:num, 2]) return lines +# Data: 40 random walks as (num_steps, 3) arrays +num_steps = 30 +walks = [random_walk(num_steps) for index in range(40)] + # Attaching 3D axis to the figure fig = plt.figure() ax = fig.add_subplot(projection="3d") -# Fifty lines of random 3-D lines -data = [gen_rand_line(25, 3) for index in range(50)] - -# Creating fifty line objects. -# NOTE: Can't pass empty arrays into 3d version of plot() -lines = [ax.plot(dat[0, 0:1], dat[1, 0:1], dat[2, 0:1])[0] for dat in data] +# Create lines initially without data +lines = [ax.plot([], [], [])[0] for _ in walks] # Setting the axes properties -ax.set_xlim3d([0.0, 1.0]) -ax.set_xlabel('X') - -ax.set_ylim3d([0.0, 1.0]) -ax.set_ylabel('Y') - -ax.set_zlim3d([0.0, 1.0]) -ax.set_zlabel('Z') - -ax.set_title('3D Test') +ax.set(xlim3d=(0, 1), xlabel='X') +ax.set(ylim3d=(0, 1), ylabel='Y') +ax.set(zlim3d=(0, 1), zlabel='Z') # Creating the Animation object -line_ani = animation.FuncAnimation( - fig, update_lines, 50, fargs=(data, lines), interval=50) +ani = animation.FuncAnimation( + fig, update_lines, num_steps, fargs=(walks, lines), interval=100) plt.show() diff --git a/examples/animation/simple_anim.py b/examples/animation/simple_anim.py index 3bb3c4d952ba..54480bc4ae6e 100644 --- a/examples/animation/simple_anim.py +++ b/examples/animation/simple_anim.py @@ -3,6 +3,7 @@ Animated line plot ================== +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np diff --git a/examples/animation/simple_scatter.py b/examples/animation/simple_scatter.py new file mode 100644 index 000000000000..a0583c81e740 --- /dev/null +++ b/examples/animation/simple_scatter.py @@ -0,0 +1,32 @@ +""" +============================= +Animated scatter saved as GIF +============================= + +Output generated via `matplotlib.animation.Animation.to_jshtml`. +""" +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.animation as animation + +fig, ax = plt.subplots() +ax.set_xlim([0, 10]) + +scat = ax.scatter(1, 0) +x = np.linspace(0, 10) + + +def animate(i): + scat.set_offsets((x[i], 0)) + return scat, + +ani = animation.FuncAnimation(fig, animate, repeat=True, + frames=len(x) - 1, interval=50) + +# To save the animation using Pillow as a gif +# writer = animation.PillowWriter(fps=15, +# metadata=dict(artist='Me'), +# bitrate=1800) +# ani.save('scatter.gif', writer=writer) + +plt.show() diff --git a/examples/animation/strip_chart.py b/examples/animation/strip_chart.py index 5e3b8fea0d04..9467ed3249b5 100644 --- a/examples/animation/strip_chart.py +++ b/examples/animation/strip_chart.py @@ -4,6 +4,8 @@ ============ Emulates an oscilloscope. + +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np @@ -26,13 +28,16 @@ def __init__(self, ax, maxt=2, dt=0.02): def update(self, y): lastt = self.tdata[-1] - if lastt > self.tdata[0] + self.maxt: # reset the arrays + if lastt >= self.tdata[0] + self.maxt: # reset the arrays self.tdata = [self.tdata[-1]] self.ydata = [self.ydata[-1]] self.ax.set_xlim(self.tdata[0], self.tdata[0] + self.maxt) self.ax.figure.canvas.draw() - t = self.tdata[-1] + self.dt + # This slightly more complex calculation avoids floating-point issues + # from just repeatedly adding `self.dt` to the previous value. + t = self.tdata[0] + len(self.tdata) * self.dt + self.tdata.append(t) self.ydata.append(y) self.line.set_data(self.tdata, self.ydata) @@ -42,11 +47,11 @@ def update(self, y): def emitter(p=0.1): """Return a random value in [0, 1) with probability p, else 0.""" while True: - v = np.random.rand(1) + v = np.random.rand() if v > p: yield 0. else: - yield np.random.rand(1) + yield np.random.rand() # Fixing random state for reproducibility @@ -58,6 +63,6 @@ def emitter(p=0.1): # pass a generator in "emitter" to produce data for the update func ani = animation.FuncAnimation(fig, scope.update, emitter, interval=50, - blit=True) + blit=True, save_count=100) plt.show() diff --git a/examples/animation/unchained.py b/examples/animation/unchained.py index 522cea10b683..3915f55dedd4 100644 --- a/examples/animation/unchained.py +++ b/examples/animation/unchained.py @@ -7,6 +7,8 @@ (mostly known because of the cover for Joy Division's Unknown Pleasures). Author: Nicolas P. Rougier + +Output generated via `matplotlib.animation.Animation.to_jshtml`. """ import numpy as np @@ -69,5 +71,5 @@ def update(*args): return lines # Construct the animation, using the update function as the animation director. -anim = animation.FuncAnimation(fig, update, interval=10) +anim = animation.FuncAnimation(fig, update, interval=10, save_count=100) plt.show() diff --git a/examples/axes_grid1/README.txt b/examples/axes_grid1/README.txt index 5c3c5d14ec96..42cc4b5af75c 100644 --- a/examples/axes_grid1/README.txt +++ b/examples/axes_grid1/README.txt @@ -2,5 +2,5 @@ .. _axes_grid1-examples-index: -Axes Grid -========= +The axes_grid1 module +===================== diff --git a/examples/axes_grid1/demo_anchored_direction_arrows.py b/examples/axes_grid1/demo_anchored_direction_arrows.py index cdf16dc05754..24d3ddfcc4ad 100644 --- a/examples/axes_grid1/demo_anchored_direction_arrows.py +++ b/examples/axes_grid1/demo_anchored_direction_arrows.py @@ -42,7 +42,7 @@ # Rotated arrow fontprops = fm.FontProperties(family='serif') -roatated_arrow = AnchoredDirectionArrows( +rotated_arrow = AnchoredDirectionArrows( ax.transAxes, '30', '120', loc='center', @@ -50,7 +50,7 @@ angle=30, fontproperties=fontprops ) -ax.add_artist(roatated_arrow) +ax.add_artist(rotated_arrow) # Altering arrow directions a1 = AnchoredDirectionArrows( diff --git a/examples/axes_grid1/demo_axes_divider.py b/examples/axes_grid1/demo_axes_divider.py index dddc2ed4760c..242c33a935a2 100644 --- a/examples/axes_grid1/demo_axes_divider.py +++ b/examples/axes_grid1/demo_axes_divider.py @@ -1,6 +1,6 @@ """ ============ -Axes Divider +Axes divider ============ Axes divider to calculate location of axes and @@ -22,7 +22,7 @@ def demo_simple_image(ax): im = ax.imshow(Z, extent=extent) cb = plt.colorbar(im) - plt.setp(cb.ax.get_yticklabels(), visible=False) + cb.ax.yaxis.set_tick_params(labelright=False) def demo_locatable_axes_hard(fig): @@ -60,7 +60,7 @@ def demo_locatable_axes_hard(fig): im = ax.imshow(Z, extent=extent) plt.colorbar(im, cax=ax_cb) - plt.setp(ax_cb.get_yticklabels(), visible=False) + ax_cb.yaxis.set_tick_params(labelright=False) def demo_locatable_axes_easy(ax): @@ -68,7 +68,7 @@ def demo_locatable_axes_easy(ax): divider = make_axes_locatable(ax) - ax_cb = divider.new_horizontal(size="5%", pad=0.05) + ax_cb = divider.append_axes("right", size="5%", pad=0.05) fig = ax.get_figure() fig.add_axes(ax_cb) @@ -86,7 +86,7 @@ def demo_images_side_by_side(ax): divider = make_axes_locatable(ax) Z, extent = get_demo_image() - ax2 = divider.new_horizontal(size="100%", pad=0.05) + ax2 = divider.append_axes("right", size="100%", pad=0.05) fig1 = ax.get_figure() fig1.add_axes(ax2) @@ -112,7 +112,7 @@ def demo(): # PLOT 3 # image and colorbar whose location is adjusted in the drawing time. - # a easy way + # an easy way ax = fig.add_subplot(2, 2, 3) demo_locatable_axes_easy(ax) diff --git a/examples/axes_grid1/demo_axes_grid.py b/examples/axes_grid1/demo_axes_grid.py index 57297ba77e6a..27d0f3268b01 100644 --- a/examples/axes_grid1/demo_axes_grid.py +++ b/examples/axes_grid1/demo_axes_grid.py @@ -3,7 +3,7 @@ Demo Axes Grid ============== -Grid of 2x2 images with single or own colorbar. +Grid of 2x2 images with a single colorbar or with one colorbar per axes. """ from matplotlib import cbook @@ -11,115 +11,70 @@ from mpl_toolkits.axes_grid1 import ImageGrid -def get_demo_image(): - z = cbook.get_sample_data("axes_grid/bivariate_normal.npy", np_load=True) - # z is a numpy array of 15x15 - return z, (-3, 4, -4, 3) - - -def demo_simple_grid(fig): - """ - A grid of 2x2 images with 0.05 inch pad between images and only - the lower-left axes is labeled. - """ - grid = ImageGrid(fig, 141, # similar to subplot(141) - nrows_ncols=(2, 2), - axes_pad=0.05, - label_mode="1", - ) - Z, extent = get_demo_image() - for ax in grid: - ax.imshow(Z, extent=extent) - # This only affects axes in first column and second row as share_all=False. - grid.axes_llc.set_xticks([-2, 0, 2]) - grid.axes_llc.set_yticks([-2, 0, 2]) - - -def demo_grid_with_single_cbar(fig): - """ - A grid of 2x2 images with a single colorbar - """ - grid = ImageGrid(fig, 142, # similar to subplot(142) - nrows_ncols=(2, 2), - axes_pad=0.0, - share_all=True, - label_mode="L", - cbar_location="top", - cbar_mode="single", - ) - - Z, extent = get_demo_image() - for ax in grid: - im = ax.imshow(Z, extent=extent) - grid.cbar_axes[0].colorbar(im) - - for cax in grid.cbar_axes: - cax.toggle_label(False) - - # This affects all axes as share_all = True. - grid.axes_llc.set_xticks([-2, 0, 2]) - grid.axes_llc.set_yticks([-2, 0, 2]) - - -def demo_grid_with_each_cbar(fig): - """ - A grid of 2x2 images. Each image has its own colorbar. - """ - grid = ImageGrid(fig, 143, # similar to subplot(143) - nrows_ncols=(2, 2), - axes_pad=0.1, - label_mode="1", - share_all=True, - cbar_location="top", - cbar_mode="each", - cbar_size="7%", - cbar_pad="2%", - ) - Z, extent = get_demo_image() - for ax, cax in zip(grid, grid.cbar_axes): - im = ax.imshow(Z, extent=extent) - cax.colorbar(im) - cax.toggle_label(False) - - # This affects all axes because we set share_all = True. - grid.axes_llc.set_xticks([-2, 0, 2]) - grid.axes_llc.set_yticks([-2, 0, 2]) - - -def demo_grid_with_each_cbar_labelled(fig): - """ - A grid of 2x2 images. Each image has its own colorbar. - """ - grid = ImageGrid(fig, 144, # similar to subplot(144) - nrows_ncols=(2, 2), - axes_pad=(0.45, 0.15), - label_mode="1", - share_all=True, - cbar_location="right", - cbar_mode="each", - cbar_size="7%", - cbar_pad="2%", - ) - Z, extent = get_demo_image() - - # Use a different colorbar range every time - limits = ((0, 1), (-2, 2), (-1.7, 1.4), (-1.5, 1)) - for ax, cax, vlim in zip(grid, grid.cbar_axes, limits): - im = ax.imshow(Z, extent=extent, vmin=vlim[0], vmax=vlim[1]) - cb = cax.colorbar(im) - cb.set_ticks((vlim[0], vlim[1])) - - # This affects all axes because we set share_all = True. - grid.axes_llc.set_xticks([-2, 0, 2]) - grid.axes_llc.set_yticks([-2, 0, 2]) +Z = cbook.get_sample_data( # (15, 15) array + "axes_grid/bivariate_normal.npy", np_load=True) +extent = (-3, 4, -4, 3) fig = plt.figure(figsize=(10.5, 2.5)) fig.subplots_adjust(left=0.05, right=0.95) -demo_simple_grid(fig) -demo_grid_with_single_cbar(fig) -demo_grid_with_each_cbar(fig) -demo_grid_with_each_cbar_labelled(fig) + +# A grid of 2x2 images with 0.05 inch pad between images and only the +# lower-left axes is labeled. +grid = ImageGrid( + fig, 141, # similar to fig.add_subplot(141). + nrows_ncols=(2, 2), axes_pad=0.05, label_mode="1") +for ax in grid: + ax.imshow(Z, extent=extent) +# This only affects axes in first column and second row as share_all=False. +grid.axes_llc.set_xticks([-2, 0, 2]) +grid.axes_llc.set_yticks([-2, 0, 2]) + + +# A grid of 2x2 images with a single colorbar +grid = ImageGrid( + fig, 142, # similar to fig.add_subplot(142). + nrows_ncols=(2, 2), axes_pad=0.0, label_mode="L", share_all=True, + cbar_location="top", cbar_mode="single") +for ax in grid: + im = ax.imshow(Z, extent=extent) +grid.cbar_axes[0].colorbar(im) +for cax in grid.cbar_axes: + cax.toggle_label(False) +# This affects all axes as share_all = True. +grid.axes_llc.set_xticks([-2, 0, 2]) +grid.axes_llc.set_yticks([-2, 0, 2]) + + +# A grid of 2x2 images. Each image has its own colorbar. +grid = ImageGrid( + fig, 143, # similar to fig.add_subplot(143). + nrows_ncols=(2, 2), axes_pad=0.1, label_mode="1", share_all=True, + cbar_location="top", cbar_mode="each", cbar_size="7%", cbar_pad="2%") +for ax, cax in zip(grid, grid.cbar_axes): + im = ax.imshow(Z, extent=extent) + cax.colorbar(im) + cax.toggle_label(False) +# This affects all axes because we set share_all = True. +grid.axes_llc.set_xticks([-2, 0, 2]) +grid.axes_llc.set_yticks([-2, 0, 2]) + + +# A grid of 2x2 images. Each image has its own colorbar. +grid = ImageGrid( + fig, 144, # similar to fig.add_subplot(144). + nrows_ncols=(2, 2), axes_pad=(0.45, 0.15), label_mode="1", share_all=True, + cbar_location="right", cbar_mode="each", cbar_size="7%", cbar_pad="2%") +# Use a different colorbar range every time +limits = ((0, 1), (-2, 2), (-1.7, 1.4), (-1.5, 1)) +for ax, cax, vlim in zip(grid, grid.cbar_axes, limits): + im = ax.imshow(Z, extent=extent, vmin=vlim[0], vmax=vlim[1]) + cb = cax.colorbar(im) + cb.set_ticks((vlim[0], vlim[1])) +# This affects all axes because we set share_all = True. +grid.axes_llc.set_xticks([-2, 0, 2]) +grid.axes_llc.set_yticks([-2, 0, 2]) + plt.show() diff --git a/examples/axes_grid1/demo_axes_grid2.py b/examples/axes_grid1/demo_axes_grid2.py index b18648186c5d..488460921471 100644 --- a/examples/axes_grid1/demo_axes_grid2.py +++ b/examples/axes_grid1/demo_axes_grid2.py @@ -58,10 +58,6 @@ def add_inner_title(ax, title, loc, **kwargs): for ax, z in zip(grid, ZS): ax.cax.toggle_label(True) - #axis = ax.cax.axis[ax.cax.orientation] - #axis.label.set_text("counts s$^{-1}$") - #axis.label.set_size(10) - #axis.major_ticklabels.set_size(6) grid[0].set_xticks([-2, 0]) grid[0].set_yticks([-2, 0, 2]) diff --git a/examples/axes_grid1/demo_axes_hbox_divider.py b/examples/axes_grid1/demo_axes_hbox_divider.py index 7bbbeff950a6..b3bfcc508468 100644 --- a/examples/axes_grid1/demo_axes_hbox_divider.py +++ b/examples/axes_grid1/demo_axes_hbox_divider.py @@ -1,43 +1,54 @@ """ -=================== -`.HBoxDivider` demo -=================== +================================ +HBoxDivider and VBoxDivider demo +================================ Using an `.HBoxDivider` to arrange subplots. + +Note that both axes' location are adjusted so that they have +equal heights while maintaining their aspect ratios. + """ import numpy as np import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1.axes_divider import HBoxDivider +from mpl_toolkits.axes_grid1.axes_divider import HBoxDivider, VBoxDivider import mpl_toolkits.axes_grid1.axes_size as Size -def make_heights_equal(fig, rect, ax1, ax2, pad): - # pad in inches - divider = HBoxDivider( - fig, rect, - horizontal=[Size.AxesX(ax1), Size.Fixed(pad), Size.AxesX(ax2)], - vertical=[Size.AxesY(ax1), Size.Scaled(1), Size.AxesY(ax2)]) - ax1.set_axes_locator(divider.new_locator(0)) - ax2.set_axes_locator(divider.new_locator(2)) +arr1 = np.arange(20).reshape((4, 5)) +arr2 = np.arange(20).reshape((5, 4)) + +fig, (ax1, ax2) = plt.subplots(1, 2) +ax1.imshow(arr1) +ax2.imshow(arr2) +pad = 0.5 # pad in inches +divider = HBoxDivider( + fig, 111, + horizontal=[Size.AxesX(ax1), Size.Fixed(pad), Size.AxesX(ax2)], + vertical=[Size.AxesY(ax1), Size.Scaled(1), Size.AxesY(ax2)]) +ax1.set_axes_locator(divider.new_locator(0)) +ax2.set_axes_locator(divider.new_locator(2)) -if __name__ == "__main__": +plt.show() - arr1 = np.arange(20).reshape((4, 5)) - arr2 = np.arange(20).reshape((5, 4)) +############################################################################### +# Using a `.VBoxDivider` to arrange subplots. +# +# Note that both axes' location are adjusted so that they have +# equal widths while maintaining their aspect ratios. - fig, (ax1, ax2) = plt.subplots(1, 2) - ax1.imshow(arr1) - ax2.imshow(arr2) +fig, (ax1, ax2) = plt.subplots(2, 1) +ax1.imshow(arr1) +ax2.imshow(arr2) - make_heights_equal(fig, 111, ax1, ax2, pad=0.5) +divider = VBoxDivider( + fig, 111, + horizontal=[Size.AxesX(ax1), Size.Scaled(1), Size.AxesX(ax2)], + vertical=[Size.AxesY(ax1), Size.Fixed(pad), Size.AxesY(ax2)]) - fig.text(.5, .5, - "Both axes' location are adjusted\n" - "so that they have equal heights\n" - "while maintaining their aspect ratios", - va="center", ha="center", - bbox=dict(boxstyle="round, pad=1", facecolor="w")) +ax1.set_axes_locator(divider.new_locator(0)) +ax2.set_axes_locator(divider.new_locator(2)) - plt.show() +plt.show() diff --git a/examples/axes_grid1/demo_axes_rgb.py b/examples/axes_grid1/demo_axes_rgb.py index 9b1ccb431d77..fa6ffdb5df3e 100644 --- a/examples/axes_grid1/demo_axes_rgb.py +++ b/examples/axes_grid1/demo_axes_rgb.py @@ -59,11 +59,8 @@ def demo_rgb2(): ax_b.imshow(im_b) for ax in fig.axes: - ax.tick_params(axis='both', direction='in') + ax.tick_params(direction='in', color='w') ax.spines[:].set_color("w") - for tick in ax.xaxis.get_major_ticks() + ax.yaxis.get_major_ticks(): - tick.tick1line.set_markeredgecolor("w") - tick.tick2line.set_markeredgecolor("w") demo_rgb1() diff --git a/examples/axes_grid1/demo_colorbar_of_inset_axes.py b/examples/axes_grid1/demo_colorbar_of_inset_axes.py index e0f31221949e..bb0023adac3f 100644 --- a/examples/axes_grid1/demo_colorbar_of_inset_axes.py +++ b/examples/axes_grid1/demo_colorbar_of_inset_axes.py @@ -1,8 +1,7 @@ """ -=========================== -Demo Colorbar of Inset Axes -=========================== - +=============================== +Adding a colorbar to inset axes +=============================== """ from matplotlib import cbook @@ -11,18 +10,15 @@ fig, ax = plt.subplots(figsize=[5, 4]) +ax.set(aspect=1, xlim=(-15, 15), ylim=(-20, 5)) Z = cbook.get_sample_data("axes_grid/bivariate_normal.npy", np_load=True) extent = (-3, 4, -4, 3) -ax.set(aspect=1, xlim=(-15, 15), ylim=(-20, 5)) - axins = zoomed_inset_axes(ax, zoom=2, loc='upper left') +axins.set(xticks=[], yticks=[]) im = axins.imshow(Z, extent=extent, origin="lower") -plt.xticks(visible=False) -plt.yticks(visible=False) - # colorbar cax = inset_axes(axins, width="5%", # width = 10% of parent_bbox width diff --git a/examples/axes_grid1/demo_colorbar_with_axes_divider.py b/examples/axes_grid1/demo_colorbar_with_axes_divider.py index 9ab4640ee8b0..920db5703b98 100644 --- a/examples/axes_grid1/demo_colorbar_with_axes_divider.py +++ b/examples/axes_grid1/demo_colorbar_with_axes_divider.py @@ -18,13 +18,13 @@ im1 = ax1.imshow([[1, 2], [3, 4]]) ax1_divider = make_axes_locatable(ax1) -# Add an axes to the right of the main axes. +# Add an Axes to the right of the main Axes. cax1 = ax1_divider.append_axes("right", size="7%", pad="2%") cb1 = fig.colorbar(im1, cax=cax1) im2 = ax2.imshow([[1, 2], [3, 4]]) ax2_divider = make_axes_locatable(ax2) -# Add an axes above the main axes. +# Add an Axes above the main Axes. cax2 = ax2_divider.append_axes("top", size="7%", pad="2%") cb2 = fig.colorbar(im2, cax=cax2, orientation="horizontal") # Change tick position to top (with the default tick position "bottom", ticks diff --git a/examples/axes_grid1/demo_colorbar_with_inset_locator.py b/examples/axes_grid1/demo_colorbar_with_inset_locator.py index 94d435d6b288..87939800021d 100644 --- a/examples/axes_grid1/demo_colorbar_with_inset_locator.py +++ b/examples/axes_grid1/demo_colorbar_with_inset_locator.py @@ -6,40 +6,37 @@ This example shows how to control the position, height, and width of colorbars using `~mpl_toolkits.axes_grid1.inset_locator.inset_axes`. -Controlling the placement of the inset axes is done similarly as that of the -legend: either by providing a location option ("upper right", "best", ...), or -by providing a locator with respect to the parent bbox. - +Inset axes placement is controlled as for legends: either by providing a *loc* +option ("upper right", "best", ...), or by providing a locator with respect to +the parent bbox. Parameters such as *bbox_to_anchor* and *borderpad* likewise +work in the same way, and are also demonstrated here. """ -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1.inset_locator import inset_axes fig, (ax1, ax2) = plt.subplots(1, 2, figsize=[6, 3]) -axins1 = inset_axes(ax1, - width="50%", # width = 50% of parent_bbox width - height="5%", # height : 5% - loc='upper right') - im1 = ax1.imshow([[1, 2], [2, 3]]) -fig.colorbar(im1, cax=axins1, orientation="horizontal", ticks=[1, 2, 3]) +axins1 = inset_axes( + ax1, + width="50%", # width: 50% of parent_bbox width + height="5%", # height: 5% + loc="upper right", +) axins1.xaxis.set_ticks_position("bottom") - -axins = inset_axes(ax2, - width="5%", # width = 5% of parent_bbox width - height="50%", # height : 50% - loc='lower left', - bbox_to_anchor=(1.05, 0., 1, 1), - bbox_transform=ax2.transAxes, - borderpad=0, - ) - -# Controlling the placement of the inset axes is basically same as that -# of the legend. you may want to play with the borderpad value and -# the bbox_to_anchor coordinate. +fig.colorbar(im1, cax=axins1, orientation="horizontal", ticks=[1, 2, 3]) im = ax2.imshow([[1, 2], [2, 3]]) +axins = inset_axes( + ax2, + width="5%", # width: 5% of parent_bbox width + height="50%", # height: 50% + loc="lower left", + bbox_to_anchor=(1.05, 0., 1, 1), + bbox_transform=ax2.transAxes, + borderpad=0, +) fig.colorbar(im, cax=axins, ticks=[1, 2, 3]) plt.show() diff --git a/examples/axes_grid1/demo_edge_colorbar.py b/examples/axes_grid1/demo_edge_colorbar.py index 74dc74c56965..17dfd952992c 100644 --- a/examples/axes_grid1/demo_edge_colorbar.py +++ b/examples/axes_grid1/demo_edge_colorbar.py @@ -1,7 +1,7 @@ """ -================== -Demo Edge Colorbar -================== +=============================== +Per-row or per-column colorbars +=============================== This example shows how to use one common colorbar for each row or column of an image grid. @@ -35,7 +35,7 @@ def demo_bottom_cbar(fig): ) Z, extent = get_demo_image() - cmaps = [plt.get_cmap("autumn"), plt.get_cmap("summer")] + cmaps = ["autumn", "summer"] for i in range(4): im = grid[i].imshow(Z, extent=extent, cmap=cmaps[i//2]) if i % 2: @@ -65,7 +65,7 @@ def demo_right_cbar(fig): cbar_pad="2%", ) Z, extent = get_demo_image() - cmaps = [plt.get_cmap("spring"), plt.get_cmap("winter")] + cmaps = ["spring", "winter"] for i in range(4): im = grid[i].imshow(Z, extent=extent, cmap=cmaps[i//2]) if i % 2: diff --git a/examples/axes_grid1/demo_fixed_size_axes.py b/examples/axes_grid1/demo_fixed_size_axes.py index 41d138b1f453..85ab092982e4 100644 --- a/examples/axes_grid1/demo_fixed_size_axes.py +++ b/examples/axes_grid1/demo_fixed_size_axes.py @@ -1,7 +1,7 @@ """ -==================== -Demo Fixed Size Axes -==================== +=============================== +Axes with a fixed physical size +=============================== """ import matplotlib.pyplot as plt diff --git a/examples/axes_grid1/demo_imagegrid_aspect.py b/examples/axes_grid1/demo_imagegrid_aspect.py index 3369777460a5..5c29b802f1a0 100644 --- a/examples/axes_grid1/demo_imagegrid_aspect.py +++ b/examples/axes_grid1/demo_imagegrid_aspect.py @@ -1,25 +1,21 @@ """ -===================== -Demo Imagegrid Aspect -===================== - +========================================= +Setting a fixed aspect on ImageGrid cells +========================================= """ -import matplotlib.pyplot as plt +import matplotlib.pyplot as plt from mpl_toolkits.axes_grid1 import ImageGrid + fig = plt.figure() grid1 = ImageGrid(fig, 121, (2, 2), axes_pad=0.1, aspect=True, share_all=True) - for i in [0, 1]: grid1[i].set_aspect(2) - grid2 = ImageGrid(fig, 122, (2, 2), axes_pad=0.1, aspect=True, share_all=True) - - for i in [1, 3]: grid2[i].set_aspect(2) diff --git a/examples/axes_grid1/inset_locator_demo.py b/examples/axes_grid1/inset_locator_demo.py index 56b5a8357e10..ae1d56cda667 100644 --- a/examples/axes_grid1/inset_locator_demo.py +++ b/examples/axes_grid1/inset_locator_demo.py @@ -1,6 +1,6 @@ """ ================== -Inset Locator Demo +Inset locator demo ================== """ @@ -45,7 +45,7 @@ ############################################################################### # The parameters *bbox_to_anchor* and *bbox_transform* can be used for a more -# fine grained control over the inset position and size or even to position +# fine-grained control over the inset position and size or even to position # the inset at completely arbitrary positions. # The *bbox_to_anchor* sets the bounding box in coordinates according to the # *bbox_transform*. @@ -54,12 +54,12 @@ fig = plt.figure(figsize=[5.5, 2.8]) ax = fig.add_subplot(121) -# We use the axes transform as bbox_transform. Therefore the bounding box +# We use the axes transform as bbox_transform. Therefore, the bounding box # needs to be specified in axes coordinates ((0, 0) is the lower left corner # of the axes, (1, 1) is the upper right corner). # The bounding box (.2, .4, .6, .5) starts at (.2, .4) and ranges to (.8, .9) # in those coordinates. -# Inside of this bounding box an inset of half the bounding box' width and +# Inside this bounding box an inset of half the bounding box' width and # three quarters of the bounding box' height is created. The lower left corner # of the inset is aligned to the lower left corner of the bounding box (loc=3). # The inset is then offset by the default 0.5 in units of the font size. @@ -69,7 +69,7 @@ bbox_transform=ax.transAxes, loc=3) # For visualization purposes we mark the bounding box by a rectangle -ax.add_patch(plt.Rectangle((.2, .4), .6, .5, ls="--", ec="c", fc="None", +ax.add_patch(plt.Rectangle((.2, .4), .6, .5, ls="--", ec="c", fc="none", transform=ax.transAxes)) # We set the axis limits to something other than the default, in order to not @@ -89,9 +89,9 @@ bbox_transform=ax3.transAxes) # For visualization purposes we mark the bounding box by a rectangle -ax2.add_patch(plt.Rectangle((0, 0), 1, 1, ls="--", lw=2, ec="c", fc="None")) +ax2.add_patch(plt.Rectangle((0, 0), 1, 1, ls="--", lw=2, ec="c", fc="none")) ax3.add_patch(plt.Rectangle((.7, .5), .3, .5, ls="--", lw=2, - ec="c", fc="None")) + ec="c", fc="none")) # Turn ticklabels off for axi in [axins2, axins3, ax2, ax3]: @@ -103,7 +103,7 @@ ############################################################################### # In the above the axes transform together with 4-tuple bounding boxes has been # used as it mostly is useful to specify an inset relative to the axes it is -# an inset to. However other use cases are equally possible. The following +# an inset to. However, other use cases are equally possible. The following # example examines some of those. # @@ -135,7 +135,7 @@ # Create an inset horizontally centered in figure coordinates and vertically # bound to line up with the axes. -from matplotlib.transforms import blended_transform_factory +from matplotlib.transforms import blended_transform_factory # noqa transform = blended_transform_factory(fig.transFigure, ax2.transAxes) axins4 = inset_axes(ax2, width="16%", height="34%", bbox_to_anchor=(0, 0, 1, 1), diff --git a/examples/axes_grid1/inset_locator_demo2.py b/examples/axes_grid1/inset_locator_demo2.py index 509f65510438..11d80702c094 100644 --- a/examples/axes_grid1/inset_locator_demo2.py +++ b/examples/axes_grid1/inset_locator_demo2.py @@ -1,12 +1,15 @@ """ -=================== -Inset Locator Demo2 -=================== +==================== +Inset locator demo 2 +==================== -This Demo shows how to create a zoomed inset via `~.zoomed_inset_axes`. -In the first subplot an `~.AnchoredSizeBar` shows the zoom effect. +This demo shows how to create a zoomed inset via `.zoomed_inset_axes`. +In the first subplot an `.AnchoredSizeBar` shows the zoom effect. In the second subplot a connection to the region of interest is -created via `~.mark_inset`. +created via `.mark_inset`. + +A version of the second subplot, not using the toolkit, is available in +:doc:`/gallery/subplots_axes_and_figures/zoom_inset_axes`. """ from matplotlib import cbook @@ -32,9 +35,7 @@ def get_demo_image(): # fix the number of ticks on the inset axes axins.yaxis.get_major_locator().set_params(nbins=7) axins.xaxis.get_major_locator().set_params(nbins=7) - -plt.setp(axins.get_xticklabels(), visible=False) -plt.setp(axins.get_yticklabels(), visible=False) +axins.tick_params(labelleft=False, labelbottom=False) def add_sizebar(ax, size): @@ -62,16 +63,14 @@ def add_sizebar(ax, size): axins2 = zoomed_inset_axes(ax2, zoom=6, loc=1) axins2.imshow(Z2, extent=extent, origin="lower") -# sub region of the original image +# subregion of the original image x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9 axins2.set_xlim(x1, x2) axins2.set_ylim(y1, y2) # fix the number of ticks on the inset axes axins2.yaxis.get_major_locator().set_params(nbins=7) axins2.xaxis.get_major_locator().set_params(nbins=7) - -plt.setp(axins2.get_xticklabels(), visible=False) -plt.setp(axins2.get_yticklabels(), visible=False) +axins2.tick_params(labelleft=False, labelbottom=False) # draw a bbox of the region of the inset axes in the parent axes and # connecting lines between the bbox and the inset axes area diff --git a/examples/axes_grid1/make_room_for_ylabel_using_axesgrid.py b/examples/axes_grid1/make_room_for_ylabel_using_axesgrid.py index af5946535cfb..802af8739b97 100644 --- a/examples/axes_grid1/make_room_for_ylabel_using_axesgrid.py +++ b/examples/axes_grid1/make_room_for_ylabel_using_axesgrid.py @@ -1,8 +1,7 @@ """ -=================================== -Make Room For Ylabel Using Axesgrid -=================================== - +==================================== +Make room for ylabel using axes_grid +==================================== """ import matplotlib.pyplot as plt @@ -11,23 +10,20 @@ from mpl_toolkits.axes_grid1.axes_divider import make_axes_area_auto_adjustable -plt.figure() -ax = plt.axes([0, 0, 1, 1]) +fig = plt.figure() +ax = fig.add_axes([0, 0, 1, 1]) -ax.set_yticks([0.5]) -ax.set_yticklabels(["very long label"]) +ax.set_yticks([0.5], labels=["very long label"]) make_axes_area_auto_adjustable(ax) ############################################################################### +fig = plt.figure() +ax1 = fig.add_axes([0, 0, 1, 0.5]) +ax2 = fig.add_axes([0, 0.5, 1, 0.5]) -plt.figure() -ax1 = plt.axes([0, 0, 1, 0.5]) -ax2 = plt.axes([0, 0.5, 1, 0.5]) - -ax1.set_yticks([0.5]) -ax1.set_yticklabels(["very long label"]) +ax1.set_yticks([0.5], labels=["very long label"]) ax1.set_ylabel("Y label") ax2.set_title("Title") @@ -37,12 +33,11 @@ ############################################################################### - fig = plt.figure() -ax1 = plt.axes([0, 0, 1, 1]) +ax1 = fig.add_axes([0, 0, 1, 1]) divider = make_axes_locatable(ax1) -ax2 = divider.new_horizontal("100%", pad=0.3, sharey=ax1) +ax2 = divider.append_axes("right", "100%", pad=0.3, sharey=ax1) ax2.tick_params(labelleft=False) fig.add_axes(ax2) @@ -53,8 +48,7 @@ divider.add_auto_adjustable_area(use_axes=[ax1, ax2], pad=0.1, adjust_dirs=["top", "bottom"]) -ax1.set_yticks([0.5]) -ax1.set_yticklabels(["very long label"]) +ax1.set_yticks([0.5], labels=["very long label"]) ax2.set_title("Title") ax2.set_xlabel("X - Label") diff --git a/examples/axes_grid1/parasite_simple.py b/examples/axes_grid1/parasite_simple.py index c76f70ee9d74..36f36f9c997e 100644 --- a/examples/axes_grid1/parasite_simple.py +++ b/examples/axes_grid1/parasite_simple.py @@ -2,13 +2,12 @@ =============== Parasite Simple =============== - """ -from mpl_toolkits.axes_grid1 import host_subplot + import matplotlib.pyplot as plt +from mpl_toolkits.axes_grid1 import host_subplot host = host_subplot(111) - par = host.twinx() host.set_xlabel("Distance") @@ -18,12 +17,9 @@ p1, = host.plot([0, 1, 2], [0, 1, 2], label="Density") p2, = par.plot([0, 1, 2], [0, 3, 2], label="Temperature") -leg = plt.legend() +host.legend(labelcolor="linecolor") host.yaxis.get_label().set_color(p1.get_color()) -leg.texts[0].set_color(p1.get_color()) - par.yaxis.get_label().set_color(p2.get_color()) -leg.texts[1].set_color(p2.get_color()) plt.show() diff --git a/examples/axes_grid1/scatter_hist_locatable_axes.py b/examples/axes_grid1/scatter_hist_locatable_axes.py index 30111e876d1c..c605e9e81258 100644 --- a/examples/axes_grid1/scatter_hist_locatable_axes.py +++ b/examples/axes_grid1/scatter_hist_locatable_axes.py @@ -3,16 +3,16 @@ Scatter Histogram (Locatable Axes) ================================== -Show the marginal distributions of a scatter as histograms at the sides of +Show the marginal distributions of a scatter plot as histograms at the sides of the plot. For a nice alignment of the main axes with the marginals, the axes positions -are defined by a ``Divider``, produced via `.make_axes_locatable`. +are defined by a ``Divider``, produced via `.make_axes_locatable`. Note that +the ``Divider`` API allows setting axes sizes and pads in inches, which is its +main feature. -An alternative method to produce a similar figure is shown in the -:doc:`/gallery/lines_bars_and_markers/scatter_hist` example. The advantage of -the locatable axes method shown below is that the marginal axes follow the -fixed aspect ratio of the main axes. +If one wants to set axes sizes and pads relative to the main Figure, see the +:doc:`/gallery/lines_bars_and_markers/scatter_hist` example. """ import numpy as np @@ -65,7 +65,7 @@ ############################################################################# # -## .. admonition:: References +# .. admonition:: References # # The use of the following functions, methods, classes and modules is shown # in this example: diff --git a/examples/axes_grid1/simple_anchored_artists.py b/examples/axes_grid1/simple_anchored_artists.py index 343d8d3aaa13..212f9797a7f7 100644 --- a/examples/axes_grid1/simple_anchored_artists.py +++ b/examples/axes_grid1/simple_anchored_artists.py @@ -73,7 +73,7 @@ def draw_sizebar(ax): ax.add_artist(asb) -ax = plt.gca() +fig, ax = plt.subplots() ax.set_aspect(1.) draw_text(ax) diff --git a/examples/axes_grid1/simple_axes_divider1.py b/examples/axes_grid1/simple_axes_divider1.py index 1ca9fd463d81..0ead9c27ae9f 100644 --- a/examples/axes_grid1/simple_axes_divider1.py +++ b/examples/axes_grid1/simple_axes_divider1.py @@ -3,16 +3,26 @@ Simple Axes Divider 1 ===================== +See also :doc:`/tutorials/toolkits/axes_grid`. """ from mpl_toolkits.axes_grid1 import Size, Divider import matplotlib.pyplot as plt +def label_axes(ax, text): + """Place a label at the center of an Axes, and remove the axis ticks.""" + ax.text(.5, .5, text, transform=ax.transAxes, + horizontalalignment="center", verticalalignment="center") + ax.tick_params(bottom=False, labelbottom=False, + left=False, labelleft=False) + + ############################################################################## # Fixed axes sizes; fixed paddings. fig = plt.figure(figsize=(6, 6)) +fig.suptitle("Fixed axes sizes, fixed paddings") # Sizes are in inches. horiz = [Size.Fixed(1.), Size.Fixed(.5), Size.Fixed(1.5), Size.Fixed(.5)] @@ -20,36 +30,39 @@ rect = (0.1, 0.1, 0.8, 0.8) # Divide the axes rectangle into a grid with sizes specified by horiz * vert. -divider = Divider(fig, rect, horiz, vert, aspect=False) +div = Divider(fig, rect, horiz, vert, aspect=False) # The rect parameter will actually be ignored and overridden by axes_locator. -ax1 = fig.add_axes(rect, axes_locator=divider.new_locator(nx=0, ny=0)) -ax2 = fig.add_axes(rect, axes_locator=divider.new_locator(nx=0, ny=2)) -ax3 = fig.add_axes(rect, axes_locator=divider.new_locator(nx=2, ny=2)) -ax4 = fig.add_axes(rect, axes_locator=divider.new_locator(nx=2, nx1=4, ny=0)) - -for ax in fig.axes: - ax.tick_params(labelbottom=False, labelleft=False) +ax1 = fig.add_axes(rect, axes_locator=div.new_locator(nx=0, ny=0)) +label_axes(ax1, "nx=0, ny=0") +ax2 = fig.add_axes(rect, axes_locator=div.new_locator(nx=0, ny=2)) +label_axes(ax2, "nx=0, ny=2") +ax3 = fig.add_axes(rect, axes_locator=div.new_locator(nx=2, ny=2)) +label_axes(ax3, "nx=2, ny=2") +ax4 = fig.add_axes(rect, axes_locator=div.new_locator(nx=2, nx1=4, ny=0)) +label_axes(ax4, "nx=2, nx1=4, ny=0") ############################################################################## # Axes sizes that scale with the figure size; fixed paddings. fig = plt.figure(figsize=(6, 6)) +fig.suptitle("Scalable axes sizes, fixed paddings") horiz = [Size.Scaled(1.5), Size.Fixed(.5), Size.Scaled(1.), Size.Scaled(.5)] vert = [Size.Scaled(1.), Size.Fixed(.5), Size.Scaled(1.5)] rect = (0.1, 0.1, 0.8, 0.8) # Divide the axes rectangle into a grid with sizes specified by horiz * vert. -divider = Divider(fig, rect, horiz, vert, aspect=False) +div = Divider(fig, rect, horiz, vert, aspect=False) # The rect parameter will actually be ignored and overridden by axes_locator. -ax1 = fig.add_axes(rect, axes_locator=divider.new_locator(nx=0, ny=0)) -ax2 = fig.add_axes(rect, axes_locator=divider.new_locator(nx=0, ny=2)) -ax3 = fig.add_axes(rect, axes_locator=divider.new_locator(nx=2, ny=2)) -ax4 = fig.add_axes(rect, axes_locator=divider.new_locator(nx=2, nx1=4, ny=0)) - -for ax in fig.axes: - ax.tick_params(labelbottom=False, labelleft=False) +ax1 = fig.add_axes(rect, axes_locator=div.new_locator(nx=0, ny=0)) +label_axes(ax1, "nx=0, ny=0") +ax2 = fig.add_axes(rect, axes_locator=div.new_locator(nx=0, ny=2)) +label_axes(ax2, "nx=0, ny=2") +ax3 = fig.add_axes(rect, axes_locator=div.new_locator(nx=2, ny=2)) +label_axes(ax3, "nx=2, ny=2") +ax4 = fig.add_axes(rect, axes_locator=div.new_locator(nx=2, nx1=4, ny=0)) +label_axes(ax4, "nx=2, nx1=4, ny=0") plt.show() diff --git a/examples/axes_grid1/simple_axes_divider3.py b/examples/axes_grid1/simple_axes_divider3.py index 6b7d9b4dd096..7f88936c2deb 100644 --- a/examples/axes_grid1/simple_axes_divider3.py +++ b/examples/axes_grid1/simple_axes_divider3.py @@ -1,9 +1,11 @@ """ ===================== -Simple Axes Divider 3 +Simple axes divider 3 ===================== +See also :doc:`/tutorials/toolkits/axes_grid`. """ + import mpl_toolkits.axes_grid1.axes_size as Size from mpl_toolkits.axes_grid1 import Divider import matplotlib.pyplot as plt @@ -11,7 +13,7 @@ fig = plt.figure(figsize=(5.5, 4)) -# the rect parameter will be ignore as we will set axes_locator +# the rect parameter will be ignored as we will set axes_locator rect = (0.1, 0.1, 0.8, 0.8) ax = [fig.add_axes(rect, label="%d" % i) for i in range(4)] diff --git a/examples/axes_grid1/simple_axisline4.py b/examples/axes_grid1/simple_axisline4.py index 91b76cf3e956..4d93beb2b0d0 100644 --- a/examples/axes_grid1/simple_axisline4.py +++ b/examples/axes_grid1/simple_axisline4.py @@ -13,9 +13,9 @@ ax.plot(xx, np.sin(xx)) ax2 = ax.twin() # ax2 is responsible for "top" axis and "right" axis -ax2.set_xticks([0., .5*np.pi, np.pi, 1.5*np.pi, 2*np.pi]) -ax2.set_xticklabels(["$0$", r"$\frac{1}{2}\pi$", - r"$\pi$", r"$\frac{3}{2}\pi$", r"$2\pi$"]) +ax2.set_xticks([0., .5*np.pi, np.pi, 1.5*np.pi, 2*np.pi], + labels=["$0$", r"$\frac{1}{2}\pi$", + r"$\pi$", r"$\frac{3}{2}\pi$", r"$2\pi$"]) ax2.axis["right"].major_ticklabels.set_visible(False) ax2.axis["top"].major_ticklabels.set_visible(True) diff --git a/examples/axes_grid1/simple_colorbar.py b/examples/axes_grid1/simple_colorbar.py index d7ca58046a53..41a34eefcab1 100644 --- a/examples/axes_grid1/simple_colorbar.py +++ b/examples/axes_grid1/simple_colorbar.py @@ -11,7 +11,7 @@ ax = plt.subplot() im = ax.imshow(np.arange(100).reshape((10, 10))) -# create an axes on the right side of ax. The width of cax will be 5% +# create an Axes on the right side of ax. The width of cax will be 5% # of ax and the padding between cax and ax will be fixed at 0.05 inch. divider = make_axes_locatable(ax) cax = divider.append_axes("right", size="5%", pad=0.05) diff --git a/examples/axisartist/README.txt b/examples/axisartist/README.txt index d6b190aedaf0..6cc9325d1027 100644 --- a/examples/axisartist/README.txt +++ b/examples/axisartist/README.txt @@ -2,5 +2,5 @@ .. _axisartist-examples-index: -Axis Artist -=========== +The axisartist module +===================== diff --git a/examples/axisartist/demo_axis_direction.py b/examples/axisartist/demo_axis_direction.py index 81fd7a6126bf..f8c03205e732 100644 --- a/examples/axisartist/demo_axis_direction.py +++ b/examples/axisartist/demo_axis_direction.py @@ -1,8 +1,7 @@ """ =================== -Demo Axis Direction +axis_direction demo =================== - """ import numpy as np diff --git a/examples/axisartist/demo_axisline_style.py b/examples/axisartist/demo_axisline_style.py index 1427a90952a1..c7270941dadf 100644 --- a/examples/axisartist/demo_axisline_style.py +++ b/examples/axisartist/demo_axisline_style.py @@ -7,8 +7,8 @@ Note: The `mpl_toolkits.axisartist` axes classes may be confusing for new users. If the only aim is to obtain arrow heads at the ends of the axes, -rather check out the -:doc:`/gallery/ticks_and_spines/centered_spines_with_arrows` example. +rather check out the :doc:`/gallery/spines/centered_spines_with_arrows` +example. """ from mpl_toolkits.axisartist.axislines import AxesZero diff --git a/examples/axisartist/demo_curvelinear_grid.py b/examples/axisartist/demo_curvelinear_grid.py index 91045ee786ab..3efe83a43c70 100644 --- a/examples/axisartist/demo_curvelinear_grid.py +++ b/examples/axisartist/demo_curvelinear_grid.py @@ -27,24 +27,17 @@ def curvelinear_test1(fig): Grid for custom transform. """ - def tr(x, y): - x, y = np.asarray(x), np.asarray(y) - return x, y - x - - def inv_tr(x, y): - x, y = np.asarray(x), np.asarray(y) - return x, y + x + def tr(x, y): return x, y - x + def inv_tr(x, y): return x, y + x grid_helper = GridHelperCurveLinear((tr, inv_tr)) ax1 = fig.add_subplot(1, 2, 1, axes_class=Axes, grid_helper=grid_helper) - # ax1 will have a ticks and gridlines defined by the given - # transform (+ transData of the Axes). Note that the transform of - # the Axes itself (i.e., transData) is not affected by the given - # transform. - - xx, yy = tr([3, 6], [5, 10]) - ax1.plot(xx, yy, linewidth=2.0) + # ax1 will have ticks and gridlines defined by the given transform (+ + # transData of the Axes). Note that the transform of the Axes itself + # (i.e., transData) is not affected by the given transform. + xx, yy = tr(np.array([3, 6]), np.array([5, 10])) + ax1.plot(xx, yy) ax1.set_aspect(1) ax1.set_xlim(0, 10) @@ -102,7 +95,6 @@ def curvelinear_test2(fig): ax2 = ax1.get_aux_axes(tr) # note that ax2.transData == tr + ax1.transData # Anything you draw in ax2 will match the ticks and grids of ax1. - ax1.parasites.append(ax2) ax2.plot(np.linspace(0, 30, 51), np.linspace(10, 10, 51), linewidth=2) ax2.pcolor(np.linspace(0, 90, 4), np.linspace(0, 10, 4), diff --git a/examples/axisartist/demo_curvelinear_grid2.py b/examples/axisartist/demo_curvelinear_grid2.py index cc07a32d1805..82944ef92575 100644 --- a/examples/axisartist/demo_curvelinear_grid2.py +++ b/examples/axisartist/demo_curvelinear_grid2.py @@ -24,14 +24,10 @@ def curvelinear_test1(fig): """Grid for custom transform.""" def tr(x, y): - sgn = np.sign(x) - x, y = np.abs(np.asarray(x)), np.asarray(y) - return sgn*x**.5, y + return np.sign(x)*abs(x)**.5, y def inv_tr(x, y): - sgn = np.sign(x) - x, y = np.asarray(x), np.asarray(y) - return sgn*x**2, y + return np.sign(x)*x**2, y grid_helper = GridHelperCurveLinear( (tr, inv_tr), diff --git a/examples/axisartist/demo_floating_axes.py b/examples/axisartist/demo_floating_axes.py index 6920ddca5233..c87b44818ff7 100644 --- a/examples/axisartist/demo_floating_axes.py +++ b/examples/axisartist/demo_floating_axes.py @@ -1,7 +1,7 @@ """ -===================================================== -:mod:`mpl_toolkits.axisartist.floating_axes` features -===================================================== +========================== +``floating_axes`` features +========================== Demonstration of features of the :mod:`.floating_axes` module: @@ -9,8 +9,8 @@ the plot. * Using `~.floating_axes.GridHelperCurveLinear` to rotate the plot and set the plot boundary. -* Using `~.floating_axes.FloatingSubplot` to create a subplot using the return - value from `~.floating_axes.GridHelperCurveLinear`. +* Using `~.Figure.add_subplot` to create a subplot using the return value from + `~.floating_axes.GridHelperCurveLinear`. * Making a sector plot by adding more features to `~.floating_axes.GridHelperCurveLinear`. """ @@ -41,6 +41,7 @@ def setup_axes1(fig, rect): ax1 = fig.add_subplot( rect, axes_class=floating_axes.FloatingAxes, grid_helper=grid_helper) + ax1.grid() aux_ax = ax1.get_aux_axes(tr) @@ -72,6 +73,7 @@ def setup_axes2(fig, rect): ax1 = fig.add_subplot( rect, axes_class=floating_axes.FloatingAxes, grid_helper=grid_helper) + ax1.grid() # create a parasite axes whose transData in RA, cz aux_ax = ax1.get_aux_axes(tr) @@ -129,6 +131,7 @@ def setup_axes3(fig, rect): ax1.axis["left"].label.set_text(r"cz [km$^{-1}$]") ax1.axis["top"].label.set_text(r"$\alpha_{1950}$") + ax1.grid() # create a parasite axes whose transData in RA, cz aux_ax = ax1.get_aux_axes(tr) diff --git a/examples/axisartist/demo_floating_axis.py b/examples/axisartist/demo_floating_axis.py index 36de8ce87dae..f607907c2654 100644 --- a/examples/axisartist/demo_floating_axis.py +++ b/examples/axisartist/demo_floating_axis.py @@ -1,14 +1,15 @@ """ ================== -Demo Floating Axis +floating_axis demo ================== -Axis within rectangular frame +Axis within rectangular frame. The following code demonstrates how to put a floating polar curve within a rectangular box. In order to get a better sense of polar curves, please look at :doc:`/gallery/axisartist/demo_curvelinear_grid`. """ + import numpy as np import matplotlib.pyplot as plt import mpl_toolkits.axisartist.angle_helper as angle_helper @@ -28,8 +29,7 @@ def curvelinear_test2(fig): lon_cycle=360, lat_cycle=None, lon_minmax=None, - lat_minmax=(0, - np.inf), + lat_minmax=(0, np.inf), ) grid_locator1 = angle_helper.LocatorDMS(12) diff --git a/examples/axisartist/demo_parasite_axes.py b/examples/axisartist/demo_parasite_axes.py index ef7d5ca5268d..b1bdcbaba091 100644 --- a/examples/axisartist/demo_parasite_axes.py +++ b/examples/axisartist/demo_parasite_axes.py @@ -9,25 +9,23 @@ This approach uses `mpl_toolkits.axes_grid1.parasite_axes.HostAxes` and `mpl_toolkits.axes_grid1.parasite_axes.ParasiteAxes`. -An alternative approach using standard Matplotlib subplots is shown in the -:doc:`/gallery/ticks_and_spines/multiple_yaxis_with_spines` example. +The standard and recommended approach is to use instead standard Matplotlib +axes, as shown in the :doc:`/gallery/spines/multiple_yaxis_with_spines` +example. -An alternative approach using :mod:`mpl_toolkits.axes_grid1` -and :mod:`mpl_toolkits.axisartist` is found in the +An alternative approach using `mpl_toolkits.axes_grid1` and +`mpl_toolkits.axisartist` is shown in the :doc:`/gallery/axisartist/demo_parasite_axes2` example. """ -from mpl_toolkits.axisartist.parasite_axes import HostAxes, ParasiteAxes +from mpl_toolkits.axisartist.parasite_axes import HostAxes import matplotlib.pyplot as plt - fig = plt.figure() host = fig.add_axes([0.15, 0.1, 0.65, 0.8], axes_class=HostAxes) -par1 = ParasiteAxes(host, sharex=host) -par2 = ParasiteAxes(host, sharex=host) -host.parasites.append(par1) -host.parasites.append(par2) +par1 = host.get_aux_axes(viewlim_mode=None, sharex=host) +par2 = host.get_aux_axes(viewlim_mode=None, sharex=host) host.axis["right"].set_visible(False) @@ -41,15 +39,9 @@ p2, = par1.plot([0, 1, 2], [0, 3, 2], label="Temperature") p3, = par2.plot([0, 1, 2], [50, 30, 15], label="Velocity") -host.set_xlim(0, 2) -host.set_ylim(0, 2) -par1.set_ylim(0, 4) -par2.set_ylim(1, 65) - -host.set_xlabel("Distance") -host.set_ylabel("Density") -par1.set_ylabel("Temperature") -par2.set_ylabel("Velocity") +host.set(xlim=(0, 2), ylim=(0, 2), xlabel="Distance", ylabel="Density") +par1.set(ylim=(0, 4), ylabel="Temperature") +par2.set(ylim=(1, 65), ylabel="Velocity") host.legend() diff --git a/examples/axisartist/demo_parasite_axes2.py b/examples/axisartist/demo_parasite_axes2.py index 3c23e37b7ae7..d72611c88cd5 100644 --- a/examples/axisartist/demo_parasite_axes2.py +++ b/examples/axisartist/demo_parasite_axes2.py @@ -11,15 +11,16 @@ of those two axis behave separately from each other: different datasets can be plotted, and the y-limits are adjusted separately. -Note that this approach uses the `mpl_toolkits.axes_grid1.parasite_axes`' -`~mpl_toolkits.axes_grid1.parasite_axes.host_subplot` and -`mpl_toolkits.axisartist.axislines.Axes`. An alternative approach using the -`~mpl_toolkits.axes_grid1.parasite_axes`'s -`~.mpl_toolkits.axes_grid1.parasite_axes.HostAxes` and -`~.mpl_toolkits.axes_grid1.parasite_axes.ParasiteAxes` is the +This approach uses `mpl_toolkits.axes_grid1.parasite_axes.host_subplot` and +`mpl_toolkits.axisartist.axislines.Axes`. + +The standard and recommended approach is to use instead standard Matplotlib +axes, as shown in the :doc:`/gallery/spines/multiple_yaxis_with_spines` +example. + +An alternative approach using `mpl_toolkits.axes_grid1.parasite_axes.HostAxes` +and `mpl_toolkits.axes_grid1.parasite_axes.ParasiteAxes` is shown in the :doc:`/gallery/axisartist/demo_parasite_axes` example. -An alternative approach using the usual Matplotlib subplots is shown in -the :doc:`/gallery/ticks_and_spines/multiple_yaxis_with_spines` example. """ from mpl_toolkits.axes_grid1 import host_subplot @@ -41,15 +42,9 @@ p2, = par1.plot([0, 1, 2], [0, 3, 2], label="Temperature") p3, = par2.plot([0, 1, 2], [50, 30, 15], label="Velocity") -host.set_xlim(0, 2) -host.set_ylim(0, 2) -par1.set_ylim(0, 4) -par2.set_ylim(1, 65) - -host.set_xlabel("Distance") -host.set_ylabel("Density") -par1.set_ylabel("Temperature") -par2.set_ylabel("Velocity") +host.set(xlim=(0, 2), ylim=(0, 2), xlabel="Distance", ylabel="Density") +par1.set(ylim=(0, 4), ylabel="Temperature") +par2.set(ylim=(1, 65), ylabel="Velocity") host.legend() diff --git a/examples/axisartist/demo_ticklabel_alignment.py b/examples/axisartist/demo_ticklabel_alignment.py index 928b3c71a64d..e6d2d0c6ae59 100644 --- a/examples/axisartist/demo_ticklabel_alignment.py +++ b/examples/axisartist/demo_ticklabel_alignment.py @@ -12,10 +12,8 @@ def setup_axes(fig, pos): ax = fig.add_subplot(pos, axes_class=axisartist.Axes) - ax.set_yticks([0.2, 0.8]) - ax.set_yticklabels(["short", "loooong"]) - ax.set_xticks([0.2, 0.8]) - ax.set_xticklabels([r"$\frac{1}{2}\pi$", r"$\pi$"]) + ax.set_yticks([0.2, 0.8], labels=["short", "loooong"]) + ax.set_xticks([0.2, 0.8], labels=[r"$\frac{1}{2}\pi$", r"$\pi$"]) return ax diff --git a/examples/axisartist/simple_axisartist1.py b/examples/axisartist/simple_axisartist1.py index 9e49a3b12a71..ed7d02d8f66b 100644 --- a/examples/axisartist/simple_axisartist1.py +++ b/examples/axisartist/simple_axisartist1.py @@ -1,26 +1,52 @@ """ -================== -Simple Axisartist1 -================== +============================= +Custom spines with axisartist +============================= +This example showcases the use of :mod:`.axisartist` to draw spines at custom +positions (here, at ``y = 0``). + +Note, however, that it is simpler to achieve this effect using standard +`.Spine` methods, as demonstrated in +:doc:`/gallery/spines/centered_spines_with_arrows`. + +.. redirect-from:: /gallery/axisartist/simple_axisline2 """ + import matplotlib.pyplot as plt from mpl_toolkits import axisartist +import numpy as np + + +fig = plt.figure(figsize=(6, 3), layout="constrained") +# To construct axes of two different classes, we need to use gridspec (or +# MATLAB-style add_subplot calls). +gs = fig.add_gridspec(1, 2) + + +ax0 = fig.add_subplot(gs[0, 0], axes_class=axisartist.Axes) +# Make a new axis along the first (x) axis which passes through y=0. +ax0.axis["y=0"] = ax0.new_floating_axis(nth_coord=0, value=0, + axis_direction="bottom") +ax0.axis["y=0"].toggle(all=True) +ax0.axis["y=0"].label.set_text("y = 0") +# Make other axis invisible. +ax0.axis["bottom", "top", "right"].set_visible(False) -fig = plt.figure() -fig.subplots_adjust(right=0.85) -ax = fig.add_subplot(axes_class=axisartist.Axes) -# make some axis invisible -ax.axis["bottom", "top", "right"].set_visible(False) +# Alternatively, one can use AxesZero, which automatically sets up two +# additional axis, named "xzero" (the y=0 axis) and "yzero" (the x=0 axis). +ax1 = fig.add_subplot(gs[0, 1], axes_class=axisartist.axislines.AxesZero) +# "xzero" and "yzero" default to invisible; make xzero axis visible. +ax1.axis["xzero"].set_visible(True) +ax1.axis["xzero"].label.set_text("Axis Zero") +# Make other axis invisible. +ax1.axis["bottom", "top", "right"].set_visible(False) -# make an new axis along the first axis axis (x-axis) which pass -# through y=0. -ax.axis["y=0"] = ax.new_floating_axis(nth_coord=0, value=0, - axis_direction="bottom") -ax.axis["y=0"].toggle(all=True) -ax.axis["y=0"].label.set_text("y = 0") -ax.set_ylim(-2, 4) +# Draw some sample data. +x = np.arange(0, 2*np.pi, 0.01) +ax0.plot(x, np.sin(x)) +ax1.plot(x, np.sin(x)) plt.show() diff --git a/examples/axisartist/simple_axisline.py b/examples/axisartist/simple_axisline.py index 7623763f1ea7..da0aad34a28a 100644 --- a/examples/axisartist/simple_axisline.py +++ b/examples/axisartist/simple_axisline.py @@ -24,9 +24,9 @@ ax.set_ylim(-2, 4) ax.set_xlabel("Label X") ax.set_ylabel("Label Y") -# or -#ax.axis["bottom"].label.set_text("Label X") -#ax.axis["left"].label.set_text("Label Y") +# Or: +# ax.axis["bottom"].label.set_text("Label X") +# ax.axis["left"].label.set_text("Label Y") # make new (right-side) yaxis, but with some offset ax.axis["right2"] = ax.new_fixed_axis(loc="right", offset=(20, 0)) diff --git a/examples/axisartist/simple_axisline2.py b/examples/axisartist/simple_axisline2.py deleted file mode 100644 index ecd49a16de77..000000000000 --- a/examples/axisartist/simple_axisline2.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -================ -Simple Axisline2 -================ - -""" -import matplotlib.pyplot as plt -from mpl_toolkits.axisartist.axislines import AxesZero -import numpy as np - -fig = plt.figure(figsize=(4, 3)) - -# a subplot with two additional axis, "xzero" and "yzero". "xzero" is -# y=0 line, and "yzero" is x=0 line. -ax = fig.add_subplot(axes_class=AxesZero) - -# make xzero axis (horizontal axis line through y=0) visible. -ax.axis["xzero"].set_visible(True) -ax.axis["xzero"].label.set_text("Axis Zero") - -# make other axis (bottom, top, right) invisible. -for n in ["bottom", "top", "right"]: - ax.axis[n].set_visible(False) - -xx = np.arange(0, 2*np.pi, 0.01) -ax.plot(xx, np.sin(xx)) - -plt.show() diff --git a/examples/color/color_demo.py b/examples/color/color_demo.py index f46f52507352..8a161442184b 100644 --- a/examples/color/color_demo.py +++ b/examples/color/color_demo.py @@ -48,9 +48,9 @@ # 3) gray level string: ax.set_title('Voltage vs. time chart', color='0.7') # 4) single letter color string -ax.set_xlabel('time (s)', color='c') +ax.set_xlabel('Time [s]', color='c') # 5) a named color: -ax.set_ylabel('voltage (mV)', color='peachpuff') +ax.set_ylabel('Voltage [mV]', color='peachpuff') # 6) a named xkcd color: ax.plot(t, s, 'xkcd:crimson') # 7) Cn notation: diff --git a/examples/color/colorbar_basics.py b/examples/color/colorbar_basics.py index adc7e6d9c7fe..f31ded0100c7 100644 --- a/examples/color/colorbar_basics.py +++ b/examples/color/colorbar_basics.py @@ -3,8 +3,8 @@ Colorbar ======== -Use `~.figure.Figure.colorbar` by specifying the mappable object (here -the `~.matplotlib.image.AxesImage` returned by `~.axes.Axes.imshow`) +Use `~.Figure.colorbar` by specifying the mappable object (here +the `.AxesImage` returned by `~.axes.Axes.imshow`) and the axes to attach the colorbar to. """ @@ -54,5 +54,5 @@ # # - `matplotlib.axes.Axes.imshow` / `matplotlib.pyplot.imshow` # - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` -# - `matplotlib.colorbar.ColorbarBase.minorticks_on` -# - `matplotlib.colorbar.ColorbarBase.minorticks_off` +# - `matplotlib.colorbar.Colorbar.minorticks_on` +# - `matplotlib.colorbar.Colorbar.minorticks_off` diff --git a/examples/color/colormap_reference.py b/examples/color/colormap_reference.py index dd08aa1d4d94..549345edffab 100644 --- a/examples/color/colormap_reference.py +++ b/examples/color/colormap_reference.py @@ -6,16 +6,17 @@ Reference for colormaps included with Matplotlib. A reversed version of each of these colormaps is available by appending -``_r`` to the name, e.g., ``viridis_r``. +``_r`` to the name, as shown in :ref:`reverse-cmap`. See :doc:`/tutorials/colors/colormaps` for an in-depth discussion about -colormaps, including colorblind-friendliness. +colormaps, including colorblind-friendliness, and +:doc:`/tutorials/colors/colormap-manipulation` for a guide to creating +colormaps. """ import numpy as np import matplotlib.pyplot as plt - cmaps = [('Perceptually Uniform Sequential', [ 'viridis', 'plasma', 'inferno', 'magma', 'cividis']), ('Sequential', [ @@ -40,7 +41,6 @@ 'gist_rainbow', 'rainbow', 'jet', 'turbo', 'nipy_spectral', 'gist_ncar'])] - gradient = np.linspace(0, 1, 256) gradient = np.vstack((gradient, gradient)) @@ -52,11 +52,11 @@ def plot_color_gradients(cmap_category, cmap_list): fig, axs = plt.subplots(nrows=nrows, figsize=(6.4, figh)) fig.subplots_adjust(top=1-.35/figh, bottom=.15/figh, left=0.2, right=0.99) - axs[0].set_title(cmap_category + ' colormaps', fontsize=14) + axs[0].set_title(f"{cmap_category} colormaps", fontsize=14) - for ax, name in zip(axs, cmap_list): - ax.imshow(gradient, aspect='auto', cmap=plt.get_cmap(name)) - ax.text(-.01, .5, name, va='center', ha='right', fontsize=10, + for ax, cmap_name in zip(axs, cmap_list): + ax.imshow(gradient, aspect='auto', cmap=cmap_name) + ax.text(-.01, .5, cmap_name, va='center', ha='right', fontsize=10, transform=ax.transAxes) # Turn off *all* ticks & spines, not just the ones with colormaps. @@ -67,7 +67,21 @@ def plot_color_gradients(cmap_category, cmap_list): for cmap_category, cmap_list in cmaps: plot_color_gradients(cmap_category, cmap_list) -plt.show() + +############################################################################### +# .. _reverse-cmap: +# +# Reversed colormaps +# ------------------ +# +# Append ``_r`` to the name of any built-in colormap to get the reversed +# version: + +plot_color_gradients("Original and reversed ", ['viridis', 'viridis_r']) + +# %% +# The built-in reversed colormaps are generated using `.Colormap.reversed`. +# For an example, see :ref:`reversing-colormap` ############################################################################# # diff --git a/examples/color/custom_cmap.py b/examples/color/custom_cmap.py index 45d8d3b9e8db..a99127d972e6 100644 --- a/examples/color/custom_cmap.py +++ b/examples/color/custom_cmap.py @@ -12,7 +12,7 @@ Creating custom colormaps -------------------------- +========================= It is also possible to create a custom mapping for a colormap. This is accomplished by creating dictionary that specifies how the RGB channels change from one end of the cmap to the other. @@ -21,54 +21,82 @@ half, green to do the same over the middle half, and blue over the top half. Then you would use:: - cdict = {'red': ((0.0, 0.0, 0.0), - (0.5, 1.0, 1.0), - (1.0, 1.0, 1.0)), - - 'green': ((0.0, 0.0, 0.0), - (0.25, 0.0, 0.0), - (0.75, 1.0, 1.0), - (1.0, 1.0, 1.0)), - - 'blue': ((0.0, 0.0, 0.0), - (0.5, 0.0, 0.0), - (1.0, 1.0, 1.0))} + cdict = { + 'red': ( + (0.0, 0.0, 0.0), + (0.5, 1.0, 1.0), + (1.0, 1.0, 1.0), + ), + 'green': ( + (0.0, 0.0, 0.0), + (0.25, 0.0, 0.0), + (0.75, 1.0, 1.0), + (1.0, 1.0, 1.0), + ), + 'blue': ( + (0.0, 0.0, 0.0), + (0.5, 0.0, 0.0), + (1.0, 1.0, 1.0), + ) + } If, as in this example, there are no discontinuities in the r, g, and b components, then it is quite simple: the second and third element of -each tuple, above, is the same--call it "y". The first element ("x") +each tuple, above, is the same -- call it "``y``". The first element ("``x``") defines interpolation intervals over the full range of 0 to 1, and it -must span that whole range. In other words, the values of x divide the -0-to-1 range into a set of segments, and y gives the end-point color +must span that whole range. In other words, the values of ``x`` divide the +0-to-1 range into a set of segments, and ``y`` gives the end-point color values for each segment. -Now consider the green. cdict['green'] is saying that for -0 <= x <= 0.25, y is zero; no green. -0.25 < x <= 0.75, y varies linearly from 0 to 1. -x > 0.75, y remains at 1, full green. - -If there are discontinuities, then it is a little more complicated. -Label the 3 elements in each row in the cdict entry for a given color as -(x, y0, y1). Then for values of x between x[i] and x[i+1] the color -value is interpolated between y1[i] and y0[i+1]. - -Going back to the cookbook example, look at cdict['red']; because y0 != -y1, it is saying that for x from 0 to 0.5, red increases from 0 to 1, -but then it jumps down, so that for x from 0.5 to 1, red increases from -0.7 to 1. Green ramps from 0 to 1 as x goes from 0 to 0.5, then jumps -back to 0, and ramps back to 1 as x goes from 0.5 to 1.:: +Now consider the green, ``cdict['green']`` is saying that for: + +- 0 <= ``x`` <= 0.25, ``y`` is zero; no green. +- 0.25 < ``x`` <= 0.75, ``y`` varies linearly from 0 to 1. +- 0.75 < ``x`` <= 1, ``y`` remains at 1, full green. + +If there are discontinuities, then it is a little more complicated. Label the 3 +elements in each row in the ``cdict`` entry for a given color as ``(x, y0, +y1)``. Then for values of ``x`` between ``x[i]`` and ``x[i+1]`` the color value +is interpolated between ``y1[i]`` and ``y0[i+1]``. + +Going back to a cookbook example:: + + cdict = { + 'red': ( + (0.0, 0.0, 0.0), + (0.5, 1.0, 0.7), + (1.0, 1.0, 1.0), + ), + 'green': ( + (0.0, 0.0, 0.0), + (0.5, 1.0, 0.0), + (1.0, 1.0, 1.0), + ), + 'blue': ( + (0.0, 0.0, 0.0), + (0.5, 0.0, 0.0), + (1.0, 1.0, 1.0), + ) + } + +and look at ``cdict['red'][1]``; because ``y0 != y1``, it is saying that for +``x`` from 0 to 0.5, red increases from 0 to 1, but then it jumps down, so that +for ``x`` from 0.5 to 1, red increases from 0.7 to 1. Green ramps from 0 to 1 +as ``x`` goes from 0 to 0.5, then jumps back to 0, and ramps back to 1 as ``x`` +goes from 0.5 to 1. :: row i: x y0 y1 - / / + / row i+1: x y0 y1 -Above is an attempt to show that for x in the range x[i] to x[i+1], the -interpolation is between y1[i] and y0[i+1]. So, y0[0] and y1[-1] are -never used. +Above is an attempt to show that for ``x`` in the range ``x[i]`` to ``x[i+1]``, +the interpolation is between ``y1[i]`` and ``y0[i+1]``. So, ``y0[0]`` and +``y1[-1]`` are never used. """ import numpy as np +import matplotlib as mpl import matplotlib.pyplot as plt from matplotlib.colors import LinearSegmentedColormap @@ -81,14 +109,15 @@ ############################################################################### -# --- Colormaps from a list --- +# Colormaps from a list +# --------------------- colors = [(1, 0, 0), (0, 1, 0), (0, 0, 1)] # R -> G -> B n_bins = [3, 6, 10, 100] # Discretizes the interpolation into bins cmap_name = 'my_list' fig, axs = plt.subplots(2, 2, figsize=(6, 9)) fig.subplots_adjust(left=0.02, bottom=0.06, right=0.95, top=0.94, wspace=0.05) -for n_bin, ax in zip(n_bins, axs.ravel()): +for n_bin, ax in zip(n_bins, axs.flat): # Create the colormap cmap = LinearSegmentedColormap.from_list(cmap_name, colors, N=n_bin) # Fewer bins will result in "coarser" colomap interpolation @@ -98,60 +127,79 @@ ############################################################################### -# --- Custom colormaps --- - -cdict1 = {'red': ((0.0, 0.0, 0.0), - (0.5, 0.0, 0.1), - (1.0, 1.0, 1.0)), - - 'green': ((0.0, 0.0, 0.0), - (1.0, 0.0, 0.0)), - - 'blue': ((0.0, 0.0, 1.0), - (0.5, 0.1, 0.0), - (1.0, 0.0, 0.0)) - } - -cdict2 = {'red': ((0.0, 0.0, 0.0), - (0.5, 0.0, 1.0), - (1.0, 0.1, 1.0)), - - 'green': ((0.0, 0.0, 0.0), - (1.0, 0.0, 0.0)), - - 'blue': ((0.0, 0.0, 0.1), - (0.5, 1.0, 0.0), - (1.0, 0.0, 0.0)) - } - -cdict3 = {'red': ((0.0, 0.0, 0.0), - (0.25, 0.0, 0.0), - (0.5, 0.8, 1.0), - (0.75, 1.0, 1.0), - (1.0, 0.4, 1.0)), - - 'green': ((0.0, 0.0, 0.0), - (0.25, 0.0, 0.0), - (0.5, 0.9, 0.9), - (0.75, 0.0, 0.0), - (1.0, 0.0, 0.0)), - - 'blue': ((0.0, 0.0, 0.4), - (0.25, 1.0, 1.0), - (0.5, 1.0, 0.8), - (0.75, 0.0, 0.0), - (1.0, 0.0, 0.0)) - } +# Custom colormaps +# ---------------- + +cdict1 = { + 'red': ( + (0.0, 0.0, 0.0), + (0.5, 0.0, 0.1), + (1.0, 1.0, 1.0), + ), + 'green': ( + (0.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + ), + 'blue': ( + (0.0, 0.0, 1.0), + (0.5, 0.1, 0.0), + (1.0, 0.0, 0.0), + ) +} + +cdict2 = { + 'red': ( + (0.0, 0.0, 0.0), + (0.5, 0.0, 1.0), + (1.0, 0.1, 1.0), + ), + 'green': ( + (0.0, 0.0, 0.0), + (1.0, 0.0, 0.0), + ), + 'blue': ( + (0.0, 0.0, 0.1), + (0.5, 1.0, 0.0), + (1.0, 0.0, 0.0), + ) +} + +cdict3 = { + 'red': ( + (0.0, 0.0, 0.0), + (0.25, 0.0, 0.0), + (0.5, 0.8, 1.0), + (0.75, 1.0, 1.0), + (1.0, 0.4, 1.0), + ), + 'green': ( + (0.0, 0.0, 0.0), + (0.25, 0.0, 0.0), + (0.5, 0.9, 0.9), + (0.75, 0.0, 0.0), + (1.0, 0.0, 0.0), + ), + 'blue': ( + (0.0, 0.0, 0.4), + (0.25, 1.0, 1.0), + (0.5, 1.0, 0.8), + (0.75, 0.0, 0.0), + (1.0, 0.0, 0.0), + ) +} # Make a modified version of cdict3 with some transparency # in the middle of the range. -cdict4 = {**cdict3, - 'alpha': ((0.0, 1.0, 1.0), - # (0.25, 1.0, 1.0), - (0.5, 0.3, 0.3), - # (0.75, 1.0, 1.0), - (1.0, 1.0, 1.0)), - } +cdict4 = { + **cdict3, + 'alpha': ( + (0.0, 1.0, 1.0), + # (0.25, 1.0, 1.0), + (0.5, 0.3, 0.3), + # (0.75, 1.0, 1.0), + (1.0, 1.0, 1.0), + ), +} ############################################################################### @@ -167,25 +215,20 @@ # of Colormap, not just # a LinearSegmentedColormap: -blue_red2 = LinearSegmentedColormap('BlueRed2', cdict2) -plt.register_cmap(cmap=blue_red2) - -plt.register_cmap(cmap=LinearSegmentedColormap('BlueRed3', cdict3)) -plt.register_cmap(cmap=LinearSegmentedColormap('BlueRedAlpha', cdict4)) +mpl.colormaps.register(LinearSegmentedColormap('BlueRed2', cdict2)) +mpl.colormaps.register(LinearSegmentedColormap('BlueRed3', cdict3)) +mpl.colormaps.register(LinearSegmentedColormap('BlueRedAlpha', cdict4)) ############################################################################### -# Make the figure: +# Make the figure, with 4 subplots: fig, axs = plt.subplots(2, 2, figsize=(6, 9)) fig.subplots_adjust(left=0.02, bottom=0.06, right=0.95, top=0.94, wspace=0.05) -# Make 4 subplots: - im1 = axs[0, 0].imshow(Z, cmap=blue_red1) fig.colorbar(im1, ax=axs[0, 0]) -cmap = plt.get_cmap('BlueRed2') -im2 = axs[1, 0].imshow(Z, cmap=cmap) +im2 = axs[1, 0].imshow(Z, cmap='BlueRed2') fig.colorbar(im2, ax=axs[1, 0]) # Now we will set the third cmap as the default. One would @@ -215,7 +258,6 @@ # colorbar after they have been plotted. im4.set_cmap('BlueRedAlpha') axs[1, 1].set_title("Varying alpha") -# fig.suptitle('Custom Blue-Red colormaps', fontsize=16) fig.subplots_adjust(top=0.9) diff --git a/examples/color/named_colors.py b/examples/color/named_colors.py index f97919814894..5a8e3667689a 100644 --- a/examples/color/named_colors.py +++ b/examples/color/named_colors.py @@ -3,56 +3,56 @@ List of named colors ==================== -This plots a list of the named colors supported in matplotlib. Note that -:ref:`xkcd colors ` are supported as well, but are not listed here -for brevity. - +This plots a list of the named colors supported in matplotlib. For more information on colors in matplotlib see * the :doc:`/tutorials/colors/colors` tutorial; * the `matplotlib.colors` API; * the :doc:`/gallery/color/color_demo`. + +---------------------------- +Helper Function for Plotting +---------------------------- +First we define a helper function for making a table of colors, then we use it +on some common color categories. """ +import math + from matplotlib.patches import Rectangle import matplotlib.pyplot as plt import matplotlib.colors as mcolors -def plot_colortable(colors, title, sort_colors=True, emptycols=0): +def plot_colortable(colors, *, ncols=4, sort_colors=True): cell_width = 212 cell_height = 22 swatch_width = 48 margin = 12 - topmargin = 40 # Sort colors by hue, saturation, value and name. if sort_colors is True: - by_hsv = sorted((tuple(mcolors.rgb_to_hsv(mcolors.to_rgb(color))), - name) - for name, color in colors.items()) - names = [name for hsv, name in by_hsv] + names = sorted( + colors, key=lambda c: tuple(mcolors.rgb_to_hsv(mcolors.to_rgb(c)))) else: names = list(colors) n = len(names) - ncols = 4 - emptycols - nrows = n // ncols + int(n % ncols > 0) + nrows = math.ceil(n / ncols) width = cell_width * 4 + 2 * margin - height = cell_height * nrows + margin + topmargin + height = cell_height * nrows + 2 * margin dpi = 72 fig, ax = plt.subplots(figsize=(width / dpi, height / dpi), dpi=dpi) fig.subplots_adjust(margin/width, margin/height, - (width-margin)/width, (height-topmargin)/height) + (width-margin)/width, (height-margin)/height) ax.set_xlim(0, cell_width * 4) ax.set_ylim(cell_height * (nrows-0.5), -cell_height/2.) ax.yaxis.set_visible(False) ax.xaxis.set_visible(False) ax.set_axis_off() - ax.set_title(title, fontsize=24, loc="left", pad=10) for i, name in enumerate(names): row = i % nrows @@ -73,22 +73,38 @@ def plot_colortable(colors, title, sort_colors=True, emptycols=0): return fig -plot_colortable(mcolors.BASE_COLORS, "Base Colors", - sort_colors=False, emptycols=1) -plot_colortable(mcolors.TABLEAU_COLORS, "Tableau Palette", - sort_colors=False, emptycols=2) +############################################################################# +# ----------- +# Base colors +# ----------- + +plot_colortable(mcolors.BASE_COLORS, ncols=3, sort_colors=False) -#sphinx_gallery_thumbnail_number = 3 -plot_colortable(mcolors.CSS4_COLORS, "CSS Colors") +############################################################################# +# --------------- +# Tableau Palette +# --------------- -# Optionally plot the XKCD colors (Caution: will produce large figure) -#xkcd_fig = plot_colortable(mcolors.XKCD_COLORS, "XKCD Colors") -#xkcd_fig.savefig("XKCD_Colors.png") +plot_colortable(mcolors.TABLEAU_COLORS, ncols=2, sort_colors=False) -plt.show() +############################################################################# +# ---------- +# CSS Colors +# ---------- +# sphinx_gallery_thumbnail_number = 3 +plot_colortable(mcolors.CSS4_COLORS) +plt.show() ############################################################################# +# ----------- +# XKCD Colors +# ----------- +# XKCD colors are supported, but they produce a large figure, so we skip them +# for now. You can use the following code if desired:: +# +# xkcd_fig = plot_colortable(mcolors.XKCD_COLORS) +# xkcd_fig.savefig("XKCD_Colors.png") # # .. admonition:: References # diff --git a/examples/event_handling/README.txt b/examples/event_handling/README.txt index 165cb66cb15a..46aed29b56bc 100644 --- a/examples/event_handling/README.txt +++ b/examples/event_handling/README.txt @@ -3,10 +3,11 @@ Event handling ============== -Matplotlib supports :doc:`event handling` with a GUI -neutral event model, so you can connect to Matplotlib events without knowledge -of what user interface Matplotlib will ultimately be plugged in to. This has -two advantages: the code you write will be more portable, and Matplotlib events -are aware of things like data coordinate space and which axes the event occurs -in so you don't have to mess with low level transformation details to go from -canvas space to data space. Object picking examples are also included. +Matplotlib supports :doc:`event handling` with +a GUI neutral event model, so you can connect to Matplotlib events without +knowledge of what user interface Matplotlib will ultimately be plugged in to. +This has two advantages: the code you write will be more portable, and +Matplotlib events are aware of things like data coordinate space and which +axes the event occurs in so you don't have to mess with low level +transformation details to go from canvas space to data space. Object picking +examples are also included. diff --git a/examples/event_handling/close_event.py b/examples/event_handling/close_event.py index 9566167bdc6c..24b45b74ea48 100644 --- a/examples/event_handling/close_event.py +++ b/examples/event_handling/close_event.py @@ -4,6 +4,14 @@ =========== Example to show connecting events that occur when the figure closes. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import matplotlib.pyplot as plt diff --git a/examples/event_handling/coords_demo.py b/examples/event_handling/coords_demo.py index 4bfb49518ef5..38a5b77f68e4 100644 --- a/examples/event_handling/coords_demo.py +++ b/examples/event_handling/coords_demo.py @@ -5,6 +5,14 @@ An example of how to interact with the plotting canvas by connecting to move and click events. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ from matplotlib.backend_bases import MouseButton @@ -18,11 +26,9 @@ def on_move(event): - # get the x and y pixel coords - x, y = event.x, event.y if event.inaxes: - ax = event.inaxes # the axes instance - print('data coords %f %f' % (event.xdata, event.ydata)) + print(f'data coords {event.xdata} {event.ydata},', + f'pixel coords {event.x} {event.y}') def on_click(event): diff --git a/examples/misc/cursor_demo.py b/examples/event_handling/cursor_demo.py similarity index 84% rename from examples/misc/cursor_demo.py rename to examples/event_handling/cursor_demo.py index 5cbdecda82cc..e4acd77bce2f 100644 --- a/examples/misc/cursor_demo.py +++ b/examples/event_handling/cursor_demo.py @@ -1,15 +1,15 @@ """ ================= -Cross hair cursor +Cross-hair cursor ================= -This example adds a cross hair as a data cursor. The cross hair is +This example adds a cross-hair as a data cursor. The cross-hair is implemented as regular line objects that are updated on mouse move. We show three implementations: 1) A simple cursor implementation that redraws the figure on every mouse move. - This is a bit slow and you may notice some lag of the cross hair movement. + This is a bit slow, and you may notice some lag of the cross-hair movement. 2) A cursor that uses blitting for speedup of the rendering. 3) A cursor that snaps to data points. @@ -21,11 +21,15 @@ __ https://github.com/joferkington/mpldatacursor __ https://github.com/anntzer/mplcursors + +.. redirect-from:: /gallery/misc/cursor_demo """ import matplotlib.pyplot as plt import numpy as np +from matplotlib.backend_bases import MouseEvent + class Cursor: """ @@ -54,8 +58,8 @@ def on_mouse_move(self, event): self.set_cross_hair_visible(True) x, y = event.xdata, event.ydata # update the line positions - self.horizontal_line.set_ydata(y) - self.vertical_line.set_xdata(x) + self.horizontal_line.set_ydata([y]) + self.vertical_line.set_xdata([x]) self.text.set_text('x=%1.2f, y=%1.2f' % (x, y)) self.ax.figure.canvas.draw() @@ -69,23 +73,29 @@ def on_mouse_move(self, event): cursor = Cursor(ax) fig.canvas.mpl_connect('motion_notify_event', cursor.on_mouse_move) +# Simulate a mouse move to (0.5, 0.5), needed for online docs +t = ax.transData +MouseEvent( + "motion_notify_event", ax.figure.canvas, *t.transform((0.5, 0.5)) +)._process() ############################################################################## # Faster redrawing using blitting # """"""""""""""""""""""""""""""" # This technique stores the rendered plot as a background image. Only the -# changed artists (cross hair lines and text) are rendered anew. They are +# changed artists (cross-hair lines and text) are rendered anew. They are # combined with the background using blitting. # # This technique is significantly faster. It requires a bit more setup because -# the background has to be stored without the cross hair lines (see +# the background has to be stored without the cross-hair lines (see # ``create_new_background()``). Additionally, a new background has to be # created whenever the figure changes. This is achieved by connecting to the # ``'draw_event'``. + class BlittedCursor: """ - A cross hair cursor using blitting for faster redraw. + A cross-hair cursor using blitting for faster redraw. """ def __init__(self, ax): self.ax = ax @@ -130,8 +140,8 @@ def on_mouse_move(self, event): self.set_cross_hair_visible(True) # update the line positions x, y = event.xdata, event.ydata - self.horizontal_line.set_ydata(y) - self.vertical_line.set_xdata(x) + self.horizontal_line.set_ydata([y]) + self.vertical_line.set_xdata([x]) self.text.set_text('x=%1.2f, y=%1.2f' % (x, y)) self.ax.figure.canvas.restore_region(self.background) @@ -150,6 +160,11 @@ def on_mouse_move(self, event): blitted_cursor = BlittedCursor(ax) fig.canvas.mpl_connect('motion_notify_event', blitted_cursor.on_mouse_move) +# Simulate a mouse move to (0.5, 0.5), needed for online docs +t = ax.transData +MouseEvent( + "motion_notify_event", ax.figure.canvas, *t.transform((0.5, 0.5)) +)._process() ############################################################################## # Snapping to data points @@ -163,9 +178,10 @@ def on_mouse_move(self, event): # the lag due to many redraws. Of course, blitting could still be added on top # for additional speedup. + class SnappingCursor: """ - A cross hair cursor that snaps to the data point of a line, which is + A cross-hair cursor that snaps to the data point of a line, which is closest to the *x* position of the cursor. For simplicity, this assumes that *x* values of the data are sorted. @@ -202,8 +218,8 @@ def on_mouse_move(self, event): x = self.x[index] y = self.y[index] # update the line positions - self.horizontal_line.set_ydata(y) - self.vertical_line.set_xdata(x) + self.horizontal_line.set_ydata([y]) + self.vertical_line.set_xdata([x]) self.text.set_text('x=%1.2f, y=%1.2f' % (x, y)) self.ax.figure.canvas.draw() @@ -216,4 +232,11 @@ def on_mouse_move(self, event): line, = ax.plot(x, y, 'o') snap_cursor = SnappingCursor(ax, line) fig.canvas.mpl_connect('motion_notify_event', snap_cursor.on_mouse_move) + +# Simulate a mouse move to (0.5, 0.5), needed for online docs +t = ax.transData +MouseEvent( + "motion_notify_event", ax.figure.canvas, *t.transform((0.5, 0.5)) +)._process() + plt.show() diff --git a/examples/event_handling/data_browser.py b/examples/event_handling/data_browser.py index 6d2c68a3741d..7e1a551ba8a8 100644 --- a/examples/event_handling/data_browser.py +++ b/examples/event_handling/data_browser.py @@ -1,13 +1,21 @@ """ ============ -Data Browser +Data browser ============ Connecting data between multiple canvases. This example covers how to interact data with multiple canvases. This -let's you select and highlight a point on one axis, and generating the +lets you select and highlight a point on one axis, and generating the data of that point on the other axis. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import numpy as np @@ -67,7 +75,7 @@ def update(self): dataind = self.lastind - ax2.cla() + ax2.clear() ax2.plot(X[dataind]) ax2.text(0.05, 0.9, f'mu={xs[dataind]:1.3f}\nsigma={ys[dataind]:1.3f}', diff --git a/examples/event_handling/figure_axes_enter_leave.py b/examples/event_handling/figure_axes_enter_leave.py index 8526346610f8..d55fb6e69d6a 100644 --- a/examples/event_handling/figure_axes_enter_leave.py +++ b/examples/event_handling/figure_axes_enter_leave.py @@ -5,6 +5,14 @@ Illustrate the figure and Axes enter and leave events by changing the frame colors on enter and leave. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import matplotlib.pyplot as plt diff --git a/examples/event_handling/ginput_manual_clabel_sgskip.py b/examples/event_handling/ginput_manual_clabel_sgskip.py index 0dc41b05230f..00e548bfccb4 100644 --- a/examples/event_handling/ginput_manual_clabel_sgskip.py +++ b/examples/event_handling/ginput_manual_clabel_sgskip.py @@ -6,10 +6,13 @@ This provides examples of uses of interactive functions, such as ginput, waitforbuttonpress and manual clabel placement. -This script must be run interactively using a backend that has a -graphical user interface (for example, using GTK3Agg backend, but not -PS backend). +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import time @@ -27,8 +30,9 @@ def tellme(s): # Define a triangle by clicking three points -plt.clf() -plt.setp(plt.gca(), autoscale_on=False) +plt.figure() +plt.xlim(0, 1) +plt.ylim(0, 1) tellme('You will define a triangle, click to begin') diff --git a/examples/event_handling/image_slices_viewer.py b/examples/event_handling/image_slices_viewer.py index 96cf8f64a3a0..8a2860902bce 100644 --- a/examples/event_handling/image_slices_viewer.py +++ b/examples/event_handling/image_slices_viewer.py @@ -1,51 +1,53 @@ """ -=================== -Image Slices Viewer -=================== +============ +Scroll event +============ -Scroll through 2D image slices of a 3D array. +In this example a scroll wheel event is used to scroll through 2D slices of +3D data. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import numpy as np import matplotlib.pyplot as plt -# Fixing random state for reproducibility -np.random.seed(19680801) - - class IndexTracker: def __init__(self, ax, X): - self.ax = ax - ax.set_title('use scroll wheel to navigate images') - + self.index = 0 self.X = X - rows, cols, self.slices = X.shape - self.ind = self.slices//2 - - self.im = ax.imshow(self.X[:, :, self.ind]) + self.ax = ax + self.im = ax.imshow(self.X[:, :, self.index]) self.update() def on_scroll(self, event): - print("%s %s" % (event.button, event.step)) - if event.button == 'up': - self.ind = (self.ind + 1) % self.slices - else: - self.ind = (self.ind - 1) % self.slices + print(event.button, event.step) + increment = 1 if event.button == 'up' else -1 + max_index = self.X.shape[-1] - 1 + self.index = np.clip(self.index + increment, 0, max_index) self.update() def update(self): - self.im.set_data(self.X[:, :, self.ind]) - self.ax.set_ylabel('slice %s' % self.ind) + self.im.set_data(self.X[:, :, self.index]) + self.ax.set_title( + f'Use scroll wheel to navigate\nindex {self.index}') self.im.axes.figure.canvas.draw() -fig, ax = plt.subplots(1, 1) - -X = np.random.rand(20, 20, 40) +x, y, z = np.ogrid[-10:10:100j, -10:10:100j, 1:10:20j] +X = np.sin(x * y * z) / (x * y * z) +fig, ax = plt.subplots() +# create an IndexTracker and make sure it lives during the whole +# lifetime of the figure by assigning it to a variable tracker = IndexTracker(ax, X) - fig.canvas.mpl_connect('scroll_event', tracker.on_scroll) plt.show() diff --git a/examples/event_handling/keypress_demo.py b/examples/event_handling/keypress_demo.py index 6aff3b617ca3..e71e7ad3dfc4 100644 --- a/examples/event_handling/keypress_demo.py +++ b/examples/event_handling/keypress_demo.py @@ -4,6 +4,14 @@ ============== Show how to connect to keypress events. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import sys import numpy as np diff --git a/examples/event_handling/lasso_demo.py b/examples/event_handling/lasso_demo.py index b5f29b2fe3a6..174c05a22cc6 100644 --- a/examples/event_handling/lasso_demo.py +++ b/examples/event_handling/lasso_demo.py @@ -9,6 +9,14 @@ This is currently a proof-of-concept implementation (though it is usable as is). There will be some refinement of the API. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ from matplotlib import colors as mcolors, path @@ -45,11 +53,14 @@ def __init__(self, ax, data): 6, sizes=(100,), facecolors=facecolors, offsets=self.xys, - transOffset=ax.transData) + offset_transform=ax.transData) ax.add_collection(self.collection) - self.cid = self.canvas.mpl_connect('button_press_event', self.on_press) + self.cid_press = self.canvas.mpl_connect('button_press_event', + self.on_press) + self.cid_release = self.canvas.mpl_connect('button_release_event', + self.on_release) def callback(self, verts): facecolors = self.collection.get_facecolors() @@ -62,7 +73,6 @@ def callback(self, verts): facecolors[i] = Datum.colorout self.canvas.draw_idle() - self.canvas.widgetlock.release(self.lasso) del self.lasso def on_press(self, event): @@ -76,6 +86,10 @@ def on_press(self, event): # acquire a lock on the widget drawing self.canvas.widgetlock(self.lasso) + def on_release(self, event): + if hasattr(self, 'lasso') and self.canvas.widgetlock.isowner(self.lasso): + self.canvas.widgetlock.release(self.lasso) + if __name__ == '__main__': diff --git a/examples/event_handling/legend_picking.py b/examples/event_handling/legend_picking.py index 5f8a3d1bb779..a33b9ba2088a 100644 --- a/examples/event_handling/legend_picking.py +++ b/examples/event_handling/legend_picking.py @@ -1,9 +1,17 @@ """ ============== -Legend Picking +Legend picking ============== Enable picking on the legend to toggle the original line on and off + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import numpy as np @@ -34,7 +42,7 @@ def on_pick(event): origline = lined[legline] visible = not origline.get_visible() origline.set_visible(visible) - # Change the alpha on the line in the legend so we can see what lines + # Change the alpha on the line in the legend, so we can see what lines # have been toggled. legline.set_alpha(1.0 if visible else 0.2) fig.canvas.draw() diff --git a/examples/event_handling/looking_glass.py b/examples/event_handling/looking_glass.py index 139afab106f3..70fe4651f79d 100644 --- a/examples/event_handling/looking_glass.py +++ b/examples/event_handling/looking_glass.py @@ -4,6 +4,14 @@ ============= Example using mouse events to simulate a looking glass for inspecting data. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import numpy as np import matplotlib.pyplot as plt diff --git a/examples/event_handling/path_editor.py b/examples/event_handling/path_editor.py index 9fe1a1470e64..972f581bf47b 100644 --- a/examples/event_handling/path_editor.py +++ b/examples/event_handling/path_editor.py @@ -1,12 +1,20 @@ """ =========== -Path Editor +Path editor =========== Sharing events across GUIs. This example demonstrates a cross-GUI application using Matplotlib event handling to interact with and modify objects on the canvas. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import numpy as np @@ -39,7 +47,7 @@ class PathInteractor: """ - An path editor. + A path editor. Press 't' to toggle vertex markers on and off. When vertex markers are on, they can be dragged with the mouse. @@ -74,17 +82,12 @@ def get_ind_under_point(self, event): Return the index of the point closest to the event position or *None* if no point is within ``self.epsilon`` to the event position. """ - # display coords - xy = np.asarray(self.pathpatch.get_path().vertices) - xyt = self.pathpatch.get_transform().transform(xy) + xy = self.pathpatch.get_path().vertices + xyt = self.pathpatch.get_transform().transform(xy) # to display coords xt, yt = xyt[:, 0], xyt[:, 1] d = np.sqrt((xt - event.x)**2 + (yt - event.y)**2) ind = d.argmin() - - if d[ind] >= self.epsilon: - ind = None - - return ind + return ind if d[ind] < self.epsilon else None def on_draw(self, event): """Callback for draws.""" diff --git a/examples/event_handling/pick_event_demo.py b/examples/event_handling/pick_event_demo.py index 84c635c27ca8..63f2bd130f17 100644 --- a/examples/event_handling/pick_event_demo.py +++ b/examples/event_handling/pick_event_demo.py @@ -1,12 +1,11 @@ """ =============== -Pick Event Demo +Pick event demo =============== - You can enable picking by setting the "picker" property of an artist -(for example, a matplotlib Line2D, Text, Patch, Polygon, AxesImage, -etc...) +(for example, a Matplotlib Line2D, Text, Patch, Polygon, AxesImage, +etc.) There are a variety of meanings of the picker property: @@ -22,7 +21,7 @@ of the data within epsilon of the pick event * function - if picker is callable, it is a user supplied function which - determines whether the artist is hit by the mouse event. + determines whether the artist is hit by the mouse event. :: hit, props = picker(artist, mouseevent) @@ -32,7 +31,7 @@ After you have enabled an artist for picking by setting the "picker" property, you need to connect to the figure canvas pick_event to get -pick callbacks on mouse press events. For example, +pick callbacks on mouse press events. For example, :: def pick_handler(event): mouseevent = event.mouseevent @@ -43,22 +42,33 @@ def pick_handler(event): The pick event (matplotlib.backend_bases.PickEvent) which is passed to your callback is always fired with two attributes: - mouseevent - the mouse event that generate the pick event. The - mouse event in turn has attributes like x and y (the coordinates in - display space, such as pixels from left, bottom) and xdata, ydata (the - coords in data space). Additionally, you can get information about - which buttons were pressed, which keys were pressed, which Axes - the mouse is over, etc. See matplotlib.backend_bases.MouseEvent - for details. +mouseevent + the mouse event that generate the pick event. + + The mouse event in turn has attributes like x and y (the coordinates in + display space, such as pixels from left, bottom) and xdata, ydata (the + coords in data space). Additionally, you can get information about + which buttons were pressed, which keys were pressed, which Axes + the mouse is over, etc. See matplotlib.backend_bases.MouseEvent + for details. - artist - the matplotlib.artist that generated the pick event. +artist + the matplotlib.artist that generated the pick event. Additionally, certain artists like Line2D and PatchCollection may -attach additional meta data like the indices into the data that meet +attach additional metadata like the indices into the data that meet the picker criteria (for example, all the points in the line that are within the specified epsilon tolerance) The examples below illustrate each of these methods. + +.. note:: + These examples exercises the interactive capabilities of Matplotlib, and + this will not appear in the static documentation. Please run this code on + your machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import matplotlib.pyplot as plt @@ -74,114 +84,125 @@ def pick_handler(event): np.random.seed(19680801) -def pick_simple(): - # simple picking, lines, rectangles and text - fig, (ax1, ax2) = plt.subplots(2, 1) - ax1.set_title('click on points, rectangles or text', picker=True) - ax1.set_ylabel('ylabel', picker=True, bbox=dict(facecolor='red')) - line, = ax1.plot(rand(100), 'o', picker=True, pickradius=5) - - # pick the rectangle - ax2.bar(range(10), rand(10), picker=True) - for label in ax2.get_xticklabels(): # make the xtick labels pickable - label.set_picker(True) - - def onpick1(event): - if isinstance(event.artist, Line2D): - thisline = event.artist - xdata = thisline.get_xdata() - ydata = thisline.get_ydata() - ind = event.ind - print('onpick1 line:', np.column_stack([xdata[ind], ydata[ind]])) - elif isinstance(event.artist, Rectangle): - patch = event.artist - print('onpick1 patch:', patch.get_path()) - elif isinstance(event.artist, Text): - text = event.artist - print('onpick1 text:', text.get_text()) - - fig.canvas.mpl_connect('pick_event', onpick1) - - -def pick_custom_hit(): - # picking with a custom hit test function - # you can define custom pickers by setting picker to a callable - # function. The function has the signature - # - # hit, props = func(artist, mouseevent) - # - # to determine the hit test. if the mouse event is over the artist, - # return hit=True and props is a dictionary of - # properties you want added to the PickEvent attributes - - def line_picker(line, mouseevent): - """ - Find the points within a certain distance from the mouseclick in - data coords and attach some extra attributes, pickx and picky - which are the data points that were picked. - """ - if mouseevent.xdata is None: - return False, dict() - xdata = line.get_xdata() - ydata = line.get_ydata() - maxd = 0.05 - d = np.sqrt( - (xdata - mouseevent.xdata)**2 + (ydata - mouseevent.ydata)**2) - - ind, = np.nonzero(d <= maxd) - if len(ind): - pickx = xdata[ind] - picky = ydata[ind] - props = dict(ind=ind, pickx=pickx, picky=picky) - return True, props - else: - return False, dict() - - def onpick2(event): - print('onpick2 line:', event.pickx, event.picky) - - fig, ax = plt.subplots() - ax.set_title('custom picker for line data') - line, = ax.plot(rand(100), rand(100), 'o', picker=line_picker) - fig.canvas.mpl_connect('pick_event', onpick2) - - -def pick_scatter_plot(): - # picking on a scatter plot (matplotlib.collections.RegularPolyCollection) - - x, y, c, s = rand(4, 100) - - def onpick3(event): +############################################################################# +# Simple picking, lines, rectangles and text +# ------------------------------------------ + +fig, (ax1, ax2) = plt.subplots(2, 1) +ax1.set_title('click on points, rectangles or text', picker=True) +ax1.set_ylabel('ylabel', picker=True, bbox=dict(facecolor='red')) +line, = ax1.plot(rand(100), 'o', picker=True, pickradius=5) + +# Pick the rectangle. +ax2.bar(range(10), rand(10), picker=True) +for label in ax2.get_xticklabels(): # Make the xtick labels pickable. + label.set_picker(True) + + +def onpick1(event): + if isinstance(event.artist, Line2D): + thisline = event.artist + xdata = thisline.get_xdata() + ydata = thisline.get_ydata() ind = event.ind - print('onpick3 scatter:', ind, x[ind], y[ind]) - - fig, ax = plt.subplots() - ax.scatter(x, y, 100*s, c, picker=True) - fig.canvas.mpl_connect('pick_event', onpick3) - - -def pick_image(): - # picking images (matplotlib.image.AxesImage) - fig, ax = plt.subplots() - ax.imshow(rand(10, 5), extent=(1, 2, 1, 2), picker=True) - ax.imshow(rand(5, 10), extent=(3, 4, 1, 2), picker=True) - ax.imshow(rand(20, 25), extent=(1, 2, 3, 4), picker=True) - ax.imshow(rand(30, 12), extent=(3, 4, 3, 4), picker=True) - ax.set(xlim=(0, 5), ylim=(0, 5)) - - def onpick4(event): - artist = event.artist - if isinstance(artist, AxesImage): - im = artist - A = im.get_array() - print('onpick4 image', A.shape) - - fig.canvas.mpl_connect('pick_event', onpick4) - - -if __name__ == '__main__': - pick_simple() - pick_custom_hit() - pick_scatter_plot() - pick_image() - plt.show() + print('onpick1 line:', np.column_stack([xdata[ind], ydata[ind]])) + elif isinstance(event.artist, Rectangle): + patch = event.artist + print('onpick1 patch:', patch.get_path()) + elif isinstance(event.artist, Text): + text = event.artist + print('onpick1 text:', text.get_text()) + + +fig.canvas.mpl_connect('pick_event', onpick1) + + +############################################################################# +# Picking with a custom hit test function +# --------------------------------------- +# You can define custom pickers by setting picker to a callable function. The +# function has the signature:: +# +# hit, props = func(artist, mouseevent) +# +# to determine the hit test. If the mouse event is over the artist, return +# ``hit=True`` and ``props`` is a dictionary of properties you want added to +# the `.PickEvent` attributes. + +def line_picker(line, mouseevent): + """ + Find the points within a certain distance from the mouseclick in + data coords and attach some extra attributes, pickx and picky + which are the data points that were picked. + """ + if mouseevent.xdata is None: + return False, dict() + xdata = line.get_xdata() + ydata = line.get_ydata() + maxd = 0.05 + d = np.sqrt( + (xdata - mouseevent.xdata)**2 + (ydata - mouseevent.ydata)**2) + + ind, = np.nonzero(d <= maxd) + if len(ind): + pickx = xdata[ind] + picky = ydata[ind] + props = dict(ind=ind, pickx=pickx, picky=picky) + return True, props + else: + return False, dict() + + +def onpick2(event): + print('onpick2 line:', event.pickx, event.picky) + + +fig, ax = plt.subplots() +ax.set_title('custom picker for line data') +line, = ax.plot(rand(100), rand(100), 'o', picker=line_picker) +fig.canvas.mpl_connect('pick_event', onpick2) + + +############################################################################# +# Picking on a scatter plot +# ------------------------- +# A scatter plot is backed by a `~matplotlib.collections.PathCollection`. + +x, y, c, s = rand(4, 100) + + +def onpick3(event): + ind = event.ind + print('onpick3 scatter:', ind, x[ind], y[ind]) + + +fig, ax = plt.subplots() +ax.scatter(x, y, 100*s, c, picker=True) +fig.canvas.mpl_connect('pick_event', onpick3) + + +############################################################################# +# Picking images +# -------------- +# Images plotted using `.Axes.imshow` are `~matplotlib.image.AxesImage` +# objects. + +fig, ax = plt.subplots() +ax.imshow(rand(10, 5), extent=(1, 2, 1, 2), picker=True) +ax.imshow(rand(5, 10), extent=(3, 4, 1, 2), picker=True) +ax.imshow(rand(20, 25), extent=(1, 2, 3, 4), picker=True) +ax.imshow(rand(30, 12), extent=(3, 4, 3, 4), picker=True) +ax.set(xlim=(0, 5), ylim=(0, 5)) + + +def onpick4(event): + artist = event.artist + if isinstance(artist, AxesImage): + im = artist + A = im.get_array() + print('onpick4 image', A.shape) + + +fig.canvas.mpl_connect('pick_event', onpick4) + +plt.show() diff --git a/examples/event_handling/pick_event_demo2.py b/examples/event_handling/pick_event_demo2.py index 162fbdf65ade..01622cb87e76 100644 --- a/examples/event_handling/pick_event_demo2.py +++ b/examples/event_handling/pick_event_demo2.py @@ -1,11 +1,19 @@ """ -================ -Pick Event Demo2 -================ +================= +Pick event demo 2 +================= Compute the mean (mu) and standard deviation (sigma) of 100 data sets and plot mu vs. sigma. When you click on one of the (mu, sigma) points, plot the raw data from the dataset that generated this point. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import numpy as np import matplotlib.pyplot as plt @@ -26,11 +34,11 @@ def onpick(event): if event.artist != line: - return True + return N = len(event.ind) if not N: - return True + return figi, axs = plt.subplots(N, squeeze=False) for ax, dataind in zip(axs.flat, event.ind): @@ -39,7 +47,7 @@ def onpick(event): transform=ax.transAxes, va='top') ax.set_ylim(-0.5, 1.5) figi.show() - return True + fig.canvas.mpl_connect('pick_event', onpick) diff --git a/examples/event_handling/pipong.py b/examples/event_handling/pipong.py deleted file mode 100644 index b3064d0c84ee..000000000000 --- a/examples/event_handling/pipong.py +++ /dev/null @@ -1,291 +0,0 @@ -""" -====== -Pipong -====== - -A Matplotlib based game of Pong illustrating one way to write interactive -animation which are easily ported to multiple backends -pipong.py was written by Paul Ivanov -""" - - -import numpy as np -import matplotlib.pyplot as plt -from numpy.random import randn, randint -from matplotlib.font_manager import FontProperties - -instructions = """ -Player A: Player B: - 'e' up 'i' - 'd' down 'k' - -press 't' -- close these instructions - (animation will be much faster) -press 'a' -- add a puck -press 'A' -- remove a puck -press '1' -- slow down all pucks -press '2' -- speed up all pucks -press '3' -- slow down distractors -press '4' -- speed up distractors -press ' ' -- reset the first puck -press 'n' -- toggle distractors on/off -press 'g' -- toggle the game on/off - - """ - - -class Pad: - def __init__(self, disp, x, y, type='l'): - self.disp = disp - self.x = x - self.y = y - self.w = .3 - self.score = 0 - self.xoffset = 0.3 - self.yoffset = 0.1 - if type == 'r': - self.xoffset *= -1.0 - - if type == 'l' or type == 'r': - self.signx = -1.0 - self.signy = 1.0 - else: - self.signx = 1.0 - self.signy = -1.0 - - def contains(self, loc): - return self.disp.get_bbox().contains(loc.x, loc.y) - - -class Puck: - def __init__(self, disp, pad, field): - self.vmax = .2 - self.disp = disp - self.field = field - self._reset(pad) - - def _reset(self, pad): - self.x = pad.x + pad.xoffset - if pad.y < 0: - self.y = pad.y + pad.yoffset - else: - self.y = pad.y - pad.yoffset - self.vx = pad.x - self.x - self.vy = pad.y + pad.w/2 - self.y - self._speedlimit() - self._slower() - self._slower() - - def update(self, pads): - self.x += self.vx - self.y += self.vy - for pad in pads: - if pad.contains(self): - self.vx *= 1.2 * pad.signx - self.vy *= 1.2 * pad.signy - fudge = .001 - # probably cleaner with something like... - if self.x < fudge: - pads[1].score += 1 - self._reset(pads[0]) - return True - if self.x > 7 - fudge: - pads[0].score += 1 - self._reset(pads[1]) - return True - if self.y < -1 + fudge or self.y > 1 - fudge: - self.vy *= -1.0 - # add some randomness, just to make it interesting - self.vy -= (randn()/300.0 + 1/300.0) * np.sign(self.vy) - self._speedlimit() - return False - - def _slower(self): - self.vx /= 5.0 - self.vy /= 5.0 - - def _faster(self): - self.vx *= 5.0 - self.vy *= 5.0 - - def _speedlimit(self): - if self.vx > self.vmax: - self.vx = self.vmax - if self.vx < -self.vmax: - self.vx = -self.vmax - - if self.vy > self.vmax: - self.vy = self.vmax - if self.vy < -self.vmax: - self.vy = -self.vmax - - -class Game: - def __init__(self, ax): - # create the initial line - self.ax = ax - ax.xaxis.set_visible(False) - ax.set_xlim([0, 7]) - ax.yaxis.set_visible(False) - ax.set_ylim([-1, 1]) - pad_a_x = 0 - pad_b_x = .50 - pad_a_y = pad_b_y = .30 - pad_b_x += 6.3 - - # pads - pA, = self.ax.barh(pad_a_y, .2, - height=.3, color='k', alpha=.5, edgecolor='b', - lw=2, label="Player B", - animated=True) - pB, = self.ax.barh(pad_b_y, .2, - height=.3, left=pad_b_x, color='k', alpha=.5, - edgecolor='r', lw=2, label="Player A", - animated=True) - - # distractors - self.x = np.arange(0, 2.22*np.pi, 0.01) - self.line, = self.ax.plot(self.x, np.sin(self.x), "r", - animated=True, lw=4) - self.line2, = self.ax.plot(self.x, np.cos(self.x), "g", - animated=True, lw=4) - self.line3, = self.ax.plot(self.x, np.cos(self.x), "g", - animated=True, lw=4) - self.line4, = self.ax.plot(self.x, np.cos(self.x), "r", - animated=True, lw=4) - - # center line - self.centerline, = self.ax.plot([3.5, 3.5], [1, -1], 'k', - alpha=.5, animated=True, lw=8) - - # puck (s) - self.puckdisp = self.ax.scatter([1], [1], label='_nolegend_', - s=200, c='g', - alpha=.9, animated=True) - - self.canvas = self.ax.figure.canvas - self.background = None - self.cnt = 0 - self.distract = True - self.res = 100.0 - self.on = False - self.inst = True # show instructions from the beginning - self.pads = [Pad(pA, pad_a_x, pad_a_y), - Pad(pB, pad_b_x, pad_b_y, 'r')] - self.pucks = [] - self.i = self.ax.annotate(instructions, (.5, 0.5), - name='monospace', - verticalalignment='center', - horizontalalignment='center', - multialignment='left', - xycoords='axes fraction', - animated=False) - self.canvas.mpl_connect('key_press_event', self.on_key_press) - - def draw(self, event): - draw_artist = self.ax.draw_artist - if self.background is None: - self.background = self.canvas.copy_from_bbox(self.ax.bbox) - - # restore the clean slate background - self.canvas.restore_region(self.background) - - # show the distractors - if self.distract: - self.line.set_ydata(np.sin(self.x + self.cnt/self.res)) - self.line2.set_ydata(np.cos(self.x - self.cnt/self.res)) - self.line3.set_ydata(np.tan(self.x + self.cnt/self.res)) - self.line4.set_ydata(np.tan(self.x - self.cnt/self.res)) - draw_artist(self.line) - draw_artist(self.line2) - draw_artist(self.line3) - draw_artist(self.line4) - - # pucks and pads - if self.on: - self.ax.draw_artist(self.centerline) - for pad in self.pads: - pad.disp.set_y(pad.y) - pad.disp.set_x(pad.x) - self.ax.draw_artist(pad.disp) - - for puck in self.pucks: - if puck.update(self.pads): - # we only get here if someone scored - self.pads[0].disp.set_label( - " " + str(self.pads[0].score)) - self.pads[1].disp.set_label( - " " + str(self.pads[1].score)) - self.ax.legend(loc='center', framealpha=.2, - facecolor='0.5', - prop=FontProperties(size='xx-large', - weight='bold')) - - self.background = None - self.ax.figure.canvas.draw_idle() - return True - puck.disp.set_offsets([[puck.x, puck.y]]) - self.ax.draw_artist(puck.disp) - - # just redraw the axes rectangle - self.canvas.blit(self.ax.bbox) - self.canvas.flush_events() - if self.cnt == 50000: - # just so we don't get carried away - print("...and you've been playing for too long!!!") - plt.close() - - self.cnt += 1 - return True - - def on_key_press(self, event): - if event.key == '3': - self.res *= 5.0 - if event.key == '4': - self.res /= 5.0 - - if event.key == 'e': - self.pads[0].y += .1 - if self.pads[0].y > 1 - .3: - self.pads[0].y = 1 - .3 - if event.key == 'd': - self.pads[0].y -= .1 - if self.pads[0].y < -1: - self.pads[0].y = -1 - - if event.key == 'i': - self.pads[1].y += .1 - if self.pads[1].y > 1 - .3: - self.pads[1].y = 1 - .3 - if event.key == 'k': - self.pads[1].y -= .1 - if self.pads[1].y < -1: - self.pads[1].y = -1 - - if event.key == 'a': - self.pucks.append(Puck(self.puckdisp, - self.pads[randint(2)], - self.ax.bbox)) - if event.key == 'A' and len(self.pucks): - self.pucks.pop() - if event.key == ' ' and len(self.pucks): - self.pucks[0]._reset(self.pads[randint(2)]) - if event.key == '1': - for p in self.pucks: - p._slower() - if event.key == '2': - for p in self.pucks: - p._faster() - - if event.key == 'n': - self.distract = not self.distract - - if event.key == 'g': - self.on = not self.on - if event.key == 't': - self.inst = not self.inst - self.i.set_visible(not self.i.get_visible()) - self.background = None - self.canvas.draw_idle() - if event.key == 'q': - plt.close() diff --git a/examples/event_handling/poly_editor.py b/examples/event_handling/poly_editor.py index 938c7b8b7b28..8779a243b925 100644 --- a/examples/event_handling/poly_editor.py +++ b/examples/event_handling/poly_editor.py @@ -5,38 +5,33 @@ This is an example to show how to build cross-GUI applications using Matplotlib event handling to interact with objects on the canvas. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ + import numpy as np from matplotlib.lines import Line2D from matplotlib.artist import Artist -def dist(x, y): - """ - Return the distance between two points. - """ - d = x - y - return np.sqrt(np.dot(d, d)) - - def dist_point_to_segment(p, s0, s1): """ - Get the distance of a point to a segment. - *p*, *s0*, *s1* are *xy* sequences - This algorithm from - http://geomalgorithms.com/a02-_lines.html + Get the distance from the point *p* to the segment (*s0*, *s1*), where + *p*, *s0*, *s1* are ``[x, y]`` arrays. """ - v = s1 - s0 - w = p - s0 - c1 = np.dot(w, v) - if c1 <= 0: - return dist(p, s0) - c2 = np.dot(v, v) - if c2 <= c1: - return dist(p, s1) - b = c1 / c2 - pb = s0 + b * v - return dist(p, pb) + s01 = s1 - s0 + s0p = p - s0 + if (s01 == 0).all(): + return np.hypot(*s0p) + # Project onto segment, without going past segment ends. + p1 = s0 + np.clip((s0p @ s01) / (s01 @ s01), 0, 1) * s01 + return np.hypot(*(p - p1)) class PolygonInteractor: diff --git a/examples/event_handling/pong_sgskip.py b/examples/event_handling/pong_sgskip.py index 416bdf048dbe..23b330165b36 100644 --- a/examples/event_handling/pong_sgskip.py +++ b/examples/event_handling/pong_sgskip.py @@ -3,23 +3,302 @@ Pong ==== -A small game demo using Matplotlib. +A Matplotlib based game of Pong illustrating one way to write interactive +animations that are easily ported to multiple backends. -.. only:: builder_html - - This example requires :download:`pipong.py ` +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ -import time +import time +import numpy as np import matplotlib.pyplot as plt -import pipong +from numpy.random import randn, randint +from matplotlib.font_manager import FontProperties + +instructions = """ +Player A: Player B: + 'e' up 'i' + 'd' down 'k' + +press 't' -- close these instructions + (animation will be much faster) +press 'a' -- add a puck +press 'A' -- remove a puck +press '1' -- slow down all pucks +press '2' -- speed up all pucks +press '3' -- slow down distractors +press '4' -- speed up distractors +press ' ' -- reset the first puck +press 'n' -- toggle distractors on/off +press 'g' -- toggle the game on/off + + """ + + +class Pad: + def __init__(self, disp, x, y, type='l'): + self.disp = disp + self.x = x + self.y = y + self.w = .3 + self.score = 0 + self.xoffset = 0.3 + self.yoffset = 0.1 + if type == 'r': + self.xoffset *= -1.0 + + if type == 'l' or type == 'r': + self.signx = -1.0 + self.signy = 1.0 + else: + self.signx = 1.0 + self.signy = -1.0 + + def contains(self, loc): + return self.disp.get_bbox().contains(loc.x, loc.y) + + +class Puck: + def __init__(self, disp, pad, field): + self.vmax = .2 + self.disp = disp + self.field = field + self._reset(pad) + + def _reset(self, pad): + self.x = pad.x + pad.xoffset + if pad.y < 0: + self.y = pad.y + pad.yoffset + else: + self.y = pad.y - pad.yoffset + self.vx = pad.x - self.x + self.vy = pad.y + pad.w/2 - self.y + self._speedlimit() + self._slower() + self._slower() + + def update(self, pads): + self.x += self.vx + self.y += self.vy + for pad in pads: + if pad.contains(self): + self.vx *= 1.2 * pad.signx + self.vy *= 1.2 * pad.signy + fudge = .001 + # probably cleaner with something like... + if self.x < fudge: + pads[1].score += 1 + self._reset(pads[0]) + return True + if self.x > 7 - fudge: + pads[0].score += 1 + self._reset(pads[1]) + return True + if self.y < -1 + fudge or self.y > 1 - fudge: + self.vy *= -1.0 + # add some randomness, just to make it interesting + self.vy -= (randn()/300.0 + 1/300.0) * np.sign(self.vy) + self._speedlimit() + return False + + def _slower(self): + self.vx /= 5.0 + self.vy /= 5.0 + + def _faster(self): + self.vx *= 5.0 + self.vy *= 5.0 + + def _speedlimit(self): + if self.vx > self.vmax: + self.vx = self.vmax + if self.vx < -self.vmax: + self.vx = -self.vmax + + if self.vy > self.vmax: + self.vy = self.vmax + if self.vy < -self.vmax: + self.vy = -self.vmax + + +class Game: + def __init__(self, ax): + # create the initial line + self.ax = ax + ax.xaxis.set_visible(False) + ax.set_xlim([0, 7]) + ax.yaxis.set_visible(False) + ax.set_ylim([-1, 1]) + pad_a_x = 0 + pad_b_x = .50 + pad_a_y = pad_b_y = .30 + pad_b_x += 6.3 + + # pads + pA, = self.ax.barh(pad_a_y, .2, + height=.3, color='k', alpha=.5, edgecolor='b', + lw=2, label="Player B", + animated=True) + pB, = self.ax.barh(pad_b_y, .2, + height=.3, left=pad_b_x, color='k', alpha=.5, + edgecolor='r', lw=2, label="Player A", + animated=True) + + # distractors + self.x = np.arange(0, 2.22*np.pi, 0.01) + self.line, = self.ax.plot(self.x, np.sin(self.x), "r", + animated=True, lw=4) + self.line2, = self.ax.plot(self.x, np.cos(self.x), "g", + animated=True, lw=4) + self.line3, = self.ax.plot(self.x, np.cos(self.x), "g", + animated=True, lw=4) + self.line4, = self.ax.plot(self.x, np.cos(self.x), "r", + animated=True, lw=4) + + # center line + self.centerline, = self.ax.plot([3.5, 3.5], [1, -1], 'k', + alpha=.5, animated=True, lw=8) + + # puck (s) + self.puckdisp = self.ax.scatter([1], [1], label='_nolegend_', + s=200, c='g', + alpha=.9, animated=True) + + self.canvas = self.ax.figure.canvas + self.background = None + self.cnt = 0 + self.distract = True + self.res = 100.0 + self.on = False + self.inst = True # show instructions from the beginning + self.pads = [Pad(pA, pad_a_x, pad_a_y), + Pad(pB, pad_b_x, pad_b_y, 'r')] + self.pucks = [] + self.i = self.ax.annotate(instructions, (.5, 0.5), + name='monospace', + verticalalignment='center', + horizontalalignment='center', + multialignment='left', + xycoords='axes fraction', + animated=False) + self.canvas.mpl_connect('key_press_event', self.on_key_press) + + def draw(self): + draw_artist = self.ax.draw_artist + if self.background is None: + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + + # restore the clean slate background + self.canvas.restore_region(self.background) + + # show the distractors + if self.distract: + self.line.set_ydata(np.sin(self.x + self.cnt/self.res)) + self.line2.set_ydata(np.cos(self.x - self.cnt/self.res)) + self.line3.set_ydata(np.tan(self.x + self.cnt/self.res)) + self.line4.set_ydata(np.tan(self.x - self.cnt/self.res)) + draw_artist(self.line) + draw_artist(self.line2) + draw_artist(self.line3) + draw_artist(self.line4) + + # pucks and pads + if self.on: + self.ax.draw_artist(self.centerline) + for pad in self.pads: + pad.disp.set_y(pad.y) + pad.disp.set_x(pad.x) + self.ax.draw_artist(pad.disp) + + for puck in self.pucks: + if puck.update(self.pads): + # we only get here if someone scored + self.pads[0].disp.set_label(f" {self.pads[0].score}") + self.pads[1].disp.set_label(f" {self.pads[1].score}") + self.ax.legend(loc='center', framealpha=.2, + facecolor='0.5', + prop=FontProperties(size='xx-large', + weight='bold')) + + self.background = None + self.ax.figure.canvas.draw_idle() + return + puck.disp.set_offsets([[puck.x, puck.y]]) + self.ax.draw_artist(puck.disp) + + # just redraw the axes rectangle + self.canvas.blit(self.ax.bbox) + self.canvas.flush_events() + if self.cnt == 50000: + # just so we don't get carried away + print("...and you've been playing for too long!!!") + plt.close() + + self.cnt += 1 + + def on_key_press(self, event): + if event.key == '3': + self.res *= 5.0 + if event.key == '4': + self.res /= 5.0 + + if event.key == 'e': + self.pads[0].y += .1 + if self.pads[0].y > 1 - .3: + self.pads[0].y = 1 - .3 + if event.key == 'd': + self.pads[0].y -= .1 + if self.pads[0].y < -1: + self.pads[0].y = -1 + + if event.key == 'i': + self.pads[1].y += .1 + if self.pads[1].y > 1 - .3: + self.pads[1].y = 1 - .3 + if event.key == 'k': + self.pads[1].y -= .1 + if self.pads[1].y < -1: + self.pads[1].y = -1 + + if event.key == 'a': + self.pucks.append(Puck(self.puckdisp, + self.pads[randint(2)], + self.ax.bbox)) + if event.key == 'A' and len(self.pucks): + self.pucks.pop() + if event.key == ' ' and len(self.pucks): + self.pucks[0]._reset(self.pads[randint(2)]) + if event.key == '1': + for p in self.pucks: + p._slower() + if event.key == '2': + for p in self.pucks: + p._faster() + + if event.key == 'n': + self.distract = not self.distract + + if event.key == 'g': + self.on = not self.on + if event.key == 't': + self.inst = not self.inst + self.i.set_visible(not self.i.get_visible()) + self.background = None + self.canvas.draw_idle() + if event.key == 'q': + plt.close() fig, ax = plt.subplots() canvas = ax.figure.canvas -animation = pipong.Game(ax) +animation = Game(ax) # disable the default key bindings if fig.canvas.manager.key_press_handler_id is not None: @@ -35,10 +314,7 @@ def on_redraw(event): def start_anim(event): canvas.mpl_disconnect(start_anim.cid) - def local_draw(): - if animation.ax.get_renderer_cache(): - animation.draw(None) - start_anim.timer.add_callback(local_draw) + start_anim.timer.add_callback(animation.draw) start_anim.timer.start() canvas.mpl_connect('draw_event', on_redraw) diff --git a/examples/event_handling/resample.py b/examples/event_handling/resample.py index 542e84f13c71..c7f94f1b7629 100644 --- a/examples/event_handling/resample.py +++ b/examples/event_handling/resample.py @@ -6,6 +6,14 @@ Downsampling lowers the sample rate or sample size of a signal. In this tutorial, the signal is downsampled when the plot is adjusted through dragging and zooming. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import numpy as np @@ -37,7 +45,7 @@ def downsample(self, xstart, xend): xdata = xdata[::ratio] ydata = ydata[::ratio] - print("using {} of {} visible points".format(len(ydata), np.sum(mask))) + print(f"using {len(ydata)} of {np.sum(mask)} visible points") return xdata, ydata diff --git a/examples/event_handling/timers.py b/examples/event_handling/timers.py index 4db7fe40bdc3..43330097cf87 100644 --- a/examples/event_handling/timers.py +++ b/examples/event_handling/timers.py @@ -5,6 +5,14 @@ Simple example of using general timer objects. This is used to update the time placed in the title of the figure. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import matplotlib.pyplot as plt import numpy as np @@ -26,10 +34,10 @@ def update_title(axes): timer.add_callback(update_title, ax) timer.start() -# Or could start the timer on first figure draw -#def start_timer(event): -# timer.start() -# fig.canvas.mpl_disconnect(drawid) -#drawid = fig.canvas.mpl_connect('draw_event', start_timer) +# Or could start the timer on first figure draw: +# def start_timer(event): +# timer.start() +# fig.canvas.mpl_disconnect(drawid) +# drawid = fig.canvas.mpl_connect('draw_event', start_timer) plt.show() diff --git a/examples/event_handling/trifinder_event_demo.py b/examples/event_handling/trifinder_event_demo.py index 6678003ab189..991c5c9a3a82 100644 --- a/examples/event_handling/trifinder_event_demo.py +++ b/examples/event_handling/trifinder_event_demo.py @@ -6,6 +6,14 @@ Example showing the use of a TriFinder object. As the mouse is moved over the triangulation, the triangle under the cursor is highlighted and the index of the triangle is displayed in the plot title. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import matplotlib.pyplot as plt from matplotlib.tri import Triangulation diff --git a/examples/event_handling/viewlims.py b/examples/event_handling/viewlims.py index d6bdf99787d3..134419300a68 100644 --- a/examples/event_handling/viewlims.py +++ b/examples/event_handling/viewlims.py @@ -5,6 +5,14 @@ Creates two identical panels. Zooming in on the right panel will show a rectangle in the first panel, denoting the zoomed region. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import numpy as np import matplotlib.pyplot as plt diff --git a/examples/event_handling/zoom_window.py b/examples/event_handling/zoom_window.py index 9afe60ce935c..c10cd820aa47 100644 --- a/examples/event_handling/zoom_window.py +++ b/examples/event_handling/zoom_window.py @@ -12,6 +12,14 @@ Note the diameter of the circles in the scatter are defined in points**2, so their size is independent of the zoom. + +.. note:: + This example exercises the interactive capabilities of Matplotlib, and this + will not appear in the static documentation. Please run this code on your + machine to see the interactivity. + + You can copy and paste individual parts, or download the entire example + using the link at the bottom of the page. """ import matplotlib.pyplot as plt @@ -21,8 +29,8 @@ # Fixing random state for reproducibility np.random.seed(19680801) -figsrc, axsrc = plt.subplots() -figzoom, axzoom = plt.subplots() +figsrc, axsrc = plt.subplots(figsize=(3.7, 3.7)) +figzoom, axzoom = plt.subplots(figsize=(3.7, 3.7)) axsrc.set(xlim=(0, 1), ylim=(0, 1), autoscale_on=False, title='Click to zoom') axzoom.set(xlim=(0.45, 0.55), ylim=(0.4, 0.6), autoscale_on=False, diff --git a/examples/frontpage/3D.py b/examples/frontpage/3D.py deleted file mode 100644 index 49ebcc008923..000000000000 --- a/examples/frontpage/3D.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -==================== -Frontpage 3D example -==================== - -This example reproduces the frontpage 3D example. -""" - -from matplotlib import cbook -from matplotlib import cm -from matplotlib.colors import LightSource -import matplotlib.pyplot as plt -import numpy as np - -dem = cbook.get_sample_data('jacksboro_fault_dem.npz', np_load=True) -z = dem['elevation'] -nrows, ncols = z.shape -x = np.linspace(dem['xmin'], dem['xmax'], ncols) -y = np.linspace(dem['ymin'], dem['ymax'], nrows) -x, y = np.meshgrid(x, y) - -region = np.s_[5:50, 5:50] -x, y, z = x[region], y[region], z[region] - -fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) - -ls = LightSource(270, 45) -# To use a custom hillshading mode, override the built-in shading and pass -# in the rgb colors of the shaded surface calculated from "shade". -rgb = ls.shade(z, cmap=cm.gist_earth, vert_exag=0.1, blend_mode='soft') -surf = ax.plot_surface(x, y, z, rstride=1, cstride=1, facecolors=rgb, - linewidth=0, antialiased=False, shade=False) -ax.set_xticks([]) -ax.set_yticks([]) -ax.set_zticks([]) -fig.savefig("surface3d_frontpage.png", dpi=25) # results in 160x120 px image diff --git a/examples/frontpage/README.txt b/examples/frontpage/README.txt deleted file mode 100644 index 1f70a8a9fda2..000000000000 --- a/examples/frontpage/README.txt +++ /dev/null @@ -1,4 +0,0 @@ -.. _front_page_examples: - -Front Page -========== diff --git a/examples/frontpage/contour.py b/examples/frontpage/contour.py deleted file mode 100644 index ddb0721c1203..000000000000 --- a/examples/frontpage/contour.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -========================= -Frontpage contour example -========================= - -This example reproduces the frontpage contour example. -""" - -import matplotlib.pyplot as plt -import numpy as np -from matplotlib import cm - -extent = (-3, 3, -3, 3) - -delta = 0.5 -x = np.arange(-3.0, 4.001, delta) -y = np.arange(-4.0, 3.001, delta) -X, Y = np.meshgrid(x, y) -Z1 = np.exp(-X**2 - Y**2) -Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) -Z = Z1 - Z2 - -norm = cm.colors.Normalize(vmax=abs(Z).max(), vmin=-abs(Z).max()) - -fig, ax = plt.subplots() -cset1 = ax.contourf( - X, Y, Z, 40, - norm=norm) -ax.set_xlim(-2, 2) -ax.set_ylim(-2, 2) -ax.set_xticks([]) -ax.set_yticks([]) -fig.savefig("contour_frontpage.png", dpi=25) # results in 160x120 px image -plt.show() diff --git a/examples/frontpage/histogram.py b/examples/frontpage/histogram.py deleted file mode 100644 index a0938bdb8916..000000000000 --- a/examples/frontpage/histogram.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -=========================== -Frontpage histogram example -=========================== - -This example reproduces the frontpage histogram example. -""" - -import matplotlib.pyplot as plt -import numpy as np - - -random_state = np.random.RandomState(19680801) -X = random_state.randn(10000) - -fig, ax = plt.subplots() -ax.hist(X, bins=25, density=True) -x = np.linspace(-5, 5, 1000) -ax.plot(x, 1 / np.sqrt(2*np.pi) * np.exp(-(x**2)/2), linewidth=4) -ax.set_xticks([]) -ax.set_yticks([]) -fig.savefig("histogram_frontpage.png", dpi=25) # results in 160x120 px image diff --git a/examples/frontpage/membrane.py b/examples/frontpage/membrane.py deleted file mode 100644 index 4e126eceda5f..000000000000 --- a/examples/frontpage/membrane.py +++ /dev/null @@ -1,24 +0,0 @@ -""" -====================== -Frontpage plot example -====================== - -This example reproduces the frontpage simple plot example. -""" - -import matplotlib.pyplot as plt -import matplotlib.cbook as cbook -import numpy as np - - -with cbook.get_sample_data('membrane.dat') as datafile: - x = np.fromfile(datafile, np.float32) -# 0.0005 is the sample interval - -fig, ax = plt.subplots() -ax.plot(x, linewidth=4) -ax.set_xlim(5000, 6000) -ax.set_ylim(-0.6, 0.1) -ax.set_xticks([]) -ax.set_yticks([]) -fig.savefig("membrane_frontpage.png", dpi=25) # results in 160x120 px image diff --git a/examples/images_contours_and_fields/barb_demo.py b/examples/images_contours_and_fields/barb_demo.py index 45a099899c1a..4530dc813b3d 100644 --- a/examples/images_contours_and_fields/barb_demo.py +++ b/examples/images_contours_and_fields/barb_demo.py @@ -47,6 +47,7 @@ masked_u[4] = 1000 # Bad value that should not be plotted when masked masked_u[4] = np.ma.masked +############################################################################# # Identical plot to panel 2 in the first figure, but with the point at # (0.5, 0.25) missing (masked) fig2, ax2 = plt.subplots() diff --git a/examples/images_contours_and_fields/colormap_interactive_adjustment.py b/examples/images_contours_and_fields/colormap_interactive_adjustment.py new file mode 100644 index 000000000000..3ab9074fd1b6 --- /dev/null +++ b/examples/images_contours_and_fields/colormap_interactive_adjustment.py @@ -0,0 +1,33 @@ +""" +======================================== +Interactive Adjustment of Colormap Range +======================================== + +Demonstration of how a colorbar can be used to interactively adjust the +range of colormapping on an image. To use the interactive feature, you must +be in either zoom mode (magnifying glass toolbar button) or +pan mode (4-way arrow toolbar button) and click inside the colorbar. + +When zooming, the bounding box of the zoom region defines the new vmin and +vmax of the norm. Zooming using the right mouse button will expand the +vmin and vmax proportionally to the selected region, in the same manner that +one can zoom out on an axis. When panning, the vmin and vmax of the norm are +both shifted according to the direction of movement. The +Home/Back/Forward buttons can also be used to get back to a previous state. + +.. redirect-from:: /gallery/userdemo/colormap_interactive_adjustment +""" +import matplotlib.pyplot as plt +import numpy as np + +t = np.linspace(0, 2 * np.pi, 1024) +data2d = np.sin(t)[:, np.newaxis] * np.cos(t)[np.newaxis, :] + +fig, ax = plt.subplots() +im = ax.imshow(data2d) +ax.set_title('Pan on the colorbar to shift the color mapping\n' + 'Zoom on the colorbar to scale the color mapping') + +fig.colorbar(im, ax=ax, label='Interactive colorbar') + +plt.show() diff --git a/examples/userdemo/colormap_normalizations.py b/examples/images_contours_and_fields/colormap_normalizations.py similarity index 93% rename from examples/userdemo/colormap_normalizations.py rename to examples/images_contours_and_fields/colormap_normalizations.py index febccf35a449..7ad3423cce68 100644 --- a/examples/userdemo/colormap_normalizations.py +++ b/examples/images_contours_and_fields/colormap_normalizations.py @@ -1,9 +1,11 @@ """ ======================= -Colormap Normalizations +Colormap normalizations ======================= Demonstration of using norm to map colormaps onto data in non-linear ways. + +.. redirect-from:: /gallery/userdemo/colormap_normalizations """ import numpy as np @@ -18,8 +20,8 @@ X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)] # A low hump with a spike coming out of the top. Needs to have -# z/colour axis on a log scale so we see both hump and spike. linear -# scale only shows the spike. +# z/colour axis on a log scale, so we see both hump and spike. +# A linear scale only shows the spike. Z1 = np.exp(-X**2 - Y**2) Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2) @@ -61,19 +63,17 @@ # Note that colorbar labels do not come out looking very good. X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)] -Z1 = 5 * np.exp(-X**2 - Y**2) -Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) -Z = (Z1 - Z2) * 2 +Z = 5 * np.exp(-X**2 - Y**2) fig, ax = plt.subplots(2, 1) -pcm = ax[0].pcolormesh(X, Y, Z1, +pcm = ax[0].pcolormesh(X, Y, Z, norm=colors.SymLogNorm(linthresh=0.03, linscale=0.03, vmin=-1.0, vmax=1.0, base=10), cmap='RdBu_r', shading='nearest') fig.colorbar(pcm, ax=ax[0], extend='both') -pcm = ax[1].pcolormesh(X, Y, Z1, cmap='RdBu_r', vmin=-np.max(Z1), +pcm = ax[1].pcolormesh(X, Y, Z, cmap='RdBu_r', vmin=-np.max(Z), shading='nearest') fig.colorbar(pcm, ax=ax[1], extend='both') diff --git a/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py new file mode 100644 index 000000000000..c5bd9d05f4dc --- /dev/null +++ b/examples/images_contours_and_fields/colormap_normalizations_symlognorm.py @@ -0,0 +1,84 @@ +""" +================================== +Colormap normalizations SymLogNorm +================================== + +Demonstration of using norm to map colormaps onto data in non-linear ways. + +.. redirect-from:: /gallery/userdemo/colormap_normalization_symlognorm +""" + +############################################################################### +# Synthetic dataset consisting of two humps, one negative and one positive, +# the positive with 8-times the amplitude. +# Linearly, the negative hump is almost invisible, +# and it is very difficult to see any detail of its profile. +# With the logarithmic scaling applied to both positive and negative values, +# it is much easier to see the shape of each hump. +# +# See `~.colors.SymLogNorm`. + +import numpy as np +import matplotlib.pyplot as plt +import matplotlib.colors as colors + + +def rbf(x, y): + return 1.0 / (1 + 5 * ((x ** 2) + (y ** 2))) + +N = 200 +gain = 8 +X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)] +Z1 = rbf(X + 0.5, Y + 0.5) +Z2 = rbf(X - 0.5, Y - 0.5) +Z = gain * Z1 - Z2 + +shadeopts = {'cmap': 'PRGn', 'shading': 'gouraud'} +colormap = 'PRGn' +lnrwidth = 0.5 + +fig, ax = plt.subplots(2, 1, sharex=True, sharey=True) + +pcm = ax[0].pcolormesh(X, Y, Z, + norm=colors.SymLogNorm(linthresh=lnrwidth, linscale=1, + vmin=-gain, vmax=gain, base=10), + **shadeopts) +fig.colorbar(pcm, ax=ax[0], extend='both') +ax[0].text(-2.5, 1.5, 'symlog') + +pcm = ax[1].pcolormesh(X, Y, Z, vmin=-gain, vmax=gain, + **shadeopts) +fig.colorbar(pcm, ax=ax[1], extend='both') +ax[1].text(-2.5, 1.5, 'linear') + + +############################################################################### +# In order to find the best visualization for any particular dataset, +# it may be necessary to experiment with multiple different color scales. +# As well as the `~.colors.SymLogNorm` scaling, there is also +# the option of using `~.colors.AsinhNorm` (experimental), which has a smoother +# transition between the linear and logarithmic regions of the transformation +# applied to the data values, "Z". +# In the plots below, it may be possible to see contour-like artifacts +# around each hump despite there being no sharp features +# in the dataset itself. The ``asinh`` scaling shows a smoother shading +# of each hump. + +fig, ax = plt.subplots(2, 1, sharex=True, sharey=True) + +pcm = ax[0].pcolormesh(X, Y, Z, + norm=colors.SymLogNorm(linthresh=lnrwidth, linscale=1, + vmin=-gain, vmax=gain, base=10), + **shadeopts) +fig.colorbar(pcm, ax=ax[0], extend='both') +ax[0].text(-2.5, 1.5, 'symlog') + +pcm = ax[1].pcolormesh(X, Y, Z, + norm=colors.AsinhNorm(linear_width=lnrwidth, + vmin=-gain, vmax=gain), + **shadeopts) +fig.colorbar(pcm, ax=ax[1], extend='both') +ax[1].text(-2.5, 1.5, 'asinh') + + +plt.show() diff --git a/examples/images_contours_and_fields/contour_corner_mask.py b/examples/images_contours_and_fields/contour_corner_mask.py index 280231acb950..c0854675c6d2 100644 --- a/examples/images_contours_and_fields/contour_corner_mask.py +++ b/examples/images_contours_and_fields/contour_corner_mask.py @@ -27,7 +27,7 @@ for ax, corner_mask in zip(axs, corner_masks): cs = ax.contourf(x, y, z, corner_mask=corner_mask) ax.contour(cs, colors='k') - ax.set_title('corner_mask = {0}'.format(corner_mask)) + ax.set_title(f'{corner_mask=}') # Plot grid. ax.grid(c='k', ls='-', alpha=0.3) diff --git a/examples/images_contours_and_fields/contour_demo.py b/examples/images_contours_and_fields/contour_demo.py index 4c98d4c8bc4c..266acccf2409 100644 --- a/examples/images_contours_and_fields/contour_demo.py +++ b/examples/images_contours_and_fields/contour_demo.py @@ -85,8 +85,7 @@ linewidths=2, extent=(-3, 3, -2, 2)) # Thicken the zero contour. -zc = CS.collections[6] -plt.setp(zc, linewidth=4) +CS.collections[6].set_linewidth(4) ax.clabel(CS, levels[1::2], # label every second level inline=True, fmt='%1.1f', fontsize=14) diff --git a/examples/images_contours_and_fields/contour_image.py b/examples/images_contours_and_fields/contour_image.py index 444d4df4a78f..a2a07f20f273 100644 --- a/examples/images_contours_and_fields/contour_image.py +++ b/examples/images_contours_and_fields/contour_image.py @@ -41,7 +41,7 @@ axs = _axs.flatten() cset1 = axs[0].contourf(X, Y, Z, levels, norm=norm, - cmap=cm.get_cmap(cmap, len(levels) - 1)) + cmap=cmap.resampled(len(levels) - 1)) # It is not necessary, but for the colormap, we need only the # number of levels minus 1. To avoid discretization error, use # either this number or a large number such as the default (256). diff --git a/examples/images_contours_and_fields/contourf_demo.py b/examples/images_contours_and_fields/contourf_demo.py index 28b8ed2f33d6..3a3f2b9564df 100644 --- a/examples/images_contours_and_fields/contourf_demo.py +++ b/examples/images_contours_and_fields/contourf_demo.py @@ -1,6 +1,6 @@ """ ============= -Contourf Demo +Contourf demo ============= How to use the `.axes.Axes.contourf` method to create filled contour plots. @@ -33,18 +33,20 @@ interior = np.sqrt(X**2 + Y**2) < 0.5 Z[interior] = np.ma.masked -# We are using automatic selection of contour levels; -# this is usually not such a good idea, because they don't -# occur on nice boundaries, but we do it here for purposes -# of illustration. +############################################################################# +# Automatic contour levels +# ------------------------ +# We are using automatic selection of contour levels; this is usually not such +# a good idea, because they don't occur on nice boundaries, but we do it here +# for purposes of illustration. -fig1, ax2 = plt.subplots(constrained_layout=True) +fig1, ax2 = plt.subplots(layout='constrained') CS = ax2.contourf(X, Y, Z, 10, cmap=plt.cm.bone, origin=origin) -# Note that in the following, we explicitly pass in a subset of -# the contour levels used for the filled contours. Alternatively, -# We could pass in additional levels to provide extra resolution, -# or leave out the levels kwarg to use all of the original levels. +# Note that in the following, we explicitly pass in a subset of the contour +# levels used for the filled contours. Alternatively, we could pass in +# additional levels to provide extra resolution, or leave out the *levels* +# keyword argument to use all of the original levels. CS2 = ax2.contour(CS, levels=CS.levels[::2], colors='r', origin=origin) @@ -58,10 +60,13 @@ # Add the contour line levels to the colorbar cbar.add_lines(CS2) -fig2, ax2 = plt.subplots(constrained_layout=True) -# Now make a contour plot with the levels specified, -# and with the colormap generated automatically from a list -# of colors. +############################################################################# +# Explicit contour levels +# ----------------------- +# Now make a contour plot with the levels specified, and with the colormap +# generated automatically from a list of colors. + +fig2, ax2 = plt.subplots(layout='constrained') levels = [-1.5, -1, -0.5, 0, 0.5, 1] CS3 = ax2.contourf(X, Y, Z, levels, colors=('r', 'g', 'b'), @@ -84,19 +89,21 @@ # needs from the ContourSet object, CS3. fig2.colorbar(CS3) +############################################################################# +# Extension settings +# ------------------ # Illustrate all 4 possible "extend" settings: extends = ["neither", "both", "min", "max"] -cmap = plt.cm.get_cmap("winter") -cmap = cmap.with_extremes(under="magenta", over="yellow") +cmap = plt.colormaps["winter"].with_extremes(under="magenta", over="yellow") # Note: contouring simply excludes masked or nan regions, so # instead of using the "bad" colormap value for them, it draws -# nothing at all in them. Therefore the following would have +# nothing at all in them. Therefore, the following would have # no effect: # cmap.set_bad("red") -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") -for ax, extend in zip(axs.ravel(), extends): +for ax, extend in zip(axs.flat, extends): cs = ax.contourf(X, Y, Z, levels, cmap=cmap, extend=extend, origin=origin) fig.colorbar(cs, ax=ax, shrink=0.9) ax.set_title("extend = %s" % extend) diff --git a/examples/images_contours_and_fields/contourf_log.py b/examples/images_contours_and_fields/contourf_log.py index 2f3976207b72..69b25d53df70 100644 --- a/examples/images_contours_and_fields/contourf_log.py +++ b/examples/images_contours_and_fields/contourf_log.py @@ -18,8 +18,8 @@ X, Y = np.meshgrid(x, y) # A low hump with a spike coming out. -# Needs to have z/colour axis on a log scale so we see both hump and spike. -# linear scale only shows the spike. +# Needs to have z/colour axis on a log scale, so we see both hump and spike. +# A linear scale only shows the spike. Z1 = np.exp(-X**2 - Y**2) Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2) z = Z1 + 50 * Z2 diff --git a/examples/images_contours_and_fields/demo_bboximage.py b/examples/images_contours_and_fields/demo_bboximage.py index acee3cc37faa..050d661a47ff 100644 --- a/examples/images_contours_and_fields/demo_bboximage.py +++ b/examples/images_contours_and_fields/demo_bboximage.py @@ -7,8 +7,10 @@ a bounding box. This demo shows how to show an image inside a `.text.Text`'s bounding box as well as how to manually create a bounding box for the image. """ -import matplotlib.pyplot as plt + import numpy as np + +import matplotlib.pyplot as plt from matplotlib.image import BboxImage from matplotlib.transforms import Bbox, TransformedBbox @@ -19,53 +21,32 @@ # Create a BboxImage with Text # ---------------------------- txt = ax1.text(0.5, 0.5, "test", size=30, ha="center", color="w") -kwargs = dict() - -bbox_image = BboxImage(txt.get_window_extent, - norm=None, - origin=None, - clip_on=False, - **kwargs - ) -a = np.arange(256).reshape(1, 256)/256. -bbox_image.set_data(a) -ax1.add_artist(bbox_image) +ax1.add_artist( + BboxImage(txt.get_window_extent, data=np.arange(256).reshape((1, -1)))) # ------------------------------------ # Create a BboxImage for each colormap # ------------------------------------ -a = np.linspace(0, 1, 256).reshape(1, -1) -a = np.vstack((a, a)) - # List of all colormaps; skip reversed colormaps. -maps = sorted(m for m in plt.colormaps() if not m.endswith("_r")) +cmap_names = sorted(m for m in plt.colormaps if not m.endswith("_r")) ncol = 2 -nrow = len(maps)//ncol + 1 +nrow = len(cmap_names) // ncol + 1 xpad_fraction = 0.3 -dx = 1./(ncol + xpad_fraction*(ncol - 1)) +dx = 1 / (ncol + xpad_fraction * (ncol - 1)) ypad_fraction = 0.3 -dy = 1./(nrow + ypad_fraction*(nrow - 1)) +dy = 1 / (nrow + ypad_fraction * (nrow - 1)) -for i, m in enumerate(maps): +for i, cmap_name in enumerate(cmap_names): ix, iy = divmod(i, nrow) - - bbox0 = Bbox.from_bounds(ix*dx*(1 + xpad_fraction), - 1. - iy*dy*(1 + ypad_fraction) - dy, + bbox0 = Bbox.from_bounds(ix*dx*(1+xpad_fraction), + 1 - iy*dy*(1+ypad_fraction) - dy, dx, dy) bbox = TransformedBbox(bbox0, ax2.transAxes) - - bbox_image = BboxImage(bbox, - cmap=plt.get_cmap(m), - norm=None, - origin=None, - **kwargs - ) - - bbox_image.set_data(a) - ax2.add_artist(bbox_image) + ax2.add_artist( + BboxImage(bbox, cmap=cmap_name, data=np.arange(256).reshape((1, -1)))) plt.show() diff --git a/examples/images_contours_and_fields/image_annotated_heatmap.py b/examples/images_contours_and_fields/image_annotated_heatmap.py index 301c180cb783..f873423f50b0 100644 --- a/examples/images_contours_and_fields/image_annotated_heatmap.py +++ b/examples/images_contours_and_fields/image_annotated_heatmap.py @@ -33,12 +33,13 @@ # tick labels (`~matplotlib.axes.Axes.set_xticklabels`), # otherwise they would become out of sync. The locations are just # the ascending integer numbers, while the ticklabels are the labels to show. -# Finally we can label the data itself by creating a `~matplotlib.text.Text` +# Finally, we can label the data itself by creating a `~matplotlib.text.Text` # within each cell showing the value of that cell. import numpy as np import matplotlib +import matplotlib as mpl import matplotlib.pyplot as plt # sphinx_gallery_thumbnail_number = 2 @@ -59,12 +60,9 @@ fig, ax = plt.subplots() im = ax.imshow(harvest) -# We want to show all ticks... -ax.set_xticks(np.arange(len(farmers))) -ax.set_yticks(np.arange(len(vegetables))) -# ... and label them with the respective list entries -ax.set_xticklabels(farmers) -ax.set_yticklabels(vegetables) +# Show all ticks and label them with the respective list entries +ax.set_xticks(np.arange(len(farmers)), labels=farmers) +ax.set_yticks(np.arange(len(vegetables)), labels=vegetables) # Rotate the tick labels and set their alignment. plt.setp(ax.get_xticklabels(), rotation=45, ha="right", @@ -100,18 +98,18 @@ def heatmap(data, row_labels, col_labels, ax=None, - cbar_kw={}, cbarlabel="", **kwargs): + cbar_kw=None, cbarlabel="", **kwargs): """ Create a heatmap from a numpy array and two lists of labels. Parameters ---------- data - A 2D numpy array of shape (N, M). + A 2D numpy array of shape (M, N). row_labels - A list or array of length N with the labels for the rows. + A list or array of length M with the labels for the rows. col_labels - A list or array of length M with the labels for the columns. + A list or array of length N with the labels for the columns. ax A `matplotlib.axes.Axes` instance to which the heatmap is plotted. If not provided, use current axes or create a new one. Optional. @@ -123,9 +121,12 @@ def heatmap(data, row_labels, col_labels, ax=None, All other arguments are forwarded to `imshow`. """ - if not ax: + if ax is None: ax = plt.gca() + if cbar_kw is None: + cbar_kw = {} + # Plot the heatmap im = ax.imshow(data, **kwargs) @@ -133,12 +134,9 @@ def heatmap(data, row_labels, col_labels, ax=None, cbar = ax.figure.colorbar(im, ax=ax, **cbar_kw) cbar.ax.set_ylabel(cbarlabel, rotation=-90, va="bottom") - # We want to show all ticks... - ax.set_xticks(np.arange(data.shape[1])) - ax.set_yticks(np.arange(data.shape[0])) - # ... and label them with the respective list entries. - ax.set_xticklabels(col_labels) - ax.set_yticklabels(row_labels) + # Show all ticks and label them with the respective list entries. + ax.set_xticks(np.arange(data.shape[1]), labels=col_labels) + ax.set_yticks(np.arange(data.shape[0]), labels=row_labels) # Let the horizontal axes labeling appear on top. ax.tick_params(top=True, bottom=False, @@ -254,8 +252,8 @@ def annotate_heatmap(im, data=None, valfmt="{x:.2f}", # use an integer format on the annotations and provide some colors. data = np.random.randint(2, 100, size=(7, 7)) -y = ["Book {}".format(i) for i in range(1, 8)] -x = ["Store {}".format(i) for i in list("ABCDEFG")] +y = [f"Book {i}" for i in range(1, 8)] +x = [f"Store {i}" for i in list("ABCDEFG")] im, _ = heatmap(data, y, x, ax=ax2, vmin=0, cmap="magma_r", cbarlabel="weekly sold copies") annotate_heatmap(im, valfmt="{x:d}", size=7, threshold=20, @@ -267,15 +265,15 @@ def annotate_heatmap(im, data=None, valfmt="{x:.2f}", # labels from an array of classes. data = np.random.randn(6, 6) -y = ["Prod. {}".format(i) for i in range(10, 70, 10)] -x = ["Cycle {}".format(i) for i in range(1, 7)] +y = [f"Prod. {i}" for i in range(10, 70, 10)] +x = [f"Cycle {i}" for i in range(1, 7)] qrates = list("ABCDEFG") norm = matplotlib.colors.BoundaryNorm(np.linspace(-3.5, 3.5, 8), 7) fmt = matplotlib.ticker.FuncFormatter(lambda x, pos: qrates[::-1][norm(x)]) im, _ = heatmap(data, y, x, ax=ax3, - cmap=plt.get_cmap("PiYG", 7), norm=norm, + cmap=mpl.colormaps["PiYG"].resampled(7), norm=norm, cbar_kw=dict(ticks=np.arange(-3, 4), format=fmt), cbarlabel="Quality Rating") @@ -294,7 +292,7 @@ def annotate_heatmap(im, data=None, valfmt="{x:.2f}", def func(x, pos): - return "{:.2f}".format(x).replace("0.", ".").replace("1.00", "") + return f"{x:.2f}".replace("0.", ".").replace("1.00", "") annotate_heatmap(im, valfmt=matplotlib.ticker.FuncFormatter(func), size=7) diff --git a/examples/images_contours_and_fields/image_antialiasing.py b/examples/images_contours_and_fields/image_antialiasing.py index 3a6774c5e7e4..eb8c431f8d96 100644 --- a/examples/images_contours_and_fields/image_antialiasing.py +++ b/examples/images_contours_and_fields/image_antialiasing.py @@ -5,12 +5,21 @@ Images are represented by discrete pixels, either on the screen or in an image file. When data that makes up the image has a different resolution -than its representation on the screen we will see aliasing effects. +than its representation on the screen we will see aliasing effects. How +noticeable these are depends on how much down-sampling takes place in +the change of resolution (if any). -The default image interpolation in Matplotlib is 'antialiased'. This uses a -hanning interpolation for reduced aliasing in most situations. Only when there -is upsampling by a factor of 1, 2 or >=3 is 'nearest' neighbor interpolation -used. +When subsampling data, aliasing is reduced by smoothing first and then +subsampling the smoothed data. In Matplotlib, we can do that +smoothing before mapping the data to colors, or we can do the smoothing +on the RGB(A) data in the final image. The differences between these are +shown below, and controlled with the *interpolation_stage* keyword argument. + +The default image interpolation in Matplotlib is 'antialiased', and +it is applied to the data. This uses a +hanning interpolation on the data provided by the user for reduced aliasing +in most situations. Only when there is upsampling by a factor of 1, 2 or +>=3 is 'nearest' neighbor interpolation used. Other anti-aliasing filters can be specified in `.Axes.imshow` using the *interpolation* keyword argument. @@ -20,30 +29,59 @@ import matplotlib.pyplot as plt ############################################################################### -# First we generate a 500x500 px image with varying frequency content: -x = np.arange(500) / 500 - 0.5 -y = np.arange(500) / 500 - 0.5 +# First we generate a 450x450 pixel image with varying frequency content: +N = 450 +x = np.arange(N) / N - 0.5 +y = np.arange(N) / N - 0.5 +aa = np.ones((N, N)) +aa[::2, :] = -1 X, Y = np.meshgrid(x, y) R = np.sqrt(X**2 + Y**2) -f0 = 10 -k = 250 +f0 = 5 +k = 100 a = np.sin(np.pi * 2 * (f0 * R + k * R**2 / 2)) +# make the left hand side of this +a[:int(N / 2), :][R[:int(N / 2), :] < 0.4] = -1 +a[:int(N / 2), :][R[:int(N / 2), :] < 0.3] = 1 +aa[:, int(N / 3):] = a[:, int(N / 3):] +a = aa +############################################################################### +# The following images are subsampled from 450 data pixels to either +# 125 pixels or 250 pixels (depending on your display). +# The Moiré patterns in the 'nearest' interpolation are caused by the +# high-frequency data being subsampled. The 'antialiased' imaged +# still has some Moiré patterns as well, but they are greatly reduced. +# +# There are substantial differences between the 'data' interpolation and +# the 'rgba' interpolation. The alternating bands of red and blue on the +# left third of the image are subsampled. By interpolating in 'data' space +# (the default) the antialiasing filter makes the stripes close to white, +# because the average of -1 and +1 is zero, and zero is white in this +# colormap. +# +# Conversely, when the anti-aliasing occurs in 'rgba' space, the red and +# blue are combined visually to make purple. This behaviour is more like a +# typical image processing package, but note that purple is not in the +# original colormap, so it is no longer possible to invert individual +# pixels back to their data value. +fig, axs = plt.subplots(2, 2, figsize=(5, 6), layout='constrained') +axs[0, 0].imshow(a, interpolation='nearest', cmap='RdBu_r') +axs[0, 0].set_xlim(100, 200) +axs[0, 0].set_ylim(275, 175) +axs[0, 0].set_title('Zoom') -############################################################################### -# The following images are subsampled from 500 data pixels to 303 rendered -# pixels. The Moire patterns in the 'nearest' interpolation are caused by the -# high-frequency data being subsampled. The 'antialiased' image -# still has some Moire patterns as well, but they are greatly reduced. -fig, axs = plt.subplots(1, 2, figsize=(7, 4), constrained_layout=True) -for ax, interp in zip(axs, ['nearest', 'antialiased']): - ax.imshow(a, interpolation=interp, cmap='gray') - ax.set_title(f"interpolation='{interp}'") +for ax, interp, space in zip(axs.flat[1:], + ['nearest', 'antialiased', 'antialiased'], + ['data', 'data', 'rgba']): + ax.imshow(a, interpolation=interp, interpolation_stage=space, + cmap='RdBu_r') + ax.set_title(f"interpolation='{interp}'\nspace='{space}'") plt.show() ############################################################################### -# Even up-sampling an image with 'nearest' interpolation will lead to Moire +# Even up-sampling an image with 'nearest' interpolation will lead to Moiré # patterns when the upsampling factor is not integer. The following image # upsamples 500 data pixels to 530 rendered pixels. You may note a grid of # 30 line-like artifacts which stem from the 524 - 500 = 24 extra pixels that @@ -63,16 +101,15 @@ plt.show() ############################################################################### -# Apart from the default 'hanning' antialiasing `~.Axes.imshow` supports a +# Apart from the default 'hanning' antialiasing, `~.Axes.imshow` supports a # number of different interpolation algorithms, which may work better or # worse depending on the pattern. -fig, axs = plt.subplots(1, 2, figsize=(7, 4), constrained_layout=True) +fig, axs = plt.subplots(1, 2, figsize=(7, 4), layout='constrained') for ax, interp in zip(axs, ['hanning', 'lanczos']): ax.imshow(a, interpolation=interp, cmap='gray') ax.set_title(f"interpolation='{interp}'") plt.show() - ############################################################################# # # .. admonition:: References diff --git a/examples/images_contours_and_fields/image_demo.py b/examples/images_contours_and_fields/image_demo.py index d3630bf67d20..7484f858427c 100644 --- a/examples/images_contours_and_fields/image_demo.py +++ b/examples/images_contours_and_fields/image_demo.py @@ -1,6 +1,6 @@ """ ========== -Image Demo +Image demo ========== Many ways to plot images in Matplotlib. @@ -46,28 +46,27 @@ with cbook.get_sample_data('grace_hopper.jpg') as image_file: image = plt.imread(image_file) -fig, ax = plt.subplots() -ax.imshow(image) -ax.axis('off') # clear x-axis and y-axis - - -# And another image - -# Data are 256x256 16-bit integers. +# And another image, using 256x256 16-bit integers. w, h = 256, 256 with cbook.get_sample_data('s1045.ima.gz') as datafile: s = datafile.read() A = np.frombuffer(s, np.uint16).astype(float).reshape((w, h)) - -fig, ax = plt.subplots() extent = (0, 25, 0, 25) -im = ax.imshow(A, cmap=plt.cm.hot, origin='upper', extent=extent) + +fig, ax = plt.subplot_mosaic([ + ['hopper', 'mri'] +], figsize=(7, 3.5)) + +ax['hopper'].imshow(image) +ax['hopper'].axis('off') # clear x-axis and y-axis + +im = ax['mri'].imshow(A, cmap=plt.cm.hot, origin='upper', extent=extent) markers = [(15.9, 14.5), (16.8, 15)] x, y = zip(*markers) -ax.plot(x, y, 'o') +ax['mri'].plot(x, y, 'o') -ax.set_title('MRI') +ax['mri'].set_title('MRI') plt.show() diff --git a/examples/images_contours_and_fields/image_masked.py b/examples/images_contours_and_fields/image_masked.py index 2aa88f4f6b64..fa5f8047a8ff 100644 --- a/examples/images_contours_and_fields/image_masked.py +++ b/examples/images_contours_and_fields/image_masked.py @@ -50,8 +50,7 @@ ax1.set_title('Green=low, Red=high, Blue=masked') cbar = fig.colorbar(im, extend='both', shrink=0.9, ax=ax1) cbar.set_label('uniform') -for ticklabel in ax1.xaxis.get_ticklabels(): - ticklabel.set_visible(False) +ax1.tick_params(axis='x', labelbottom=False) # Plot using a small number of colors, with unevenly spaced boundaries. im = ax2.imshow(Zm, interpolation='nearest', @@ -79,4 +78,4 @@ # - `matplotlib.axes.Axes.imshow` / `matplotlib.pyplot.imshow` # - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` # - `matplotlib.colors.BoundaryNorm` -# - `matplotlib.colorbar.ColorbarBase.set_label` +# - `matplotlib.colorbar.Colorbar.set_label` diff --git a/examples/images_contours_and_fields/image_nonuniform.py b/examples/images_contours_and_fields/image_nonuniform.py index 9c1012310b63..80554673eff3 100644 --- a/examples/images_contours_and_fields/image_nonuniform.py +++ b/examples/images_contours_and_fields/image_nonuniform.py @@ -1,10 +1,10 @@ """ ================ -Image Nonuniform +Image nonuniform ================ This illustrates the NonUniformImage class. It is not -available via an Axes method but it is easily added to an +available via an Axes method, but it is easily added to an Axes instance as shown here. """ @@ -25,7 +25,7 @@ z = np.sqrt(x[np.newaxis, :]**2 + y[:, np.newaxis]**2) -fig, axs = plt.subplots(nrows=2, ncols=2, constrained_layout=True) +fig, axs = plt.subplots(nrows=2, ncols=2, layout='constrained') fig.suptitle('NonUniformImage class', fontsize='large') ax = axs[0, 0] im = NonUniformImage(ax, interpolation=interp, extent=(-4, 4, -4, 4), diff --git a/examples/images_contours_and_fields/image_zcoord.py b/examples/images_contours_and_fields/image_zcoord.py index 7fc57ca10710..8e210eb7c603 100644 --- a/examples/images_contours_and_fields/image_zcoord.py +++ b/examples/images_contours_and_fields/image_zcoord.py @@ -3,12 +3,11 @@ Modifying the coordinate formatter ================================== -Modify the coordinate formatter to report the image "z" -value of the nearest pixel given x and y. -This functionality is built in by default, but it -is still useful to show how to customize the -`~.axes.Axes.format_coord` function. +Modify the coordinate formatter to report the image "z" value of the nearest +pixel given x and y. This functionality is built in by default; this example +just showcases how to customize the `~.axes.Axes.format_coord` function. """ + import numpy as np import matplotlib.pyplot as plt @@ -21,17 +20,17 @@ fig, ax = plt.subplots() ax.imshow(X) -numrows, numcols = X.shape - def format_coord(x, y): - col = int(x + 0.5) - row = int(y + 0.5) - if 0 <= col < numcols and 0 <= row < numrows: + col = round(x) + row = round(y) + nrows, ncols = X.shape + if 0 <= col < ncols and 0 <= row < nrows: z = X[row, col] - return 'x=%1.4f, y=%1.4f, z=%1.4f' % (x, y, z) + return f'x={x:1.4f}, y={y:1.4f}, z={z:1.4f}' else: - return 'x=%1.4f, y=%1.4f' % (x, y) + return f'x={x:1.4f}, y={y:1.4f}' + ax.format_coord = format_coord plt.show() diff --git a/examples/images_contours_and_fields/interpolation_methods.py b/examples/images_contours_and_fields/interpolation_methods.py index 1e59e721d9fa..4d3151696dcd 100644 --- a/examples/images_contours_and_fields/interpolation_methods.py +++ b/examples/images_contours_and_fields/interpolation_methods.py @@ -10,12 +10,12 @@ If the interpolation is ``'none'``, then no interpolation is performed for the Agg, ps and pdf backends. Other backends will default to ``'antialiased'``. -For the Agg, ps and pdf backends, ``interpolation = 'none'`` works well when a -big image is scaled down, while ``interpolation = 'nearest'`` works well when +For the Agg, ps and pdf backends, ``interpolation='none'`` works well when a +big image is scaled down, while ``interpolation='nearest'`` works well when a small image is scaled up. See :doc:`/gallery/images_contours_and_fields/image_antialiasing` for a -discussion on the default ``interpolation="antialiased"`` option. +discussion on the default ``interpolation='antialiased'`` option. """ import matplotlib.pyplot as plt diff --git a/examples/images_contours_and_fields/irregulardatagrid.py b/examples/images_contours_and_fields/irregulardatagrid.py index 51be48fbbb07..aedf8033c9b4 100644 --- a/examples/images_contours_and_fields/irregulardatagrid.py +++ b/examples/images_contours_and_fields/irregulardatagrid.py @@ -52,8 +52,8 @@ # Note that scipy.interpolate provides means to interpolate data on a grid # as well. The following would be an alternative to the four lines above: -#from scipy.interpolate import griddata -#zi = griddata((x, y), z, (xi[None, :], yi[:, None]), method='linear') +# from scipy.interpolate import griddata +# zi = griddata((x, y), z, (xi[None, :], yi[:, None]), method='linear') ax1.contour(xi, yi, zi, levels=14, linewidths=0.5, colors='k') cntr1 = ax1.contourf(xi, yi, zi, levels=14, cmap="RdBu_r") diff --git a/examples/images_contours_and_fields/matshow.py b/examples/images_contours_and_fields/matshow.py index 43a06bda9353..0add6bebf9a4 100644 --- a/examples/images_contours_and_fields/matshow.py +++ b/examples/images_contours_and_fields/matshow.py @@ -1,7 +1,7 @@ """ -======= -Matshow -======= +=============================== +Visualize matrices with matshow +=============================== `~.axes.Axes.matshow` visualizes a 2D matrix or array as color-coded image. """ diff --git a/examples/images_contours_and_fields/multi_image.py b/examples/images_contours_and_fields/multi_image.py index 82b1a072a1b6..5ca598a7e48c 100644 --- a/examples/images_contours_and_fields/multi_image.py +++ b/examples/images_contours_and_fields/multi_image.py @@ -1,7 +1,7 @@ """ -=========== -Multi Image -=========== +=============== +Multiple images +=============== Make a set of images with a single colormap, norm, and colorbar. """ @@ -47,7 +47,7 @@ def update(changed_image): for im in images: - im.callbacksSM.connect('changed', update) + im.callbacks.connect('changed', update) plt.show() diff --git a/examples/images_contours_and_fields/pcolor_demo.py b/examples/images_contours_and_fields/pcolor_demo.py index bc578f25cf09..2e17cd7db90b 100644 --- a/examples/images_contours_and_fields/pcolor_demo.py +++ b/examples/images_contours_and_fields/pcolor_demo.py @@ -1,6 +1,6 @@ """ =========== -Pcolor Demo +Pcolor demo =========== Generating images with `~.axes.Axes.pcolor`. @@ -92,8 +92,8 @@ X, Y = np.meshgrid(np.linspace(-3, 3, N), np.linspace(-2, 2, N)) # A low hump with a spike coming out. -# Needs to have z/colour axis on a log scale so we see both hump and spike. -# linear scale only shows the spike. +# Needs to have z/colour axis on a log scale, so we see both hump and spike. +# A linear scale only shows the spike. Z1 = np.exp(-X**2 - Y**2) Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2) Z = Z1 + 50 * Z2 diff --git a/examples/images_contours_and_fields/pcolormesh_grids.py b/examples/images_contours_and_fields/pcolormesh_grids.py index 3b628efe58bd..7faaf3a86696 100644 --- a/examples/images_contours_and_fields/pcolormesh_grids.py +++ b/examples/images_contours_and_fields/pcolormesh_grids.py @@ -54,11 +54,10 @@ def _annotate(ax, x, y, title): # ----------------------------- # # Often, however, data is provided where *X* and *Y* match the shape of *Z*. -# While this makes sense for other ``shading`` types, it is no longer permitted -# when ``shading='flat'`` (and will raise a MatplotlibDeprecationWarning as of -# Matplotlib v3.3). Historically, Matplotlib silently dropped the last row and -# column of *Z* in this case, to match Matlab's behavior. If this behavior is -# still desired, simply drop the last row and column manually: +# While this makes sense for other ``shading`` types, it is not permitted +# when ``shading='flat'``. Historically, Matplotlib silently dropped the last +# row and column of *Z* in this case, to match Matlab's behavior. If this +# behavior is still desired, simply drop the last row and column manually: x = np.arange(ncols) # note *not* ncols + 1 as before y = np.arange(nrows) @@ -90,7 +89,7 @@ def _annotate(ax, x, y, title): # to use, in this case ``shading='auto'`` will decide whether to use 'flat' or # 'nearest' shading based on the shapes of *X*, *Y* and *Z*. -fig, axs = plt.subplots(2, 1, constrained_layout=True) +fig, axs = plt.subplots(2, 1, layout='constrained') ax = axs[0] x = np.arange(ncols) y = np.arange(nrows) @@ -111,7 +110,7 @@ def _annotate(ax, x, y, title): # be specified, where the color in the quadrilaterals is linearly interpolated # between the grid points. The shapes of *X*, *Y*, *Z* must be the same. -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout='constrained') x = np.arange(ncols) y = np.arange(nrows) ax.pcolormesh(x, y, Z, shading='gouraud', vmin=Z.min(), vmax=Z.max()) diff --git a/examples/images_contours_and_fields/pcolormesh_levels.py b/examples/images_contours_and_fields/pcolormesh_levels.py index 143a83cf2683..d462304ea4c5 100644 --- a/examples/images_contours_and_fields/pcolormesh_levels.py +++ b/examples/images_contours_and_fields/pcolormesh_levels.py @@ -3,8 +3,8 @@ pcolormesh ========== -`.axes.Axes.pcolormesh` allows you to generate 2D image-style plots. Note it -is faster than the similar `~.axes.Axes.pcolor`. +`.axes.Axes.pcolormesh` allows you to generate 2D image-style plots. +Note that it is faster than the similar `~.axes.Axes.pcolor`. """ @@ -52,9 +52,8 @@ # Often a user wants to pass *X* and *Y* with the same sizes as *Z* to # `.axes.Axes.pcolormesh`. This is also allowed if ``shading='auto'`` is # passed (default set by :rc:`pcolor.shading`). Pre Matplotlib 3.3, -# ``shading='flat'`` would drop the last column and row of *Z*; while that -# is still allowed for back compatibility purposes, a DeprecationWarning is -# raised. If this is really what you want, then simply drop the last row and +# ``shading='flat'`` would drop the last column and row of *Z*, but now gives +# an error. If this is really what you want, then simply drop the last row and # column of Z manually: x = np.arange(10) # len = 10 @@ -94,7 +93,7 @@ # pick the desired colormap, sensible levels, and define a normalization # instance which takes data values and translates those into levels. -cmap = plt.get_cmap('PiYG') +cmap = plt.colormaps['PiYG'] norm = BoundaryNorm(levels, ncolors=cmap.N, clip=True) fig, (ax0, ax1) = plt.subplots(nrows=2) diff --git a/examples/images_contours_and_fields/plot_streamplot.py b/examples/images_contours_and_fields/plot_streamplot.py index fca97d304207..fd587527fbc5 100644 --- a/examples/images_contours_and_fields/plot_streamplot.py +++ b/examples/images_contours_and_fields/plot_streamplot.py @@ -11,10 +11,12 @@ * Varying the line width along a streamline. * Controlling the starting points of streamlines. * Streamlines skipping masked regions and NaN values. +* Unbroken streamlines even when exceeding the limit of lines within a single + grid cell. """ import numpy as np import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec + w = 3 Y, X = np.mgrid[-w:w:100j, -w:w:100j] @@ -22,38 +24,34 @@ V = 1 + X - Y**2 speed = np.sqrt(U**2 + V**2) -fig = plt.figure(figsize=(7, 9)) -gs = gridspec.GridSpec(nrows=3, ncols=2, height_ratios=[1, 1, 2]) +fig, axs = plt.subplots(3, 2, figsize=(7, 9), height_ratios=[1, 1, 2]) +axs = axs.flat # Varying density along a streamline -ax0 = fig.add_subplot(gs[0, 0]) -ax0.streamplot(X, Y, U, V, density=[0.5, 1]) -ax0.set_title('Varying Density') +axs[0].streamplot(X, Y, U, V, density=[0.5, 1]) +axs[0].set_title('Varying Density') # Varying color along a streamline -ax1 = fig.add_subplot(gs[0, 1]) -strm = ax1.streamplot(X, Y, U, V, color=U, linewidth=2, cmap='autumn') +strm = axs[1].streamplot(X, Y, U, V, color=U, linewidth=2, cmap='autumn') fig.colorbar(strm.lines) -ax1.set_title('Varying Color') +axs[1].set_title('Varying Color') # Varying line width along a streamline -ax2 = fig.add_subplot(gs[1, 0]) lw = 5*speed / speed.max() -ax2.streamplot(X, Y, U, V, density=0.6, color='k', linewidth=lw) -ax2.set_title('Varying Line Width') +axs[2].streamplot(X, Y, U, V, density=0.6, color='k', linewidth=lw) +axs[2].set_title('Varying Line Width') # Controlling the starting points of the streamlines seed_points = np.array([[-2, -1, 0, 1, 2, -1], [-2, -1, 0, 1, 2, 2]]) -ax3 = fig.add_subplot(gs[1, 1]) -strm = ax3.streamplot(X, Y, U, V, color=U, linewidth=2, - cmap='autumn', start_points=seed_points.T) +strm = axs[3].streamplot(X, Y, U, V, color=U, linewidth=2, + cmap='autumn', start_points=seed_points.T) fig.colorbar(strm.lines) -ax3.set_title('Controlling Starting Points') +axs[3].set_title('Controlling Starting Points') # Displaying the starting points with blue symbols. -ax3.plot(seed_points[0], seed_points[1], 'bo') -ax3.set(xlim=(-w, w), ylim=(-w, w)) +axs[3].plot(seed_points[0], seed_points[1], 'bo') +axs[3].set(xlim=(-w, w), ylim=(-w, w)) # Create a mask mask = np.zeros(U.shape, dtype=bool) @@ -61,12 +59,15 @@ U[:20, :20] = np.nan U = np.ma.array(U, mask=mask) -ax4 = fig.add_subplot(gs[2:, :]) -ax4.streamplot(X, Y, U, V, color='r') -ax4.set_title('Streamplot with Masking') +axs[4].streamplot(X, Y, U, V, color='r') +axs[4].set_title('Streamplot with Masking') + +axs[4].imshow(~mask, extent=(-w, w, -w, w), alpha=0.5, cmap='gray', + aspect='auto') +axs[4].set_aspect('equal') -ax4.imshow(~mask, extent=(-w, w, -w, w), alpha=0.5, cmap='gray', aspect='auto') -ax4.set_aspect('equal') +axs[5].streamplot(X, Y, U, V, broken_streamlines=False) +axs[5].set_title('Streamplot with unbroken streamlines') plt.tight_layout() plt.show() diff --git a/examples/images_contours_and_fields/quadmesh_demo.py b/examples/images_contours_and_fields/quadmesh_demo.py index 9310f5ad00c7..430c428f5170 100644 --- a/examples/images_contours_and_fields/quadmesh_demo.py +++ b/examples/images_contours_and_fields/quadmesh_demo.py @@ -9,7 +9,7 @@ This demo illustrates a bug in quadmesh with masked data. """ -from matplotlib import cm, pyplot as plt +from matplotlib import pyplot as plt import numpy as np n = 12 @@ -29,7 +29,7 @@ axs[0].set_title('Without masked values') # You can control the color of the masked region. -cmap = cm.get_cmap(plt.rcParams['image.cmap']).with_extremes(bad='y') +cmap = plt.colormaps[plt.rcParams['image.cmap']].with_extremes(bad='y') axs[1].pcolormesh(Qx, Qz, Zm, shading='gouraud', cmap=cmap) axs[1].set_title('With masked values') diff --git a/examples/images_contours_and_fields/shading_example.py b/examples/images_contours_and_fields/shading_example.py index 63858a8fb19e..c3b969dcff8b 100644 --- a/examples/images_contours_and_fields/shading_example.py +++ b/examples/images_contours_and_fields/shading_example.py @@ -7,7 +7,7 @@ `Generic Mapping Tools`_. .. _Mathematica: http://reference.wolfram.com/mathematica/ref/ReliefPlot.html -.. _Generic Mapping Tools: https://gmt.soest.hawaii.edu/ +.. _Generic Mapping Tools: https://www.generic-mapping-tools.org/ """ import numpy as np diff --git a/examples/images_contours_and_fields/tricontour_demo.py b/examples/images_contours_and_fields/tricontour_demo.py index 6b7da8d25d85..a5d4651b7818 100644 --- a/examples/images_contours_and_fields/tricontour_demo.py +++ b/examples/images_contours_and_fields/tricontour_demo.py @@ -45,6 +45,41 @@ ax1.tricontour(triang, z, colors='k') ax1.set_title('Contour plot of Delaunay triangulation') + +############################################################################### +# You could also specify hatching patterns along with different cmaps. + +fig2, ax2 = plt.subplots() +ax2.set_aspect("equal") +tcf = ax2.tricontourf( + triang, + z, + hatches=["*", "-", "/", "//", "\\", None], + cmap="cividis" +) +fig2.colorbar(tcf) +ax2.tricontour(triang, z, linestyles="solid", colors="k", linewidths=2.0) +ax2.set_title("Hatched Contour plot of Delaunay triangulation") + +############################################################################### +# You could also generate hatching patterns labeled with no color. + +fig3, ax3 = plt.subplots() +n_levels = 7 +tcf = ax3.tricontourf( + triang, + z, + n_levels, + colors="none", + hatches=[".", "/", "\\", None, "\\\\", "*"], +) +ax3.tricontour(triang, z, n_levels, colors="black", linestyles="-") + + +# create a legend for the contour set +artists, labels = tcf.legend_elements(str_format="{:2.1f}".format) +ax3.legend(artists, labels, handleheight=2, framealpha=1) + ############################################################################### # You can specify your own triangulation rather than perform a Delaunay # triangulation of the points, where each triangle is given by the indices of @@ -101,13 +136,13 @@ # object if the same triangulation was to be used more than once to save # duplicated calculations. -fig2, ax2 = plt.subplots() -ax2.set_aspect('equal') -tcf = ax2.tricontourf(x, y, triangles, z) -fig2.colorbar(tcf) -ax2.set_title('Contour plot of user-specified triangulation') -ax2.set_xlabel('Longitude (degrees)') -ax2.set_ylabel('Latitude (degrees)') +fig4, ax4 = plt.subplots() +ax4.set_aspect('equal') +tcf = ax4.tricontourf(x, y, triangles, z) +fig4.colorbar(tcf) +ax4.set_title('Contour plot of user-specified triangulation') +ax4.set_xlabel('Longitude (degrees)') +ax4.set_ylabel('Latitude (degrees)') plt.show() @@ -120,3 +155,6 @@ # # - `matplotlib.axes.Axes.tricontourf` / `matplotlib.pyplot.tricontourf` # - `matplotlib.tri.Triangulation` +# - `matplotlib.figure.Figure.colorbar` / `matplotlib.pyplot.colorbar` +# - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` +# - `matplotlib.contour.ContourSet.legend_elements` diff --git a/examples/images_contours_and_fields/tricontour_smooth_delaunay.py b/examples/images_contours_and_fields/tricontour_smooth_delaunay.py index 6d4b481ab931..0815e78fb32d 100644 --- a/examples/images_contours_and_fields/tricontour_smooth_delaunay.py +++ b/examples/images_contours_and_fields/tricontour_smooth_delaunay.py @@ -25,13 +25,12 @@ """ from matplotlib.tri import Triangulation, TriAnalyzer, UniformTriRefiner import matplotlib.pyplot as plt -import matplotlib.cm as cm import numpy as np -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Analytical test function -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- def experiment_res(x, y): """An analytic function representing experiment results.""" x = 2 * x @@ -44,9 +43,9 @@ def experiment_res(x, y): 2 * (x**2 + y**2)) return (np.max(z) - z) / (np.max(z) - np.min(z)) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Generating the initial data test points and triangulation for the demo -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # User parameters for data test points # Number of test data points, tested from 3 to 5000 for subdiv=3 @@ -83,9 +82,9 @@ def experiment_res(x, y): tri.set_mask(mask_init) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Improving the triangulation before high-res plots: removing flat triangles -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # masking badly shaped triangles at the border of the triangular mesh. mask = TriAnalyzer(tri).get_flat_tri_mask(min_circle_ratio) tri.set_mask(mask) @@ -102,9 +101,9 @@ def experiment_res(x, y): flat_tri.set_mask(~mask) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Now the plots -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # User options for plots plot_tri = True # plot of base triangulation plot_masked_tri = True # plot of excessively flat excluded triangles @@ -114,7 +113,6 @@ def experiment_res(x, y): # Graphical options for tricontouring levels = np.arange(0., 1., 0.025) -cmap = cm.get_cmap(name='Blues', lut=None) fig, ax = plt.subplots() ax.set_aspect('equal') @@ -122,11 +120,11 @@ def experiment_res(x, y): "(application to high-resolution tricontouring)") # 1) plot of the refined (computed) data contours: -ax.tricontour(tri_refi, z_test_refi, levels=levels, cmap=cmap, +ax.tricontour(tri_refi, z_test_refi, levels=levels, cmap='Blues', linewidths=[2.0, 0.5, 1.0, 0.5]) # 2) plot of the expected (analytical) data contours (dashed): if plot_expected: - ax.tricontour(tri_refi, z_expected, levels=levels, cmap=cmap, + ax.tricontour(tri_refi, z_expected, levels=levels, cmap='Blues', linestyles='--') # 3) plot of the fine mesh on which interpolation was done: if plot_refi_tri: diff --git a/examples/images_contours_and_fields/tricontour_smooth_user.py b/examples/images_contours_and_fields/tricontour_smooth_user.py index fa5c3897bdee..c5a0a408f8e5 100644 --- a/examples/images_contours_and_fields/tricontour_smooth_user.py +++ b/examples/images_contours_and_fields/tricontour_smooth_user.py @@ -8,13 +8,12 @@ """ import matplotlib.tri as tri import matplotlib.pyplot as plt -import matplotlib.cm as cm import numpy as np -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Analytical test function -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- def function_z(x, y): r1 = np.sqrt((0.5 - x)**2 + (0.5 - y)**2) theta1 = np.arctan2(0.5 - x, 0.5 - y) @@ -25,9 +24,9 @@ def function_z(x, y): 0.7 * (x**2 + y**2)) return (np.max(z) - z) / (np.max(z) - np.min(z)) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Creating a Triangulation -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # First create the x and y coordinates of the points. n_angles = 20 n_radii = 10 @@ -52,22 +51,21 @@ def function_z(x, y): y[triang.triangles].mean(axis=1)) < min_radius) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Refine data -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- refiner = tri.UniformTriRefiner(triang) tri_refi, z_test_refi = refiner.refine_field(z, subdiv=3) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Plot the triangulation and the high-res iso-contours -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- fig, ax = plt.subplots() ax.set_aspect('equal') ax.triplot(triang, lw=0.5, color='white') levels = np.arange(0., 1., 0.025) -cmap = cm.get_cmap(name='terrain', lut=None) -ax.tricontourf(tri_refi, z_test_refi, levels=levels, cmap=cmap) +ax.tricontourf(tri_refi, z_test_refi, levels=levels, cmap='terrain') ax.tricontour(tri_refi, z_test_refi, levels=levels, colors=['0.25', '0.5', '0.5', '0.5', '0.5'], linewidths=[1.0, 0.5, 0.5, 0.5, 0.5]) diff --git a/examples/images_contours_and_fields/trigradient_demo.py b/examples/images_contours_and_fields/trigradient_demo.py index b785c89d6547..5abda65f5307 100644 --- a/examples/images_contours_and_fields/trigradient_demo.py +++ b/examples/images_contours_and_fields/trigradient_demo.py @@ -9,13 +9,12 @@ from matplotlib.tri import ( Triangulation, UniformTriRefiner, CubicTriInterpolator) import matplotlib.pyplot as plt -import matplotlib.cm as cm import numpy as np -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Electrical potential of a dipole -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- def dipole_potential(x, y): """The electric dipole potential V, at position *x*, *y*.""" r_sq = x**2 + y**2 @@ -24,9 +23,9 @@ def dipole_potential(x, y): return (np.max(z) - z) / (np.max(z) - np.min(z)) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Creating a Triangulation -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # First create the x and y coordinates of the points. n_angles = 30 n_radii = 10 @@ -50,23 +49,23 @@ def dipole_potential(x, y): y[triang.triangles].mean(axis=1)) < min_radius) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Refine data - interpolates the electrical potential V -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- refiner = UniformTriRefiner(triang) tri_refi, z_test_refi = refiner.refine_field(V, subdiv=3) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Computes the electrical field (Ex, Ey) as gradient of electrical potential -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- tci = CubicTriInterpolator(triang, -V) # Gradient requested here at the mesh nodes but could be anywhere else: (Ex, Ey) = tci.gradient(triang.x, triang.y) E_norm = np.sqrt(Ex**2 + Ey**2) -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- # Plot the triangulation, the potential iso-contours and the vector field -#----------------------------------------------------------------------------- +# ---------------------------------------------------------------------------- fig, ax = plt.subplots() ax.set_aspect('equal') # Enforce the margins, and enlarge them to give room for the vectors. @@ -76,8 +75,7 @@ def dipole_potential(x, y): ax.triplot(triang, color='0.8') levels = np.arange(0., 1., 0.01) -cmap = cm.get_cmap(name='hot', lut=None) -ax.tricontour(tri_refi, z_test_refi, levels=levels, cmap=cmap, +ax.tricontour(tri_refi, z_test_refi, levels=levels, cmap='hot', linewidths=[2.0, 1.0, 1.0, 1.0]) # Plots direction of the electrical vector field ax.quiver(triang.x, triang.y, Ex/E_norm, Ey/E_norm, diff --git a/examples/images_contours_and_fields/tripcolor_demo.py b/examples/images_contours_and_fields/tripcolor_demo.py index b77333f7bbcb..f78d275df2fc 100644 --- a/examples/images_contours_and_fields/tripcolor_demo.py +++ b/examples/images_contours_and_fields/tripcolor_demo.py @@ -113,7 +113,7 @@ # object if the same triangulation was to be used more than once to save # duplicated calculations. # Can specify one color value per face rather than one per point by using the -# facecolors kwarg. +# *facecolors* keyword argument. fig3, ax3 = plt.subplots() ax3.set_aspect('equal') diff --git a/examples/lines_bars_and_markers/bar_colors.py b/examples/lines_bars_and_markers/bar_colors.py new file mode 100644 index 000000000000..35e7a64ef605 --- /dev/null +++ b/examples/lines_bars_and_markers/bar_colors.py @@ -0,0 +1,26 @@ +""" +============== +Bar color demo +============== + +This is an example showing how to control bar color and legend entries +using the *color* and *label* parameters of `~matplotlib.pyplot.bar`. +Note that labels with a preceding underscore won't show up in the legend. +""" + +import matplotlib.pyplot as plt + +fig, ax = plt.subplots() + +fruits = ['apple', 'blueberry', 'cherry', 'orange'] +counts = [40, 100, 30, 55] +bar_labels = ['red', 'blue', '_red', 'orange'] +bar_colors = ['tab:red', 'tab:blue', 'tab:red', 'tab:orange'] + +ax.bar(fruits, counts, label=bar_labels, color=bar_colors) + +ax.set_ylabel('fruit supply') +ax.set_title('Fruit supply by kind and color') +ax.legend(title='Fruit color') + +plt.show() diff --git a/examples/lines_bars_and_markers/bar_label_demo.py b/examples/lines_bars_and_markers/bar_label_demo.py index 9b353376a53e..146b6934bb48 100644 --- a/examples/lines_bars_and_markers/bar_label_demo.py +++ b/examples/lines_bars_and_markers/bar_label_demo.py @@ -18,36 +18,27 @@ import numpy as np ############################################################################### -# Define the data +# data from https://allisonhorst.github.io/palmerpenguins/ -N = 5 -menMeans = (20, 35, 30, 35, -27) -womenMeans = (25, 32, 34, 20, -25) -menStd = (2, 3, 4, 1, 2) -womenStd = (3, 5, 2, 3, 3) -ind = np.arange(N) # the x locations for the groups -width = 0.35 # the width of the bars: can also be len(x) sequence +species = ('Adelie', 'Chinstrap', 'Gentoo') +sex_counts = { + 'Male': np.array([73, 34, 61]), + 'Female': np.array([73, 34, 58]), +} +width = 0.6 # the width of the bars: can also be len(x) sequence -############################################################################### -# Stacked bar plot with error bars fig, ax = plt.subplots() +bottom = np.zeros(3) -p1 = ax.bar(ind, menMeans, width, yerr=menStd, label='Men') -p2 = ax.bar(ind, womenMeans, width, - bottom=menMeans, yerr=womenStd, label='Women') +for sex, sex_count in sex_counts.items(): + p = ax.bar(species, sex_count, width, label=sex, bottom=bottom) + bottom += sex_count -ax.axhline(0, color='grey', linewidth=0.8) -ax.set_ylabel('Scores') -ax.set_title('Scores by group and gender') -ax.set_xticks(ind) -ax.set_xticklabels(('G1', 'G2', 'G3', 'G4', 'G5')) -ax.legend() + ax.bar_label(p, label_type='center') -# Label with label_type 'center' instead of the default 'edge' -ax.bar_label(p1, label_type='center') -ax.bar_label(p2, label_type='center') -ax.bar_label(p2) +ax.set_title('Number of penguins by sex') +ax.legend() plt.show() @@ -66,8 +57,7 @@ fig, ax = plt.subplots() hbars = ax.barh(y_pos, performance, xerr=error, align='center') -ax.set_yticks(y_pos) -ax.set_yticklabels(people) +ax.set_yticks(y_pos, labels=people) ax.invert_yaxis() # labels read top-to-bottom ax.set_xlabel('Performance') ax.set_title('How fast do you want to go today?') @@ -84,8 +74,7 @@ fig, ax = plt.subplots() hbars = ax.barh(y_pos, performance, xerr=error, align='center') -ax.set_yticks(y_pos) -ax.set_yticklabels(people) +ax.set_yticks(y_pos, labels=people) ax.invert_yaxis() # labels read top-to-bottom ax.set_xlabel('Performance') ax.set_title('How fast do you want to go today?') @@ -97,6 +86,30 @@ plt.show() +############################################################################### +# Bar labels using {}-style format string + +fruit_names = ['Coffee', 'Salted Caramel', 'Pistachio'] +fruit_counts = [4000, 2000, 7000] + +fig, ax = plt.subplots() +bar_container = ax.bar(fruit_names, fruit_counts) +ax.set(ylabel='pints sold', title='Gelato sales by flavor', ylim=(0, 8000)) +ax.bar_label(bar_container, fmt='{:,.0f}') + +############################################################################### +# Bar labels using a callable + +animal_names = ['Lion', 'Gazelle', 'Cheetah'] +mph_speed = [50, 60, 75] + +fig, ax = plt.subplots() +bar_container = ax.bar(animal_names, mph_speed) +ax.set(ylabel='speed in MPH', title='Running speeds', ylim=(0, 80)) +ax.bar_label( + bar_container, fmt=lambda x: '{:.1f} km/h'.format(x * 1.61) +) + ############################################################################# # # .. admonition:: References diff --git a/examples/lines_bars_and_markers/bar_stacked.py b/examples/lines_bars_and_markers/bar_stacked.py index 90e59f987249..81ee305e7072 100644 --- a/examples/lines_bars_and_markers/bar_stacked.py +++ b/examples/lines_bars_and_markers/bar_stacked.py @@ -3,30 +3,34 @@ Stacked bar chart ================= -This is an example of creating a stacked bar plot with error bars -using `~matplotlib.pyplot.bar`. Note the parameters *yerr* used for -error bars, and *bottom* to stack the women's bars on top of the men's -bars. +This is an example of creating a stacked bar plot +using `~matplotlib.pyplot.bar`. """ import matplotlib.pyplot as plt +import numpy as np +# data from https://allisonhorst.github.io/palmerpenguins/ -labels = ['G1', 'G2', 'G3', 'G4', 'G5'] -men_means = [20, 35, 30, 35, 27] -women_means = [25, 32, 34, 20, 25] -men_std = [2, 3, 4, 1, 2] -women_std = [3, 5, 2, 3, 3] -width = 0.35 # the width of the bars: can also be len(x) sequence +species = ( + "Adelie\n $\\mu=$3700.66g", + "Chinstrap\n $\\mu=$3733.09g", + "Gentoo\n $\\mu=5076.02g$", +) +weight_counts = { + "Below": np.array([70, 31, 58]), + "Above": np.array([82, 37, 66]), +} +width = 0.5 fig, ax = plt.subplots() +bottom = np.zeros(3) -ax.bar(labels, men_means, width, yerr=men_std, label='Men') -ax.bar(labels, women_means, width, yerr=women_std, bottom=men_means, - label='Women') +for boolean, weight_count in weight_counts.items(): + p = ax.bar(species, weight_count, width, label=boolean, bottom=bottom) + bottom += weight_count -ax.set_ylabel('Scores') -ax.set_title('Scores by group and gender') -ax.legend() +ax.set_title("Number of penguins with above average body mass") +ax.legend(loc="upper right") plt.show() diff --git a/examples/lines_bars_and_markers/barchart.py b/examples/lines_bars_and_markers/barchart.py index 71ae2ca0f8f3..da30bbf75937 100644 --- a/examples/lines_bars_and_markers/barchart.py +++ b/examples/lines_bars_and_markers/barchart.py @@ -7,32 +7,36 @@ bars with labels. """ +# data from https://allisonhorst.github.io/palmerpenguins/ + import matplotlib.pyplot as plt import numpy as np +species = ("Adelie", "Chinstrap", "Gentoo") +penguin_means = { + 'Bill Depth': (18.35, 18.43, 14.98), + 'Bill Length': (38.79, 48.83, 47.50), + 'Flipper Length': (189.95, 195.82, 217.19), +} -labels = ['G1', 'G2', 'G3', 'G4', 'G5'] -men_means = [20, 34, 30, 35, 27] -women_means = [25, 32, 34, 20, 25] +x = np.arange(len(species)) # the label locations +width = 0.25 # the width of the bars +multiplier = 0 -x = np.arange(len(labels)) # the label locations -width = 0.35 # the width of the bars +fig, ax = plt.subplots(layout='constrained') -fig, ax = plt.subplots() -rects1 = ax.bar(x - width/2, men_means, width, label='Men') -rects2 = ax.bar(x + width/2, women_means, width, label='Women') +for attribute, measurement in penguin_means.items(): + offset = width * multiplier + rects = ax.bar(x + offset, measurement, width, label=attribute) + ax.bar_label(rects, padding=3) + multiplier += 1 # Add some text for labels, title and custom x-axis tick labels, etc. -ax.set_ylabel('Scores') -ax.set_title('Scores by group and gender') -ax.set_xticks(x) -ax.set_xticklabels(labels) -ax.legend() - -ax.bar_label(rects1, padding=3) -ax.bar_label(rects2, padding=3) - -fig.tight_layout() +ax.set_ylabel('Length (mm)') +ax.set_title('Penguin attributes by species') +ax.set_xticks(x + width, species) +ax.legend(loc='upper left', ncols=3) +ax.set_ylim(0, 250) plt.show() diff --git a/examples/lines_bars_and_markers/barh.py b/examples/lines_bars_and_markers/barh.py index c537c0d9b215..64d5a5137906 100644 --- a/examples/lines_bars_and_markers/barh.py +++ b/examples/lines_bars_and_markers/barh.py @@ -22,8 +22,7 @@ error = np.random.rand(len(people)) ax.barh(y_pos, performance, xerr=error, align='center') -ax.set_yticks(y_pos) -ax.set_yticklabels(people) +ax.set_yticks(y_pos, labels=people) ax.invert_yaxis() # labels read top-to-bottom ax.set_xlabel('Performance') ax.set_title('How fast do you want to go today?') diff --git a/examples/lines_bars_and_markers/broken_barh.py b/examples/lines_bars_and_markers/broken_barh.py index c0691beaf255..6da38f1e465f 100644 --- a/examples/lines_bars_and_markers/broken_barh.py +++ b/examples/lines_bars_and_markers/broken_barh.py @@ -7,6 +7,7 @@ """ import matplotlib.pyplot as plt +# Horizontal bar plot with gaps fig, ax = plt.subplots() ax.broken_barh([(110, 30), (150, 10)], (10, 9), facecolors='tab:blue') ax.broken_barh([(10, 50), (100, 20), (130, 10)], (20, 9), @@ -14,9 +15,8 @@ ax.set_ylim(5, 35) ax.set_xlim(0, 200) ax.set_xlabel('seconds since start') -ax.set_yticks([15, 25]) -ax.set_yticklabels(['Bill', 'Jim']) -ax.grid(True) +ax.set_yticks([15, 25], labels=['Bill', 'Jim']) # Modify y-axis tick labels +ax.grid(True) # Make grid lines visible ax.annotate('race interrupted', (61, 25), xytext=(0.8, 0.9), textcoords='axes fraction', arrowprops=dict(facecolor='black', shrink=0.05), diff --git a/examples/lines_bars_and_markers/cohere.py b/examples/lines_bars_and_markers/cohere.py index 370149695398..7881a0a31b1e 100644 --- a/examples/lines_bars_and_markers/cohere.py +++ b/examples/lines_bars_and_markers/cohere.py @@ -16,19 +16,19 @@ nse1 = np.random.randn(len(t)) # white noise 1 nse2 = np.random.randn(len(t)) # white noise 2 -# Two signals with a coherent part at 10Hz and a random part +# Two signals with a coherent part at 10 Hz and a random part s1 = np.sin(2 * np.pi * 10 * t) + nse1 s2 = np.sin(2 * np.pi * 10 * t) + nse2 fig, axs = plt.subplots(2, 1) axs[0].plot(t, s1, t, s2) axs[0].set_xlim(0, 2) -axs[0].set_xlabel('time') +axs[0].set_xlabel('Time') axs[0].set_ylabel('s1 and s2') axs[0].grid(True) cxy, f = axs[1].cohere(s1, s2, 256, 1. / dt) -axs[1].set_ylabel('coherence') +axs[1].set_ylabel('Coherence') fig.tight_layout() plt.show() diff --git a/examples/lines_bars_and_markers/csd_demo.py b/examples/lines_bars_and_markers/csd_demo.py index 1eacc649b204..8894333f94d0 100644 --- a/examples/lines_bars_and_markers/csd_demo.py +++ b/examples/lines_bars_and_markers/csd_demo.py @@ -33,10 +33,10 @@ ax1.plot(t, s1, t, s2) ax1.set_xlim(0, 5) -ax1.set_xlabel('time') +ax1.set_xlabel('Time') ax1.set_ylabel('s1 and s2') ax1.grid(True) cxy, f = ax2.csd(s1, s2, 256, 1. / dt) -ax2.set_ylabel('CSD (db)') +ax2.set_ylabel('CSD (dB)') plt.show() diff --git a/examples/lines_bars_and_markers/curve_error_band.py b/examples/lines_bars_and_markers/curve_error_band.py index 56d95977a54c..4d0e09482b31 100644 --- a/examples/lines_bars_and_markers/curve_error_band.py +++ b/examples/lines_bars_and_markers/curve_error_band.py @@ -10,7 +10,6 @@ # sphinx_gallery_thumbnail_number = 2 import numpy as np -from scipy.interpolate import splprep, splev import matplotlib.pyplot as plt from matplotlib.path import Path @@ -22,8 +21,8 @@ x, y = r * np.cos(t), r * np.sin(t) fig, ax = plt.subplots() -ax.plot(x, y) -plt.show() +ax.plot(x, y, "k") +ax.set(aspect=1) ############################################################################# # An error band can be used to indicate the uncertainty of the curve. @@ -39,34 +38,42 @@ # `~.Axes.fill_between` method (see also # :doc:`/gallery/lines_bars_and_markers/fill_between_demo`). -# Error amplitudes depending on the curve parameter *t* -# (actual values are arbitrary and only for illustrative purposes): -err = 0.05 * np.sin(2 * t) ** 2 + 0.04 + 0.02 * np.cos(9 * t + 2) -# calculate normals via derivatives of splines -tck, u = splprep([x, y], s=0) -dx, dy = splev(u, tck, der=1) -l = np.hypot(dx, dy) -nx = dy / l -ny = -dx / l +def draw_error_band(ax, x, y, err, **kwargs): + # Calculate normals via centered finite differences (except the first point + # which uses a forward difference and the last point which uses a backward + # difference). + dx = np.concatenate([[x[1] - x[0]], x[2:] - x[:-2], [x[-1] - x[-2]]]) + dy = np.concatenate([[y[1] - y[0]], y[2:] - y[:-2], [y[-1] - y[-2]]]) + l = np.hypot(dx, dy) + nx = dy / l + ny = -dx / l -# end points of errors -xp = x + nx * err -yp = y + ny * err -xn = x - nx * err -yn = y - ny * err + # end points of errors + xp = x + nx * err + yp = y + ny * err + xn = x - nx * err + yn = y - ny * err -vertices = np.block([[xp, xn[::-1]], - [yp, yn[::-1]]]).T -codes = Path.LINETO * np.ones(len(vertices), dtype=Path.code_type) -codes[0] = codes[len(xp)] = Path.MOVETO -path = Path(vertices, codes) + vertices = np.block([[xp, xn[::-1]], + [yp, yn[::-1]]]).T + codes = np.full(len(vertices), Path.LINETO) + codes[0] = codes[len(xp)] = Path.MOVETO + path = Path(vertices, codes) + ax.add_patch(PathPatch(path, **kwargs)) -patch = PathPatch(path, facecolor='C0', edgecolor='none', alpha=0.3) -fig, ax = plt.subplots() -ax.plot(x, y) -ax.add_patch(patch) +_, axs = plt.subplots(1, 2, layout='constrained', sharex=True, sharey=True) +errs = [ + (axs[0], "constant error", 0.05), + (axs[1], "variable error", 0.05 * np.sin(2 * t) ** 2 + 0.04), +] +for i, (ax, title, err) in enumerate(errs): + ax.set(title=title, aspect=1, xticks=[], yticks=[]) + ax.plot(x, y, "k") + draw_error_band(ax, x, y, err=err, + facecolor=f"C{i}", edgecolor="none", alpha=.3) + plt.show() ############################################################################# diff --git a/examples/lines_bars_and_markers/eventcollection_demo.py b/examples/lines_bars_and_markers/eventcollection_demo.py index abdb6e6c05f5..e4fe6f61bdab 100644 --- a/examples/lines_bars_and_markers/eventcollection_demo.py +++ b/examples/lines_bars_and_markers/eventcollection_demo.py @@ -1,10 +1,10 @@ -""" +r""" ==================== EventCollection Demo ==================== -Plot two curves, then use EventCollections to mark the locations of the x -and y data points on the respective axes for each curve +Plot two curves, then use `.EventCollection`\s to mark the locations of the x +and y data points on the respective axes for each curve. """ import matplotlib.pyplot as plt diff --git a/examples/lines_bars_and_markers/eventplot_demo.py b/examples/lines_bars_and_markers/eventplot_demo.py index 48ea3a6a4fdf..44e3c183bb51 100644 --- a/examples/lines_bars_and_markers/eventplot_demo.py +++ b/examples/lines_bars_and_markers/eventplot_demo.py @@ -1,10 +1,10 @@ """ ============== -Eventplot Demo +Eventplot demo ============== -An eventplot showing sequences of events with various line properties. -The plot is shown in both horizontal and vertical orientations. +An `~.axes.Axes.eventplot` showing sequences of events with various line +properties. The plot is shown in both horizontal and vertical orientations. """ import matplotlib.pyplot as plt @@ -20,7 +20,7 @@ data1 = np.random.random([6, 50]) # set different colors for each set of positions -colors1 = ['C{}'.format(i) for i in range(6)] +colors1 = [f'C{i}' for i in range(6)] # set different line properties for each set of positions # note that some overlap diff --git a/examples/lines_bars_and_markers/fill.py b/examples/lines_bars_and_markers/fill.py index 158a16f863d9..79642a9e5ed5 100644 --- a/examples/lines_bars_and_markers/fill.py +++ b/examples/lines_bars_and_markers/fill.py @@ -3,7 +3,7 @@ Filled polygon ============== -`~.Axes.fill()` draws a filled polygon based based on lists of point +`~.Axes.fill()` draws a filled polygon based on lists of point coordinates *x*, *y*. This example uses the `Koch snowflake`_ as an example polygon. @@ -62,7 +62,7 @@ def _koch_snowflake_complex(order): plt.show() ############################################################################### -# Use keyword arguments *facecolor* and *edgecolor* to modify the the colors +# Use keyword arguments *facecolor* and *edgecolor* to modify the colors # of the polygon. Since the *linewidth* of the edge is 0 in the default # Matplotlib style, we have to set it as well for the edge to become visible. diff --git a/examples/lines_bars_and_markers/fill_between_alpha.py b/examples/lines_bars_and_markers/fill_between_alpha.py index acd6cebbbd7f..9f4351b0006f 100644 --- a/examples/lines_bars_and_markers/fill_between_alpha.py +++ b/examples/lines_bars_and_markers/fill_between_alpha.py @@ -7,9 +7,9 @@ It has a very handy ``where`` argument to combine filling with logical ranges, e.g., to just fill in a curve over some threshold value. -At its most basic level, ``fill_between`` can be use to enhance a graphs visual -appearance. Let's compare two graphs of a financial times with a simple line -plot on the left and a filled line on the right. +At its most basic level, ``fill_between`` can be used to enhance a graph's +visual appearance. Let's compare two graphs of financial data with a simple +line plot on the left and a filled line on the right. """ import matplotlib.pyplot as plt @@ -17,9 +17,6 @@ import matplotlib.cbook as cbook -# Fixing random state for reproducibility -np.random.seed(19680801) - # load up some sample financial data r = (cbook.get_sample_data('goog.npz', np_load=True)['price_data'] .view(np.recarray)) @@ -29,14 +26,13 @@ pricemin = r.close.min() ax1.plot(r.date, r.close, lw=2) -ax2.fill_between(r.date, pricemin, r.close, facecolor='blue', alpha=0.5) +ax2.fill_between(r.date, pricemin, r.close, alpha=0.7) for ax in ax1, ax2: ax.grid(True) + ax.label_outer() ax1.set_ylabel('price') -for label in ax2.get_yticklabels(): - label.set_visible(False) fig.suptitle('Google (GOOG) daily closing price') fig.autofmt_xdate() @@ -52,16 +48,19 @@ # # Our next example computes two populations of random walkers with a # different mean and standard deviation of the normal distributions from -# which the steps are drawn. We use shared regions to plot +/- one +# which the steps are drawn. We use filled regions to plot +/- one # standard deviation of the mean position of the population. Here the # alpha channel is useful, not just aesthetic. +# Fixing random state for reproducibility +np.random.seed(19680801) + Nsteps, Nwalkers = 100, 250 t = np.arange(Nsteps) # an (Nsteps x Nwalkers) array of random walk steps -S1 = 0.002 + 0.01*np.random.randn(Nsteps, Nwalkers) -S2 = 0.004 + 0.02*np.random.randn(Nsteps, Nwalkers) +S1 = 0.004 + 0.02*np.random.randn(Nsteps, Nwalkers) +S2 = 0.002 + 0.01*np.random.randn(Nsteps, Nwalkers) # an (Nsteps x Nwalkers) array of random walker positions X1 = S1.cumsum(axis=0) @@ -77,10 +76,10 @@ # plot it! fig, ax = plt.subplots(1) -ax.plot(t, mu1, lw=2, label='mean population 1', color='blue') -ax.plot(t, mu2, lw=2, label='mean population 2', color='yellow') -ax.fill_between(t, mu1+sigma1, mu1-sigma1, facecolor='blue', alpha=0.5) -ax.fill_between(t, mu2+sigma2, mu2-sigma2, facecolor='yellow', alpha=0.5) +ax.plot(t, mu1, lw=2, label='mean population 1') +ax.plot(t, mu2, lw=2, label='mean population 2') +ax.fill_between(t, mu1+sigma1, mu1-sigma1, facecolor='C0', alpha=0.4) +ax.fill_between(t, mu2+sigma2, mu2-sigma2, facecolor='C1', alpha=0.4) ax.set_title(r'random walkers empirical $\mu$ and $\pm \sigma$ interval') ax.legend(loc='upper left') ax.set_xlabel('num steps') @@ -93,11 +92,14 @@ # as the x, ymin and ymax arguments, and only fills in the region where # the boolean mask is True. In the example below, we simulate a single # random walker and compute the analytic mean and standard deviation of -# the population positions. The population mean is shown as the black -# dashed line, and the plus/minus one sigma deviation from the mean is -# shown as the yellow filled region. We use the where mask -# ``X > upper_bound`` to find the region where the walker is above the one -# sigma boundary, and shade that region blue. +# the population positions. The population mean is shown as the dashed +# line, and the plus/minus one sigma deviation from the mean is shown +# as the filled region. We use the where mask ``X > upper_bound`` to +# find the region where the walker is outside the one sigma boundary, +# and shade that region red. + +# Fixing random state for reproducibility +np.random.seed(1) Nsteps = 500 t = np.arange(Nsteps) @@ -114,23 +116,23 @@ upper_bound = mu*t + sigma*np.sqrt(t) fig, ax = plt.subplots(1) -ax.plot(t, X, lw=2, label='walker position', color='blue') -ax.plot(t, mu*t, lw=1, label='population mean', color='black', ls='--') -ax.fill_between(t, lower_bound, upper_bound, facecolor='yellow', alpha=0.5, +ax.plot(t, X, lw=2, label='walker position') +ax.plot(t, mu*t, lw=1, label='population mean', color='C0', ls='--') +ax.fill_between(t, lower_bound, upper_bound, facecolor='C0', alpha=0.4, label='1 sigma range') ax.legend(loc='upper left') # here we use the where argument to only fill the region where the # walker is above the population 1 sigma boundary -ax.fill_between(t, upper_bound, X, where=X > upper_bound, facecolor='blue', - alpha=0.5) +ax.fill_between(t, upper_bound, X, where=X > upper_bound, fc='red', alpha=0.4) +ax.fill_between(t, lower_bound, X, where=X < lower_bound, fc='red', alpha=0.4) ax.set_xlabel('num steps') ax.set_ylabel('position') ax.grid() ############################################################################### # Another handy use of filled regions is to highlight horizontal or vertical -# spans of an axes -- for that Matplotlib has the helper functions +# spans of an Axes -- for that Matplotlib has the helper functions # `~matplotlib.axes.Axes.axhspan` and `~matplotlib.axes.Axes.axvspan`. See # :doc:`/gallery/subplots_axes_and_figures/axhspan_demo`. diff --git a/examples/lines_bars_and_markers/fill_between_demo.py b/examples/lines_bars_and_markers/fill_between_demo.py index cc9d4ddab11a..2da1f682835b 100644 --- a/examples/lines_bars_and_markers/fill_between_demo.py +++ b/examples/lines_bars_and_markers/fill_between_demo.py @@ -52,7 +52,7 @@ x = np.linspace(0, 10, 11) y = [3.9, 4.4, 10.8, 10.3, 11.2, 13.1, 14.1, 9.9, 13.9, 15.1, 12.5] -# fit a linear curve an estimate its y-values and their error. +# fit a linear curve and estimate its y-values and their error. a, b = np.polyfit(x, y, deg=1) y_est = a * x + b y_err = x.std() * np.sqrt(1/len(x) + @@ -114,7 +114,7 @@ # ------------------------------------------------------------ # The same selection mechanism can be applied to fill the full vertical height # of the axes. To be independent of y-limits, we add a transform that -# interprets the x-values in data coorindates and the y-values in axes +# interprets the x-values in data coordinates and the y-values in axes # coordinates. # # The following example marks the regions in which the y-data are above a diff --git a/examples/lines_bars_and_markers/fill_betweenx_demo.py b/examples/lines_bars_and_markers/fill_betweenx_demo.py index 1f449c372348..06f219d9fb31 100644 --- a/examples/lines_bars_and_markers/fill_betweenx_demo.py +++ b/examples/lines_bars_and_markers/fill_betweenx_demo.py @@ -26,9 +26,12 @@ ax3.fill_betweenx(y, x1, x2) ax3.set_title('between (x1, x2)') -# now fill between x1 and x2 where a logical condition is met. Note -# this is different than calling +############################################################################# +# Now fill between x1 and x2 where a logical condition is met. Note this is +# different than calling:: +# # fill_between(y[where], x1[where], x2[where]) +# # because of edge effects over multiple contiguous regions. fig, [ax, ax1] = plt.subplots(1, 2, sharey=True, figsize=(6, 6)) @@ -44,9 +47,9 @@ ax1.fill_betweenx(y, x1, x2, where=x2 <= x1, facecolor='red') ax1.set_title('regions with x2 > 1 are masked') -# This example illustrates a problem; because of the data -# gridding, there are undesired unfilled triangles at the crossover -# points. A brute-force solution would be to interpolate all -# arrays to a very fine grid before plotting. +############################################################################# +# This example illustrates a problem; because of the data gridding, there are +# undesired unfilled triangles at the crossover points. A brute-force solution +# would be to interpolate all arrays to a very fine grid before plotting. plt.show() diff --git a/examples/lines_bars_and_markers/filled_step.py b/examples/lines_bars_and_markers/filled_step.py index 6f37377cb331..102f2498ca4c 100644 --- a/examples/lines_bars_and_markers/filled_step.py +++ b/examples/lines_bars_and_markers/filled_step.py @@ -7,7 +7,6 @@ """ import itertools -from collections import OrderedDict from functools import partial import numpy as np @@ -21,8 +20,6 @@ def filled_hist(ax, edges, values, bottoms=None, orientation='v', """ Draw a histogram as a stepped patch. - Extra kwargs are passed through to `fill_between` - Parameters ---------- ax : Axes @@ -42,6 +39,9 @@ def filled_hist(ax, edges, values, bottoms=None, orientation='v', Orientation of the histogram. 'v' (default) has the bars increasing in the positive y-direction. + **kwargs + Extra keyword arguments are passed through to `.fill_between`. + Returns ------- ret : PolyCollection @@ -49,16 +49,16 @@ def filled_hist(ax, edges, values, bottoms=None, orientation='v', """ print(orientation) if orientation not in 'hv': - raise ValueError("orientation must be in {{'h', 'v'}} " - "not {o}".format(o=orientation)) + raise ValueError(f"orientation must be in {{'h', 'v'}} " + f"not {orientation}") kwargs.setdefault('step', 'post') + kwargs.setdefault('alpha', 0.7) edges = np.asarray(edges) values = np.asarray(values) if len(edges) - 1 != len(values): - raise ValueError('Must provide one more bin edge than value not: ' - 'len(edges): {lb} len(values): {lv}'.format( - lb=len(edges), lv=len(values))) + raise ValueError(f'Must provide one more bin edge than value not: ' + f'{len(edges)=} {len(values)=}') if bottoms is None: bottoms = 0 @@ -86,7 +86,7 @@ def stack_hist(ax, stacked_data, sty_cycle, bottoms=None, The axes to add artists too stacked_data : array or Mapping - A (N, M) shaped array. The first dimension will be iterated over to + A (M, N) shaped array. The first dimension will be iterated over to compute histograms row-wise sty_cycle : Cycler or operable of dict @@ -104,11 +104,11 @@ def stack_hist(ax, stacked_data, sty_cycle, bottoms=None, If not given and stacked data is an array defaults to 'default set {n}' - If stacked_data is a mapping, and labels is None, default to the keys - (which may come out in a random order). + If *stacked_data* is a mapping, and *labels* is None, default to the + keys. - If stacked_data is a mapping and labels is given then only - the columns listed by be plotted. + If *stacked_data* is a mapping and *labels* is given then only the + columns listed will be plotted. plot_func : callable, optional Function to call to draw the histogram must have signature: @@ -117,9 +117,9 @@ def stack_hist(ax, stacked_data, sty_cycle, bottoms=None, label=label, **kwargs) plot_kwargs : dict, optional - Any extra kwargs to pass through to the plotting function. This - will be the same for all calls to the plotting function and will - over-ride the values in cycle. + Any extra keyword arguments to pass through to the plotting function. + This will be the same for all calls to the plotting function and will + override the values in *sty_cycle*. Returns ------- @@ -158,7 +158,7 @@ def stack_hist(ax, stacked_data, sty_cycle, bottoms=None, arts = {} for j, (data, label, sty) in loop_iter: if label is None: - label = 'dflt set {n}'.format(n=j) + label = f'dflt set {j}' label = sty.pop('label', label) vals, edges = hist_func(data) if bottoms is None: @@ -181,14 +181,14 @@ def stack_hist(ax, stacked_data, sty_cycle, bottoms=None, # set up style cycles color_cycle = cycler(facecolor=plt.rcParams['axes.prop_cycle'][:4]) -label_cycle = cycler(label=['set {n}'.format(n=n) for n in range(4)]) +label_cycle = cycler(label=[f'set {n}' for n in range(4)]) hatch_cycle = cycler(hatch=['/', '*', '+', '|']) # Fixing random state for reproducibility np.random.seed(19680801) stack_data = np.random.randn(4, 12250) -dict_data = OrderedDict(zip((c['label'] for c in label_cycle), stack_data)) +dict_data = dict(zip((c['label'] for c in label_cycle), stack_data)) ############################################################################### # Work with plain arrays diff --git a/examples/lines_bars_and_markers/gradient_bar.py b/examples/lines_bars_and_markers/gradient_bar.py index 7f39267ddf86..1c61a4bb908e 100644 --- a/examples/lines_bars_and_markers/gradient_bar.py +++ b/examples/lines_bars_and_markers/gradient_bar.py @@ -12,18 +12,18 @@ by a unit vector *v*. The values at the corners are then obtained by the lengths of the projections of the corner vectors on *v*. -A similar approach can be used to create a gradient background for an axes. -In that case, it is helpful to uses Axes coordinates (``extent=(0, 1, 0, 1), +A similar approach can be used to create a gradient background for an Axes. +In that case, it is helpful to use Axes coordinates (``extent=(0, 1, 0, 1), transform=ax.transAxes``) to be independent of the data coordinates. - """ + import matplotlib.pyplot as plt import numpy as np np.random.seed(19680801) -def gradient_image(ax, extent, direction=0.3, cmap_range=(0, 1), **kwargs): +def gradient_image(ax, direction=0.3, cmap_range=(0, 1), **kwargs): """ Draw a gradient image based on a colormap. @@ -31,10 +31,6 @@ def gradient_image(ax, extent, direction=0.3, cmap_range=(0, 1), **kwargs): ---------- ax : Axes The axes to draw on. - extent - The extent of the image as (xmin, xmax, ymin, ymax). - By default, this is in Axes coordinates but may be - changed using the *transform* kwarg. direction : float The direction of the gradient. This is a number in range 0 (=vertical) to 1 (=horizontal). @@ -43,7 +39,7 @@ def gradient_image(ax, extent, direction=0.3, cmap_range=(0, 1), **kwargs): used for the gradient, where the complete colormap is (0, 1). **kwargs Other parameters are passed on to `.Axes.imshow()`. - In particular useful is *cmap*. + In particular, *cmap*, *extent*, and *transform* may be useful. """ phi = direction * np.pi / 2 v = np.array([np.cos(phi), np.sin(phi)]) @@ -51,8 +47,8 @@ def gradient_image(ax, extent, direction=0.3, cmap_range=(0, 1), **kwargs): [v @ [0, 0], v @ [0, 1]]]) a, b = cmap_range X = a + (b - a) / X.max() * X - im = ax.imshow(X, extent=extent, interpolation='bicubic', - vmin=0, vmax=1, **kwargs) + im = ax.imshow(X, interpolation='bicubic', clim=(0, 1), + aspect='auto', **kwargs) return im @@ -63,19 +59,15 @@ def gradient_bar(ax, x, y, width=0.5, bottom=0): cmap=plt.cm.Blues_r, cmap_range=(0, 0.8)) -xmin, xmax = xlim = 0, 10 -ymin, ymax = ylim = 0, 1 - fig, ax = plt.subplots() -ax.set(xlim=xlim, ylim=ylim, autoscale_on=False) +ax.set(xlim=(0, 10), ylim=(0, 1)) # background image -gradient_image(ax, direction=0, extent=(0, 1, 0, 1), transform=ax.transAxes, - cmap=plt.cm.Oranges, cmap_range=(0.1, 0.6)) +gradient_image(ax, direction=1, extent=(0, 1, 0, 1), transform=ax.transAxes, + cmap=plt.cm.RdYlGn, cmap_range=(0.2, 0.8), alpha=0.5) N = 10 x = np.arange(N) + 0.15 y = np.random.rand(N) gradient_bar(ax, x, y, width=0.7) -ax.set_aspect('auto') plt.show() diff --git a/examples/lines_bars_and_markers/hat_graph.py b/examples/lines_bars_and_markers/hat_graph.py index f7658c97b036..6c939167b536 100644 --- a/examples/lines_bars_and_markers/hat_graph.py +++ b/examples/lines_bars_and_markers/hat_graph.py @@ -40,8 +40,7 @@ def label_bars(heights, rects): values = np.asarray(values) x = np.arange(values.shape[1]) - ax.set_xticks(x) - ax.set_xticklabels(xlabels) + ax.set_xticks(x, labels=xlabels) spacing = 0.3 # spacing between hat groups width = (1 - spacing) / values.shape[0] heights0 = values[0] diff --git a/examples/lines_bars_and_markers/horizontal_barchart_distribution.py b/examples/lines_bars_and_markers/horizontal_barchart_distribution.py index 8bb4ed7d4f0d..3ec12ba00e8d 100644 --- a/examples/lines_bars_and_markers/horizontal_barchart_distribution.py +++ b/examples/lines_bars_and_markers/horizontal_barchart_distribution.py @@ -43,7 +43,7 @@ def survey(results, category_names): labels = list(results.keys()) data = np.array(list(results.values())) data_cum = data.cumsum(axis=1) - category_colors = plt.get_cmap('RdYlGn')( + category_colors = plt.colormaps['RdYlGn']( np.linspace(0.15, 0.85, data.shape[1])) fig, ax = plt.subplots(figsize=(9.2, 5)) @@ -60,7 +60,7 @@ def survey(results, category_names): r, g, b, _ = color text_color = 'white' if r * g * b < 0.5 else 'darkgrey' ax.bar_label(rects, label_type='center', color=text_color) - ax.legend(ncol=len(category_names), bbox_to_anchor=(0, 1), + ax.legend(ncols=len(category_names), bbox_to_anchor=(0, 1), loc='lower left', fontsize='small') return fig, ax diff --git a/examples/lines_bars_and_markers/line_demo_dash_control.py b/examples/lines_bars_and_markers/line_demo_dash_control.py index 78043dfed2ff..9d02981597c0 100644 --- a/examples/lines_bars_and_markers/line_demo_dash_control.py +++ b/examples/lines_bars_and_markers/line_demo_dash_control.py @@ -17,6 +17,11 @@ :doc:`property_cycle ` by passing a list of dash sequences using the keyword *dashes* to the cycler. This is not shown within this example. + +Other attributes of the dash may also be set either with the relevant method +(`~.Line2D.set_dash_capstyle`, `~.Line2D.set_dash_joinstyle`, +`~.Line2D.set_gapcolor`) or by passing the property through a plotting +function. """ import numpy as np import matplotlib.pyplot as plt @@ -24,14 +29,21 @@ x = np.linspace(0, 10, 500) y = np.sin(x) +plt.rc('lines', linewidth=2.5) fig, ax = plt.subplots() -# Using set_dashes() to modify dashing of an existing line -line1, = ax.plot(x, y, label='Using set_dashes()') -line1.set_dashes([2, 2, 10, 2]) # 2pt line, 2pt break, 10pt line, 2pt break +# Using set_dashes() and set_capstyle() to modify dashing of an existing line. +line1, = ax.plot(x, y, label='Using set_dashes() and set_dash_capstyle()') +line1.set_dashes([2, 2, 10, 2]) # 2pt line, 2pt break, 10pt line, 2pt break. +line1.set_dash_capstyle('round') -# Using plot(..., dashes=...) to set the dashing when creating a line +# Using plot(..., dashes=...) to set the dashing when creating a line. line2, = ax.plot(x, y - 0.2, dashes=[6, 2], label='Using the dashes parameter') -ax.legend() +# Using plot(..., dashes=..., gapcolor=...) to set the dashing and +# alternating color when creating a line. +line3, = ax.plot(x, y - 0.4, dashes=[4, 4], gapcolor='tab:pink', + label='Using the dashes and gapcolor parameters') + +ax.legend(handlelength=4) plt.show() diff --git a/examples/lines_bars_and_markers/lines_with_ticks_demo.py b/examples/lines_bars_and_markers/lines_with_ticks_demo.py index c7b902f09ddf..f7ef646c647f 100644 --- a/examples/lines_bars_and_markers/lines_with_ticks_demo.py +++ b/examples/lines_bars_and_markers/lines_with_ticks_demo.py @@ -15,10 +15,12 @@ import matplotlib.pyplot as plt from matplotlib import patheffects +# Plot a straight diagonal line with ticked style path fig, ax = plt.subplots(figsize=(6, 6)) ax.plot([0, 1], [0, 1], label="Line", path_effects=[patheffects.withTickedStroke(spacing=7, angle=135)]) +# Plot a curved line with ticked style path nx = 101 x = np.linspace(0.0, 1.0, nx) y = 0.3*np.sin(x*8) + 0.4 diff --git a/examples/lines_bars_and_markers/linestyles.py b/examples/lines_bars_and_markers/linestyles.py index 35920617c90c..96c9113ee4cc 100644 --- a/examples/lines_bars_and_markers/linestyles.py +++ b/examples/lines_bars_and_markers/linestyles.py @@ -6,8 +6,9 @@ Simple linestyles can be defined using the strings "solid", "dotted", "dashed" or "dashdot". More refined control can be achieved by providing a dash tuple ``(offset, (on_off_seq))``. For example, ``(0, (3, 10, 1, 15))`` means -(3pt line, 10pt space, 1pt line, 15pt space) with no offset. See also -`.Line2D.set_linestyle`. +(3pt line, 10pt space, 1pt line, 15pt space) with no offset, while +``(5, (10, 3))``, means (10pt line, 3pt space), but skip the first 5pt line. +See also `.Line2D.set_linestyle`. *Note*: The dash style can also be configured via `.Line2D.set_dashes` as shown in :doc:`/gallery/lines_bars_and_markers/line_demo_dash_control` @@ -19,7 +20,7 @@ linestyle_str = [ ('solid', 'solid'), # Same as (0, ()) or '-' - ('dotted', 'dotted'), # Same as (0, (1, 1)) or '.' + ('dotted', 'dotted'), # Same as (0, (1, 1)) or ':' ('dashed', 'dashed'), # Same as '--' ('dashdot', 'dashdot')] # Same as '-.' @@ -27,7 +28,7 @@ ('loosely dotted', (0, (1, 10))), ('dotted', (0, (1, 1))), ('densely dotted', (0, (1, 1))), - + ('long dash with offset', (5, (10, 3))), ('loosely dashed', (0, (5, 10))), ('dashed', (0, (5, 5))), ('densely dashed', (0, (5, 1))), @@ -65,9 +66,7 @@ def plot_linestyles(ax, linestyles, title): color="blue", fontsize=8, ha="right", family="monospace") -ax0, ax1 = (plt.figure(figsize=(10, 8)) - .add_gridspec(2, 1, height_ratios=[1, 3]) - .subplots()) +fig, (ax0, ax1) = plt.subplots(2, 1, figsize=(10, 8), height_ratios=[1, 3]) plot_linestyles(ax0, linestyle_str[::-1], title='Named linestyles') plot_linestyles(ax1, linestyle_tuple[::-1], title='Parametrized linestyles') diff --git a/examples/lines_bars_and_markers/marker_reference.py b/examples/lines_bars_and_markers/marker_reference.py index 3d73ddee94ab..0c742801998b 100644 --- a/examples/lines_bars_and_markers/marker_reference.py +++ b/examples/lines_bars_and_markers/marker_reference.py @@ -9,17 +9,20 @@ - `Unfilled markers`_ - `Filled markers`_ - `Markers created from TeX symbols`_ -- Custom markers can be created from paths. See - :doc:`/gallery/shapes_and_collections/marker_path`. +- `Markers created from Paths`_ For a list of all markers see also the `matplotlib.markers` documentation. For example usages see :doc:`/gallery/lines_bars_and_markers/scatter_star_poly`. + +.. redirect-from:: /gallery/shapes_and_collections/marker_path """ +from matplotlib.markers import MarkerStyle import matplotlib.pyplot as plt from matplotlib.lines import Line2D +from matplotlib.transforms import Affine2D text_style = dict(horizontalalignment='right', verticalalignment='center', @@ -57,9 +60,6 @@ def split_list(a_list): ax.plot([y] * 3, marker=marker, **marker_style) format_axes(ax) -plt.show() - - ############################################################################### # Filled markers # ============== @@ -72,8 +72,6 @@ def split_list(a_list): ax.plot([y] * 3, marker=marker, **marker_style) format_axes(ax) -plt.show() - ############################################################################### # .. _marker_fill_styles: # @@ -99,9 +97,6 @@ def split_list(a_list): ax.plot([y] * 3, fillstyle=fill_style, **filled_marker_style) format_axes(ax) -plt.show() - - ############################################################################### # Markers created from TeX symbols # ================================ @@ -116,7 +111,7 @@ def split_list(a_list): fig.suptitle('Mathtext markers', fontsize=14) fig.subplots_adjust(left=0.4) -marker_style.update(markeredgecolor="None", markersize=15) +marker_style.update(markeredgecolor="none", markersize=15) markers = ["$1$", r"$\frac{1}{2}$", "$f$", "$\u266B$", r"$\mathcal{A}$"] for y, marker in enumerate(markers): @@ -125,4 +120,122 @@ def split_list(a_list): ax.plot([y] * 3, marker=marker, **marker_style) format_axes(ax) +############################################################################### +# Markers created from Paths +# ========================== +# +# Any `~.path.Path` can be used as a marker. The following example shows two +# simple paths *star* and *circle*, and a more elaborate path of a circle with +# a cut-out star. + +import matplotlib.path as mpath +import numpy as np + +star = mpath.Path.unit_regular_star(6) +circle = mpath.Path.unit_circle() +# concatenate the circle with an internal cutout of the star +cut_star = mpath.Path( + vertices=np.concatenate([circle.vertices, star.vertices[::-1, ...]]), + codes=np.concatenate([circle.codes, star.codes])) + +fig, ax = plt.subplots() +fig.suptitle('Path markers', fontsize=14) +fig.subplots_adjust(left=0.4) + +markers = {'star': star, 'circle': circle, 'cut_star': cut_star} + +for y, (name, marker) in enumerate(markers.items()): + ax.text(-0.5, y, name, **text_style) + ax.plot([y] * 3, marker=marker, **marker_style) +format_axes(ax) + +############################################################################### +# Advanced marker modifications with transform +# ============================================ +# +# Markers can be modified by passing a transform to the MarkerStyle +# constructor. Following example shows how a supplied rotation is applied to +# several marker shapes. + +common_style = {k: v for k, v in filled_marker_style.items() if k != 'marker'} +angles = [0, 10, 20, 30, 45, 60, 90] + +fig, ax = plt.subplots() +fig.suptitle('Rotated markers', fontsize=14) + +ax.text(-0.5, 0, 'Filled marker', **text_style) +for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + ax.plot(x, 0, marker=MarkerStyle('o', 'left', t), **common_style) + +ax.text(-0.5, 1, 'Un-filled marker', **text_style) +for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + ax.plot(x, 1, marker=MarkerStyle('1', 'left', t), **common_style) + +ax.text(-0.5, 2, 'Equation marker', **text_style) +for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + eq = r'$\frac{1}{x}$' + ax.plot(x, 2, marker=MarkerStyle(eq, 'left', t), **common_style) + +for x, theta in enumerate(angles): + ax.text(x, 2.5, f"{theta}°", horizontalalignment="center") +format_axes(ax) + +fig.tight_layout() + +############################################################################### +# Setting marker cap style and join style +# ======================================= +# +# Markers have default cap and join styles, but these can be +# customized when creating a MarkerStyle. + +from matplotlib.markers import JoinStyle, CapStyle + +marker_inner = dict(markersize=35, + markerfacecolor='tab:blue', + markerfacecoloralt='lightsteelblue', + markeredgecolor='brown', + markeredgewidth=8, + ) + +marker_outer = dict(markersize=35, + markerfacecolor='tab:blue', + markerfacecoloralt='lightsteelblue', + markeredgecolor='white', + markeredgewidth=1, + ) + +fig, ax = plt.subplots() +fig.suptitle('Marker CapStyle', fontsize=14) +fig.subplots_adjust(left=0.1) + +for y, cap_style in enumerate(CapStyle): + ax.text(-0.5, y, cap_style.name, **text_style) + for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + m = MarkerStyle('1', transform=t, capstyle=cap_style) + ax.plot(x, y, marker=m, **marker_inner) + ax.plot(x, y, marker=m, **marker_outer) + ax.text(x, len(CapStyle) - .5, f'{theta}°', ha='center') +format_axes(ax) + +############################################################################### +# Modifying the join style: + +fig, ax = plt.subplots() +fig.suptitle('Marker JoinStyle', fontsize=14) +fig.subplots_adjust(left=0.05) + +for y, join_style in enumerate(JoinStyle): + ax.text(-0.5, y, join_style.name, **text_style) + for x, theta in enumerate(angles): + t = Affine2D().rotate_deg(theta) + m = MarkerStyle('*', transform=t, joinstyle=join_style) + ax.plot(x, y, marker=m, **marker_inner) + ax.text(x, len(JoinStyle) - .5, f'{theta}°', ha='center') +format_axes(ax) + plt.show() diff --git a/examples/lines_bars_and_markers/markevery_demo.py b/examples/lines_bars_and_markers/markevery_demo.py index caf12c9563c7..d385515043d5 100644 --- a/examples/lines_bars_and_markers/markevery_demo.py +++ b/examples/lines_bars_and_markers/markevery_demo.py @@ -3,99 +3,96 @@ Markevery Demo ============== -This example demonstrates the various options for showing a marker at a -subset of data points using the ``markevery`` property of a Line2D object. - -Integer arguments are fairly intuitive. e.g. ``markevery=5`` will plot every -5th marker starting from the first data point. - -Float arguments allow markers to be spaced at approximately equal distances -along the line. The theoretical distance along the line between markers is -determined by multiplying the display-coordinate distance of the axes -bounding-box diagonal by the value of ``markevery``. The data points closest -to the theoretical distances will be shown. - -A slice or list/array can also be used with ``markevery`` to specify the -markers to show. +The ``markevery`` property of `.Line2D` allows drawing markers at a subset of +data points. + +The list of possible parameters is specified at `.Line2D.set_markevery`. +In short: + +- A single integer N draws every N-th marker. +- A tuple of integers (start, N) draws every N-th marker, starting at data + index *start*. +- A list of integers draws the markers at the specified indices. +- A slice draws the markers at the sliced indices. +- A float specifies the distance between markers as a fraction of the Axes + diagonal in screen space. This will lead to a visually uniform distribution + of the points along the line, irrespective of scales and zooming. """ import numpy as np import matplotlib.pyplot as plt # define a list of markevery cases to plot -cases = [None, - 8, - (30, 8), - [16, 24, 30], [0, -1], - slice(100, 200, 3), - 0.1, 0.3, 1.5, - (0.0, 0.1), (0.45, 0.1)] - -# define the figure size and grid layout properties -figsize = (10, 8) -cols = 3 -rows = len(cases) // cols + 1 -# define the data for cartesian plots +cases = [ + None, + 8, + (30, 8), + [16, 24, 32], + [0, -1], + slice(100, 200, 3), + 0.1, + 0.4, + (0.2, 0.4) +] + +# data points delta = 0.11 x = np.linspace(0, 10 - 2 * delta, 200) + delta y = np.sin(x) + 1.0 + delta - -def trim_axs(axs, N): - """ - Reduce *axs* to *N* Axes. All further Axes are removed from the figure. - """ - axs = axs.flat - for ax in axs[N:]: - ax.remove() - return axs[:N] - ############################################################################### -# Plot each markevery case for linear x and y scales +# markevery with linear scales +# ---------------------------- -axs = plt.figure(figsize=figsize, constrained_layout=True).subplots(rows, cols) -axs = trim_axs(axs, len(cases)) -for ax, case in zip(axs, cases): - ax.set_title('markevery=%s' % str(case)) - ax.plot(x, y, 'o', ls='-', ms=4, markevery=case) +fig, axs = plt.subplots(3, 3, figsize=(10, 6), layout='constrained') +for ax, markevery in zip(axs.flat, cases): + ax.set_title(f'markevery={markevery}') + ax.plot(x, y, 'o', ls='-', ms=4, markevery=markevery) ############################################################################### -# Plot each markevery case for log x and y scales - -axs = plt.figure(figsize=figsize, constrained_layout=True).subplots(rows, cols) -axs = trim_axs(axs, len(cases)) -for ax, case in zip(axs, cases): - ax.set_title('markevery=%s' % str(case)) +# markevery with log scales +# ------------------------- +# +# Note that the log scale causes a visual asymmetry in the marker distance for +# when subsampling the data using an integer. In contrast, subsampling on +# fraction of figure size creates even distributions, because it's based on +# fractions of the Axes diagonal, not on data coordinates or data indices. + +fig, axs = plt.subplots(3, 3, figsize=(10, 6), layout='constrained') +for ax, markevery in zip(axs.flat, cases): + ax.set_title(f'markevery={markevery}') ax.set_xscale('log') ax.set_yscale('log') - ax.plot(x, y, 'o', ls='-', ms=4, markevery=case) + ax.plot(x, y, 'o', ls='-', ms=4, markevery=markevery) ############################################################################### -# Plot each markevery case for linear x and y scales but zoomed in -# note the behaviour when zoomed in. When a start marker offset is specified -# it is always interpreted with respect to the first data point which might be -# different to the first visible data point. - -axs = plt.figure(figsize=figsize, constrained_layout=True).subplots(rows, cols) -axs = trim_axs(axs, len(cases)) -for ax, case in zip(axs, cases): - ax.set_title('markevery=%s' % str(case)) - ax.plot(x, y, 'o', ls='-', ms=4, markevery=case) +# markevery on zoomed plots +# ------------------------- +# +# Integer-based *markevery* specifications select points from the underlying +# data and are independent on the view. In contrast, float-based specifications +# are related to the Axes diagonal. While zooming does not change the Axes +# diagonal, it changes the displayed data range, and more points will be +# displayed when zooming. + +fig, axs = plt.subplots(3, 3, figsize=(10, 6), layout='constrained') +for ax, markevery in zip(axs.flat, cases): + ax.set_title(f'markevery={markevery}') + ax.plot(x, y, 'o', ls='-', ms=4, markevery=markevery) ax.set_xlim((6, 6.7)) ax.set_ylim((1.1, 1.7)) -# define data for polar plots +############################################################################### +# markevery on polar plots +# ------------------------ + r = np.linspace(0, 3.0, 200) theta = 2 * np.pi * r -############################################################################### -# Plot each markevery case for polar plots - -axs = plt.figure(figsize=figsize, constrained_layout=True).subplots( - rows, cols, subplot_kw={'projection': 'polar'}) -axs = trim_axs(axs, len(cases)) -for ax, case in zip(axs, cases): - ax.set_title('markevery=%s' % str(case)) - ax.plot(theta, r, 'o', ls='-', ms=4, markevery=case) +fig, axs = plt.subplots(3, 3, figsize=(10, 6), layout='constrained', + subplot_kw={'projection': 'polar'}) +for ax, markevery in zip(axs.flat, cases): + ax.set_title(f'markevery={markevery}') + ax.plot(theta, r, 'o', ls='-', ms=4, markevery=markevery) plt.show() diff --git a/examples/lines_bars_and_markers/markevery_prop_cycle.py b/examples/lines_bars_and_markers/markevery_prop_cycle.py deleted file mode 100644 index 295de4756736..000000000000 --- a/examples/lines_bars_and_markers/markevery_prop_cycle.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -========================================= -prop_cycle property markevery in rcParams -========================================= - -This example demonstrates a working solution to issue #8576, providing full -support of the markevery property for axes.prop_cycle assignments through -rcParams. Makes use of the same list of markevery cases from the -:doc:`markevery demo -`. - -Renders a plot with shifted-sine curves along each column with -a unique markevery value for each sine curve. -""" -from cycler import cycler -import numpy as np -import matplotlib as mpl -import matplotlib.pyplot as plt - -# Define a list of markevery cases and color cases to plot -cases = [None, - 8, - (30, 8), - [16, 24, 30], - [0, -1], - slice(100, 200, 3), - 0.1, - 0.3, - 1.5, - (0.0, 0.1), - (0.45, 0.1)] - -colors = ['#1f77b4', - '#ff7f0e', - '#2ca02c', - '#d62728', - '#9467bd', - '#8c564b', - '#e377c2', - '#7f7f7f', - '#bcbd22', - '#17becf', - '#1a55FF'] - -# Configure rcParams axes.prop_cycle to simultaneously cycle cases and colors. -mpl.rcParams['axes.prop_cycle'] = cycler(markevery=cases, color=colors) - -# Create data points and offsets -x = np.linspace(0, 2 * np.pi) -offsets = np.linspace(0, 2 * np.pi, 11, endpoint=False) -yy = np.transpose([np.sin(x + phi) for phi in offsets]) - -# Set the plot curve with markers and a title -fig = plt.figure() -ax = fig.add_axes([0.1, 0.1, 0.6, 0.75]) - -for i in range(len(cases)): - ax.plot(yy[:, i], marker='o', label=str(cases[i])) - ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.) - -plt.title('Support for axes.prop_cycle cycler with markevery') - -plt.show() diff --git a/examples/lines_bars_and_markers/masked_demo.py b/examples/lines_bars_and_markers/masked_demo.py index 52ea1a68b772..1f94a4f61891 100644 --- a/examples/lines_bars_and_markers/masked_demo.py +++ b/examples/lines_bars_and_markers/masked_demo.py @@ -15,7 +15,7 @@ plotting with a line, it will be broken there. .. _masked array: - https://docs.scipy.org/doc/numpy/reference/maskedarray.generic.html + https://numpy.org/doc/stable/reference/maskedarray.generic.html The following example illustrates the three cases: diff --git a/examples/lines_bars_and_markers/multicolored_line.py b/examples/lines_bars_and_markers/multicolored_line.py index c23de022f23f..da15a570686b 100644 --- a/examples/lines_bars_and_markers/multicolored_line.py +++ b/examples/lines_bars_and_markers/multicolored_line.py @@ -3,7 +3,7 @@ Multicolored lines ================== -This example shows how to make a multi-colored line. In this example, the line +This example shows how to make a multicolored line. In this example, the line is colored based on its derivative. """ @@ -17,7 +17,7 @@ dydx = np.cos(0.5 * (x[:-1] + x[1:])) # first derivative # Create a set of line segments so that we can color them individually -# This creates the points as a N x 1 x 2 array so that we can stack points +# This creates the points as an N x 1 x 2 array so that we can stack points # together easily to get the segments. The segments array for line collection # needs to be (numlines) x (points per line) x 2 (for x and y) points = np.array([x, y]).T.reshape(-1, 1, 2) diff --git a/examples/lines_bars_and_markers/multivariate_marker_plot.py b/examples/lines_bars_and_markers/multivariate_marker_plot.py new file mode 100644 index 000000000000..7627375b99f5 --- /dev/null +++ b/examples/lines_bars_and_markers/multivariate_marker_plot.py @@ -0,0 +1,46 @@ +""" +============================================== +Mapping marker properties to multivariate data +============================================== + +This example shows how to use different properties of markers to plot +multivariate datasets. Here we represent a successful baseball throw as a +smiley face with marker size mapped to the skill of thrower, marker rotation to +the take-off angle, and thrust to the marker color. +""" + +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.markers import MarkerStyle +from matplotlib.transforms import Affine2D +from matplotlib.text import TextPath +from matplotlib.colors import Normalize + +SUCCESS_SYMBOLS = [ + TextPath((0, 0), "☹"), + TextPath((0, 0), "😒"), + TextPath((0, 0), "☺"), +] + +N = 25 +np.random.seed(42) +skills = np.random.uniform(5, 80, size=N) * 0.1 + 5 +takeoff_angles = np.random.normal(0, 90, N) +thrusts = np.random.uniform(size=N) +successful = np.random.randint(0, 3, size=N) +positions = np.random.normal(size=(N, 2)) * 5 +data = zip(skills, takeoff_angles, thrusts, successful, positions) + +cmap = plt.colormaps["plasma"] +fig, ax = plt.subplots() +fig.suptitle("Throwing success", size=14) +for skill, takeoff, thrust, mood, pos in data: + t = Affine2D().scale(skill).rotate_deg(takeoff) + m = MarkerStyle(SUCCESS_SYMBOLS[mood], transform=t) + ax.plot(pos[0], pos[1], marker=m, color=cmap(thrust)) +fig.colorbar(plt.cm.ScalarMappable(norm=Normalize(0, 1), cmap=cmap), + ax=ax, label="Normalized Thrust [a.u.]") +ax.set_xlabel("X position [m]") +ax.set_ylabel("Y position [m]") + +plt.show() diff --git a/examples/lines_bars_and_markers/psd_demo.py b/examples/lines_bars_and_markers/psd_demo.py index 43fcc9bd47b4..b4949019a2d6 100644 --- a/examples/lines_bars_and_markers/psd_demo.py +++ b/examples/lines_bars_and_markers/psd_demo.py @@ -12,7 +12,6 @@ import matplotlib.pyplot as plt import numpy as np import matplotlib.mlab as mlab -import matplotlib.gridspec as gridspec # Fixing random state for reproducibility np.random.seed(19680801) @@ -58,39 +57,44 @@ y = y + np.random.randn(*t.shape) # Plot the raw time series -fig = plt.figure(constrained_layout=True) -gs = gridspec.GridSpec(2, 3, figure=fig) -ax = fig.add_subplot(gs[0, :]) -ax.plot(t, y) -ax.set_xlabel('time [s]') -ax.set_ylabel('signal') +fig, axs = plt.subplot_mosaic([ + ['signal', 'signal', 'signal'], + ['zero padding', 'block size', 'overlap'], +], layout='constrained') + +axs['signal'].plot(t, y) +axs['signal'].set_xlabel('time [s]') +axs['signal'].set_ylabel('signal') # Plot the PSD with different amounts of zero padding. This uses the entire # time series at once -ax2 = fig.add_subplot(gs[1, 0]) -ax2.psd(y, NFFT=len(t), pad_to=len(t), Fs=fs) -ax2.psd(y, NFFT=len(t), pad_to=len(t) * 2, Fs=fs) -ax2.psd(y, NFFT=len(t), pad_to=len(t) * 4, Fs=fs) -ax2.set_title('zero padding') +axs['zero padding'].psd(y, NFFT=len(t), pad_to=len(t), Fs=fs) +axs['zero padding'].psd(y, NFFT=len(t), pad_to=len(t) * 2, Fs=fs) +axs['zero padding'].psd(y, NFFT=len(t), pad_to=len(t) * 4, Fs=fs) # Plot the PSD with different block sizes, Zero pad to the length of the # original data sequence. -ax3 = fig.add_subplot(gs[1, 1], sharex=ax2, sharey=ax2) -ax3.psd(y, NFFT=len(t), pad_to=len(t), Fs=fs) -ax3.psd(y, NFFT=len(t) // 2, pad_to=len(t), Fs=fs) -ax3.psd(y, NFFT=len(t) // 4, pad_to=len(t), Fs=fs) -ax3.set_ylabel('') -ax3.set_title('block size') +axs['block size'].psd(y, NFFT=len(t), pad_to=len(t), Fs=fs) +axs['block size'].psd(y, NFFT=len(t) // 2, pad_to=len(t), Fs=fs) +axs['block size'].psd(y, NFFT=len(t) // 4, pad_to=len(t), Fs=fs) +axs['block size'].set_ylabel('') # Plot the PSD with different amounts of overlap between blocks -ax4 = fig.add_subplot(gs[1, 2], sharex=ax2, sharey=ax2) -ax4.psd(y, NFFT=len(t) // 2, pad_to=len(t), noverlap=0, Fs=fs) -ax4.psd(y, NFFT=len(t) // 2, pad_to=len(t), - noverlap=int(0.05 * len(t) / 2.), Fs=fs) -ax4.psd(y, NFFT=len(t) // 2, pad_to=len(t), - noverlap=int(0.2 * len(t) / 2.), Fs=fs) -ax4.set_ylabel('') -ax4.set_title('overlap') +axs['overlap'].psd(y, NFFT=len(t) // 2, pad_to=len(t), noverlap=0, Fs=fs) +axs['overlap'].psd(y, NFFT=len(t) // 2, pad_to=len(t), + noverlap=int(0.025 * len(t)), Fs=fs) +axs['overlap'].psd(y, NFFT=len(t) // 2, pad_to=len(t), + noverlap=int(0.1 * len(t)), Fs=fs) +axs['overlap'].set_ylabel('') +axs['overlap'].set_title('overlap') + +for title, ax in axs.items(): + if title == 'signal': + continue + + ax.set_title(title) + ax.sharex(axs['zero padding']) + ax.sharey(axs['zero padding']) plt.show() @@ -107,7 +111,7 @@ xn = (A * np.sin(2 * np.pi * f * t)).sum(axis=0) xn += 5 * np.random.randn(*t.shape) -fig, (ax0, ax1) = plt.subplots(ncols=2, constrained_layout=True) +fig, (ax0, ax1) = plt.subplots(ncols=2, layout='constrained') yticks = np.arange(-50, 30, 10) yrange = (yticks[0], yticks[-1]) @@ -147,7 +151,7 @@ f = np.array([150, 140]).reshape(-1, 1) xn = (A * np.exp(2j * np.pi * f * t)).sum(axis=0) + 5 * prng.randn(*t.shape) -fig, (ax0, ax1) = plt.subplots(ncols=2, constrained_layout=True) +fig, (ax0, ax1) = plt.subplots(ncols=2, layout='constrained') yticks = np.arange(-50, 30, 10) yrange = (yticks[0], yticks[-1]) diff --git a/examples/lines_bars_and_markers/scatter_custom_symbol.py b/examples/lines_bars_and_markers/scatter_custom_symbol.py index 428a68318671..b8e57b0e5e1f 100644 --- a/examples/lines_bars_and_markers/scatter_custom_symbol.py +++ b/examples/lines_bars_and_markers/scatter_custom_symbol.py @@ -1,18 +1,43 @@ """ -===================== -Scatter Custom Symbol -===================== - -Creating a custom ellipse symbol in scatter plot. +================================= +Scatter plots with custom symbols +================================= +.. redirect-from:: /gallery/lines_bars_and_markers/scatter_symbol +.. redirect-from:: /gallery/lines_bars_and_markers/scatter_piecharts """ + +############################################################################## +# Using TeX symbols +# ----------------- +# An easy way to customize scatter symbols is passing a TeX symbol name +# enclosed in $-signs as a marker. Below we use ``marker=r'$\clubsuit$'``. + import matplotlib.pyplot as plt import numpy as np - # Fixing random state for reproducibility np.random.seed(19680801) + +x = np.arange(0.0, 50.0, 2.0) +y = x ** 1.3 + np.random.rand(*x.shape) * 30.0 +sizes = np.random.rand(*x.shape) * 800 + 500 + +fig, ax = plt.subplots() +ax.scatter(x, y, sizes, c="green", alpha=0.5, marker=r'$\clubsuit$', + label="Luck") +ax.set_xlabel("Leprechauns") +ax.set_ylabel("Gold") +ax.legend() +plt.show() + +############################################################################## +# Using a custom path +# ------------------- +# Alternatively, one can also pass a custom path of N vertices as a Nx2 array +# of x, y values as *marker*. + # unit area ellipse rx, ry = 3., 1. area = rx * ry * np.pi diff --git a/examples/lines_bars_and_markers/scatter_demo2.py b/examples/lines_bars_and_markers/scatter_demo2.py index 7a669ff05d11..ec7e8183f8c4 100644 --- a/examples/lines_bars_and_markers/scatter_demo2.py +++ b/examples/lines_bars_and_markers/scatter_demo2.py @@ -9,9 +9,10 @@ import matplotlib.pyplot as plt import matplotlib.cbook as cbook -# Load a numpy record array from yahoo csv data with fields date, open, close, -# volume, adj_close from the mpl-data/example directory. The record array -# stores the date as an np.datetime64 with a day unit ('D') in the date column. +# Load a numpy record array from yahoo csv data with fields date, open, high, +# low, close, volume, adj_close from the mpl-data/sample_data directory. The +# record array stores the date as an np.datetime64 with a day unit ('D') in +# the date column. price_data = (cbook.get_sample_data('goog.npz', np_load=True)['price_data'] .view(np.recarray)) price_data = price_data[-250:] # get the most recent 250 trading days diff --git a/examples/lines_bars_and_markers/scatter_hist.py b/examples/lines_bars_and_markers/scatter_hist.py index 1ad5d8ac1c8b..7819808261f3 100644 --- a/examples/lines_bars_and_markers/scatter_hist.py +++ b/examples/lines_bars_and_markers/scatter_hist.py @@ -3,18 +3,22 @@ Scatter plot with histograms ============================ -Show the marginal distributions of a scatter as histograms at the sides of +Show the marginal distributions of a scatter plot as histograms at the sides of the plot. For a nice alignment of the main axes with the marginals, two options are shown -below. +below: -* the axes positions are defined in terms of rectangles in figure coordinates -* the axes positions are defined via a gridspec +.. contents:: + :local: + +While `.Axes.inset_axes` may be a bit more complex, it allows correct handling +of main axes with a fixed aspect ratio. An alternative method to produce a similar figure using the ``axes_grid1`` -toolkit is shown in the -:doc:`/gallery/axes_grid1/scatter_hist_locatable_axes` example. +toolkit is shown in the :doc:`/gallery/axes_grid1/scatter_hist_locatable_axes` +example. Finally, it is also possible to position all axes in absolute +coordinates using `.Figure.add_axes` (not shown here). Let us first define a function that takes x and y data as input, as well as three axes, the main axes for the scatter, and two marginal axes. It will @@ -52,60 +56,53 @@ def scatter_hist(x, y, ax, ax_histx, ax_histy): ############################################################################# # -# Axes in figure coordinates -# -------------------------- -# -# To define the axes positions, `.Figure.add_axes` is provided with a rectangle -# ``[left, bottom, width, height]`` in figure coordinates. The marginal axes -# share one dimension with the main axes. - -# definitions for the axes -left, width = 0.1, 0.65 -bottom, height = 0.1, 0.65 -spacing = 0.005 - - -rect_scatter = [left, bottom, width, height] -rect_histx = [left, bottom + height + spacing, width, 0.2] -rect_histy = [left + width + spacing, bottom, 0.2, height] - -# start with a square Figure -fig = plt.figure(figsize=(8, 8)) - -ax = fig.add_axes(rect_scatter) -ax_histx = fig.add_axes(rect_histx, sharex=ax) -ax_histy = fig.add_axes(rect_histy, sharey=ax) - -# use the previously defined function -scatter_hist(x, y, ax, ax_histx, ax_histy) - -plt.show() - - -############################################################################# -# -# Using a gridspec -# ---------------- +# Defining the axes positions using a gridspec +# -------------------------------------------- # -# We may equally define a gridspec with unequal width- and height-ratios to -# achieve desired layout. Also see the :doc:`/tutorials/intermediate/gridspec` -# tutorial. - -# start with a square Figure -fig = plt.figure(figsize=(8, 8)) +# We define a gridspec with unequal width- and height-ratios to achieve desired +# layout. Also see the :doc:`/tutorials/intermediate/arranging_axes` tutorial. -# Add a gridspec with two rows and two columns and a ratio of 2 to 7 between +# Start with a square Figure. +fig = plt.figure(figsize=(6, 6)) +# Add a gridspec with two rows and two columns and a ratio of 1 to 4 between # the size of the marginal axes and the main axes in both directions. # Also adjust the subplot parameters for a square plot. -gs = fig.add_gridspec(2, 2, width_ratios=(7, 2), height_ratios=(2, 7), +gs = fig.add_gridspec(2, 2, width_ratios=(4, 1), height_ratios=(1, 4), left=0.1, right=0.9, bottom=0.1, top=0.9, wspace=0.05, hspace=0.05) - +# Create the Axes. ax = fig.add_subplot(gs[1, 0]) ax_histx = fig.add_subplot(gs[0, 0], sharex=ax) ax_histy = fig.add_subplot(gs[1, 1], sharey=ax) +# Draw the scatter plot and marginals. +scatter_hist(x, y, ax, ax_histx, ax_histy) + -# use the previously defined function +############################################################################# +# +# Defining the axes positions using inset_axes +# -------------------------------------------- +# +# `~.Axes.inset_axes` can be used to position marginals *outside* the main +# axes. The advantage of doing so is that the aspect ratio of the main axes +# can be fixed, and the marginals will always be drawn relative to the position +# of the axes. + +# Create a Figure, which doesn't have to be square. +fig = plt.figure(layout='constrained') +# Create the main axes, leaving 25% of the figure space at the top and on the +# right to position marginals. +ax = fig.add_gridspec(top=0.75, right=0.75).subplots() +# The main axes' aspect can be fixed. +ax.set(aspect=1) +# Create marginal axes, which have 25% of the size of the main axes. Note that +# the inset axes are positioned *outside* (on the right and the top) of the +# main axes, by specifying axes coordinates greater than 1. Axes coordinates +# less than 0 would likewise specify positions on the left and the bottom of +# the main axes. +ax_histx = ax.inset_axes([0, 1.05, 1, 0.25], sharex=ax) +ax_histy = ax.inset_axes([1.05, 0, 0.25, 1], sharey=ax) +# Draw the scatter plot and marginals. scatter_hist(x, y, ax, ax_histx, ax_histy) plt.show() @@ -118,8 +115,8 @@ def scatter_hist(x, y, ax, ax_histx, ax_histy): # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.figure.Figure.add_axes` # - `matplotlib.figure.Figure.add_subplot` # - `matplotlib.figure.Figure.add_gridspec` +# - `matplotlib.axes.Axes.inset_axes` # - `matplotlib.axes.Axes.scatter` # - `matplotlib.axes.Axes.hist` diff --git a/examples/lines_bars_and_markers/scatter_piecharts.py b/examples/lines_bars_and_markers/scatter_piecharts.py deleted file mode 100644 index 09eb1c0b6be3..000000000000 --- a/examples/lines_bars_and_markers/scatter_piecharts.py +++ /dev/null @@ -1,52 +0,0 @@ -""" -=================================== -Scatter plot with pie chart markers -=================================== - -This example makes custom 'pie charts' as the markers for a scatter plot. - -Thanks to Manuel Metz for the example. -""" - -import numpy as np -import matplotlib.pyplot as plt - -# first define the ratios -r1 = 0.2 # 20% -r2 = r1 + 0.4 # 40% - -# define some sizes of the scatter marker -sizes = np.array([60, 80, 120]) - -# calculate the points of the first pie marker -# these are just the origin (0, 0) + some (cos, sin) points on a circle -x1 = np.cos(2 * np.pi * np.linspace(0, r1)) -y1 = np.sin(2 * np.pi * np.linspace(0, r1)) -xy1 = np.row_stack([[0, 0], np.column_stack([x1, y1])]) -s1 = np.abs(xy1).max() - -x2 = np.cos(2 * np.pi * np.linspace(r1, r2)) -y2 = np.sin(2 * np.pi * np.linspace(r1, r2)) -xy2 = np.row_stack([[0, 0], np.column_stack([x2, y2])]) -s2 = np.abs(xy2).max() - -x3 = np.cos(2 * np.pi * np.linspace(r2, 1)) -y3 = np.sin(2 * np.pi * np.linspace(r2, 1)) -xy3 = np.row_stack([[0, 0], np.column_stack([x3, y3])]) -s3 = np.abs(xy3).max() - -fig, ax = plt.subplots() -ax.scatter(range(3), range(3), marker=xy1, s=s1**2 * sizes, facecolor='blue') -ax.scatter(range(3), range(3), marker=xy2, s=s2**2 * sizes, facecolor='green') -ax.scatter(range(3), range(3), marker=xy3, s=s3**2 * sizes, facecolor='red') - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.scatter` / `matplotlib.pyplot.scatter` diff --git a/examples/lines_bars_and_markers/scatter_symbol.py b/examples/lines_bars_and_markers/scatter_symbol.py deleted file mode 100644 index d99b6a80a740..000000000000 --- a/examples/lines_bars_and_markers/scatter_symbol.py +++ /dev/null @@ -1,25 +0,0 @@ -""" -============== -Scatter Symbol -============== - -Scatter plot with clover symbols. - -""" -import matplotlib.pyplot as plt -import numpy as np - -# Fixing random state for reproducibility -np.random.seed(19680801) - - -x = np.arange(0.0, 50.0, 2.0) -y = x ** 1.3 + np.random.rand(*x.shape) * 30.0 -s = np.random.rand(*x.shape) * 800 + 500 - -plt.scatter(x, y, s, c="g", alpha=0.5, marker=r'$\clubsuit$', - label="Luck") -plt.xlabel("Leprechauns") -plt.ylabel("Gold") -plt.legend(loc='upper left') -plt.show() diff --git a/examples/lines_bars_and_markers/scatter_with_legend.py b/examples/lines_bars_and_markers/scatter_with_legend.py index fed8dd1c44f7..1562092f82d7 100644 --- a/examples/lines_bars_and_markers/scatter_with_legend.py +++ b/examples/lines_bars_and_markers/scatter_with_legend.py @@ -12,9 +12,10 @@ """ import numpy as np -np.random.seed(19680801) import matplotlib.pyplot as plt +np.random.seed(19680801) + fig, ax = plt.subplots() for color in ['tab:blue', 'tab:orange', 'tab:green']: @@ -56,7 +57,7 @@ loc="lower left", title="Classes") ax.add_artist(legend1) -# produce a legend with a cross section of sizes from the scatter +# produce a legend with a cross-section of sizes from the scatter handles, labels = scatter.legend_elements(prop="sizes", alpha=0.6) legend2 = ax.legend(handles, labels, loc="upper right", title="Sizes") diff --git a/examples/lines_bars_and_markers/span_regions.py b/examples/lines_bars_and_markers/span_regions.py index 79ebfcd008f2..f5d1cce9cf9b 100644 --- a/examples/lines_bars_and_markers/span_regions.py +++ b/examples/lines_bars_and_markers/span_regions.py @@ -1,37 +1,23 @@ """ -================ -Using span_where -================ - -Illustrate some helper functions for shading regions where a logical -mask is True. - -See `matplotlib.collections.BrokenBarHCollection.span_where`. +========================================================== +Shade regions defined by a logical mask using fill_between +========================================================== """ import numpy as np import matplotlib.pyplot as plt -import matplotlib.collections as collections t = np.arange(0.0, 2, 0.01) -s1 = np.sin(2*np.pi*t) -s2 = 1.2*np.sin(4*np.pi*t) - +s = np.sin(2*np.pi*t) fig, ax = plt.subplots() -ax.set_title('using span_where') -ax.plot(t, s1, color='black') -ax.axhline(0, color='black', lw=2) - -collection = collections.BrokenBarHCollection.span_where( - t, ymin=0, ymax=1, where=s1 > 0, facecolor='green', alpha=0.5) -ax.add_collection(collection) -collection = collections.BrokenBarHCollection.span_where( - t, ymin=-1, ymax=0, where=s1 < 0, facecolor='red', alpha=0.5) -ax.add_collection(collection) +ax.plot(t, s, color='black') +ax.axhline(0, color='black') +ax.fill_between(t, 1, where=s > 0, facecolor='green', alpha=.5) +ax.fill_between(t, -1, where=s < 0, facecolor='red', alpha=.5) plt.show() @@ -43,7 +29,4 @@ # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.collections.BrokenBarHCollection` -# - `matplotlib.collections.BrokenBarHCollection.span_where` -# - `matplotlib.axes.Axes.add_collection` -# - `matplotlib.axes.Axes.axhline` +# - `matplotlib.axes.Axes.fill_between` diff --git a/examples/lines_bars_and_markers/stackplot_demo.py b/examples/lines_bars_and_markers/stackplot_demo.py index 6c9acfcacc34..142b3d2a0ce0 100644 --- a/examples/lines_bars_and_markers/stackplot_demo.py +++ b/examples/lines_bars_and_markers/stackplot_demo.py @@ -29,7 +29,7 @@ fig, ax = plt.subplots() ax.stackplot(year, population_by_continent.values(), - labels=population_by_continent.keys()) + labels=population_by_continent.keys(), alpha=0.8) ax.legend(loc='upper left') ax.set_title('World population') ax.set_xlabel('Year') diff --git a/examples/lines_bars_and_markers/stem_plot.py b/examples/lines_bars_and_markers/stem_plot.py index c32b679d3329..4f74d50bde16 100644 --- a/examples/lines_bars_and_markers/stem_plot.py +++ b/examples/lines_bars_and_markers/stem_plot.py @@ -21,7 +21,7 @@ # The parameters *linefmt*, *markerfmt*, and *basefmt* control basic format # properties of the plot. However, in contrast to `~.pyplot.plot` not all # properties are configurable via keyword arguments. For more advanced -# control adapt the line objects returned by `~.pyplot`. +# control adapt the line objects returned by `.pyplot`. markerline, stemlines, baseline = plt.stem( x, y, linefmt='grey', markerfmt='D', bottom=1.1) diff --git a/examples/lines_bars_and_markers/timeline.py b/examples/lines_bars_and_markers/timeline.py index 087e7320f6b8..72327f18fc84 100644 --- a/examples/lines_bars_and_markers/timeline.py +++ b/examples/lines_bars_and_markers/timeline.py @@ -23,7 +23,7 @@ url = 'https://api.github.com/repos/matplotlib/matplotlib/releases' url += '?per_page=100' - data = json.loads(urllib.request.urlopen(url, timeout=.4).read().decode()) + data = json.loads(urllib.request.urlopen(url, timeout=1).read().decode()) dates = [] names = [] @@ -55,7 +55,7 @@ ############################################################################## # Next, we'll create a stem plot with some variation in levels as to # distinguish even close-by events. We add markers on the baseline for visual -# emphasis on the one-dimensional nature of the time line. +# emphasis on the one-dimensional nature of the timeline. # # For each event, we add a text label via `~.Axes.annotate`, which is offset # in units of points from the tip of the event line. @@ -68,7 +68,7 @@ int(np.ceil(len(dates)/6)))[:len(dates)] # Create figure and plot a stem plot with the date -fig, ax = plt.subplots(figsize=(8.8, 4), constrained_layout=True) +fig, ax = plt.subplots(figsize=(8.8, 4), layout="constrained") ax.set(title="Matplotlib release dates") ax.vlines(dates, 0, levels, color="tab:red") # The vertical stems. @@ -82,12 +82,12 @@ horizontalalignment="right", verticalalignment="bottom" if l > 0 else "top") -# format xaxis with 4 month intervals +# format x-axis with 4-month intervals ax.xaxis.set_major_locator(mdates.MonthLocator(interval=4)) ax.xaxis.set_major_formatter(mdates.DateFormatter("%b %Y")) plt.setp(ax.get_xticklabels(), rotation=30, ha="right") -# remove y axis and spines +# remove y-axis and spines ax.yaxis.set_visible(False) ax.spines[["left", "top", "right"]].set_visible(False) diff --git a/examples/misc/agg_buffer.py b/examples/misc/agg_buffer.py deleted file mode 100644 index e63dcbfda1f6..000000000000 --- a/examples/misc/agg_buffer.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -========== -Agg Buffer -========== - -Use backend agg to access the figure canvas as an RGBA buffer, convert it to an -array, and pass it to Pillow for rendering. -""" - -import numpy as np - -from matplotlib.backends.backend_agg import FigureCanvasAgg -import matplotlib.pyplot as plt - -plt.plot([1, 2, 3]) - -canvas = plt.gcf().canvas - -agg = canvas.switch_backends(FigureCanvasAgg) -agg.draw() -X = np.asarray(agg.buffer_rgba()) - -# Pass off to PIL. -from PIL import Image -im = Image.fromarray(X) - -# Uncomment this line to display the image using ImageMagick's `display` tool. -# im.show() diff --git a/examples/misc/agg_buffer_to_array.py b/examples/misc/agg_buffer_to_array.py deleted file mode 100644 index 1202702367ae..000000000000 --- a/examples/misc/agg_buffer_to_array.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -=================== -Agg Buffer To Array -=================== - -Convert a rendered figure to its image (NumPy array) representation. -""" -import matplotlib.pyplot as plt -import numpy as np -from matplotlib.figure import Figure -from matplotlib.backends.backend_agg import FigureCanvas - -# Create a figure that pyplot does not know about. -fig = Figure() -# attach a non-interactive Agg canvas to the figure -# (as a side-effect of the ``__init__``) -canvas = FigureCanvas(fig) -ax = fig.subplots() -ax.plot([1, 2, 3]) -ax.set_title('a simple figure') -# Force a draw so we can grab the pixel buffer -canvas.draw() -# grab the pixel buffer and dump it into a numpy array -X = np.array(canvas.renderer.buffer_rgba()) - -# now display the array X as an Axes in a new figure -fig2 = plt.figure() -ax2 = fig2.add_subplot(frameon=False) -ax2.imshow(X) -plt.show() diff --git a/examples/misc/anchored_artists.py b/examples/misc/anchored_artists.py index 80176e40f3ec..101c692ced3a 100644 --- a/examples/misc/anchored_artists.py +++ b/examples/misc/anchored_artists.py @@ -9,76 +9,43 @@ :doc:`/gallery/axes_grid1/simple_anchored_artists`, but it is implemented using only the matplotlib namespace, without the help of additional toolkits. + +.. redirect-from:: /gallery/userdemo/anchored_box01 +.. redirect-from:: /gallery/userdemo/anchored_box02 +.. redirect-from:: /gallery/userdemo/anchored_box03 """ from matplotlib import pyplot as plt from matplotlib.lines import Line2D -from matplotlib.patches import Ellipse +from matplotlib.patches import Circle, Ellipse from matplotlib.offsetbox import ( AnchoredOffsetbox, AuxTransformBox, DrawingArea, TextArea, VPacker) -class AnchoredText(AnchoredOffsetbox): - def __init__(self, s, loc, pad=0.4, borderpad=0.5, - prop=None, frameon=True): - self.txt = TextArea(s) - super().__init__(loc, pad=pad, borderpad=borderpad, - child=self.txt, prop=prop, frameon=frameon) - - def draw_text(ax): - """ - Draw a text-box anchored to the upper-left corner of the figure. - """ - at = AnchoredText("Figure 1a", loc='upper left', frameon=True) - at.patch.set_boxstyle("round,pad=0.,rounding_size=0.2") - ax.add_artist(at) - - -class AnchoredDrawingArea(AnchoredOffsetbox): - def __init__(self, width, height, xdescent, ydescent, - loc, pad=0.4, borderpad=0.5, prop=None, frameon=True): - self.da = DrawingArea(width, height, xdescent, ydescent) - super().__init__(loc, pad=pad, borderpad=borderpad, - child=self.da, prop=None, frameon=frameon) - - -def draw_circle(ax): - """ - Draw a circle in axis coordinates - """ - from matplotlib.patches import Circle - ada = AnchoredDrawingArea(20, 20, 0, 0, - loc='upper right', pad=0., frameon=False) - p = Circle((10, 10), 10) - ada.da.add_artist(p) - ax.add_artist(ada) + """Draw a text-box anchored to the upper-left corner of the figure.""" + box = AnchoredOffsetbox(child=TextArea("Figure 1a"), + loc="upper left", frameon=True) + box.patch.set_boxstyle("round,pad=0.,rounding_size=0.2") + ax.add_artist(box) -class AnchoredEllipse(AnchoredOffsetbox): - def __init__(self, transform, width, height, angle, loc, - pad=0.1, borderpad=0.1, prop=None, frameon=True): - """ - Draw an ellipse the size in data coordinate of the give axes. - - pad, borderpad in fraction of the legend font size (or prop) - """ - self._box = AuxTransformBox(transform) - self.ellipse = Ellipse((0, 0), width, height, angle) - self._box.add_artist(self.ellipse) - super().__init__(loc, pad=pad, borderpad=borderpad, - child=self._box, prop=prop, frameon=frameon) +def draw_circles(ax): + """Draw circles in axes coordinates.""" + area = DrawingArea(40, 20, 0, 0) + area.add_artist(Circle((10, 10), 10, fc="tab:blue")) + area.add_artist(Circle((30, 10), 5, fc="tab:red")) + box = AnchoredOffsetbox( + child=area, loc="upper right", pad=0, frameon=False) + ax.add_artist(box) def draw_ellipse(ax): - """ - Draw an ellipse of width=0.1, height=0.15 in data coordinates - """ - ae = AnchoredEllipse(ax.transData, width=0.1, height=0.15, angle=0., - loc='lower left', pad=0.5, borderpad=0.4, - frameon=True) - - ax.add_artist(ae) + """Draw an ellipse of width=0.1, height=0.15 in data coordinates.""" + aux_tr_box = AuxTransformBox(ax.transData) + aux_tr_box.add_artist(Ellipse((0, 0), width=0.1, height=0.15)) + box = AnchoredOffsetbox(child=aux_tr_box, loc="lower left", frameon=True) + ax.add_artist(box) class AnchoredSizeBar(AnchoredOffsetbox): @@ -115,11 +82,11 @@ def draw_sizebar(ax): ax.add_artist(asb) -ax = plt.gca() -ax.set_aspect(1.) +fig, ax = plt.subplots() +ax.set_aspect(1) draw_text(ax) -draw_circle(ax) +draw_circles(ax) draw_ellipse(ax) draw_sizebar(ax) diff --git a/examples/misc/coords_report.py b/examples/misc/coords_report.py index 84ce03e09a7f..127ce712fc1e 100644 --- a/examples/misc/coords_report.py +++ b/examples/misc/coords_report.py @@ -3,7 +3,8 @@ Coords Report ============= -Override the default reporting of coords. +Override the default reporting of coords as the mouse moves over the axes +in an interactive backend. """ import matplotlib.pyplot as plt @@ -11,14 +12,14 @@ def millions(x): - return '$%1.1fM' % (x*1e-6) + return '$%1.1fM' % (x * 1e-6) # Fixing random state for reproducibility np.random.seed(19680801) x = np.random.rand(20) -y = 1e7*np.random.rand(20) +y = 1e7 * np.random.rand(20) fig, ax = plt.subplots() ax.fmt_ydata = millions diff --git a/examples/misc/custom_projection.py b/examples/misc/custom_projection.py index 7e5a4b0d405c..c5c13e174087 100644 --- a/examples/misc/custom_projection.py +++ b/examples/misc/custom_projection.py @@ -50,10 +50,10 @@ def _init_axis(self): # Do not register xaxis or yaxis with spines -- as done in # Axes._init_axis() -- until GeoAxes.xaxis.clear() works. # self.spines['geo'].register_axis(self.yaxis) - self._update_transScale() - def cla(self): - super().cla() + def clear(self): + # docstring inherited + super().clear() self.set_longitude_grid(30) self.set_latitude_grid(15) @@ -102,7 +102,7 @@ def _set_lim_and_transforms(self): # 2) The above has an output range that is not in the unit # rectangle, so scale and translate it so it fits correctly # within the axes. The peculiar calculations of xscale and - # yscale are specific to a Aitoff-Hammer projection, so don't + # yscale are specific to an Aitoff-Hammer projection, so don't # worry about them too much. self.transAffine = self._get_affine_transform() @@ -270,14 +270,8 @@ def format_coord(self, lon, lat): In this case, we want them to be displayed in degrees N/S/E/W. """ lon, lat = np.rad2deg([lon, lat]) - if lat >= 0.0: - ns = 'N' - else: - ns = 'S' - if lon >= 0.0: - ew = 'E' - else: - ew = 'W' + ns = 'N' if lat >= 0.0 else 'S' + ew = 'E' if lon >= 0.0 else 'W' return ('%f\N{DEGREE SIGN}%s, %f\N{DEGREE SIGN}%s' % (abs(lat), ns, abs(lon), ew)) @@ -338,17 +332,17 @@ def get_data_ratio(self): # so we override all of the following methods to disable it. def can_zoom(self): """ - Return whether this axes supports the zoom box button functionality. + Return whether this Axes supports the zoom box button functionality. - This axes object does not support interactive zoom box. + This Axes object does not support interactive zoom box. """ return False def can_pan(self): """ - Return whether this axes supports the pan/zoom button functionality. + Return whether this Axes supports the pan/zoom button functionality. - This axes object does not support interactive pan/zoom. + This Axes object does not support interactive pan/zoom. """ return False @@ -430,7 +424,7 @@ def __init__(self, *args, **kwargs): self._longitude_cap = np.pi / 2.0 super().__init__(*args, **kwargs) self.set_aspect(0.5, adjustable='box', anchor='C') - self.cla() + self.clear() def _get_core_transform(self, resolution): return self.HammerTransform(resolution) diff --git a/examples/misc/customize_rc.py b/examples/misc/customize_rc.py index d4149c46dd44..3cf877059151 100644 --- a/examples/misc/customize_rc.py +++ b/examples/misc/customize_rc.py @@ -3,7 +3,7 @@ Customize Rc ============ -I'm not trying to make a good looking figure here, but just to show +I'm not trying to make a good-looking figure here, but just to show some examples of customizing `.rcParams` on the fly. If you like to work interactively, and need to create different sets diff --git a/examples/misc/demo_agg_filter.py b/examples/misc/demo_agg_filter.py index 16f1221563d2..0cdbf81f50b5 100644 --- a/examples/misc/demo_agg_filter.py +++ b/examples/misc/demo_agg_filter.py @@ -7,7 +7,7 @@ rendering. You can modify the rendering of Artists by applying a filter via `.Artist.set_agg_filter`. -.. _Anti-Grain Geometry (AGG): http://antigrain.com +.. _Anti-Grain Geometry (AGG): http://agg.sourceforge.net/antigrain.com """ import matplotlib.cm as cm @@ -19,7 +19,7 @@ def smooth1d(x, window_len): - # copied from http://www.scipy.org/Cookbook/SignalSmooth + # copied from https://scipy-cookbook.readthedocs.io/items/SignalSmooth.html s = np.r_[2*x[0] - x[window_len:1:-1], x, 2*x[-1] - x[-1:-window_len:-1]] w = np.hanning(window_len) y = np.convolve(w/w.sum(), s, mode='same') @@ -38,7 +38,7 @@ class BaseFilter: def get_pad(self, dpi): return 0 - def process_image(padded_src, dpi): + def process_image(self, padded_src, dpi): raise NotImplementedError("Should be overridden by subclasses") def __call__(self, im, dpi): @@ -99,8 +99,19 @@ def process_image(self, padded_src, dpi): class LightFilter(BaseFilter): - - def __init__(self, sigma, fraction=0.5): + """Apply LightSource filter""" + + def __init__(self, sigma, fraction=1): + """ + Parameters + ---------- + sigma : float + sigma for gaussian filter + fraction: number, default: 1 + Increases or decreases the contrast of the hillshade. + See `matplotlib.colors.LightSource` + + """ self.gauss_filter = GaussianFilter(sigma, alpha=1) self.light_source = LightSource() self.fraction = fraction @@ -114,7 +125,8 @@ def process_image(self, padded_src, dpi): rgb = padded_src[:, :, :3] alpha = padded_src[:, :, 3:] rgb2 = self.light_source.shade_rgb(rgb, elevation, - fraction=self.fraction) + fraction=self.fraction, + blend_mode="overlay") return np.concatenate([rgb2, alpha], -1) @@ -213,10 +225,9 @@ def drop_shadow_line(ax): shadow.update_from(l) # offset transform - ot = mtransforms.offset_copy(l.get_transform(), ax.figure, - x=4.0, y=-6.0, units='points') - - shadow.set_transform(ot) + transform = mtransforms.offset_copy(l.get_transform(), ax.figure, + x=4.0, y=-6.0, units='points') + shadow.set_transform(transform) # adjust zorder of the shadow lines so that it is drawn below the # original lines @@ -234,20 +245,19 @@ def drop_shadow_line(ax): def drop_shadow_patches(ax): # Copied from barchart_demo.py N = 5 - men_means = [20, 35, 30, 35, 27] + group1_means = [20, 35, 30, 35, 27] ind = np.arange(N) # the x locations for the groups width = 0.35 # the width of the bars - rects1 = ax.bar(ind, men_means, width, color='r', ec="w", lw=2) + rects1 = ax.bar(ind, group1_means, width, color='r', ec="w", lw=2) - women_means = [25, 32, 34, 20, 25] - rects2 = ax.bar(ind + width + 0.1, women_means, width, + group2_means = [25, 32, 34, 20, 25] + rects2 = ax.bar(ind + width + 0.1, group2_means, width, color='y', ec="w", lw=2) - # gauss = GaussianFilter(1.5, offsets=(1, 1)) - gauss = DropShadowFilter(5, offsets=(1, 1)) - shadow = FilteredArtistList(rects1 + rects2, gauss) + drop = DropShadowFilter(5, offsets=(1, 1)) + shadow = FilteredArtistList(rects1 + rects2, drop) ax.add_artist(shadow) shadow.set_zorder(rects1[0].get_zorder() - 0.1) @@ -259,7 +269,7 @@ def drop_shadow_patches(ax): def light_filter_pie(ax): fracs = [15, 30, 45, 10] - explode = (0, 0.05, 0, 0) + explode = (0.1, 0.2, 0.1, 0.1) pies = ax.pie(fracs, explode=explode) light_filter = LightFilter(9) @@ -269,7 +279,7 @@ def light_filter_pie(ax): p.set(ec="none", lw=2) - gauss = DropShadowFilter(9, offsets=(3, 4), alpha=0.7) + gauss = DropShadowFilter(9, offsets=(3, -4), alpha=0.7) shadow = FilteredArtistList(pies[0], gauss) ax.add_artist(shadow) shadow.set_zorder(pies[0][0].get_zorder() - 0.1) diff --git a/examples/misc/demo_ribbon_box.py b/examples/misc/demo_ribbon_box.py index 9e350182e3dd..ea4fa579e8c5 100644 --- a/examples/misc/demo_ribbon_box.py +++ b/examples/misc/demo_ribbon_box.py @@ -24,7 +24,7 @@ class RibbonBox: nx = original_image.shape[1] def __init__(self, color): - rgb = mcolors.to_rgba(color)[:3] + rgb = mcolors.to_rgb(color) self.im = np.dstack( [self.b_and_h - self.color * (1 - np.array(rgb)), self.alpha]) diff --git a/examples/pyplots/fig_x.py b/examples/misc/fig_x.py similarity index 93% rename from examples/pyplots/fig_x.py rename to examples/misc/fig_x.py index 9ca5a3b13d8a..eaa16d80fa35 100644 --- a/examples/pyplots/fig_x.py +++ b/examples/misc/fig_x.py @@ -4,6 +4,8 @@ ======================= Adding lines to a figure without any axes. + +.. redirect-from:: /gallery/pyplots/fig_x """ import matplotlib.pyplot as plt diff --git a/examples/misc/histogram_path.py b/examples/misc/histogram_path.py index 8cbb64977623..e45f990e2053 100644 --- a/examples/misc/histogram_path.py +++ b/examples/misc/histogram_path.py @@ -4,14 +4,14 @@ ======================================================== Using a path patch to draw rectangles. -The technique of using lots of Rectangle instances, or -the faster method of using PolyCollections, were implemented before we -had proper paths with moveto/lineto, closepoly etc in mpl. Now that -we have them, we can draw collections of regularly shaped objects with -homogeneous properties more efficiently with a PathCollection. This -example makes a histogram -- it's more work to set up the vertex arrays -at the outset, but it should be much faster for large numbers of -objects. + +The technique of using lots of `.Rectangle` instances, or the faster method of +using `.PolyCollection`, were implemented before we had proper paths with +moveto, lineto, closepoly, etc. in Matplotlib. Now that we have them, we can +draw collections of regularly shaped objects with homogeneous properties more +efficiently with a PathCollection. This example makes a histogram -- it's more +work to set up the vertex arrays at the outset, but it should be much faster +for large numbers of objects. """ import numpy as np @@ -19,14 +19,11 @@ import matplotlib.patches as patches import matplotlib.path as path -fig, ax = plt.subplots() - -# Fixing random state for reproducibility -np.random.seed(19680801) +fig, axs = plt.subplots(2) +np.random.seed(19680801) # Fixing random state for reproducibility # histogram our data with numpy - data = np.random.randn(1000) n, bins = np.histogram(data, 50) @@ -36,7 +33,6 @@ bottom = np.zeros(len(left)) top = bottom + n - # we need a (numrects x numsides x 2) numpy array for the path helper # function to build a compound path XY = np.array([[left, left, right, right], [bottom, top, top, bottom]]).T @@ -44,20 +40,16 @@ # get the Path object barpath = path.Path.make_compound_path_from_polys(XY) -# make a patch out of it +# make a patch out of it, don't add a margin at y=0 patch = patches.PathPatch(barpath) -ax.add_patch(patch) - -# update the view limits -ax.set_xlim(left[0], right[-1]) -ax.set_ylim(bottom.min(), top.max()) - -plt.show() +patch.sticky_edges.y[:] = [0] +axs[0].add_patch(patch) +axs[0].autoscale_view() ############################################################################# -# It should be noted that instead of creating a three-dimensional array and -# using `~.path.Path.make_compound_path_from_polys`, we could as well create -# the compound path directly using vertices and codes as shown below +# Instead of creating a three-dimensional array and using +# `~.path.Path.make_compound_path_from_polys`, we could as well create the +# compound path directly using vertices and codes as shown below nrects = len(left) nverts = nrects*(1+3+1) @@ -76,6 +68,14 @@ barpath = path.Path(verts, codes) +# make a patch out of it, don't add a margin at y=0 +patch = patches.PathPatch(barpath) +patch.sticky_edges.y[:] = [0] +axs[1].add_patch(patch) +axs[1].autoscale_view() + +plt.show() + ############################################################################# # # .. admonition:: References diff --git a/examples/misc/hyperlinks_sgskip.py b/examples/misc/hyperlinks_sgskip.py index 7f9cade91a29..2d0f6819cfb8 100644 --- a/examples/misc/hyperlinks_sgskip.py +++ b/examples/misc/hyperlinks_sgskip.py @@ -18,7 +18,7 @@ fig = plt.figure() s = plt.scatter([1, 2, 3], [4, 5, 6]) -s.set_urls(['http://www.bbc.co.uk/news', 'http://www.google.com', None]) +s.set_urls(['https://www.bbc.com/news', 'https://www.google.com/', None]) fig.savefig('scatter.svg') ############################################################################### @@ -34,5 +34,5 @@ im = plt.imshow(Z, interpolation='bilinear', cmap=cm.gray, origin='lower', extent=[-3, 3, -3, 3]) -im.set_url('http://www.google.com') +im.set_url('https://www.google.com/') fig.savefig('image.svg') diff --git a/examples/misc/image_thumbnail_sgskip.py b/examples/misc/image_thumbnail_sgskip.py index 97a9e9a627ea..68e3ba85ed38 100644 --- a/examples/misc/image_thumbnail_sgskip.py +++ b/examples/misc/image_thumbnail_sgskip.py @@ -20,7 +20,7 @@ description="Build thumbnails of all images in a directory.") parser.add_argument("imagedir", type=Path) args = parser.parse_args() -if not args.imagedir.isdir(): +if not args.imagedir.is_dir(): sys.exit(f"Could not find input directory {args.imagedir}") outdir = Path("thumbs") diff --git a/examples/misc/keyword_plotting.py b/examples/misc/keyword_plotting.py index 1d8e2c675148..7c8182330aed 100644 --- a/examples/misc/keyword_plotting.py +++ b/examples/misc/keyword_plotting.py @@ -7,8 +7,8 @@ access particular variables with strings: for example, with `numpy.recarray` or `pandas.DataFrame`. -Matplotlib allows you provide such an object with the ``data`` keyword -argument. If provided, then you may generate plots with the strings +Matplotlib allows you to provide such an object with the ``data`` keyword +argument. If provided, you may generate plots with the strings corresponding to these variables. """ diff --git a/examples/misc/load_converter.py b/examples/misc/load_converter.py deleted file mode 100644 index 793de7dc9264..000000000000 --- a/examples/misc/load_converter.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -============== -Load converter -============== - -This example demonstrates passing a custom converter to `numpy.genfromtxt` to -extract dates from a CSV file. -""" - -import dateutil.parser -from matplotlib import cbook -import matplotlib.pyplot as plt -import numpy as np - - -datafile = cbook.get_sample_data('msft.csv', asfileobj=False) -print('loading', datafile) - -data = np.genfromtxt( - datafile, delimiter=',', names=True, - dtype=None, converters={0: dateutil.parser.parse}) - -fig, ax = plt.subplots() -ax.plot(data['Date'], data['High'], '-') -fig.autofmt_xdate() -plt.show() diff --git a/examples/misc/logos2.py b/examples/misc/logos2.py index 528f09e92c18..8febb8b57d60 100644 --- a/examples/misc/logos2.py +++ b/examples/misc/logos2.py @@ -11,7 +11,7 @@ import matplotlib.cm as cm import matplotlib.font_manager from matplotlib.patches import Rectangle, PathPatch -from matplotlib.textpath import TextPath +from matplotlib.text import TextPath import matplotlib.transforms as mtrans MPL_BLUE = '#11557c' @@ -89,7 +89,7 @@ def create_icon_axes(fig, ax_position, lw_bars, lw_grid, lw_border, rgrid): def create_text_axes(fig, height_px): - """Create an axes in *fig* that contains 'matplotlib' as Text.""" + """Create an Axes in *fig* that contains 'matplotlib' as Text.""" ax = fig.add_axes((0, 0, 1, 1)) ax.set_aspect("equal") ax.set_axis_off() diff --git a/examples/misc/multiprocess_sgskip.py b/examples/misc/multiprocess_sgskip.py index d9c0ca9769d1..0ab57e6e3b7c 100644 --- a/examples/misc/multiprocess_sgskip.py +++ b/examples/misc/multiprocess_sgskip.py @@ -1,7 +1,7 @@ """ -============ -Multiprocess -============ +=============== +Multiprocessing +=============== Demo of using multiprocessing for generating data in one process and plotting in another. @@ -94,7 +94,7 @@ def plot(self, finished=False): def main(): pl = NBPlot() - for ii in range(10): + for _ in range(10): pl.plot() time.sleep(0.5) pl.plot(finished=True) diff --git a/examples/misc/pythonic_matplotlib.py b/examples/misc/pythonic_matplotlib.py deleted file mode 100644 index b04d931264f0..000000000000 --- a/examples/misc/pythonic_matplotlib.py +++ /dev/null @@ -1,79 +0,0 @@ -""" -=================== -Pythonic Matplotlib -=================== - -Some people prefer to write more pythonic, object-oriented code -rather than use the pyplot interface to matplotlib. This example shows -you how. - -Unless you are an application developer, I recommend using part of the -pyplot interface, particularly the figure, close, subplot, axes, and -show commands. These hide a lot of complexity from you that you don't -need to see in normal figure creation, like instantiating DPI -instances, managing the bounding boxes of the figure elements, -creating and realizing GUI windows and embedding figures in them. - -If you are an application developer and want to embed matplotlib in -your application, follow the lead of examples/embedding_in_wx.py, -examples/embedding_in_gtk.py or examples/embedding_in_tk.py. In this -case you will want to control the creation of all your figures, -embedding them in application windows, etc. - -If you are a web application developer, you may want to use the -example in webapp_demo.py, which shows how to use the backend agg -figure canvas directly, with none of the globals (current figure, -current axes) that are present in the pyplot interface. Note that -there is no reason why the pyplot interface won't work for web -application developers, however. - -If you see an example in the examples dir written in pyplot interface, -and you want to emulate that using the true python method calls, there -is an easy mapping. Many of those examples use 'set' to control -figure properties. Here's how to map those commands onto instance -methods - -The syntax of set is:: - - plt.setp(object or sequence, somestring, attribute) - -if called with an object, set calls:: - - object.set_somestring(attribute) - -if called with a sequence, set does:: - - for object in sequence: - object.set_somestring(attribute) - -So for your example, if a is your axes object, you can do:: - - a.set_xticklabels([]) - a.set_yticklabels([]) - a.set_xticks([]) - a.set_yticks([]) -""" - -import matplotlib.pyplot as plt -import numpy as np - -t = np.arange(0.0, 1.0, 0.01) - -fig, (ax1, ax2) = plt.subplots(2) - -ax1.plot(t, np.sin(2*np.pi * t)) -ax1.grid(True) -ax1.set_ylim((-2, 2)) -ax1.set_ylabel('1 Hz') -ax1.set_title('A sine wave or two') - -ax1.xaxis.set_tick_params(labelcolor='r') - -ax2.plot(t, np.sin(2 * 2*np.pi * t)) -ax2.grid(True) -ax2.set_ylim((-2, 2)) -l = ax2.set_xlabel('Hi mom') -l.set_color('g') -l.set_fontsize('large') - -plt.show() diff --git a/examples/misc/rasterization_demo.py b/examples/misc/rasterization_demo.py index 824b6ffb3c97..4030587b693a 100644 --- a/examples/misc/rasterization_demo.py +++ b/examples/misc/rasterization_demo.py @@ -44,7 +44,7 @@ xx = x*np.cos(theta) - y*np.sin(theta) # rotate x by -theta yy = x*np.sin(theta) + y*np.cos(theta) # rotate y by -theta -fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, constrained_layout=True) +fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, layout="constrained") # pcolormesh without rasterization ax1.set_aspect(1) @@ -54,7 +54,7 @@ # pcolormesh with rasterization; enabled by keyword argument ax2.set_aspect(1) ax2.set_title("Rasterization") -m = ax2.pcolormesh(xx, yy, d, rasterized=True) +ax2.pcolormesh(xx, yy, d, rasterized=True) # pcolormesh with an overlaid text without rasterization ax3.set_aspect(1) diff --git a/examples/misc/svg_filter_line.py b/examples/misc/svg_filter_line.py index d329e6f243b1..da67250890df 100644 --- a/examples/misc/svg_filter_line.py +++ b/examples/misc/svg_filter_line.py @@ -3,12 +3,14 @@ SVG Filter Line =============== -Demonstrate SVG filtering effects which might be used with mpl. +Demonstrate SVG filtering effects which might be used with Matplotlib. -Note that the filtering effects are only effective if your svg renderer +Note that the filtering effects are only effective if your SVG renderer support it. """ +import io +import xml.etree.ElementTree as ET import matplotlib.pyplot as plt import matplotlib.transforms as mtransforms @@ -39,10 +41,9 @@ shadow.set_zorder(l.get_zorder() - 0.5) # offset transform - ot = mtransforms.offset_copy(l.get_transform(), fig1, - x=4.0, y=-6.0, units='points') - - shadow.set_transform(ot) + transform = mtransforms.offset_copy(l.get_transform(), fig1, + x=4.0, y=-6.0, units='points') + shadow.set_transform(transform) # set the id for a later use shadow.set_gid(l.get_label() + "_shadow") @@ -52,13 +53,10 @@ ax.set_ylim(0., 1.) # save the figure as a bytes string in the svg format. -from io import BytesIO -f = BytesIO() +f = io.BytesIO() plt.savefig(f, format="svg") -import xml.etree.ElementTree as ET - # filter definition for a gaussian blur filter_def = """ `. - -Note that a similar result would be achieved using `~.Figure.tight_layout` -or `~.Figure.set_constrained_layout`; this example shows how one could -customize the subplot parameter adjustment. -""" - -import matplotlib.pyplot as plt -import matplotlib.transforms as mtransforms - -fig, ax = plt.subplots() -ax.plot(range(10)) -ax.set_yticks((2, 5, 7)) -labels = ax.set_yticklabels(('really, really, really', 'long', 'labels')) - -def on_draw(event): - bboxes = [] - for label in labels: - bbox = label.get_window_extent() - # the figure transform goes from relative coords->pixels and we - # want the inverse of that - bboxi = bbox.transformed(fig.transFigure.inverted()) - bboxes.append(bboxi) - # the bbox that bounds all the bboxes, again in relative figure coords - bbox = mtransforms.Bbox.union(bboxes) - if fig.subplotpars.left < bbox.width: - # we need to move it over - fig.subplots_adjust(left=1.1*bbox.width) # pad a little - fig.canvas.draw() - -fig.canvas.mpl_connect('draw_event', on_draw) - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.artist.Artist.get_window_extent` -# - `matplotlib.transforms.Bbox` -# - `matplotlib.transforms.BboxBase.transformed` -# - `matplotlib.transforms.BboxBase.union` -# - `matplotlib.transforms.Transform.inverted` -# - `matplotlib.figure.Figure.subplots_adjust` -# - `matplotlib.figure.SubplotParams` -# - `matplotlib.backend_bases.FigureCanvasBase.mpl_connect` diff --git a/examples/pyplots/axline.py b/examples/pyplots/axline.py index 68149eaafc4f..5da9dc1a68ef 100644 --- a/examples/pyplots/axline.py +++ b/examples/pyplots/axline.py @@ -10,6 +10,7 @@ `~.axes.Axes.axline` draws infinite straight lines in arbitrary directions. """ + import numpy as np import matplotlib.pyplot as plt diff --git a/examples/pyplots/boxplot_demo_pyplot.py b/examples/pyplots/boxplot_demo_pyplot.py deleted file mode 100644 index 501eb2cf3447..000000000000 --- a/examples/pyplots/boxplot_demo_pyplot.py +++ /dev/null @@ -1,89 +0,0 @@ -""" -============ -Boxplot Demo -============ - -Example boxplot code -""" - -import numpy as np -import matplotlib.pyplot as plt - -# Fixing random state for reproducibility -np.random.seed(19680801) - -# fake up some data -spread = np.random.rand(50) * 100 -center = np.ones(25) * 50 -flier_high = np.random.rand(10) * 100 + 100 -flier_low = np.random.rand(10) * -100 -data = np.concatenate((spread, center, flier_high, flier_low)) - -############################################################################### - -fig1, ax1 = plt.subplots() -ax1.set_title('Basic Plot') -ax1.boxplot(data) - -############################################################################### - -fig2, ax2 = plt.subplots() -ax2.set_title('Notched boxes') -ax2.boxplot(data, notch=True) - -############################################################################### - -green_diamond = dict(markerfacecolor='g', marker='D') -fig3, ax3 = plt.subplots() -ax3.set_title('Changed Outlier Symbols') -ax3.boxplot(data, flierprops=green_diamond) - -############################################################################### - -fig4, ax4 = plt.subplots() -ax4.set_title('Hide Outlier Points') -ax4.boxplot(data, showfliers=False) - -############################################################################### - -red_square = dict(markerfacecolor='r', marker='s') -fig5, ax5 = plt.subplots() -ax5.set_title('Horizontal Boxes') -ax5.boxplot(data, vert=False, flierprops=red_square) - -############################################################################### - -fig6, ax6 = plt.subplots() -ax6.set_title('Shorter Whisker Length') -ax6.boxplot(data, flierprops=red_square, vert=False, whis=0.75) - -############################################################################### -# Fake up some more data - -spread = np.random.rand(50) * 100 -center = np.ones(25) * 40 -flier_high = np.random.rand(10) * 100 + 100 -flier_low = np.random.rand(10) * -100 -d2 = np.concatenate((spread, center, flier_high, flier_low)) - -############################################################################### -# Making a 2-D array only works if all the columns are the -# same length. If they are not, then use a list instead. -# This is actually more efficient because boxplot converts -# a 2-D array into a list of vectors internally anyway. - -data = [data, d2, d2[::2]] -fig7, ax7 = plt.subplots() -ax7.set_title('Multiple Samples with Different sizes') -ax7.boxplot(data) - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.boxplot` / `matplotlib.pyplot.boxplot` diff --git a/examples/pyplots/fig_axes_labels_simple.py b/examples/pyplots/fig_axes_labels_simple.py deleted file mode 100644 index 41423b0b4845..000000000000 --- a/examples/pyplots/fig_axes_labels_simple.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -================== -Simple axes labels -================== - -Label the axes of a plot. -""" -import numpy as np -import matplotlib.pyplot as plt - -fig = plt.figure() -fig.subplots_adjust(top=0.8) -ax1 = fig.add_subplot(211) -ax1.set_ylabel('volts') -ax1.set_title('a sine wave') - -t = np.arange(0.0, 1.0, 0.01) -s = np.sin(2 * np.pi * t) -line, = ax1.plot(t, s, lw=2) - -# Fixing random state for reproducibility -np.random.seed(19680801) - -ax2 = fig.add_axes([0.15, 0.1, 0.7, 0.3]) -n, bins, patches = ax2.hist(np.random.randn(1000), 50) -ax2.set_xlabel('time (s)') - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.set_xlabel` -# - `matplotlib.axes.Axes.set_ylabel` -# - `matplotlib.axes.Axes.set_title` -# - `matplotlib.axes.Axes.plot` -# - `matplotlib.axes.Axes.hist` -# - `matplotlib.figure.Figure.add_axes` diff --git a/examples/pyplots/pyplot_formatstr.py b/examples/pyplots/pyplot_formatstr.py deleted file mode 100644 index e9be3830ec98..000000000000 --- a/examples/pyplots/pyplot_formatstr.py +++ /dev/null @@ -1,21 +0,0 @@ -""" -==================== -plot() format string -==================== - -Use a format string (here, 'ro') to set the color and markers of a -`~matplotlib.axes.Axes.plot`. -""" - -import matplotlib.pyplot as plt -plt.plot([1, 2, 3, 4], [1, 4, 9, 16], 'ro') -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.plot` / `matplotlib.pyplot.plot` diff --git a/examples/pyplots/pyplot_mathtext.py b/examples/pyplots/pyplot_mathtext.py deleted file mode 100644 index af4db39a4e95..000000000000 --- a/examples/pyplots/pyplot_mathtext.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -=============== -Pyplot Mathtext -=============== - -Use mathematical expressions in text labels. For an overview over MathText -see :doc:`/tutorials/text/mathtext`. -""" -import numpy as np -import matplotlib.pyplot as plt -t = np.arange(0.0, 2.0, 0.01) -s = np.sin(2*np.pi*t) - -plt.plot(t, s) -plt.title(r'$\alpha_i > \beta_i$', fontsize=20) -plt.text(1, -0.6, r'$\sum_{i=0}^\infty x_i$', fontsize=20) -plt.text(0.6, 0.6, r'$\mathcal{A}\mathrm{sin}(2 \omega t)$', - fontsize=20) -plt.xlabel('time (s)') -plt.ylabel('volts (mV)') -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.text` / `matplotlib.pyplot.text` diff --git a/examples/pyplots/pyplot_simple.py b/examples/pyplots/pyplot_simple.py index 7ecdcd406b21..096323b626dc 100644 --- a/examples/pyplots/pyplot_simple.py +++ b/examples/pyplots/pyplot_simple.py @@ -1,14 +1,19 @@ """ -============= -Pyplot Simple -============= +=========== +Simple plot +=========== -A very simple pyplot where a list of numbers are ploted against their -index. Creates a straight line due to the rate of change being 1 for -both the X and Y axis. +A simple plot where a list of numbers are plotted against their index, +resulting in a straight line. Use a format string (here, 'o-r') to set the +markers (circles), linestyle (solid line) and color (red). + +.. redirect-from:: /gallery/pyplots/fig_axes_labels_simple +.. redirect-from:: /gallery/pyplots/pyplot_formatstr """ + import matplotlib.pyplot as plt -plt.plot([1, 2, 3, 4]) + +plt.plot([1, 2, 3, 4], 'o-r') plt.ylabel('some numbers') plt.show() diff --git a/examples/pyplots/pyplot_text.py b/examples/pyplots/pyplot_text.py index e50e9038827a..00e738ef414b 100644 --- a/examples/pyplots/pyplot_text.py +++ b/examples/pyplots/pyplot_text.py @@ -1,29 +1,29 @@ """ -=========== -Pyplot Text -=========== +============================== +Text and mathtext using pyplot +============================== -""" -import numpy as np -import matplotlib.pyplot as plt +Set the special text objects `~.pyplot.title`, `~.pyplot.xlabel`, and +`~.pyplot.ylabel` through the dedicated pyplot functions. Additional text +objects can be placed in the axes using `~.pyplot.text`. -# Fixing random state for reproducibility -np.random.seed(19680801) +You can use TeX-like mathematical typesetting in all texts; see also +:doc:`/tutorials/text/mathtext`. -mu, sigma = 100, 15 -x = mu + sigma * np.random.randn(10000) +.. redirect-from:: /gallery/pyplots/pyplot_mathtext +""" -# the histogram of the data -n, bins, patches = plt.hist(x, 50, density=True, facecolor='g', alpha=0.75) +import numpy as np +import matplotlib.pyplot as plt +t = np.arange(0.0, 2.0, 0.01) +s = np.sin(2*np.pi*t) -plt.xlabel('Smarts') -plt.ylabel('Probability') -plt.title('Histogram of IQ') -plt.text(60, .025, r'$\mu=100,\ \sigma=15$') -plt.xlim(40, 160) -plt.ylim(0, 0.03) -plt.grid(True) +plt.plot(t, s) +plt.text(0, -1, r'Hello, world!', fontsize=15) +plt.title(r'$\mathcal{A}\sin(\omega t)$', fontsize=20) +plt.xlabel('Time [s]') +plt.ylabel('Voltage [mV]') plt.show() ############################################################################# diff --git a/examples/pyplots/pyplot_three.py b/examples/pyplots/pyplot_three.py index 476796b45f81..7dfcddb7bb94 100644 --- a/examples/pyplots/pyplot_three.py +++ b/examples/pyplots/pyplot_three.py @@ -1,10 +1,11 @@ """ -============ -Pyplot Three -============ +=========================== +Multiple lines using pyplot +=========================== -Plot three line plots in a single call to `~matplotlib.pyplot.plot`. +Plot three datasets with a single call to `~matplotlib.pyplot.plot`. """ + import numpy as np import matplotlib.pyplot as plt diff --git a/examples/pyplots/pyplot_two_subplots.py b/examples/pyplots/pyplot_two_subplots.py index 5280fb0bb37a..d316da2fc930 100644 --- a/examples/pyplots/pyplot_two_subplots.py +++ b/examples/pyplots/pyplot_two_subplots.py @@ -1,10 +1,11 @@ """ -=================== -Pyplot Two Subplots -=================== +========================= +Two subplots using pyplot +========================= -Create a figure with two subplots with `.pyplot.subplot`. +Create a figure with two subplots using `.pyplot.subplot`. """ + import numpy as np import matplotlib.pyplot as plt diff --git a/examples/pyplots/text_layout.py b/examples/pyplots/text_layout.py deleted file mode 100644 index 641aa77320a5..000000000000 --- a/examples/pyplots/text_layout.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -=========== -Text Layout -=========== - -Create text with different alignment and rotation. -""" - -import matplotlib.pyplot as plt -import matplotlib.patches as patches - -fig = plt.figure() - -left, width = .25, .5 -bottom, height = .25, .5 -right = left + width -top = bottom + height - -# Draw a rectangle in figure coordinates ((0, 0) is bottom left and (1, 1) is -# upper right). -p = patches.Rectangle((left, bottom), width, height, fill=False) -fig.add_artist(p) - -# Figure.text (aka. plt.figtext) behaves like Axes.text (aka. plt.text), with -# the sole exception that the coordinates are relative to the figure ((0, 0) is -# bottom left and (1, 1) is upper right). -fig.text(left, bottom, 'left top', - horizontalalignment='left', verticalalignment='top') -fig.text(left, bottom, 'left bottom', - horizontalalignment='left', verticalalignment='bottom') -fig.text(right, top, 'right bottom', - horizontalalignment='right', verticalalignment='bottom') -fig.text(right, top, 'right top', - horizontalalignment='right', verticalalignment='top') -fig.text(right, bottom, 'center top', - horizontalalignment='center', verticalalignment='top') -fig.text(left, 0.5*(bottom+top), 'right center', - horizontalalignment='right', verticalalignment='center', - rotation='vertical') -fig.text(left, 0.5*(bottom+top), 'left center', - horizontalalignment='left', verticalalignment='center', - rotation='vertical') -fig.text(0.5*(left+right), 0.5*(bottom+top), 'middle', - horizontalalignment='center', verticalalignment='center', - fontsize=20, color='red') -fig.text(right, 0.5*(bottom+top), 'centered', - horizontalalignment='center', verticalalignment='center', - rotation='vertical') -fig.text(left, top, 'rotated\nwith newlines', - horizontalalignment='center', verticalalignment='center', - rotation=45) - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.figure.Figure.add_artist` -# - `matplotlib.figure.Figure.text` diff --git a/examples/pyplots/whats_new_1_subplot3d.py b/examples/pyplots/whats_new_1_subplot3d.py deleted file mode 100644 index 69c8036a6e74..000000000000 --- a/examples/pyplots/whats_new_1_subplot3d.py +++ /dev/null @@ -1,48 +0,0 @@ -""" -====================== -What's New 1 Subplot3d -====================== - -Create two three-dimensional plots in the same figure. -""" - -from matplotlib import cm -#from matplotlib.ticker import LinearLocator, FixedLocator, FormatStrFormatter -import matplotlib.pyplot as plt -import numpy as np - -fig = plt.figure() - -ax = fig.add_subplot(1, 2, 1, projection='3d') -X = np.arange(-5, 5, 0.25) -Y = np.arange(-5, 5, 0.25) -X, Y = np.meshgrid(X, Y) -R = np.sqrt(X**2 + Y**2) -Z = np.sin(R) -surf = ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.viridis, - linewidth=0, antialiased=False) -ax.set_zlim3d(-1.01, 1.01) - -#ax.zaxis.set_major_locator(LinearLocator(10)) -#ax.zaxis.set_major_formatter(FormatStrFormatter('%.03f')) - -fig.colorbar(surf, shrink=0.5, aspect=5) - -from mpl_toolkits.mplot3d.axes3d import get_test_data -ax = fig.add_subplot(1, 2, 2, projection='3d') -X, Y, Z = get_test_data(0.05) -ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.figure.Figure.add_subplot` -# - `mpl_toolkits.mplot3d.axes3d.Axes3D.plot_surface` -# - `mpl_toolkits.mplot3d.axes3d.Axes3D.plot_wireframe` -# - `mpl_toolkits.mplot3d.axes3d.Axes3D.set_zlim3d` diff --git a/examples/pyplots/whats_new_98_4_fill_between.py b/examples/pyplots/whats_new_98_4_fill_between.py deleted file mode 100644 index 4a61d7b8a0ab..000000000000 --- a/examples/pyplots/whats_new_98_4_fill_between.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -============ -Fill Between -============ - -Fill the area between two curves. -""" -import matplotlib.pyplot as plt -import numpy as np - -x = np.arange(-5, 5, 0.01) -y1 = -5*x*x + x + 10 -y2 = 5*x*x + x - -fig, ax = plt.subplots() -ax.plot(x, y1, x, y2, color='black') -ax.fill_between(x, y1, y2, where=(y2 > y1), facecolor='yellow', alpha=0.5) -ax.fill_between(x, y1, y2, where=(y2 <= y1), facecolor='red', alpha=0.5) -ax.set_title('Fill Between') - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.fill_between` / `matplotlib.pyplot.fill_between` diff --git a/examples/pyplots/whats_new_98_4_legend.py b/examples/pyplots/whats_new_98_4_legend.py deleted file mode 100644 index 01e7e940fc4d..000000000000 --- a/examples/pyplots/whats_new_98_4_legend.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -======================== -What's New 0.98.4 Legend -======================== - -Create a legend and tweak it with a shadow and a box. -""" -import matplotlib.pyplot as plt -import numpy as np - - -ax = plt.subplot() -t1 = np.arange(0.0, 1.0, 0.01) -for n in [1, 2, 3, 4]: - plt.plot(t1, t1**n, label=f"n={n}") - -leg = plt.legend(loc='best', ncol=2, mode="expand", shadow=True, fancybox=True) -leg.get_frame().set_alpha(0.5) - - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axes.Axes.legend` / `matplotlib.pyplot.legend` -# - `matplotlib.legend.Legend` -# - `matplotlib.legend.Legend.get_frame` diff --git a/examples/pyplots/whats_new_99_axes_grid.py b/examples/pyplots/whats_new_99_axes_grid.py deleted file mode 100644 index fe55e37dad67..000000000000 --- a/examples/pyplots/whats_new_99_axes_grid.py +++ /dev/null @@ -1,60 +0,0 @@ -""" -========================= -What's New 0.99 Axes Grid -========================= - -Create RGB composite images. -""" -import numpy as np -import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1.axes_rgb import RGBAxes - - -def get_demo_image(): - # prepare image - delta = 0.5 - - extent = (-3, 4, -4, 3) - x = np.arange(-3.0, 4.001, delta) - y = np.arange(-4.0, 3.001, delta) - X, Y = np.meshgrid(x, y) - Z1 = np.exp(-X**2 - Y**2) - Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) - Z = (Z1 - Z2) * 2 - - return Z, extent - - -def get_rgb(): - Z, extent = get_demo_image() - - Z[Z < 0] = 0. - Z = Z / Z.max() - - R = Z[:13, :13] - G = Z[2:, 2:] - B = Z[:13, 2:] - - return R, G, B - - -fig = plt.figure() -ax = RGBAxes(fig, [0.1, 0.1, 0.8, 0.8]) - -r, g, b = get_rgb() -ax.imshow_rgb(r, g, b, origin="lower") - -ax.RGB.set_xlim(0., 9.5) -ax.RGB.set_ylim(0.9, 10.6) - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `mpl_toolkits.axes_grid1.axes_rgb.RGBAxes` -# - `mpl_toolkits.axes_grid1.axes_rgb.RGBAxes.imshow_rgb` diff --git a/examples/pyplots/whats_new_99_mplot3d.py b/examples/pyplots/whats_new_99_mplot3d.py deleted file mode 100644 index cfc0cc484638..000000000000 --- a/examples/pyplots/whats_new_99_mplot3d.py +++ /dev/null @@ -1,34 +0,0 @@ -""" -======================= -What's New 0.99 Mplot3d -======================= - -Create a 3D surface plot. -""" -import numpy as np -import matplotlib.pyplot as plt -from matplotlib import cm -from mpl_toolkits.mplot3d import Axes3D - -X = np.arange(-5, 5, 0.25) -Y = np.arange(-5, 5, 0.25) -X, Y = np.meshgrid(X, Y) -R = np.sqrt(X**2 + Y**2) -Z = np.sin(R) - -fig = plt.figure() -ax = Axes3D(fig, auto_add_to_figure=False) -fig.add_axes(ax) -ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap=cm.viridis) - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `mpl_toolkits.mplot3d.axes3d.Axes3D` -# - `mpl_toolkits.mplot3d.axes3d.Axes3D.plot_surface` diff --git a/examples/pyplots/whats_new_99_spines.py b/examples/pyplots/whats_new_99_spines.py deleted file mode 100644 index 33cf07223d2a..000000000000 --- a/examples/pyplots/whats_new_99_spines.py +++ /dev/null @@ -1,67 +0,0 @@ -""" -====================== -What's New 0.99 Spines -====================== - -""" -import matplotlib.pyplot as plt -import numpy as np - - -def adjust_spines(ax, spines): - for loc, spine in ax.spines.items(): - if loc in spines: - spine.set_position(('outward', 10)) # outward by 10 points - else: - spine.set_color('none') # don't draw spine - - # turn off ticks where there is no spine - if 'left' in spines: - ax.yaxis.set_ticks_position('left') - else: - # no yaxis ticks - ax.yaxis.set_ticks([]) - - if 'bottom' in spines: - ax.xaxis.set_ticks_position('bottom') - else: - # no xaxis ticks - ax.xaxis.set_ticks([]) - -fig = plt.figure() - -x = np.linspace(0, 2*np.pi, 100) -y = 2*np.sin(x) - -ax = fig.add_subplot(2, 2, 1) -ax.plot(x, y) -adjust_spines(ax, ['left']) - -ax = fig.add_subplot(2, 2, 2) -ax.plot(x, y) -adjust_spines(ax, []) - -ax = fig.add_subplot(2, 2, 3) -ax.plot(x, y) -adjust_spines(ax, ['left', 'bottom']) - -ax = fig.add_subplot(2, 2, 4) -ax.plot(x, y) -adjust_spines(ax, ['bottom']) - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.axis.Axis.set_ticks` -# - `matplotlib.axis.XAxis.set_ticks_position` -# - `matplotlib.axis.YAxis.set_ticks_position` -# - `matplotlib.spines` -# - `matplotlib.spines.Spine` -# - `matplotlib.spines.Spine.set_color` -# - `matplotlib.spines.Spine.set_position` diff --git a/examples/scales/asinh_demo.py b/examples/scales/asinh_demo.py new file mode 100644 index 000000000000..854e634a640f --- /dev/null +++ b/examples/scales/asinh_demo.py @@ -0,0 +1,109 @@ +""" +============ +Asinh Demo +============ + +Illustration of the `asinh <.scale.AsinhScale>` axis scaling, +which uses the transformation + +.. math:: + + a \\rightarrow a_0 \\sinh^{-1} (a / a_0) + +For coordinate values close to zero (i.e. much smaller than +the "linear width" :math:`a_0`), this leaves values essentially unchanged: + +.. math:: + + a \\rightarrow a + \\mathcal{O}(a^3) + +but for larger values (i.e. :math:`|a| \\gg a_0`, this is asymptotically + +.. math:: + + a \\rightarrow a_0 \\, \\mathrm{sgn}(a) \\ln |a| + \\mathcal{O}(1) + +As with the `symlog <.scale.SymmetricalLogScale>` scaling, +this allows one to plot quantities +that cover a very wide dynamic range that includes both positive +and negative values. However, ``symlog`` involves a transformation +that has discontinuities in its gradient because it is built +from *separate* linear and logarithmic transformations. +The ``asinh`` scaling uses a transformation that is smooth +for all (finite) values, which is both mathematically cleaner +and reduces visual artifacts associated with an abrupt +transition between linear and logarithmic regions of the plot. + +.. note:: + `.scale.AsinhScale` is experimental, and the API may change. + +See `~.scale.AsinhScale`, `~.scale.SymmetricalLogScale`. +""" + +import numpy as np +import matplotlib.pyplot as plt + +# Prepare sample values for variations on y=x graph: +x = np.linspace(-3, 6, 500) + +############################################################################### +# Compare "symlog" and "asinh" behaviour on sample y=x graph, +# where there is a discontinuous gradient in "symlog" near y=2: +fig1 = plt.figure() +ax0, ax1 = fig1.subplots(1, 2, sharex=True) + +ax0.plot(x, x) +ax0.set_yscale('symlog') +ax0.grid() +ax0.set_title('symlog') + +ax1.plot(x, x) +ax1.set_yscale('asinh') +ax1.grid() +ax1.set_title('asinh') + + +############################################################################### +# Compare "asinh" graphs with different scale parameter "linear_width": +fig2 = plt.figure(layout='constrained') +axs = fig2.subplots(1, 3, sharex=True) +for ax, (a0, base) in zip(axs, ((0.2, 2), (1.0, 0), (5.0, 10))): + ax.set_title(f'linear_width={a0:.3g}') + ax.plot(x, x, label='y=x') + ax.plot(x, 10*x, label='y=10x') + ax.plot(x, 100*x, label='y=100x') + ax.set_yscale('asinh', linear_width=a0, base=base) + ax.grid() + ax.legend(loc='best', fontsize='small') + + +############################################################################### +# Compare "symlog" and "asinh" scalings +# on 2D Cauchy-distributed random numbers, +# where one may be able to see more subtle artifacts near y=2 +# due to the gradient-discontinuity in "symlog": +fig3 = plt.figure() +ax = fig3.subplots(1, 1) +r = 3 * np.tan(np.random.uniform(-np.pi / 2.02, np.pi / 2.02, + size=(5000,))) +th = np.random.uniform(0, 2*np.pi, size=r.shape) + +ax.scatter(r * np.cos(th), r * np.sin(th), s=4, alpha=0.5) +ax.set_xscale('asinh') +ax.set_yscale('symlog') +ax.set_xlabel('asinh') +ax.set_ylabel('symlog') +ax.set_title('2D Cauchy random deviates') +ax.set_xlim(-50, 50) +ax.set_ylim(-50, 50) +ax.grid() + +plt.show() + +############################################################################### +# +# .. admonition:: References +# +# - `matplotlib.scale.AsinhScale` +# - `matplotlib.ticker.AsinhLocator` +# - `matplotlib.scale.SymmetricalLogScale` diff --git a/examples/scales/custom_scale.py b/examples/scales/custom_scale.py index ea2c2df00b34..bfeb8b99bd98 100644 --- a/examples/scales/custom_scale.py +++ b/examples/scales/custom_scale.py @@ -6,7 +6,7 @@ Create a custom scale, by implementing the scaling use for latitude data in a Mercator Projection. -Unless you are making special use of the `~.Transform` class, you probably +Unless you are making special use of the `.Transform` class, you probably don't need to use this verbose method, and instead can use `~.scale.FuncScale` and the ``'function'`` option of `~.Axes.set_xscale` and `~.Axes.set_yscale`. See the last example in :doc:`/gallery/scales/scales`. @@ -34,12 +34,12 @@ class MercatorLatitudeScale(mscale.ScaleBase): there is user-defined threshold, above and below which nothing will be plotted. This defaults to +/- 85 degrees. - __ http://en.wikipedia.org/wiki/Mercator_projection + __ https://en.wikipedia.org/wiki/Mercator_projection """ # The scale class must have a member ``name`` that defines the string used - # to select the scale. For example, ``gca().set_yscale("mercator")`` would - # be used to select this scale. + # to select the scale. For example, ``ax.set_yscale("mercator")`` would be + # used to select this scale. name = 'mercator' def __init__(self, axis, *, thresh=np.deg2rad(85), **kwargs): @@ -159,7 +159,7 @@ def inverted(self): s = np.radians(t)/2. plt.plot(t, s, '-', lw=2) - plt.gca().set_yscale('mercator') + plt.yscale('mercator') plt.xlabel('Longitude') plt.ylabel('Latitude') diff --git a/examples/scales/log_bar.py b/examples/scales/log_bar.py index bcbdee2a7f98..239069806c5c 100644 --- a/examples/scales/log_bar.py +++ b/examples/scales/log_bar.py @@ -20,8 +20,7 @@ y = [d[i] for d in data] b = ax.bar(x + i * dimw, y, dimw, bottom=0.001) -ax.set_xticks(x + dimw / 2) -ax.set_xticklabels(map(str, x)) +ax.set_xticks(x + dimw / 2, labels=map(str, x)) ax.set_yscale('log') ax.set_xlabel('x') diff --git a/examples/scales/scales.py b/examples/scales/scales.py index 25ec82608f01..803c801b4080 100644 --- a/examples/scales/scales.py +++ b/examples/scales/scales.py @@ -23,8 +23,7 @@ x = np.arange(len(y)) # plot with various axes scales -fig, axs = plt.subplots(3, 2, figsize=(6, 8), - constrained_layout=True) +fig, axs = plt.subplots(3, 2, figsize=(6, 8), layout='constrained') # linear ax = axs[0, 0] diff --git a/examples/scales/log_test.py b/examples/scales/semilogx_demo.py similarity index 86% rename from examples/scales/log_test.py rename to examples/scales/semilogx_demo.py index 72c04d61aaae..00c708a03479 100644 --- a/examples/scales/log_test.py +++ b/examples/scales/semilogx_demo.py @@ -3,6 +3,8 @@ Log Axis ======== +.. redirect-from:: /gallery/scales/log_test + This is an example of assigning a log-scale for the x-axis using `~.axes.Axes.semilogx`. """ diff --git a/examples/scales/symlog_demo.py b/examples/scales/symlog_demo.py index e1c433b22b88..6c5f04ade8d6 100644 --- a/examples/scales/symlog_demo.py +++ b/examples/scales/symlog_demo.py @@ -33,3 +33,17 @@ fig.tight_layout() plt.show() + +############################################################################### +# It should be noted that the coordinate transform used by ``symlog`` +# has a discontinuous gradient at the transition between its linear +# and logarithmic regions. The ``asinh`` axis scale is an alternative +# technique that may avoid visual artifacts caused by these discontinuities. + +############################################################################### +# +# .. admonition:: References +# +# - `matplotlib.scale.SymmetricalLogScale` +# - `matplotlib.ticker.SymmetricalLogLocator` +# - `matplotlib.scale.AsinhScale` diff --git a/examples/shapes_and_collections/arrow_guide.py b/examples/shapes_and_collections/arrow_guide.py index 284944e95bb8..efe169d909f3 100644 --- a/examples/shapes_and_collections/arrow_guide.py +++ b/examples/shapes_and_collections/arrow_guide.py @@ -23,13 +23,16 @@ 3. Entire patch fixed in data space Below each use case is presented in turn. + +.. redirect-from:: /gallery/text_labels_and_annotations/arrow_simple_demo """ + import matplotlib.patches as mpatches import matplotlib.pyplot as plt x_tail = 0.1 -y_tail = 0.1 +y_tail = 0.5 x_head = 0.9 -y_head = 0.9 +y_head = 0.8 dx = x_head - x_tail dy = y_head - y_tail @@ -38,7 +41,7 @@ # Head shape fixed in display space and anchor points fixed in data space # ----------------------------------------------------------------------- # -# This is useful if you are annotating a plot, and don't want the arrow to +# This is useful if you are annotating a plot, and don't want the arrow # to change shape or position if you pan or scale the plot. # # In this case we use `.patches.FancyArrowPatch`. @@ -54,8 +57,7 @@ arrow = mpatches.FancyArrowPatch((x_tail, y_tail), (x_head, y_head), mutation_scale=100) axs[1].add_patch(arrow) -axs[1].set_xlim(0, 2) -axs[1].set_ylim(0, 2) +axs[1].set(xlim=(0, 2), ylim=(0, 2)) ############################################################################### # Head shape and anchor points fixed in display space @@ -81,28 +83,42 @@ mutation_scale=100, transform=axs[1].transAxes) axs[1].add_patch(arrow) -axs[1].set_xlim(0, 2) -axs[1].set_ylim(0, 2) +axs[1].set(xlim=(0, 2), ylim=(0, 2)) ############################################################################### # Head shape and anchor points fixed in data space # ------------------------------------------------ # -# In this case we use `.patches.Arrow`. +# In this case we use `.patches.Arrow`, or `.patches.FancyArrow` (the latter is +# in orange). # # Note that when the axis limits are changed, the arrow shape and location # change. +# +# `.FancyArrow`'s API is relatively awkward, and requires in particular passing +# ``length_includes_head=True`` so that the arrow *tip* is ``(dx, dy)`` away +# from the arrow start. It is only included in this reference because it is +# the arrow class returned by `.Axes.arrow` (in green). fig, axs = plt.subplots(nrows=2) arrow = mpatches.Arrow(x_tail, y_tail, dx, dy) axs[0].add_patch(arrow) +arrow = mpatches.FancyArrow(x_tail, y_tail - .4, dx, dy, + width=.1, length_includes_head=True, color="C1") +axs[0].add_patch(arrow) +axs[0].arrow(x_tail + 1, y_tail - .4, dx, dy, + width=.1, length_includes_head=True, color="C2") arrow = mpatches.Arrow(x_tail, y_tail, dx, dy) axs[1].add_patch(arrow) -axs[1].set_xlim(0, 2) -axs[1].set_ylim(0, 2) +arrow = mpatches.FancyArrow(x_tail, y_tail - .4, dx, dy, + width=.1, length_includes_head=True, color="C1") +axs[1].add_patch(arrow) +axs[1].arrow(x_tail + 1, y_tail - .4, dx, dy, + width=.1, length_includes_head=True, color="C2") +axs[1].set(xlim=(0, 2), ylim=(0, 2)) ############################################################################### diff --git a/examples/shapes_and_collections/artist_reference.py b/examples/shapes_and_collections/artist_reference.py index 5bda4cd217ca..4ab6d67c0542 100644 --- a/examples/shapes_and_collections/artist_reference.py +++ b/examples/shapes_and_collections/artist_reference.py @@ -3,66 +3,25 @@ Reference for Matplotlib artists ================================ -This example displays several of Matplotlib's graphics primitives (artists) -drawn using matplotlib API. A full list of artists and the documentation is -available at :ref:`the artist API `. +This example displays several of Matplotlib's graphics primitives (artists). +A full list of artists is documented at :ref:`the artist API `. + +See also :doc:`/gallery/shapes_and_collections/patch_collection`, which groups +all artists into a single `.PatchCollection` instead. Copyright (c) 2010, Bartosz Telenczuk BSD License """ -import matplotlib.pyplot as plt -import numpy as np -import matplotlib.path as mpath + +import matplotlib as mpl import matplotlib.lines as mlines import matplotlib.patches as mpatches -from matplotlib.collections import PatchCollection - - -def label(xy, text): - y = xy[1] - 0.15 # shift y-value for label so that it's below the artist - plt.text(xy[0], y, text, ha="center", family='sans-serif', size=14) - - -fig, ax = plt.subplots() -# create 3x3 grid to plot the artists -grid = np.mgrid[0.2:0.8:3j, 0.2:0.8:3j].reshape(2, -1).T - -patches = [] - -# add a circle -circle = mpatches.Circle(grid[0], 0.1, ec="none") -patches.append(circle) -label(grid[0], "Circle") - -# add a rectangle -rect = mpatches.Rectangle(grid[1] - [0.025, 0.05], 0.05, 0.1, ec="none") -patches.append(rect) -label(grid[1], "Rectangle") - -# add a wedge -wedge = mpatches.Wedge(grid[2], 0.1, 30, 270, ec="none") -patches.append(wedge) -label(grid[2], "Wedge") - -# add a Polygon -polygon = mpatches.RegularPolygon(grid[3], 5, 0.1) -patches.append(polygon) -label(grid[3], "Polygon") - -# add an ellipse -ellipse = mpatches.Ellipse(grid[4], 0.2, 0.1) -patches.append(ellipse) -label(grid[4], "Ellipse") - -# add an arrow -arrow = mpatches.Arrow(grid[5, 0] - 0.05, grid[5, 1] - 0.05, 0.1, 0.1, - width=0.1) -patches.append(arrow) -label(grid[5], "Arrow") +import matplotlib.path as mpath +import matplotlib.pyplot as plt -# add a path patch +# Prepare the data for the PathPatch below. Path = mpath.Path -path_data = [ +codes, verts = zip(*[ (Path.MOVETO, [0.018, -0.11]), (Path.CURVE4, [-0.031, -0.051]), (Path.CURVE4, [-0.115, 0.073]), @@ -71,35 +30,28 @@ def label(xy, text): (Path.CURVE4, [0.043, 0.121]), (Path.CURVE4, [0.075, -0.005]), (Path.CURVE4, [0.035, -0.027]), - (Path.CLOSEPOLY, [0.018, -0.11])] -codes, verts = zip(*path_data) -path = mpath.Path(verts + grid[6], codes) -patch = mpatches.PathPatch(path) -patches.append(patch) -label(grid[6], "PathPatch") - -# add a fancy box -fancybox = mpatches.FancyBboxPatch( - grid[7] - [0.025, 0.05], 0.05, 0.1, - boxstyle=mpatches.BoxStyle("Round", pad=0.02)) -patches.append(fancybox) -label(grid[7], "FancyBboxPatch") - -# add a line -x, y = ([-0.06, 0.0, 0.1], [0.05, -0.05, 0.05]) -line = mlines.Line2D(x + grid[8, 0], y + grid[8, 1], lw=5., alpha=0.3) -label(grid[8], "Line2D") - -colors = np.linspace(0, 1, len(patches)) -collection = PatchCollection(patches, cmap=plt.cm.hsv, alpha=0.3) -collection.set_array(colors) -ax.add_collection(collection) -ax.add_line(line) - -plt.axis('equal') -plt.axis('off') -plt.tight_layout() - + (Path.CLOSEPOLY, [0.018, -0.11])]) + +artists = [ + mpatches.Circle((0, 0), 0.1, ec="none"), + mpatches.Rectangle((-0.025, -0.05), 0.05, 0.1, ec="none"), + mpatches.Wedge((0, 0), 0.1, 30, 270, ec="none"), + mpatches.RegularPolygon((0, 0), 5, radius=0.1), + mpatches.Ellipse((0, 0), 0.2, 0.1), + mpatches.Arrow(-0.05, -0.05, 0.1, 0.1, width=0.1), + mpatches.PathPatch(mpath.Path(verts, codes), ec="none"), + mpatches.FancyBboxPatch((-0.025, -0.05), 0.05, 0.1, ec="none", + boxstyle=mpatches.BoxStyle("Round", pad=0.02)), + mlines.Line2D([-0.06, 0.0, 0.1], [0.05, -0.05, 0.05], lw=5), +] + +axs = plt.figure(figsize=(6, 6), layout="constrained").subplots(3, 3) +for i, (ax, artist) in enumerate(zip(axs.flat, artists)): + artist.set(color=mpl.colormaps["hsv"](i / len(artists))) + ax.add_artist(artist) + ax.set(title=type(artist).__name__, + aspect=1, xlim=(-.2, .2), ylim=(-.2, .2)) + ax.set_axis_off() plt.show() ############################################################################# @@ -122,8 +74,4 @@ def label(xy, text): # - `matplotlib.patches.PathPatch` # - `matplotlib.patches.FancyBboxPatch` # - `matplotlib.patches.RegularPolygon` -# - `matplotlib.collections` -# - `matplotlib.collections.PatchCollection` -# - `matplotlib.cm.ScalarMappable.set_array` -# - `matplotlib.axes.Axes.add_collection` -# - `matplotlib.axes.Axes.add_line` +# - `matplotlib.axes.Axes.add_artist` diff --git a/examples/shapes_and_collections/collections.py b/examples/shapes_and_collections/collections.py index 399976e28522..b7ce0d6340ac 100644 --- a/examples/shapes_and_collections/collections.py +++ b/examples/shapes_and_collections/collections.py @@ -3,23 +3,22 @@ Line, Poly and RegularPoly Collection with autoscaling ========================================================= -For the first two subplots, we will use spirals. Their -size will be set in plot units, not data units. Their positions -will be set in data units by using the "offsets" and "transOffset" -kwargs of the `~.collections.LineCollection` and -`~.collections.PolyCollection`. +For the first two subplots, we will use spirals. Their size will be set in +plot units, not data units. Their positions will be set in data units by using +the *offsets* and *offset_transform* keyword arguments of the `.LineCollection` +and `.PolyCollection`. The third subplot will make regular polygons, with the same type of scaling and positioning as in the first two. -The last subplot illustrates the use of "offsets=(xo, yo)", +The last subplot illustrates the use of ``offsets=(xo, yo)``, that is, a single tuple instead of a list of tuples, to generate successively offset curves, with the offset given in data units. This behavior is available only for the LineCollection. """ import matplotlib.pyplot as plt -from matplotlib import collections, colors, transforms +from matplotlib import collections, transforms import numpy as np nverts = 50 @@ -39,16 +38,15 @@ xyo = rs.randn(npts, 2) # Make a list of colors cycling through the default series. -colors = [colors.to_rgba(c) - for c in plt.rcParams['axes.prop_cycle'].by_key()['color']] +colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2) fig.subplots_adjust(top=0.92, left=0.07, right=0.97, hspace=0.3, wspace=0.3) -col = collections.LineCollection([spiral], offsets=xyo, - transOffset=ax1.transData) +col = collections.LineCollection( + [spiral], offsets=xyo, offset_transform=ax1.transData) trans = fig.dpi_scale_trans + transforms.Affine2D().scale(1.0/72.0) col.set_transform(trans) # the points to pixels transform # Note: the first argument to the collection initializer @@ -60,7 +58,7 @@ # but it is good enough to generate a plot that you can use # as a starting point. If you know beforehand the range of # x and y that you want to show, it is better to set them -# explicitly, leave out the autolim kwarg (or set it to False), +# explicitly, leave out the *autolim* keyword argument (or set it to False), # and omit the 'ax1.autoscale_view()' call below. # Make a transform for the line segments such that their size is @@ -72,8 +70,8 @@ # The same data as above, but fill the curves. -col = collections.PolyCollection([spiral], offsets=xyo, - transOffset=ax2.transData) +col = collections.PolyCollection( + [spiral], offsets=xyo, offset_transform=ax2.transData) trans = transforms.Affine2D().scale(fig.dpi/72.0) col.set_transform(trans) # the points to pixels transform ax2.add_collection(col, autolim=True) @@ -86,7 +84,7 @@ # 7-sided regular polygons col = collections.RegularPolyCollection( - 7, sizes=np.abs(xx) * 10.0, offsets=xyo, transOffset=ax3.transData) + 7, sizes=np.abs(xx) * 10.0, offsets=xyo, offset_transform=ax3.transData) trans = transforms.Affine2D().scale(fig.dpi / 72.0) col.set_transform(trans) # the points to pixels transform ax3.add_collection(col, autolim=True) diff --git a/examples/shapes_and_collections/compound_path.py b/examples/shapes_and_collections/compound_path.py index 76a11703ab9b..d721eaf1c392 100644 --- a/examples/shapes_and_collections/compound_path.py +++ b/examples/shapes_and_collections/compound_path.py @@ -23,7 +23,7 @@ path = Path(vertices, codes) -pathpatch = PathPatch(path, facecolor='None', edgecolor='green') +pathpatch = PathPatch(path, facecolor='none', edgecolor='green') fig, ax = plt.subplots() ax.add_patch(pathpatch) diff --git a/examples/shapes_and_collections/ellipse_collection.py b/examples/shapes_and_collections/ellipse_collection.py index 695bd759bdb2..7a92eff580ee 100644 --- a/examples/shapes_and_collections/ellipse_collection.py +++ b/examples/shapes_and_collections/ellipse_collection.py @@ -7,6 +7,7 @@ a `~.collections.EllipseCollection` or `~.collections.PathCollection`, the use of an `~.collections.EllipseCollection` allows for much shorter code. """ + import matplotlib.pyplot as plt import numpy as np from matplotlib.collections import EllipseCollection @@ -25,7 +26,7 @@ fig, ax = plt.subplots() ec = EllipseCollection(ww, hh, aa, units='x', offsets=XY, - transOffset=ax.transData) + offset_transform=ax.transData) ec.set_array((X + Y).ravel()) ax.add_collection(ec) ax.autoscale_view() diff --git a/examples/shapes_and_collections/ellipse_demo.py b/examples/shapes_and_collections/ellipse_demo.py index da6ae568e90b..8d2615af7717 100644 --- a/examples/shapes_and_collections/ellipse_demo.py +++ b/examples/shapes_and_collections/ellipse_demo.py @@ -42,10 +42,6 @@ # Draw many ellipses with different angles. # -import matplotlib.pyplot as plt -import numpy as np -from matplotlib.patches import Ellipse - angle_step = 45 # degrees angles = np.arange(0, 180, angle_step) diff --git a/examples/shapes_and_collections/fancybox_demo.py b/examples/shapes_and_collections/fancybox_demo.py index 0d1588ad9e4a..ea1383d01941 100644 --- a/examples/shapes_and_collections/fancybox_demo.py +++ b/examples/shapes_and_collections/fancybox_demo.py @@ -6,6 +6,8 @@ The following examples show how to plot boxes with different visual properties. """ +import inspect + import matplotlib.pyplot as plt import matplotlib.transforms as mtransforms import matplotlib.patches as mpatch @@ -15,17 +17,26 @@ # First we'll show some sample boxes with fancybox. styles = mpatch.BoxStyle.get_styles() -spacing = 1.2 - -figheight = (spacing * len(styles) + .5) -fig = plt.figure(figsize=(4 / 1.5, figheight / 1.5)) -fontsize = 0.3 * 72 - -for i, stylename in enumerate(sorted(styles)): - fig.text(0.5, (spacing * (len(styles) - i) - 0.5) / figheight, stylename, - ha="center", - size=fontsize, - bbox=dict(boxstyle=stylename, fc="w", ec="k")) +ncol = 2 +nrow = (len(styles) + 1) // ncol +axs = (plt.figure(figsize=(3 * ncol, 1 + nrow)) + .add_gridspec(1 + nrow, ncol, wspace=.5).subplots()) +for ax in axs.flat: + ax.set_axis_off() +for ax in axs[0, :]: + ax.text(.2, .5, "boxstyle", + transform=ax.transAxes, size="large", color="tab:blue", + horizontalalignment="right", verticalalignment="center") + ax.text(.4, .5, "default parameters", + transform=ax.transAxes, + horizontalalignment="left", verticalalignment="center") +for ax, (stylename, stylecls) in zip(axs[1:, :].T.flat, styles.items()): + ax.text(.2, .5, stylename, bbox=dict(boxstyle=stylename, fc="w", ec="k"), + transform=ax.transAxes, size="large", color="tab:blue", + horizontalalignment="right", verticalalignment="center") + ax.text(.4, .5, str(inspect.signature(stylecls))[1:-1].replace(", ", "\n"), + transform=ax.transAxes, + horizontalalignment="left", verticalalignment="center") ############################################################################### @@ -33,7 +44,7 @@ def add_fancy_patch_around(ax, bb, **kwargs): - fancy = FancyBboxPatch((bb.xmin, bb.ymin), bb.width, bb.height, + fancy = FancyBboxPatch(bb.p0, bb.width, bb.height, fc=(1, 0.8, 1, 0.5), ec=(1, 0.5, 1, 0.5), **kwargs) ax.add_patch(fancy) diff --git a/examples/shapes_and_collections/hatch_style_reference.py b/examples/shapes_and_collections/hatch_style_reference.py index 5fa13163ff33..1b31f28740a9 100644 --- a/examples/shapes_and_collections/hatch_style_reference.py +++ b/examples/shapes_and_collections/hatch_style_reference.py @@ -16,7 +16,7 @@ import matplotlib.pyplot as plt from matplotlib.patches import Rectangle -fig, axs = plt.subplots(2, 5, constrained_layout=True, figsize=(6.4, 3.2)) +fig, axs = plt.subplots(2, 5, layout='constrained', figsize=(6.4, 3.2)) hatches = ['/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'] @@ -33,7 +33,7 @@ def hatches_plot(ax, h): ############################################################################### # Hatching patterns can be repeated to increase the density. -fig, axs = plt.subplots(2, 5, constrained_layout=True, figsize=(6.4, 3.2)) +fig, axs = plt.subplots(2, 5, layout='constrained', figsize=(6.4, 3.2)) hatches = ['//', '\\\\', '||', '--', '++', 'xx', 'oo', 'OO', '..', '**'] @@ -43,7 +43,7 @@ def hatches_plot(ax, h): ############################################################################### # Hatching patterns can be combined to create additional patterns. -fig, axs = plt.subplots(2, 5, constrained_layout=True, figsize=(6.4, 3.2)) +fig, axs = plt.subplots(2, 5, layout='constrained', figsize=(6.4, 3.2)) hatches = ['/o', '\\|', '|*', '-\\', '+o', 'x*', 'o-', 'O|', 'O.', '*-'] diff --git a/examples/shapes_and_collections/line_collection.py b/examples/shapes_and_collections/line_collection.py index 9adfd8024e5b..c0c44a7d8b8b 100644 --- a/examples/shapes_and_collections/line_collection.py +++ b/examples/shapes_and_collections/line_collection.py @@ -1,23 +1,17 @@ """ -=============== -Line Collection -=============== +============================================= +Plotting multiple lines with a LineCollection +============================================= -Plotting lines with Matplotlib. - -`~matplotlib.collections.LineCollection` allows one to plot multiple -lines on a figure. Below we show off some of its properties. +Matplotlib can efficiently draw multiple lines at once using a +`~.LineCollection`, as showcased below. """ import matplotlib.pyplot as plt from matplotlib.collections import LineCollection -from matplotlib import colors as mcolors import numpy as np -# In order to efficiently plot many lines in a single set of axes, -# Matplotlib has the ability to add the lines all at once. Here is a -# simple example showing how it is done. x = np.arange(100) # Here are many sets of y to plot vs. x @@ -30,7 +24,7 @@ # Mask some values to test masked array support: segs = np.ma.masked_where((segs > 50) & (segs < 60), segs) -# We need to set the plot limits. +# We need to set the plot limits, they will not autoscale fig, ax = plt.subplots() ax.set_xlim(x.min(), x.max()) ax.set_ylim(ys.min(), ys.max()) @@ -41,8 +35,7 @@ # onoffseq is an even length tuple of on and off ink in points. If linestyle # is omitted, 'solid' is used. # See `matplotlib.collections.LineCollection` for more information. -colors = [mcolors.to_rgba(c) - for c in plt.rcParams['axes.prop_cycle'].by_key()['color']] +colors = plt.rcParams['axes.prop_cycle'].by_key()['color'] line_segments = LineCollection(segs, linewidths=(0.5, 1, 1.5, 2), colors=colors, linestyle='solid') @@ -51,32 +44,22 @@ plt.show() ############################################################################### -# In order to efficiently plot many lines in a single set of axes, -# Matplotlib has the ability to add the lines all at once. Here is a -# simple example showing how it is done. +# In the following example, instead of passing a list of colors +# (``colors=colors``), we pass an array of values (``array=x``) that get +# colormapped. N = 50 x = np.arange(N) -# Here are many sets of y to plot vs. x -ys = [x + i for i in x] +ys = [x + i for i in x] # Many sets of y to plot vs. x +segs = [np.column_stack([x, y]) for y in ys] -# We need to set the plot limits, they will not autoscale fig, ax = plt.subplots() ax.set_xlim(np.min(x), np.max(x)) ax.set_ylim(np.min(ys), np.max(ys)) -# colors is sequence of rgba tuples -# linestyle is a string or dash tuple. Legal string values are -# solid|dashed|dashdot|dotted. The dash tuple is (offset, onoffseq) -# where onoffseq is an even length tuple of on and off ink in points. -# If linestyle is omitted, 'solid' is used -# See `matplotlib.collections.LineCollection` for more information - -# Make a sequence of (x, y) pairs. -line_segments = LineCollection([np.column_stack([x, y]) for y in ys], +line_segments = LineCollection(segs, array=x, linewidths=(0.5, 1, 1.5, 2), linestyles='solid') -line_segments.set_array(x) ax.add_collection(line_segments) axcb = fig.colorbar(line_segments) axcb.set_label('Line Number') diff --git a/examples/shapes_and_collections/marker_path.py b/examples/shapes_and_collections/marker_path.py deleted file mode 100644 index 7a3894c1b84d..000000000000 --- a/examples/shapes_and_collections/marker_path.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -=========== -Marker Path -=========== - -Using a `~.path.Path` as marker for a `~.axes.Axes.plot`. -""" -import matplotlib.pyplot as plt -import matplotlib.path as mpath -import numpy as np - - -star = mpath.Path.unit_regular_star(6) -circle = mpath.Path.unit_circle() -# concatenate the circle with an internal cutout of the star -verts = np.concatenate([circle.vertices, star.vertices[::-1, ...]]) -codes = np.concatenate([circle.codes, star.codes]) -cut_star = mpath.Path(verts, codes) - - -plt.plot(np.arange(10)**2, '--r', marker=cut_star, markersize=15) - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.path` -# - `matplotlib.path.Path` -# - `matplotlib.path.Path.unit_regular_star` -# - `matplotlib.path.Path.unit_circle` -# - `matplotlib.axes.Axes.plot` / `matplotlib.pyplot.plot` diff --git a/examples/shapes_and_collections/patch_collection.py b/examples/shapes_and_collections/patch_collection.py index b980694627f9..98533c451cd1 100644 --- a/examples/shapes_and_collections/patch_collection.py +++ b/examples/shapes_and_collections/patch_collection.py @@ -4,6 +4,9 @@ ============================ This example demonstrates how to use `.collections.PatchCollection`. + +See also :doc:`/gallery/shapes_and_collections/artist_reference`, which instead +adds each artist separately to its own axes. """ import numpy as np @@ -45,7 +48,7 @@ ] for i in range(N): - polygon = Polygon(np.random.rand(N, 2), True) + polygon = Polygon(np.random.rand(N, 2), closed=True) patches.append(polygon) colors = 100 * np.random.rand(len(patches)) diff --git a/examples/showcase/anatomy.py b/examples/showcase/anatomy.py index 9a362df386e1..f53aa2bb9d9e 100644 --- a/examples/showcase/anatomy.py +++ b/examples/showcase/anatomy.py @@ -6,10 +6,18 @@ This figure shows the name of several matplotlib elements composing a figure """ + import numpy as np import matplotlib.pyplot as plt +from matplotlib.patches import Circle +from matplotlib.patheffects import withStroke from matplotlib.ticker import AutoMinorLocator, MultipleLocator +royal_blue = [0, 20/256, 82/256] + + +# make the figure + np.random.seed(19680801) X = np.linspace(0.5, 3.5, 100) @@ -17,130 +25,81 @@ Y2 = 1+np.cos(1+X/0.75)/2 Y3 = np.random.uniform(Y1, Y2, len(X)) -fig = plt.figure(figsize=(8, 8)) -ax = fig.add_subplot(1, 1, 1, aspect=1) - - -def minor_tick(x, pos): - if not x % 1.0: - return "" - return f"{x:.2f}" +fig = plt.figure(figsize=(7.5, 7.5)) +ax = fig.add_axes([0.2, 0.17, 0.68, 0.7], aspect=1) ax.xaxis.set_major_locator(MultipleLocator(1.000)) ax.xaxis.set_minor_locator(AutoMinorLocator(4)) ax.yaxis.set_major_locator(MultipleLocator(1.000)) ax.yaxis.set_minor_locator(AutoMinorLocator(4)) -# FuncFormatter is created and used automatically -ax.xaxis.set_minor_formatter(minor_tick) +ax.xaxis.set_minor_formatter("{x:.2f}") ax.set_xlim(0, 4) ax.set_ylim(0, 4) -ax.tick_params(which='major', width=1.0) -ax.tick_params(which='major', length=10) -ax.tick_params(which='minor', width=1.0, labelsize=10) -ax.tick_params(which='minor', length=5, labelsize=10, labelcolor='0.25') +ax.tick_params(which='major', width=1.0, length=10, labelsize=14) +ax.tick_params(which='minor', width=1.0, length=5, labelsize=10, + labelcolor='0.25') ax.grid(linestyle="--", linewidth=0.5, color='.25', zorder=-10) -ax.plot(X, Y1, c=(0.25, 0.25, 1.00), lw=2, label="Blue signal", zorder=10) -ax.plot(X, Y2, c=(1.00, 0.25, 0.25), lw=2, label="Red signal") -ax.plot(X, Y3, linewidth=0, - marker='o', markerfacecolor='w', markeredgecolor='k') +ax.plot(X, Y1, c='C0', lw=2.5, label="Blue signal", zorder=10) +ax.plot(X, Y2, c='C1', lw=2.5, label="Orange signal") +ax.plot(X[::3], Y3[::3], linewidth=0, markersize=9, + marker='s', markerfacecolor='none', markeredgecolor='C4', + markeredgewidth=2.5) ax.set_title("Anatomy of a figure", fontsize=20, verticalalignment='bottom') -ax.set_xlabel("X axis label") -ax.set_ylabel("Y axis label") - -ax.legend() - - -def circle(x, y, radius=0.15): - from matplotlib.patches import Circle - from matplotlib.patheffects import withStroke - circle = Circle((x, y), radius, clip_on=False, zorder=10, linewidth=1, - edgecolor='black', facecolor=(0, 0, 0, .0125), - path_effects=[withStroke(linewidth=5, foreground='w')]) - ax.add_artist(circle) - - -def text(x, y, text): - ax.text(x, y, text, backgroundcolor="white", - ha='center', va='top', weight='bold', color='blue') - - -# Minor tick -circle(0.50, -0.10) -text(0.50, -0.32, "Minor tick label") - -# Major tick -circle(-0.03, 4.00) -text(0.03, 3.80, "Major tick") - -# Minor tick -circle(0.00, 3.50) -text(0.00, 3.30, "Minor tick") - -# Major tick label -circle(-0.15, 3.00) -text(-0.15, 2.80, "Major tick label") - -# X Label -circle(1.80, -0.27) -text(1.80, -0.45, "X axis label") - -# Y Label -circle(-0.27, 1.80) -text(-0.27, 1.6, "Y axis label") - -# Title -circle(1.60, 4.13) -text(1.60, 3.93, "Title") - -# Blue plot -circle(1.75, 2.80) -text(1.75, 2.60, "Line\n(line plot)") - -# Red plot -circle(1.20, 0.60) -text(1.20, 0.40, "Line\n(line plot)") - -# Scatter plot -circle(3.20, 1.75) -text(3.20, 1.55, "Markers\n(scatter plot)") - -# Grid -circle(3.00, 3.00) -text(3.00, 2.80, "Grid") - -# Legend -circle(3.70, 3.80) -text(3.70, 3.60, "Legend") - -# Axes -circle(0.5, 0.5) -text(0.5, 0.3, "Axes") - -# Figure -circle(-0.3, 0.65) -text(-0.3, 0.45, "Figure") - -color = 'blue' -ax.annotate('Spines', xy=(4.0, 0.35), xytext=(3.3, 0.5), - weight='bold', color=color, - arrowprops=dict(arrowstyle='->', - connectionstyle="arc3", - color=color)) - -ax.annotate('', xy=(3.15, 0.0), xytext=(3.45, 0.45), - weight='bold', color=color, - arrowprops=dict(arrowstyle='->', - connectionstyle="arc3", - color=color)) - -ax.text(4.0, -0.4, "Made with https://matplotlib.org", - fontsize=10, ha="right", color='.5') - +ax.set_xlabel("x Axis label", fontsize=14) +ax.set_ylabel("y Axis label", fontsize=14) +ax.legend(loc="upper right", fontsize=14) + + +# Annotate the figure + +def annotate(x, y, text, code): + # Circle marker + c = Circle((x, y), radius=0.15, clip_on=False, zorder=10, linewidth=2.5, + edgecolor=royal_blue + [0.6], facecolor='none', + path_effects=[withStroke(linewidth=7, foreground='white')]) + ax.add_artist(c) + + # use path_effects as a background for the texts + # draw the path_effects and the colored text separately so that the + # path_effects cannot clip other texts + for path_effects in [[withStroke(linewidth=7, foreground='white')], []]: + color = 'white' if path_effects else royal_blue + ax.text(x, y-0.2, text, zorder=100, + ha='center', va='top', weight='bold', color=color, + style='italic', fontfamily='Courier New', + path_effects=path_effects) + + color = 'white' if path_effects else 'black' + ax.text(x, y-0.33, code, zorder=100, + ha='center', va='top', weight='normal', color=color, + fontfamily='monospace', fontsize='medium', + path_effects=path_effects) + + +annotate(3.5, -0.13, "Minor tick label", "ax.xaxis.set_minor_formatter") +annotate(-0.03, 1.0, "Major tick", "ax.yaxis.set_major_locator") +annotate(0.00, 3.75, "Minor tick", "ax.yaxis.set_minor_locator") +annotate(-0.15, 3.00, "Major tick label", "ax.yaxis.set_major_formatter") +annotate(1.68, -0.39, "xlabel", "ax.set_xlabel") +annotate(-0.38, 1.67, "ylabel", "ax.set_ylabel") +annotate(1.52, 4.15, "Title", "ax.set_title") +annotate(1.75, 2.80, "Line", "ax.plot") +annotate(2.25, 1.54, "Markers", "ax.scatter") +annotate(3.00, 3.00, "Grid", "ax.grid") +annotate(3.60, 3.58, "Legend", "ax.legend") +annotate(2.5, 0.55, "Axes", "fig.subplots") +annotate(4, 4.5, "Figure", "plt.figure") +annotate(0.65, 0.01, "x Axis", "ax.xaxis") +annotate(0, 0.36, "y Axis", "ax.yaxis") +annotate(4.0, 0.7, "Spine", "ax.spines") + +# frame around figure +fig.patch.set(linewidth=4, edgecolor='0.5') plt.show() diff --git a/examples/showcase/bachelors_degrees_by_gender.py b/examples/showcase/bachelors_degrees_by_gender.py deleted file mode 100644 index 9ee181c62240..000000000000 --- a/examples/showcase/bachelors_degrees_by_gender.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -============================ -Bachelor's degrees by gender -============================ - -A graph of multiple time series which demonstrates extensive custom -styling of plot frame, tick lines and labels, and line graph properties. - -Also demonstrates the custom placement of text labels along the right edge -as an alternative to a conventional legend. -""" - -import numpy as np -import matplotlib.pyplot as plt -from matplotlib.cbook import get_sample_data - - -fname = get_sample_data('percent_bachelors_degrees_women_usa.csv', - asfileobj=False) -gender_degree_data = np.genfromtxt(fname, delimiter=',', names=True) - -# You typically want your plot to be ~1.33x wider than tall. This plot -# is a rare exception because of the number of lines being plotted on it. -# Common sizes: (10, 7.5) and (12, 9) -fig, ax = plt.subplots(1, 1, figsize=(12, 14)) - -# These are the colors that will be used in the plot -ax.set_prop_cycle(color=[ - '#1f77b4', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', - '#d62728', '#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', - '#e377c2', '#f7b6d2', '#7f7f7f', '#c7c7c7', '#bcbd22', '#dbdb8d', - '#17becf', '#9edae5']) - -# Remove the plot frame lines. They are unnecessary here. -ax.spines[:].set_visible(False) - -# Ensure that the axis ticks only show up on the bottom and left of the plot. -# Ticks on the right and top of the plot are generally unnecessary. -ax.xaxis.tick_bottom() -ax.yaxis.tick_left() - -fig.subplots_adjust(left=.06, right=.75, bottom=.02, top=.94) -# Limit the range of the plot to only where the data is. -# Avoid unnecessary whitespace. -ax.set_xlim(1969.5, 2011.1) -ax.set_ylim(-0.25, 90) - -# Set a fixed location and format for ticks. -ax.set_xticks(range(1970, 2011, 10)) -ax.set_yticks(range(0, 91, 10)) -# Use automatic StrMethodFormatter creation -ax.xaxis.set_major_formatter('{x:.0f}') -ax.yaxis.set_major_formatter('{x:.0f}%') - -# Provide tick lines across the plot to help your viewers trace along -# the axis ticks. Make sure that the lines are light and small so they -# don't obscure the primary data lines. -ax.grid(True, 'major', 'y', ls='--', lw=.5, c='k', alpha=.3) - -# Remove the tick marks; they are unnecessary with the tick lines we just -# plotted. Make sure your axis ticks are large enough to be easily read. -# You don't want your viewers squinting to read your plot. -ax.tick_params(axis='both', which='both', labelsize=14, - bottom=False, top=False, labelbottom=True, - left=False, right=False, labelleft=True) - -# Now that the plot is prepared, it's time to actually plot the data! -# Note that I plotted the majors in order of the highest % in the final year. -majors = ['Health Professions', 'Public Administration', 'Education', - 'Psychology', 'Foreign Languages', 'English', - 'Communications\nand Journalism', 'Art and Performance', 'Biology', - 'Agriculture', 'Social Sciences and History', 'Business', - 'Math and Statistics', 'Architecture', 'Physical Sciences', - 'Computer Science', 'Engineering'] - -y_offsets = {'Foreign Languages': 0.5, 'English': -0.5, - 'Communications\nand Journalism': 0.75, - 'Art and Performance': -0.25, 'Agriculture': 1.25, - 'Social Sciences and History': 0.25, 'Business': -0.75, - 'Math and Statistics': 0.75, 'Architecture': -0.75, - 'Computer Science': 0.75, 'Engineering': -0.25} - -for column in majors: - # Plot each line separately with its own color. - column_rec_name = column.replace('\n', '_').replace(' ', '_') - - line, = ax.plot('Year', column_rec_name, data=gender_degree_data, - lw=2.5) - - # Add a text label to the right end of every line. Most of the code below - # is adding specific offsets y position because some labels overlapped. - y_pos = gender_degree_data[column_rec_name][-1] - 0.5 - - if column in y_offsets: - y_pos += y_offsets[column] - - # Again, make sure that all labels are large enough to be easily read - # by the viewer. - ax.text(2011.5, y_pos, column, fontsize=14, color=line.get_color()) - -# Make the title big enough so it spans the entire plot, but don't make it -# so big that it requires two lines to show. - -# Note that if the title is descriptive enough, it is unnecessary to include -# axis labels; they are self-evident, in this plot's case. -fig.suptitle("Percentage of Bachelor's degrees conferred to women in " - "the U.S.A. by major (1970-2011)", fontsize=18, ha="center") - -# Finally, save the figure as a PNG. -# You can also save it as a PDF, JPEG, etc. -# Just change the file extension in this call. -# fig.savefig('percent-bachelors-degrees-women-usa.png', bbox_inches='tight') -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.pyplot.subplots` -# - `matplotlib.axes.Axes.text` -# - `matplotlib.axis.Axis.set_major_formatter` -# - `matplotlib.axis.XAxis.tick_bottom` -# - `matplotlib.axis.YAxis.tick_left` -# - `matplotlib.artist.Artist.set_visible` -# - `matplotlib.ticker.StrMethodFormatter` diff --git a/examples/showcase/firefox.py b/examples/showcase/firefox.py index 713809b8292d..69025d39eba1 100644 --- a/examples/showcase/firefox.py +++ b/examples/showcase/firefox.py @@ -12,8 +12,8 @@ from matplotlib.path import Path import matplotlib.patches as patches -# From: http://raphaeljs.com/icons/#firefox -firefox = "M28.4,22.469c0.479-0.964,0.851-1.991,1.095-3.066c0.953-3.661,0.666-6.854,0.666-6.854l-0.327,2.104c0,0-0.469-3.896-1.044-5.353c-0.881-2.231-1.273-2.214-1.274-2.21c0.542,1.379,0.494,2.169,0.483,2.288c-0.01-0.016-0.019-0.032-0.027-0.047c-0.131-0.324-0.797-1.819-2.225-2.878c-2.502-2.481-5.943-4.014-9.745-4.015c-4.056,0-7.705,1.745-10.238,4.525C5.444,6.5,5.183,5.938,5.159,5.317c0,0-0.002,0.002-0.006,0.005c0-0.011-0.003-0.021-0.003-0.031c0,0-1.61,1.247-1.436,4.612c-0.299,0.574-0.56,1.172-0.777,1.791c-0.375,0.817-0.75,2.004-1.059,3.746c0,0,0.133-0.422,0.399-0.988c-0.064,0.482-0.103,0.971-0.116,1.467c-0.09,0.845-0.118,1.865-0.039,3.088c0,0,0.032-0.406,0.136-1.021c0.834,6.854,6.667,12.165,13.743,12.165l0,0c1.86,0,3.636-0.37,5.256-1.036C24.938,27.771,27.116,25.196,28.4,22.469zM16.002,3.356c2.446,0,4.73,0.68,6.68,1.86c-2.274-0.528-3.433-0.261-3.423-0.248c0.013,0.015,3.384,0.589,3.981,1.411c0,0-1.431,0-2.856,0.41c-0.065,0.019,5.242,0.663,6.327,5.966c0,0-0.582-1.213-1.301-1.42c0.473,1.439,0.351,4.17-0.1,5.528c-0.058,0.174-0.118-0.755-1.004-1.155c0.284,2.037-0.018,5.268-1.432,6.158c-0.109,0.07,0.887-3.189,0.201-1.93c-4.093,6.276-8.959,2.539-10.934,1.208c1.585,0.388,3.267,0.108,4.242-0.559c0.982-0.672,1.564-1.162,2.087-1.047c0.522,0.117,0.87-0.407,0.464-0.872c-0.405-0.466-1.392-1.105-2.725-0.757c-0.94,0.247-2.107,1.287-3.886,0.233c-1.518-0.899-1.507-1.63-1.507-2.095c0-0.366,0.257-0.88,0.734-1.028c0.58,0.062,1.044,0.214,1.537,0.466c0.005-0.135,0.006-0.315-0.001-0.519c0.039-0.077,0.015-0.311-0.047-0.596c-0.036-0.287-0.097-0.582-0.19-0.851c0.01-0.002,0.017-0.007,0.021-0.021c0.076-0.344,2.147-1.544,2.299-1.659c0.153-0.114,0.55-0.378,0.506-1.183c-0.015-0.265-0.058-0.294-2.232-0.286c-0.917,0.003-1.425-0.894-1.589-1.245c0.222-1.231,0.863-2.11,1.919-2.704c0.02-0.011,0.015-0.021-0.008-0.027c0.219-0.127-2.524-0.006-3.76,1.604C9.674,8.045,9.219,7.95,8.71,7.95c-0.638,0-1.139,0.07-1.603,0.187c-0.05,0.013-0.122,0.011-0.208-0.001C6.769,8.04,6.575,7.88,6.365,7.672c0.161-0.18,0.324-0.356,0.495-0.526C9.201,4.804,12.43,3.357,16.002,3.356z" +# From: https://dmitrybaranovskiy.github.io/raphael/icons/#firefox +firefox = "M28.4,22.469c0.479-0.964,0.851-1.991,1.095-3.066c0.953-3.661,0.666-6.854,0.666-6.854l-0.327,2.104c0,0-0.469-3.896-1.044-5.353c-0.881-2.231-1.273-2.214-1.274-2.21c0.542,1.379,0.494,2.169,0.483,2.288c-0.01-0.016-0.019-0.032-0.027-0.047c-0.131-0.324-0.797-1.819-2.225-2.878c-2.502-2.481-5.943-4.014-9.745-4.015c-4.056,0-7.705,1.745-10.238,4.525C5.444,6.5,5.183,5.938,5.159,5.317c0,0-0.002,0.002-0.006,0.005c0-0.011-0.003-0.021-0.003-0.031c0,0-1.61,1.247-1.436,4.612c-0.299,0.574-0.56,1.172-0.777,1.791c-0.375,0.817-0.75,2.004-1.059,3.746c0,0,0.133-0.422,0.399-0.988c-0.064,0.482-0.103,0.971-0.116,1.467c-0.09,0.845-0.118,1.865-0.039,3.088c0,0,0.032-0.406,0.136-1.021c0.834,6.854,6.667,12.165,13.743,12.165l0,0c1.86,0,3.636-0.37,5.256-1.036C24.938,27.771,27.116,25.196,28.4,22.469zM16.002,3.356c2.446,0,4.73,0.68,6.68,1.86c-2.274-0.528-3.433-0.261-3.423-0.248c0.013,0.015,3.384,0.589,3.981,1.411c0,0-1.431,0-2.856,0.41c-0.065,0.019,5.242,0.663,6.327,5.966c0,0-0.582-1.213-1.301-1.42c0.473,1.439,0.351,4.17-0.1,5.528c-0.058,0.174-0.118-0.755-1.004-1.155c0.284,2.037-0.018,5.268-1.432,6.158c-0.109,0.07,0.887-3.189,0.201-1.93c-4.093,6.276-8.959,2.539-10.934,1.208c1.585,0.388,3.267,0.108,4.242-0.559c0.982-0.672,1.564-1.162,2.087-1.047c0.522,0.117,0.87-0.407,0.464-0.872c-0.405-0.466-1.392-1.105-2.725-0.757c-0.94,0.247-2.107,1.287-3.886,0.233c-1.518-0.899-1.507-1.63-1.507-2.095c0-0.366,0.257-0.88,0.734-1.028c0.58,0.062,1.044,0.214,1.537,0.466c0.005-0.135,0.006-0.315-0.001-0.519c0.039-0.077,0.015-0.311-0.047-0.596c-0.036-0.287-0.097-0.582-0.19-0.851c0.01-0.002,0.017-0.007,0.021-0.021c0.076-0.344,2.147-1.544,2.299-1.659c0.153-0.114,0.55-0.378,0.506-1.183c-0.015-0.265-0.058-0.294-2.232-0.286c-0.917,0.003-1.425-0.894-1.589-1.245c0.222-1.231,0.863-2.11,1.919-2.704c0.02-0.011,0.015-0.021-0.008-0.027c0.219-0.127-2.524-0.006-3.76,1.604C9.674,8.045,9.219,7.95,8.71,7.95c-0.638,0-1.139,0.07-1.603,0.187c-0.05,0.013-0.122,0.011-0.208-0.001C6.769,8.04,6.575,7.88,6.365,7.672c0.161-0.18,0.324-0.356,0.495-0.526C9.201,4.804,12.43,3.357,16.002,3.356z" # noqa def svg_parse(path): diff --git a/examples/showcase/integral.py b/examples/showcase/integral.py index e0d07fec3de2..731f7b9ecd50 100644 --- a/examples/showcase/integral.py +++ b/examples/showcase/integral.py @@ -42,12 +42,8 @@ def func(x): fig.text(0.9, 0.05, '$x$') fig.text(0.1, 0.9, '$y$') -ax.spines.right.set_visible(False) -ax.spines.top.set_visible(False) -ax.xaxis.set_ticks_position('bottom') - -ax.set_xticks((a, b)) -ax.set_xticklabels(('$a$', '$b$')) +ax.spines[['top', 'right']].set_visible(False) +ax.set_xticks([a, b], labels=['$a$', '$b$']) ax.set_yticks([]) plt.show() diff --git a/examples/showcase/mandelbrot.py b/examples/showcase/mandelbrot.py index 53db00ae3d0d..6806fc38a5cc 100644 --- a/examples/showcase/mandelbrot.py +++ b/examples/showcase/mandelbrot.py @@ -44,7 +44,7 @@ def mandelbrot_set(xmin, xmax, ymin, ymax, xn, yn, maxiter, horizon=2.0): # https://linas.org/art-gallery/escape/smooth.html # https://web.archive.org/web/20160331171238/https://www.ibm.com/developerworks/community/blogs/jfp/entry/My_Christmas_Gift?lang=en - # This line will generate warnings for null values but it is faster to + # This line will generate warnings for null values, but it is faster to # process them afterwards using the nan_to_num with np.errstate(invalid='ignore'): M = np.nan_to_num(N + 1 - np.log2(np.log(abs(Z))) + log_horizon) diff --git a/examples/showcase/stock_prices.py b/examples/showcase/stock_prices.py new file mode 100644 index 000000000000..7f7ffba08f03 --- /dev/null +++ b/examples/showcase/stock_prices.py @@ -0,0 +1,119 @@ +""" +========================== +Stock prices over 32 years +========================== + +.. redirect-from:: /gallery/showcase/bachelors_degrees_by_gender + +A graph of multiple time series that demonstrates custom styling of plot frame, +tick lines, tick labels, and line graph properties. It also uses custom +placement of text labels along the right edge as an alternative to a +conventional legend. + +Note: The third-party mpl style dufte_ produces similar-looking plots with less +code. + +.. _dufte: https://github.com/nschloe/dufte +""" + +import numpy as np +import matplotlib.transforms as mtransforms +import matplotlib.pyplot as plt +from matplotlib.cbook import get_sample_data + + +def convertdate(x): + return np.datetime64(x, 'D') + + +fname = get_sample_data('Stocks.csv', asfileobj=False) +stock_data = np.genfromtxt(fname, encoding='utf-8', delimiter=',', + names=True, dtype=None, converters={0: convertdate}, + skip_header=1) + + +fig, ax = plt.subplots(1, 1, figsize=(6, 8), layout='constrained') + +# These are the colors that will be used in the plot +ax.set_prop_cycle(color=[ + '#1f77b4', '#aec7e8', '#ff7f0e', '#ffbb78', '#2ca02c', '#98df8a', + '#d62728', '#ff9896', '#9467bd', '#c5b0d5', '#8c564b', '#c49c94', + '#e377c2', '#f7b6d2', '#7f7f7f', '#c7c7c7', '#bcbd22', '#dbdb8d', + '#17becf', '#9edae5']) + +stocks_name = ['IBM', 'Apple', 'Microsoft', 'Xerox', 'Amazon', 'Dell', + 'Alphabet', 'Adobe', 'S&P 500', 'NASDAQ'] +stocks_ticker = ['IBM', 'AAPL', 'MSFT', 'XRX', 'AMZN', 'DELL', 'GOOGL', + 'ADBE', 'GSPC', 'IXIC'] + +# Manually adjust the label positions vertically (units are points = 1/72 inch) +y_offsets = {k: 0 for k in stocks_ticker} +y_offsets['IBM'] = 5 +y_offsets['AAPL'] = -5 +y_offsets['AMZN'] = -6 + +for nn, column in enumerate(stocks_ticker): + # Plot each line separately with its own color. + # don't include any data with NaN. + good = np.nonzero(np.isfinite(stock_data[column])) + line, = ax.plot(stock_data['Date'][good], stock_data[column][good], lw=2.5) + + # Add a text label to the right end of every line. Most of the code below + # is adding specific offsets y position because some labels overlapped. + y_pos = stock_data[column][-1] + + # Use an offset transform, in points, for any text that needs to be nudged + # up or down. + offset = y_offsets[column] / 72 + trans = mtransforms.ScaledTranslation(0, offset, fig.dpi_scale_trans) + trans = ax.transData + trans + + # Again, make sure that all labels are large enough to be easily read + # by the viewer. + ax.text(np.datetime64('2022-10-01'), y_pos, stocks_name[nn], + color=line.get_color(), transform=trans) + +ax.set_xlim(np.datetime64('1989-06-01'), np.datetime64('2023-01-01')) + +fig.suptitle("Technology company stocks prices dollars (1990-2022)", + ha="center") + +# Remove the plot frame lines. They are unnecessary here. +ax.spines[:].set_visible(False) + +# Ensure that the axis ticks only show up on the bottom and left of the plot. +# Ticks on the right and top of the plot are generally unnecessary. +ax.xaxis.tick_bottom() +ax.yaxis.tick_left() +ax.set_yscale('log') + +# Provide tick lines across the plot to help your viewers trace along +# the axis ticks. Make sure that the lines are light and small so they +# don't obscure the primary data lines. +ax.grid(True, 'major', 'both', ls='--', lw=.5, c='k', alpha=.3) + +# Remove the tick marks; they are unnecessary with the tick lines we just +# plotted. Make sure your axis ticks are large enough to be easily read. +# You don't want your viewers squinting to read your plot. +ax.tick_params(axis='both', which='both', labelsize='large', + bottom=False, top=False, labelbottom=True, + left=False, right=False, labelleft=True) + +# Finally, save the figure as a PNG. +# You can also save it as a PDF, JPEG, etc. +# Just change the file extension in this call. +# fig.savefig('stock-prices.png', bbox_inches='tight') +plt.show() + +############################################################################# +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.pyplot.subplots` +# - `matplotlib.axes.Axes.text` +# - `matplotlib.axis.XAxis.tick_bottom` +# - `matplotlib.axis.YAxis.tick_left` +# - `matplotlib.artist.Artist.set_visible` diff --git a/examples/showcase/xkcd.py b/examples/showcase/xkcd.py index 4ab16c66a536..4088a0133814 100644 --- a/examples/showcase/xkcd.py +++ b/examples/showcase/xkcd.py @@ -16,8 +16,7 @@ fig = plt.figure() ax = fig.add_axes((0.1, 0.2, 0.8, 0.7)) - ax.spines.right.set_color('none') - ax.spines.top.set_color('none') + ax.spines[['top', 'right']].set_visible(False) ax.set_xticks([]) ax.set_yticks([]) ax.set_ylim([-30, 10]) @@ -47,8 +46,7 @@ fig = plt.figure() ax = fig.add_axes((0.1, 0.2, 0.8, 0.7)) ax.bar([0, 1], [0, 100], 0.25) - ax.spines.right.set_color('none') - ax.spines.top.set_color('none') + ax.spines[['top', 'right']].set_visible(False) ax.xaxis.set_ticks_position('bottom') ax.set_xticks([0, 1]) ax.set_xticklabels(['CONFIRMED BY\nEXPERIMENT', 'REFUTED BY\nEXPERIMENT']) diff --git a/examples/specialty_plots/README.txt b/examples/specialty_plots/README.txt index d186022d93db..fc1bf44d3b92 100644 --- a/examples/specialty_plots/README.txt +++ b/examples/specialty_plots/README.txt @@ -1,4 +1,4 @@ .. _specialty_plots_examples: -Specialty Plots +Specialty plots =============== diff --git a/examples/specialty_plots/leftventricle_bulleye.py b/examples/specialty_plots/leftventricle_bullseye.py similarity index 84% rename from examples/specialty_plots/leftventricle_bulleye.py rename to examples/specialty_plots/leftventricle_bullseye.py index 3f32dafd2b08..169c08447ad1 100644 --- a/examples/specialty_plots/leftventricle_bulleye.py +++ b/examples/specialty_plots/leftventricle_bullseye.py @@ -5,6 +5,8 @@ This example demonstrates how to create the 17 segment model for the left ventricle recommended by the American Heart Association (AHA). + +.. redirect-from:: /gallery/specialty_plots/leftventricle_bulleye """ import numpy as np @@ -56,6 +58,9 @@ def bullseye_plot(ax, data, seg_bold=None, cmap=None, norm=None): theta = np.linspace(0, 2 * np.pi, 768) r = np.linspace(0.2, 1, 4) + # Remove grid + ax.grid(False) + # Create the bound for the segment 17 for i in range(r.shape[0]): ax.plot(theta, np.repeat(r[i], theta.shape), '-k', lw=linewidth) @@ -77,7 +82,7 @@ def bullseye_plot(ax, data, seg_bold=None, cmap=None, norm=None): # First segment start at 60 degrees theta0 = theta[i * 128:i * 128 + 128] + np.deg2rad(60) theta0 = np.repeat(theta0[:, np.newaxis], 2, axis=1) - z = np.ones((128, 2)) * data[i] + z = np.ones((128 - 1, 2 - 1)) * data[i] ax.pcolormesh(theta0, r0, z, cmap=cmap, norm=norm, shading='auto') if i + 1 in seg_bold: ax.plot(theta0, r0, '-k', lw=linewidth + 2) @@ -91,7 +96,7 @@ def bullseye_plot(ax, data, seg_bold=None, cmap=None, norm=None): # First segment start at 60 degrees theta0 = theta[i * 128:i * 128 + 128] + np.deg2rad(60) theta0 = np.repeat(theta0[:, np.newaxis], 2, axis=1) - z = np.ones((128, 2)) * data[i + 6] + z = np.ones((128 - 1, 2 - 1)) * data[i + 6] ax.pcolormesh(theta0, r0, z, cmap=cmap, norm=norm, shading='auto') if i + 7 in seg_bold: ax.plot(theta0, r0, '-k', lw=linewidth + 2) @@ -105,7 +110,7 @@ def bullseye_plot(ax, data, seg_bold=None, cmap=None, norm=None): # First segment start at 45 degrees theta0 = theta[i * 192:i * 192 + 192] + np.deg2rad(45) theta0 = np.repeat(theta0[:, np.newaxis], 2, axis=1) - z = np.ones((192, 2)) * data[i + 12] + z = np.ones((192 - 1, 2 - 1)) * data[i + 12] ax.pcolormesh(theta0, r0, z, cmap=cmap, norm=norm, shading='auto') if i + 13 in seg_bold: ax.plot(theta0, r0, '-k', lw=linewidth + 2) @@ -117,7 +122,7 @@ def bullseye_plot(ax, data, seg_bold=None, cmap=None, norm=None): r0 = np.array([0, r[0]]) r0 = np.repeat(r0[:, np.newaxis], theta.size, axis=1).T theta0 = np.repeat(theta[:, np.newaxis], 2, axis=1) - z = np.ones((theta.size, 2)) * data[16] + z = np.ones((theta.size - 1, 2 - 1)) * data[16] ax.pcolormesh(theta0, r0, z, cmap=cmap, norm=norm, shading='auto') if 17 in seg_bold: ax.plot(theta0, r0, '-k', lw=linewidth + 2) @@ -132,15 +137,11 @@ def bullseye_plot(ax, data, seg_bold=None, cmap=None, norm=None): # Make a figure and axes with dimensions as desired. -fig, ax = plt.subplots(figsize=(12, 8), nrows=1, ncols=3, - subplot_kw=dict(projection='polar')) +fig = plt.figure(figsize=(10, 5), layout="constrained") +fig.get_layout_engine().set(wspace=.1, w_pad=.2) +axs = fig.subplots(1, 3, subplot_kw=dict(projection='polar')) fig.canvas.manager.set_window_title('Left Ventricle Bulls Eyes (AHA)') -# Create the axis for the colorbars -axl = fig.add_axes([0.14, 0.15, 0.2, 0.05]) -axl2 = fig.add_axes([0.41, 0.15, 0.2, 0.05]) -axl3 = fig.add_axes([0.69, 0.15, 0.2, 0.05]) - # Set the colormap and norm to correspond to the data for which # the colorbar will be used. @@ -149,14 +150,16 @@ def bullseye_plot(ax, data, seg_bold=None, cmap=None, norm=None): # Create an empty ScalarMappable to set the colorbar's colormap and norm. # The following gives a basic continuous colorbar with ticks and labels. fig.colorbar(mpl.cm.ScalarMappable(cmap=cmap, norm=norm), - cax=axl, orientation='horizontal', label='Some Units') + cax=axs[0].inset_axes([0, -.15, 1, .1]), + orientation='horizontal', label='Some Units') # And again for the second colorbar. cmap2 = mpl.cm.cool norm2 = mpl.colors.Normalize(vmin=1, vmax=17) fig.colorbar(mpl.cm.ScalarMappable(cmap=cmap2, norm=norm2), - cax=axl2, orientation='horizontal', label='Some other units') + cax=axs[1].inset_axes([0, -.15, 1, .1]), + orientation='horizontal', label='Some other units') # The second example illustrates the use of a ListedColormap, a @@ -170,9 +173,7 @@ def bullseye_plot(ax, data, seg_bold=None, cmap=None, norm=None): bounds = [2, 3, 7, 9, 15] norm3 = mpl.colors.BoundaryNorm(bounds, cmap3.N) fig.colorbar(mpl.cm.ScalarMappable(cmap=cmap3, norm=norm3), - cax=axl3, - # to use 'extend', you must specify two extra boundaries: - boundaries=[0] + bounds + [18], + cax=axs[2].inset_axes([0, -.15, 1, .1]), extend='both', ticks=bounds, # optional spacing='proportional', @@ -181,14 +182,14 @@ def bullseye_plot(ax, data, seg_bold=None, cmap=None, norm=None): # Create the 17 segment model -bullseye_plot(ax[0], data, cmap=cmap, norm=norm) -ax[0].set_title('Bulls Eye (AHA)') +bullseye_plot(axs[0], data, cmap=cmap, norm=norm) +axs[0].set_title('Bulls Eye (AHA)') -bullseye_plot(ax[1], data, cmap=cmap2, norm=norm2) -ax[1].set_title('Bulls Eye (AHA)') +bullseye_plot(axs[1], data, cmap=cmap2, norm=norm2) +axs[1].set_title('Bulls Eye (AHA)') -bullseye_plot(ax[2], data, seg_bold=[3, 5, 6, 11, 12, 16], +bullseye_plot(axs[2], data, seg_bold=[3, 5, 6, 11, 12, 16], cmap=cmap3, norm=norm3) -ax[2].set_title('Segments [3, 5, 6, 11, 12, 16] in bold') +axs[2].set_title('Segments [3, 5, 6, 11, 12, 16] in bold') plt.show() diff --git a/examples/specialty_plots/mri_with_eeg.py b/examples/specialty_plots/mri_with_eeg.py index 0f622bf42a67..911a26bcbff7 100644 --- a/examples/specialty_plots/mri_with_eeg.py +++ b/examples/specialty_plots/mri_with_eeg.py @@ -1,6 +1,6 @@ """ ============ -MRI With EEG +MRI with EEG ============ Displays a set of subplots with an MRI image, its intensity @@ -17,7 +17,7 @@ fig = plt.figure("MRI_with_EEG") -# Load the MRI data (256x256 16 bit integers) +# Load the MRI data (256x256 16-bit integers) with cbook.get_sample_data('s1045.ima.gz') as dfile: im = np.frombuffer(dfile.read(), np.uint16).reshape((256, 256)) @@ -64,12 +64,11 @@ offsets = np.zeros((n_rows, 2), dtype=float) offsets[:, 1] = ticklocs -lines = LineCollection(segs, offsets=offsets, transOffset=None) +lines = LineCollection(segs, offsets=offsets, offset_transform=None) ax2.add_collection(lines) -# Set the yticks to use axes coordinates on the y axis -ax2.set_yticks(ticklocs) -ax2.set_yticklabels(['PG3', 'PG5', 'PG7', 'PG9']) +# Set the yticks to use axes coordinates on the y-axis +ax2.set_yticks(ticklocs, labels=['PG3', 'PG5', 'PG7', 'PG9']) ax2.set_xlabel('Time (s)') diff --git a/examples/specialty_plots/radar_chart.py b/examples/specialty_plots/radar_chart.py index 751d83b40a2d..ae76308f52e6 100644 --- a/examples/specialty_plots/radar_chart.py +++ b/examples/specialty_plots/radar_chart.py @@ -8,10 +8,10 @@ Although this example allows a frame of either 'circle' or 'polygon', polygon frames don't have proper gridlines (the lines are circles instead of polygons). It's possible to get a polygon grid by setting GRIDLINE_INTERPOLATION_STEPS in -matplotlib.axis to the desired number of vertices, but the orientation of the +`matplotlib.axis` to the desired number of vertices, but the orientation of the polygon is not aligned with the radial axes. -.. [1] http://en.wikipedia.org/wiki/Radar_chart +.. [1] https://en.wikipedia.org/wiki/Radar_chart """ import numpy as np @@ -42,11 +42,20 @@ def radar_factory(num_vars, frame='circle'): # calculate evenly-spaced axis angles theta = np.linspace(0, 2*np.pi, num_vars, endpoint=False) + class RadarTransform(PolarAxes.PolarTransform): + + def transform_path_non_affine(self, path): + # Paths with non-unit interpolation steps correspond to gridlines, + # in which case we force interpolation (to defeat PolarTransform's + # autoconversion to circular arcs). + if path._interpolation_steps > 1: + path = path.interpolated(num_vars) + return Path(self.transform(path.vertices), path.codes) + class RadarAxes(PolarAxes): name = 'radar' - # use 1 line segment to connect specified points - RESOLUTION = 1 + PolarTransform = RadarTransform def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -122,7 +131,7 @@ def example_data(): # Organic Carbon fraction 1 (OC) # Organic Carbon fraction 2 (OC2) # Organic Carbon fraction 3 (OC3) - # Pyrolized Organic Carbon (OP) + # Pyrolyzed Organic Carbon (OP) # 2)Inclusion of gas-phase specie carbon monoxide (CO) # 3)Inclusion of gas-phase specie ozone (O3). # 4)Inclusion of both gas-phase species is present... @@ -175,7 +184,7 @@ def example_data(): horizontalalignment='center', verticalalignment='center') for d, color in zip(case_data, colors): ax.plot(theta, d, color=color) - ax.fill(theta, d, facecolor=color, alpha=0.25) + ax.fill(theta, d, facecolor=color, alpha=0.25, label='_nolegend_') ax.set_varlabels(spoke_labels) # add legend relative to top-left plot diff --git a/examples/specialty_plots/topographic_hillshading.py b/examples/specialty_plots/topographic_hillshading.py index 1b97a8a57808..b9a980448b7f 100644 --- a/examples/specialty_plots/topographic_hillshading.py +++ b/examples/specialty_plots/topographic_hillshading.py @@ -13,8 +13,8 @@ In most cases, hillshading is used purely for visual purposes, and *dx*/*dy* can be safely ignored. In that case, you can tweak *vert_exag* (vertical exaggeration) by trial and error to give the desired visual effect. However, -this example demonstrates how to use the *dx* and *dy* kwargs to ensure that -the *vert_exag* parameter is the true vertical exaggeration. +this example demonstrates how to use the *dx* and *dy* keyword arguments to +ensure that the *vert_exag* parameter is the true vertical exaggeration. """ import numpy as np import matplotlib.pyplot as plt @@ -24,7 +24,7 @@ dem = get_sample_data('jacksboro_fault_dem.npz', np_load=True) z = dem['elevation'] -#-- Optional dx and dy for accurate vertical exaggeration --------------------- +# -- Optional dx and dy for accurate vertical exaggeration -------------------- # If you need topographically accurate vertical exaggeration, or you don't want # to guess at what *vert_exag* should be, you'll need to specify the cellsize # of the grid (i.e. the *dx* and *dy* parameters). Otherwise, any *vert_exag* @@ -36,7 +36,7 @@ dx, dy = dem['dx'], dem['dy'] dy = 111200 * dy dx = 111200 * dx * np.cos(np.radians(dem['ymin'])) -#------------------------------------------------------------------------------ +# ----------------------------------------------------------------------------- # Shade from the northwest, with the sun 45 degrees from horizontal ls = LightSource(azdeg=315, altdeg=45) @@ -58,7 +58,7 @@ # Label rows and columns for ax, ve in zip(axs[0], [0.1, 1, 10]): - ax.set_title('{0}'.format(ve), size=18) + ax.set_title(f'{ve}', size=18) for ax, mode in zip(axs[:, 0], ['Hillshade', 'hsv', 'overlay', 'soft']): ax.set_ylabel(mode, size=18) diff --git a/examples/spines/README.txt b/examples/spines/README.txt new file mode 100644 index 000000000000..40bc3952eacd --- /dev/null +++ b/examples/spines/README.txt @@ -0,0 +1,4 @@ +.. _spines_examples: + +Spines +====== diff --git a/examples/ticks_and_spines/centered_spines_with_arrows.py b/examples/spines/centered_spines_with_arrows.py similarity index 100% rename from examples/ticks_and_spines/centered_spines_with_arrows.py rename to examples/spines/centered_spines_with_arrows.py diff --git a/examples/spines/multiple_yaxis_with_spines.py b/examples/spines/multiple_yaxis_with_spines.py new file mode 100644 index 000000000000..c1dc95a62612 --- /dev/null +++ b/examples/spines/multiple_yaxis_with_spines.py @@ -0,0 +1,46 @@ +r""" +=========================== +Multiple y-axis with Spines +=========================== + +Create multiple y axes with a shared x-axis. This is done by creating +a `~.axes.Axes.twinx` axes, turning all spines but the right one invisible +and offset its position using `~.spines.Spine.set_position`. + +Note that this approach uses `matplotlib.axes.Axes` and their +`~matplotlib.spines.Spine`\s. Alternative approaches using non-standard axes +are shown in the :doc:`/gallery/axisartist/demo_parasite_axes` and +:doc:`/gallery/axisartist/demo_parasite_axes2` examples. +""" + +import matplotlib.pyplot as plt + +fig, ax = plt.subplots() +fig.subplots_adjust(right=0.75) + +twin1 = ax.twinx() +twin2 = ax.twinx() + +# Offset the right spine of twin2. The ticks and label have already been +# placed on the right by twinx above. +twin2.spines.right.set_position(("axes", 1.2)) + +p1, = ax.plot([0, 1, 2], [0, 1, 2], "C0", label="Density") +p2, = twin1.plot([0, 1, 2], [0, 3, 2], "C1", label="Temperature") +p3, = twin2.plot([0, 1, 2], [50, 30, 15], "C2", label="Velocity") + +ax.set(xlim=(0, 2), ylim=(0, 2), xlabel="Distance", ylabel="Density") +twin1.set(ylim=(0, 4), ylabel="Temperature") +twin2.set(ylim=(1, 65), ylabel="Velocity") + +ax.yaxis.label.set_color(p1.get_color()) +twin1.yaxis.label.set_color(p2.get_color()) +twin2.yaxis.label.set_color(p3.get_color()) + +ax.tick_params(axis='y', colors=p1.get_color()) +twin1.tick_params(axis='y', colors=p2.get_color()) +twin2.tick_params(axis='y', colors=p3.get_color()) + +ax.legend(handles=[p1, p2, p3]) + +plt.show() diff --git a/examples/ticks_and_spines/spine_placement_demo.py b/examples/spines/spine_placement_demo.py similarity index 57% rename from examples/ticks_and_spines/spine_placement_demo.py rename to examples/spines/spine_placement_demo.py index e567d8160d9c..7d14d854f41a 100644 --- a/examples/ticks_and_spines/spine_placement_demo.py +++ b/examples/spines/spine_placement_demo.py @@ -1,12 +1,12 @@ """ =============== -Spine Placement +Spine placement =============== -Adjusting the location and appearance of axis spines. +The position of the axis spines can be influenced using `~.Spine.set_position`. Note: If you want to obtain arrow heads at the ends of the axes, also check -out the :doc:`/gallery/ticks_and_spines/centered_spines_with_arrows` example. +out the :doc:`/gallery/spines/centered_spines_with_arrows` example. """ import numpy as np import matplotlib.pyplot as plt @@ -14,49 +14,41 @@ ############################################################################### -fig = plt.figure() -x = np.linspace(-np.pi, np.pi, 100) +x = np.linspace(0, 2*np.pi, 100) y = 2 * np.sin(x) -ax = fig.add_subplot(2, 2, 1) -ax.set_title('centered spines') +fig, ax_dict = plt.subplot_mosaic( + [['center', 'zero'], + ['axes', 'data']] +) +fig.suptitle('Spine positions') + + +ax = ax_dict['center'] +ax.set_title("'center'") ax.plot(x, y) -ax.spines.left.set_position('center') -ax.spines.right.set_color('none') -ax.spines.bottom.set_position('center') -ax.spines.top.set_color('none') -ax.xaxis.set_ticks_position('bottom') -ax.yaxis.set_ticks_position('left') +ax.spines[['left', 'bottom']].set_position('center') +ax.spines[['top', 'right']].set_visible(False) -ax = fig.add_subplot(2, 2, 2) -ax.set_title('zeroed spines') +ax = ax_dict['zero'] +ax.set_title("'zero'") ax.plot(x, y) -ax.spines.left.set_position('zero') -ax.spines.right.set_color('none') -ax.spines.bottom.set_position('zero') -ax.spines.top.set_color('none') -ax.xaxis.set_ticks_position('bottom') -ax.yaxis.set_ticks_position('left') +ax.spines[['left', 'bottom']].set_position('zero') +ax.spines[['top', 'right']].set_visible(False) -ax = fig.add_subplot(2, 2, 3) -ax.set_title('spines at axes (0.6, 0.1)') +ax = ax_dict['axes'] +ax.set_title("'axes' (0.2, 0.2)") ax.plot(x, y) -ax.spines.left.set_position(('axes', 0.6)) -ax.spines.right.set_color('none') -ax.spines.bottom.set_position(('axes', 0.1)) -ax.spines.top.set_color('none') -ax.xaxis.set_ticks_position('bottom') -ax.yaxis.set_ticks_position('left') +ax.spines.left.set_position(('axes', 0.2)) +ax.spines.bottom.set_position(('axes', 0.2)) +ax.spines[['top', 'right']].set_visible(False) -ax = fig.add_subplot(2, 2, 4) -ax.set_title('spines at data (1, 2)') +ax = ax_dict['data'] +ax.set_title("'data' (1, 2)") ax.plot(x, y) ax.spines.left.set_position(('data', 1)) -ax.spines.right.set_color('none') ax.spines.bottom.set_position(('data', 2)) -ax.spines.top.set_color('none') -ax.xaxis.set_ticks_position('bottom') -ax.yaxis.set_ticks_position('left') +ax.spines[['top', 'right']].set_visible(False) ############################################################################### # Define a method that adjusts the location of the axis spines diff --git a/examples/spines/spines.py b/examples/spines/spines.py new file mode 100644 index 000000000000..659f9fc3e8c2 --- /dev/null +++ b/examples/spines/spines.py @@ -0,0 +1,56 @@ +""" +====== +Spines +====== + +This demo compares: + +- normal Axes, with spines on all four sides; +- an Axes with spines only on the left and bottom; +- an Axes using custom bounds to limit the extent of the spine. + +Each `.axes.Axes` has a list of `.Spine` objects, accessible +via the container ``ax.spines``. + +.. redirect-from:: /gallery/spines/spines_bounds + +""" +import numpy as np +import matplotlib.pyplot as plt + + +x = np.linspace(0, 2 * np.pi, 100) +y = 2 * np.sin(x) + +# Constrained layout makes sure the labels don't overlap the axes. +fig, (ax0, ax1, ax2) = plt.subplots(nrows=3, layout='constrained') + +ax0.plot(x, y) +ax0.set_title('normal spines') + +ax1.plot(x, y) +ax1.set_title('bottom-left spines') + +# Hide the right and top spines +ax1.spines.right.set_visible(False) +ax1.spines.top.set_visible(False) + +ax2.plot(x, y) +ax2.set_title('spines with bounds limited to data range') + +# Only draw spines for the data range, not in the margins +ax2.spines.bottom.set_bounds(x.min(), x.max()) +ax2.spines.left.set_bounds(y.min(), y.max()) +# Hide the right and top spines +ax2.spines.right.set_visible(False) +ax2.spines.top.set_visible(False) + +plt.show() + +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.spines.Spine.set_visible` +# - `matplotlib.spines.Spine.set_bounds` diff --git a/examples/ticks_and_spines/spines_dropped.py b/examples/spines/spines_dropped.py similarity index 62% rename from examples/ticks_and_spines/spines_dropped.py rename to examples/spines/spines_dropped.py index 954e1c7ffb3a..94487e86d1d3 100644 --- a/examples/ticks_and_spines/spines_dropped.py +++ b/examples/spines/spines_dropped.py @@ -18,13 +18,8 @@ ax.set_title('dropped spines') # Move left and bottom spines outward by 10 points -ax.spines.left.set_position(('outward', 10)) -ax.spines.bottom.set_position(('outward', 10)) +ax.spines[['left', 'bottom']].set_position(('outward', 10)) # Hide the right and top spines -ax.spines.right.set_visible(False) -ax.spines.top.set_visible(False) -# Only show ticks on the left and bottom spines -ax.yaxis.set_ticks_position('left') -ax.xaxis.set_ticks_position('bottom') +ax.spines[['top', 'right']].set_visible(False) plt.show() diff --git a/examples/statistics/barchart_demo.py b/examples/statistics/barchart_demo.py index 8c549b0cdb70..d91a093d21cb 100644 --- a/examples/statistics/barchart_demo.py +++ b/examples/statistics/barchart_demo.py @@ -15,23 +15,16 @@ just make up some data for little Johnny Doe. """ +from collections import namedtuple import numpy as np import matplotlib.pyplot as plt -from matplotlib.ticker import MaxNLocator -from collections import namedtuple -np.random.seed(42) Student = namedtuple('Student', ['name', 'grade', 'gender']) -Score = namedtuple('Score', ['score', 'percentile']) +Score = namedtuple('Score', ['value', 'unit', 'percentile']) -# GLOBAL CONSTANTS -test_names = ['Pacer Test', 'Flexed Arm\n Hang', 'Mile Run', 'Agility', - 'Push Ups'] -test_units = dict(zip(test_names, ['laps', 'sec', 'min:sec', 'sec', ''])) - -def attach_ordinal(num): +def to_ordinal(num): """Convert an integer to an ordinal string, e.g. 2 -> '2nd'.""" suffixes = {str(i): v for i, v in enumerate(['th', 'st', 'nd', 'rd', 'th', @@ -43,120 +36,68 @@ def attach_ordinal(num): return v + suffixes[v[-1]] -def format_score(score, test): +def format_score(score): """ Create score labels for the right y-axis as the test name followed by the measurement unit (if any), split over two lines. """ - unit = test_units[test] - if unit: - return f'{score}\n{unit}' - else: # If no unit, don't include a newline, so that label stays centered. - return score - + return f'{score.value}\n{score.unit}' if score.unit else str(score.value) -def format_ycursor(y): - y = int(y) - if y < 0 or y >= len(test_names): - return '' - else: - return test_names[y] - -def plot_student_results(student, scores, cohort_size): - fig, ax1 = plt.subplots(figsize=(9, 7)) # Create the figure - fig.subplots_adjust(left=0.115, right=0.88) +def plot_student_results(student, scores_by_test, cohort_size): + fig, ax1 = plt.subplots(figsize=(9, 7), layout='constrained') fig.canvas.manager.set_window_title('Eldorado K-8 Fitness Chart') - pos = np.arange(len(test_names)) - - rects = ax1.barh(pos, [scores[k].percentile for k in test_names], - align='center', - height=0.5, - tick_label=test_names) - ax1.set_title(student.name) + ax1.set_xlabel( + 'Percentile Ranking Across {grade} Grade {gender}s\n' + 'Cohort Size: {cohort_size}'.format( + grade=to_ordinal(student.grade), + gender=student.gender.title(), + cohort_size=cohort_size)) + + test_names = list(scores_by_test.keys()) + percentiles = [score.percentile for score in scores_by_test.values()] + + rects = ax1.barh(test_names, percentiles, align='center', height=0.5) + # Partition the percentile values to be able to draw large numbers in + # white within the bar, and small numbers in black outside the bar. + large_percentiles = [to_ordinal(p) if p > 40 else '' for p in percentiles] + small_percentiles = [to_ordinal(p) if p <= 40 else '' for p in percentiles] + ax1.bar_label(rects, small_percentiles, + padding=5, color='black', fontweight='bold') + ax1.bar_label(rects, large_percentiles, + padding=-32, color='white', fontweight='bold') ax1.set_xlim([0, 100]) - ax1.xaxis.set_major_locator(MaxNLocator(11)) + ax1.set_xticks([0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100]) ax1.xaxis.grid(True, linestyle='--', which='major', color='grey', alpha=.25) - - # Plot a solid vertical gridline to highlight the median position - ax1.axvline(50, color='grey', alpha=0.25) + ax1.axvline(50, color='grey', alpha=0.25) # median position # Set the right-hand Y-axis ticks and labels ax2 = ax1.twinx() - - # Set the tick locations - ax2.set_yticks(pos) # Set equal limits on both yaxis so that the ticks line up ax2.set_ylim(ax1.get_ylim()) - - # Set the tick labels - ax2.set_yticklabels([format_score(scores[k].score, k) for k in test_names]) + # Set the tick locations and labels + ax2.set_yticks( + np.arange(len(scores_by_test)), + labels=[format_score(score) for score in scores_by_test.values()]) ax2.set_ylabel('Test Scores') - xlabel = ('Percentile Ranking Across {grade} Grade {gender}s\n' - 'Cohort Size: {cohort_size}') - ax1.set_xlabel(xlabel.format(grade=attach_ordinal(student.grade), - gender=student.gender.title(), - cohort_size=cohort_size)) - - rect_labels = [] - # Lastly, write in the ranking inside each bar to aid in interpretation - for rect in rects: - # Rectangle widths are already integer-valued but are floating - # type, so it helps to remove the trailing decimal point and 0 by - # converting width to int type - width = int(rect.get_width()) - - rank_str = attach_ordinal(width) - # The bars aren't wide enough to print the ranking inside - if width < 40: - # Shift the text to the right side of the right edge - xloc = 5 - # Black against white background - clr = 'black' - align = 'left' - else: - # Shift the text to the left side of the right edge - xloc = -5 - # White on magenta - clr = 'white' - align = 'right' - - # Center the text vertically in the bar - yloc = rect.get_y() + rect.get_height() / 2 - label = ax1.annotate( - rank_str, xy=(width, yloc), xytext=(xloc, 0), - textcoords="offset points", - horizontalalignment=align, verticalalignment='center', - color=clr, weight='bold', clip_on=True) - rect_labels.append(label) - - # Make the interactive mouse over give the bar title - ax2.fmt_ydata = format_ycursor - # Return all of the artists created - return {'fig': fig, - 'ax': ax1, - 'ax_right': ax2, - 'bars': rects, - 'perc_labels': rect_labels} - - -student = Student('Johnny Doe', 2, 'boy') -scores = dict(zip( - test_names, - (Score(v, p) for v, p in - zip(['7', '48', '12:52', '17', '14'], - np.round(np.random.uniform(0, 100, len(test_names)), 0))))) -cohort_size = 62 # The number of other 2nd grade boys - -arts = plot_student_results(student, scores, cohort_size) -plt.show() +student = Student(name='Johnny Doe', grade=2, gender='Boy') +scores_by_test = { + 'Pacer Test': Score(7, 'laps', percentile=37), + 'Flexed Arm\n Hang': Score(48, 'sec', percentile=95), + 'Mile Run': Score('12:52', 'min:sec', percentile=73), + 'Agility': Score(17, 'sec', percentile=60), + 'Push Ups': Score(14, '', percentile=16), +} + +plot_student_results(student, scores_by_test, cohort_size=62) +plt.show() ############################################################################# # @@ -166,5 +107,5 @@ def plot_student_results(student, scores, cohort_size): # in this example: # # - `matplotlib.axes.Axes.bar` / `matplotlib.pyplot.bar` -# - `matplotlib.axes.Axes.annotate` / `matplotlib.pyplot.annotate` +# - `matplotlib.axes.Axes.bar_label` / `matplotlib.pyplot.bar_label` # - `matplotlib.axes.Axes.twinx` / `matplotlib.pyplot.twinx` diff --git a/examples/statistics/boxplot.py b/examples/statistics/boxplot.py index 7ade156c7bb4..fa8500cfc42f 100644 --- a/examples/statistics/boxplot.py +++ b/examples/statistics/boxplot.py @@ -3,16 +3,15 @@ Artist customization in box plots ================================= -This example demonstrates how to use the various kwargs -to fully customize box plots. The first figure demonstrates -how to remove and add individual components (note that the -mean is the only value not shown by default). The second -figure demonstrates how the styles of the artists can -be customized. It also demonstrates how to set the limit -of the whiskers to specific percentiles (lower right axes) - -A good general reference on boxplots and their history can be found -here: http://vita.had.co.nz/papers/boxplots.pdf +This example demonstrates how to use the various keyword arguments to fully +customize box plots. The first figure demonstrates how to remove and add +individual components (note that the mean is the only value not shown by +default). The second figure demonstrates how the styles of the artists can be +customized. It also demonstrates how to set the limit of the whiskers to +specific percentiles (lower right axes) + +A good general reference on boxplots and their history can be found here: +https://vita.had.co.nz/papers/boxplots.pdf """ diff --git a/examples/statistics/boxplot_demo.py b/examples/statistics/boxplot_demo.py index b3bb44a1a96c..252ca25bd58d 100644 --- a/examples/statistics/boxplot_demo.py +++ b/examples/statistics/boxplot_demo.py @@ -8,6 +8,8 @@ The following examples show off how to visualize boxplots with Matplotlib. There are many options to control their appearance and the statistics that they use to summarize the data. + +.. redirect-from:: /gallery/pyplots/boxplot_demo_pyplot """ import matplotlib.pyplot as plt @@ -105,7 +107,7 @@ fig.canvas.manager.set_window_title('A Boxplot Example') fig.subplots_adjust(left=0.075, right=0.95, top=0.9, bottom=0.25) -bp = ax1.boxplot(data, notch=0, sym='+', vert=1, whis=1.5) +bp = ax1.boxplot(data, notch=False, sym='+', vert=True, whis=1.5) plt.setp(bp['boxes'], color='black') plt.setp(bp['whiskers'], color='black') plt.setp(bp['fliers'], color='red', marker='+') @@ -221,7 +223,7 @@ def fake_bootstrapper(n): fig, ax = plt.subplots() pos = np.arange(len(treatments)) + 1 bp = ax.boxplot(treatments, sym='k+', positions=pos, - notch=1, bootstrap=5000, + notch=True, bootstrap=5000, usermedians=medians, conf_intervals=conf_intervals) @@ -231,6 +233,18 @@ def fake_bootstrapper(n): plt.setp(bp['fliers'], markersize=3.0) plt.show() + +############################################################################### +# Here we customize the widths of the caps . + +x = np.linspace(-7, 7, 140) +x = np.hstack([-25, x, 25]) +fig, ax = plt.subplots() + +ax.boxplot([x, x], notch=True, capwidths=[0.01, 0.2]) + +plt.show() + ############################################################################# # # .. admonition:: References diff --git a/examples/statistics/boxplot_vs_violin.py b/examples/statistics/boxplot_vs_violin.py index 6f5d8de06f3b..f1162a1ef7a7 100644 --- a/examples/statistics/boxplot_vs_violin.py +++ b/examples/statistics/boxplot_vs_violin.py @@ -45,13 +45,11 @@ # adding horizontal grid lines for ax in axs: ax.yaxis.grid(True) - ax.set_xticks([y + 1 for y in range(len(all_data))]) + ax.set_xticks([y + 1 for y in range(len(all_data))], + labels=['x1', 'x2', 'x3', 'x4']) ax.set_xlabel('Four separate samples') ax.set_ylabel('Observed values') -# add x-tick labels -plt.setp(axs, xticks=[y + 1 for y in range(len(all_data))], - xticklabels=['x1', 'x2', 'x3', 'x4']) plt.show() ############################################################################# diff --git a/examples/statistics/confidence_ellipse.py b/examples/statistics/confidence_ellipse.py index 16b9d32c557e..c67da152ad7d 100644 --- a/examples/statistics/confidence_ellipse.py +++ b/examples/statistics/confidence_ellipse.py @@ -34,9 +34,9 @@ # # The radiuses of the ellipse can be controlled by n_std which is the number # of standard deviations. The default value is 3 which makes the ellipse -# enclose 99.4% of the points if the data is normally distributed +# enclose 98.9% of the points if the data is normally distributed # like in these examples (3 standard deviations in 1-D contain 99.7% -# of the data, which is 99.4% of the data in 2-D). +# of the data, which is 98.9% of the data in 2-D). def confidence_ellipse(x, y, ax, n_std=3.0, facecolor='none', **kwargs): @@ -67,19 +67,19 @@ def confidence_ellipse(x, y, ax, n_std=3.0, facecolor='none', **kwargs): cov = np.cov(x, y) pearson = cov[0, 1]/np.sqrt(cov[0, 0] * cov[1, 1]) # Using a special case to obtain the eigenvalues of this - # two-dimensionl dataset. + # two-dimensional dataset. ell_radius_x = np.sqrt(1 + pearson) ell_radius_y = np.sqrt(1 - pearson) ellipse = Ellipse((0, 0), width=ell_radius_x * 2, height=ell_radius_y * 2, facecolor=facecolor, **kwargs) - # Calculating the stdandard deviation of x from + # Calculating the standard deviation of x from # the squareroot of the variance and multiplying # with the given number of standard deviations. scale_x = np.sqrt(cov[0, 0]) * n_std mean_x = np.mean(x) - # calculating the stdandard deviation of y ... + # calculating the standard deviation of y ... scale_y = np.sqrt(cov[1, 1]) * n_std mean_y = np.mean(y) @@ -97,7 +97,7 @@ def confidence_ellipse(x, y, ax, n_std=3.0, facecolor='none', **kwargs): # A helper function to create a correlated dataset # """""""""""""""""""""""""""""""""""""""""""""""" # -# Creates a random two-dimesional dataset with the specified +# Creates a random two-dimensional dataset with the specified # two-dimensional mean (mu) and dimensions (scale). # The correlation can be controlled by the param 'dependency', # a 2x2 matrix. @@ -190,7 +190,7 @@ def get_correlated_dataset(n, dependency, mu, scale): # Using the keyword arguments # """"""""""""""""""""""""""" # -# Use the kwargs specified for matplotlib.patches.Patch in order +# Use the keyword arguments specified for `matplotlib.patches.Patch` in order # to have the ellipse rendered in different ways. fig, ax_kwargs = plt.subplots(figsize=(6, 6)) @@ -210,7 +210,7 @@ def get_correlated_dataset(n, dependency, mu, scale): ax_kwargs.scatter(x, y, s=0.5) ax_kwargs.scatter(mu[0], mu[1], c='red', s=3) -ax_kwargs.set_title('Using kwargs') +ax_kwargs.set_title('Using keyword arguments') fig.subplots_adjust(hspace=0.25) plt.show() diff --git a/examples/statistics/customized_violin.py b/examples/statistics/customized_violin.py index 22ef8407b832..59978815e57e 100644 --- a/examples/statistics/customized_violin.py +++ b/examples/statistics/customized_violin.py @@ -3,12 +3,11 @@ Violin plot customization ========================= -This example demonstrates how to fully customize violin plots. -The first plot shows the default style by providing only -the data. The second plot first limits what matplotlib draws -with additional kwargs. Then a simplified representation of -a box plot is drawn on top. Lastly, the styles of the artists -of the violins are modified. +This example demonstrates how to fully customize violin plots. The first plot +shows the default style by providing only the data. The second plot first +limits what Matplotlib draws with additional keyword arguments. Then a +simplified representation of a box plot is drawn on top. Lastly, the styles of +the artists of the violins are modified. For more information on violin plots, the scikit-learn docs have a great section: https://scikit-learn.org/stable/modules/density.html @@ -28,10 +27,7 @@ def adjacent_values(vals, q1, q3): def set_axis_style(ax, labels): - ax.xaxis.set_tick_params(direction='out') - ax.xaxis.set_ticks_position('bottom') - ax.set_xticks(np.arange(1, len(labels) + 1)) - ax.set_xticklabels(labels) + ax.set_xticks(np.arange(1, len(labels) + 1), labels=labels) ax.set_xlim(0.25, len(labels) + 0.75) ax.set_xlabel('Sample name') diff --git a/examples/statistics/errorbars_and_boxes.py b/examples/statistics/errorbars_and_boxes.py index 582edfbb27e1..a8bdd7a46384 100644 --- a/examples/statistics/errorbars_and_boxes.py +++ b/examples/statistics/errorbars_and_boxes.py @@ -12,9 +12,9 @@ 1. an `~.axes.Axes` object is passed directly to the function 2. the function operates on the ``Axes`` methods directly, not through the ``pyplot`` interface - 3. plotting kwargs that could be abbreviated are spelled out for - better code readability in the future (for example we use - ``facecolor`` instead of ``fc``) + 3. plotting keyword arguments that could be abbreviated are spelled out for + better code readability in the future (for example we use *facecolor* + instead of *fc*) 4. the artists returned by the ``Axes`` plotting methods are then returned by the function so that, if desired, their styles can be modified later outside of the function (they are not @@ -40,7 +40,7 @@ def make_error_boxes(ax, xdata, ydata, xerror, yerror, facecolor='r', - edgecolor='None', alpha=0.5): + edgecolor='none', alpha=0.5): # Loop over data points; create box from errors at each point errorboxes = [Rectangle((x - xe[0], y - ye[0]), xe.sum(), ye.sum()) @@ -55,7 +55,7 @@ def make_error_boxes(ax, xdata, ydata, xerror, yerror, facecolor='r', # Plot errorbars artists = ax.errorbar(xdata, ydata, xerr=xerror, yerr=yerror, - fmt='None', ecolor='k') + fmt='none', ecolor='k') return artists diff --git a/examples/statistics/hexbin_demo.py b/examples/statistics/hexbin_demo.py index 759be95658f6..b97e4f6c4347 100644 --- a/examples/statistics/hexbin_demo.py +++ b/examples/statistics/hexbin_demo.py @@ -1,14 +1,10 @@ """ -=========== -Hexbin Demo -=========== +===================== +Hexagonal binned plot +===================== -Plotting hexbins with Matplotlib. - -Hexbin is an axes method or pyplot function that is essentially -a pcolor of a 2D histogram with hexagonal cells. It can be -much more informative than a scatter plot. In the first plot -below, try substituting 'scatter' for 'hexbin'. +`~.Axes.hexbin` is a 2D histogram plot, in which the bins are hexagons and +the color represents the number of data points within each bin. """ import numpy as np @@ -17,29 +13,23 @@ # Fixing random state for reproducibility np.random.seed(19680801) -n = 100000 +n = 100_000 x = np.random.standard_normal(n) y = 2.0 + 3.0 * x + 4.0 * np.random.standard_normal(n) -xmin = x.min() -xmax = x.max() -ymin = y.min() -ymax = y.max() - -fig, axs = plt.subplots(ncols=2, sharey=True, figsize=(7, 4)) -fig.subplots_adjust(hspace=0.5, left=0.07, right=0.93) -ax = axs[0] -hb = ax.hexbin(x, y, gridsize=50, cmap='inferno') -ax.set(xlim=(xmin, xmax), ylim=(ymin, ymax)) -ax.set_title("Hexagon binning") -cb = fig.colorbar(hb, ax=ax) -cb.set_label('counts') - -ax = axs[1] -hb = ax.hexbin(x, y, gridsize=50, bins='log', cmap='inferno') -ax.set(xlim=(xmin, xmax), ylim=(ymin, ymax)) -ax.set_title("With a log color scale") -cb = fig.colorbar(hb, ax=ax) -cb.set_label('log10(N)') +xlim = x.min(), x.max() +ylim = y.min(), y.max() + +fig, (ax0, ax1) = plt.subplots(ncols=2, sharey=True, figsize=(9, 4)) + +hb = ax0.hexbin(x, y, gridsize=50, cmap='inferno') +ax0.set(xlim=xlim, ylim=ylim) +ax0.set_title("Hexagon binning") +cb = fig.colorbar(hb, ax=ax0, label='counts') + +hb = ax1.hexbin(x, y, gridsize=50, bins='log', cmap='inferno') +ax1.set(xlim=xlim, ylim=ylim) +ax1.set_title("With a log color scale") +cb = fig.colorbar(hb, ax=ax1, label='log10(N)') plt.show() diff --git a/examples/statistics/hist.py b/examples/statistics/hist.py index 3ca0871f4d04..6c880961c922 100644 --- a/examples/statistics/hist.py +++ b/examples/statistics/hist.py @@ -3,7 +3,7 @@ Histograms ========== -Demonstrates how to plot histograms with matplotlib. +How to plot histograms with Matplotlib. """ import matplotlib.pyplot as plt @@ -11,9 +11,8 @@ from matplotlib import colors from matplotlib.ticker import PercentFormatter -# Fixing random state for reproducibility -np.random.seed(19680801) - +# Create a random number generator with a fixed seed for reproducibility +rng = np.random.default_rng(19680801) ############################################################################### # Generate data and plot a simple histogram @@ -26,15 +25,15 @@ N_points = 100000 n_bins = 20 -# Generate a normal distribution, center at x=0 and y=5 -x = np.random.randn(N_points) -y = .4 * x + np.random.randn(100000) + 5 +# Generate two normal distributions +dist1 = rng.standard_normal(N_points) +dist2 = 0.4 * rng.standard_normal(N_points) + 5 fig, axs = plt.subplots(1, 2, sharey=True, tight_layout=True) -# We can set the number of bins with the `bins` kwarg -axs[0].hist(x, bins=n_bins) -axs[1].hist(y, bins=n_bins) +# We can set the number of bins with the *bins* keyword argument. +axs[0].hist(dist1, bins=n_bins) +axs[1].hist(dist2, bins=n_bins) ############################################################################### @@ -49,7 +48,7 @@ fig, axs = plt.subplots(1, 2, tight_layout=True) # N is the count in each bin, bins is the lower-limit of the bin -N, bins, patches = axs[0].hist(x, bins=n_bins) +N, bins, patches = axs[0].hist(dist1, bins=n_bins) # We'll color code by height, but you could use any scalar fracs = N / N.max() @@ -63,7 +62,7 @@ thispatch.set_facecolor(color) # We can also normalize our inputs by the total number of counts -axs[1].hist(x, bins=n_bins, density=True) +axs[1].hist(dist1, bins=n_bins, density=True) # Now we format the y-axis to display percentage axs[1].yaxis.set_major_formatter(PercentFormatter(xmax=1)) @@ -77,7 +76,7 @@ # corresponding to each axis of the histogram. fig, ax = plt.subplots(tight_layout=True) -hist = ax.hist2d(x, y) +hist = ax.hist2d(dist1, dist2) ############################################################################### @@ -91,13 +90,13 @@ tight_layout=True) # We can increase the number of bins on each axis -axs[0].hist2d(x, y, bins=40) +axs[0].hist2d(dist1, dist2, bins=40) # As well as define normalization of the colors -axs[1].hist2d(x, y, bins=40, norm=colors.LogNorm()) +axs[1].hist2d(dist1, dist2, bins=40, norm=colors.LogNorm()) # We can also define custom numbers of bins for each axis -axs[2].hist2d(x, y, bins=(80, 10), norm=colors.LogNorm()) +axs[2].hist2d(dist1, dist2, bins=(80, 10), norm=colors.LogNorm()) plt.show() diff --git a/examples/statistics/histogram_cumulative.py b/examples/statistics/histogram_cumulative.py index 68151d027c44..443d30e69ee0 100644 --- a/examples/statistics/histogram_cumulative.py +++ b/examples/statistics/histogram_cumulative.py @@ -7,14 +7,13 @@ step function in order to visualize the empirical cumulative distribution function (CDF) of a sample. We also show the theoretical CDF. -A couple of other options to the ``hist`` function are demonstrated. -Namely, we use the ``normed`` parameter to normalize the histogram and -a couple of different options to the ``cumulative`` parameter. -The ``normed`` parameter takes a boolean value. When ``True``, the bin -heights are scaled such that the total area of the histogram is 1. The -``cumulative`` kwarg is a little more nuanced. Like ``normed``, you -can pass it True or False, but you can also pass it -1 to reverse the -distribution. +A couple of other options to the ``hist`` function are demonstrated. Namely, we +use the *normed* parameter to normalize the histogram and a couple of different +options to the *cumulative* parameter. The *normed* parameter takes a boolean +value. When ``True``, the bin heights are scaled such that the total area of +the histogram is 1. The *cumulative* keyword argument is a little more nuanced. +Like *normed*, you can pass it True or False, but you can also pass it -1 to +reverse the distribution. Since we're showing a normalized and cumulative histogram, these curves are effectively the cumulative distribution functions (CDFs) of the @@ -25,7 +24,7 @@ 225 on the x-axis corresponds to about 0.85 on the y-axis, so there's an 85% chance that an observation in the sample does not exceed 225. Conversely, setting, ``cumulative`` to -1 as is done in the -last series for this example, creates a "exceedance" curve. +last series for this example, creates an "exceedance" curve. Selecting different bin counts and sizes can significantly affect the shape of a histogram. The Astropy docs have a great section on how to diff --git a/examples/statistics/multiple_histograms_side_by_side.py b/examples/statistics/multiple_histograms_side_by_side.py index b62dbf175355..814229636682 100644 --- a/examples/statistics/multiple_histograms_side_by_side.py +++ b/examples/statistics/multiple_histograms_side_by_side.py @@ -9,7 +9,7 @@ to violin plots. To make this highly specialized plot, we can't use the standard ``hist`` -method. Instead we use ``barh`` to draw the horizontal bars directly. The +method. Instead, we use ``barh`` to draw the horizontal bars directly. The vertical positions and lengths of the bars are computed via the ``np.histogram`` function. The histograms for all the samples are computed using the same range (min and max values) and number of bins, @@ -45,8 +45,8 @@ # The bin_edges are the same for all of the histograms bin_edges = np.linspace(hist_range[0], hist_range[1], number_of_bins + 1) -centers = 0.5 * (bin_edges + np.roll(bin_edges, 1))[:-1] heights = np.diff(bin_edges) +centers = bin_edges[:-1] + heights / 2 # Cycle through and plot each histogram fig, ax = plt.subplots() @@ -54,8 +54,7 @@ lefts = x_loc - 0.5 * binned_data ax.barh(centers, binned_data, height=heights, left=lefts) -ax.set_xticks(x_locations) -ax.set_xticklabels(labels) +ax.set_xticks(x_locations, labels) ax.set_ylabel("Data values") ax.set_xlabel("Data sets") diff --git a/examples/statistics/time_series_histogram.py b/examples/statistics/time_series_histogram.py index 94f904fab91e..d35b6393504e 100644 --- a/examples/statistics/time_series_histogram.py +++ b/examples/statistics/time_series_histogram.py @@ -31,7 +31,7 @@ import matplotlib.pyplot as plt from matplotlib.colors import LogNorm -fig, axes = plt.subplots(nrows=3, figsize=(6, 8), constrained_layout=True) +fig, axes = plt.subplots(nrows=3, figsize=(6, 8), layout='constrained') # Make some data; a 1D random walk + small fraction of sine waves num_series = 1000 @@ -41,7 +41,7 @@ # Generate unbiased Gaussian random walks Y = np.cumsum(np.random.randn(num_series, num_points), axis=-1) # Generate sinusoidal signals -num_signal = int(round(SNR * num_series)) +num_signal = round(SNR * num_series) phi = (np.pi / 8) * np.random.randn(num_signal, 1) # small random offset Y[-num_signal:] = ( np.sqrt(np.arange(num_points))[None, :] # random walk RMS scaling factor diff --git a/examples/statistics/violinplot.py b/examples/statistics/violinplot.py index 7c9c894f7aec..7d519554a795 100644 --- a/examples/statistics/violinplot.py +++ b/examples/statistics/violinplot.py @@ -10,7 +10,7 @@ compute an empirical distribution of the sample. That computation is controlled by several parameters. This example demonstrates how to modify the number of points at which the KDE is evaluated (``points``) -and how to modify the band-width of the KDE (``bw_method``). +and how to modify the bandwidth of the KDE (``bw_method``). For more information on violin plots and KDE, the scikit-learn docs have a great section: https://scikit-learn.org/stable/modules/density.html diff --git a/examples/style_sheets/ggplot.py b/examples/style_sheets/ggplot.py index a1563f1bd0d2..52139c348b82 100644 --- a/examples/style_sheets/ggplot.py +++ b/examples/style_sheets/ggplot.py @@ -8,7 +8,7 @@ These settings were shamelessly stolen from [1]_ (with permission). -.. [1] https://web.archive.org/web/20111215111010/http://www.huyng.com/archives/sane-color-scheme-for-matplotlib/691/ +.. [1] https://everyhue.me/posts/sane-color-scheme-for-matplotlib/ .. _ggplot: https://ggplot2.tidyverse.org/ .. _R: https://www.r-project.org/ @@ -23,7 +23,7 @@ np.random.seed(19680801) fig, axs = plt.subplots(ncols=2, nrows=2) -ax1, ax2, ax3, ax4 = axs.ravel() +ax1, ax2, ax3, ax4 = axs.flat # scatter plot (Note: `plt.scatter` doesn't use default colors) x, y = np.random.normal(size=(2, 200)) @@ -45,8 +45,7 @@ ax3.bar(x, y1, width) ax3.bar(x + width, y2, width, color=list(plt.rcParams['axes.prop_cycle'])[2]['color']) -ax3.set_xticks(x + width) -ax3.set_xticklabels(['a', 'b', 'c', 'd', 'e']) +ax3.set_xticks(x + width, labels=['a', 'b', 'c', 'd', 'e']) # circles with colors from default color cycle for i, color in enumerate(plt.rcParams['axes.prop_cycle']): diff --git a/examples/style_sheets/plot_solarizedlight2.py b/examples/style_sheets/plot_solarizedlight2.py index 530e4027cdc6..5fa82b295932 100644 --- a/examples/style_sheets/plot_solarizedlight2.py +++ b/examples/style_sheets/plot_solarizedlight2.py @@ -6,7 +6,7 @@ This shows an example of "Solarized_Light" styling, which tries to replicate the styles of: -- http://ethanschoonover.com/solarized +- https://ethanschoonover.com/solarized/ - https://github.com/jrnold/ggthemes - http://www.pygal.org/en/stable/documentation/builtin_styles.html#light-solarized diff --git a/examples/style_sheets/style_sheets_reference.py b/examples/style_sheets/style_sheets_reference.py index 8bdeb0c62bc7..8557cc10a203 100644 --- a/examples/style_sheets/style_sheets_reference.py +++ b/examples/style_sheets/style_sheets_reference.py @@ -11,6 +11,8 @@ import numpy as np import matplotlib.pyplot as plt +import matplotlib.colors as mcolors +from matplotlib.patches import Rectangle # Fixing random state for reproducibility np.random.seed(19680801) @@ -26,15 +28,19 @@ def plot_scatter(ax, prng, nb_samples=100): return ax -def plot_colored_sinusoidal_lines(ax): - """Plot sinusoidal lines with colors following the style color cycle.""" - L = 2 * np.pi - x = np.linspace(0, L) +def plot_colored_lines(ax): + """Plot lines with colors following the style color cycle.""" + t = np.linspace(-10, 10, 100) + + def sigmoid(t, t0): + return 1 / (1 + np.exp(-(t - t0))) + nb_colors = len(plt.rcParams['axes.prop_cycle']) - shift = np.linspace(0, L, nb_colors, endpoint=False) - for s in shift: - ax.plot(x, np.sin(x + s), '-') - ax.set_xlim([x[0], x[-1]]) + shifts = np.linspace(-5, 5, nb_colors) + amplitudes = np.linspace(1, 1.5, nb_colors) + for t0, a in zip(shifts, amplitudes): + ax.plot(t, a * sigmoid(t, t0), '-') + ax.set_xlim(-10, 10) return ax @@ -45,8 +51,7 @@ def plot_bar_graphs(ax, prng, min_value=5, max_value=25, nb_samples=5): width = 0.25 ax.bar(x, ya, width) ax.bar(x + width, yb, width, color='C2') - ax.set_xticks(x + width) - ax.set_xticklabels(['a', 'b', 'c', 'd', 'e']) + ax.set_xticks(x + width, labels=['a', 'b', 'c', 'd', 'e']) return ax @@ -58,11 +63,15 @@ def plot_colored_circles(ax, prng, nb_samples=15): the color cycle, because different styles may have different numbers of colors. """ - for sty_dict, j in zip(plt.rcParams['axes.prop_cycle'], range(nb_samples)): + for sty_dict, j in zip(plt.rcParams['axes.prop_cycle'](), + range(nb_samples)): ax.add_patch(plt.Circle(prng.normal(scale=3, size=2), radius=1.0, color=sty_dict['color'])) - # Force the limits to be the same across the styles (because different - # styles may have different numbers of available colors). + ax.grid(visible=True) + + # Add title for enabling grid + plt.title('ax.grid(True)', family='monospace', fontsize='small') + ax.set_xlim([-4, 8]) ax.set_ylim([-5, 6]) ax.set_aspect('equal', adjustable='box') # to plot circles as circles @@ -87,6 +96,7 @@ def plot_histograms(ax, prng, nb_samples=10000): values = prng.beta(a, b, size=nb_samples) ax.hist(values, histtype="stepfilled", bins=30, alpha=0.8, density=True) + # Add a small annotation. ax.annotate('Annotation', xy=(0.25, 4.25), xytext=(0.9, 0.9), textcoords=ax.transAxes, @@ -105,40 +115,48 @@ def plot_figure(style_label=""): # across the different figures. prng = np.random.RandomState(96917002) - # Tweak the figure size to be better suited for a row of numerous plots: - # double the width and halve the height. NB: use relative changes because - # some styles may have a figure size different from the default one. - (fig_width, fig_height) = plt.rcParams['figure.figsize'] - fig_size = [fig_width * 2, fig_height / 2] - fig, axs = plt.subplots(ncols=6, nrows=1, num=style_label, - figsize=fig_size, squeeze=True) - axs[0].set_ylabel(style_label) + figsize=(14.8, 2.8), layout='constrained') + + # make a suptitle, in the same style for all subfigures, + # except those with dark backgrounds, which get a lighter color: + background_color = mcolors.rgb_to_hsv( + mcolors.to_rgb(plt.rcParams['figure.facecolor']))[2] + if background_color < 0.5: + title_color = [0.8, 0.8, 1] + else: + title_color = np.array([19, 6, 84]) / 256 + fig.suptitle(style_label, x=0.01, ha='left', color=title_color, + fontsize=14, fontfamily='DejaVu Sans', fontweight='normal') plot_scatter(axs[0], prng) plot_image_and_patch(axs[1], prng) plot_bar_graphs(axs[2], prng) - plot_colored_circles(axs[3], prng) - plot_colored_sinusoidal_lines(axs[4]) - plot_histograms(axs[5], prng) - - fig.tight_layout() + plot_colored_lines(axs[3]) + plot_histograms(axs[4], prng) + plot_colored_circles(axs[5], prng) - return fig + # add divider + rec = Rectangle((1 + 0.025, -2), 0.05, 16, + clip_on=False, color='gray') + axs[4].add_artist(rec) if __name__ == "__main__": - # Setup a list of all available styles, in alphabetical order but + # Set up a list of all available styles, in alphabetical order but # the `default` and `classic` ones, which will be forced resp. in # first and second position. + # styles with leading underscores are for internal use such as testing + # and plot types gallery. These are excluded here. style_list = ['default', 'classic'] + sorted( - style for style in plt.style.available if style != 'classic') + style for style in plt.style.available + if style != 'classic' and not style.startswith('_')) # Plot a demonstration figure for every available style sheet. for style_label in style_list: with plt.rc_context({"figure.max_open_warning": len(style_list)}): with plt.style.context(style_label): - fig = plot_figure(style_label=style_label) + plot_figure(style_label=style_label) plt.show() diff --git a/examples/subplots_axes_and_figures/align_labels_demo.py b/examples/subplots_axes_and_figures/align_labels_demo.py index 4be8081ee1f0..0e600b790c47 100644 --- a/examples/subplots_axes_and_figures/align_labels_demo.py +++ b/examples/subplots_axes_and_figures/align_labels_demo.py @@ -30,8 +30,7 @@ ax.set_ylabel('YLabel1 %d' % i) ax.set_xlabel('XLabel1 %d' % i) if i == 0: - for tick in ax.get_xticklabels(): - tick.set_rotation(55) + ax.tick_params(axis='x', rotation=55) fig.align_labels() # same as fig.align_xlabels(); fig.align_ylabels() plt.show() diff --git a/examples/subplots_axes_and_figures/auto_subplots_adjust.py b/examples/subplots_axes_and_figures/auto_subplots_adjust.py new file mode 100644 index 000000000000..bd6326b8291f --- /dev/null +++ b/examples/subplots_axes_and_figures/auto_subplots_adjust.py @@ -0,0 +1,86 @@ +""" +=============================================== +Programmatically controlling subplot adjustment +=============================================== + +.. note:: + + This example is primarily intended to show some advanced concepts in + Matplotlib. + + If you are only looking for having enough space for your labels, it is + almost always simpler and good enough to either set the subplot parameters + manually using `.Figure.subplots_adjust`, or use one of the automatic + layout mechanisms + (:doc:`/tutorials/intermediate/constrainedlayout_guide` or + :doc:`/tutorials/intermediate/tight_layout_guide`). + +This example describes a user-defined way to read out Artist sizes and +set the subplot parameters accordingly. Its main purpose is to illustrate +some advanced concepts like reading out text positions, working with +bounding boxes and transforms and using +:ref:`events `. But it can also serve as a starting +point if you want to automate the layouting and need more flexibility than +tight layout and constrained layout. + +Below, we collect the bounding boxes of all y-labels and move the left border +of the subplot to the right so that it leaves enough room for the union of all +the bounding boxes. + +There's one catch with calculating text bounding boxes: +Querying the text bounding boxes (`.Text.get_window_extent`) needs a +renderer (`.RendererBase` instance), to calculate the text size. This renderer +is only available after the figure has been drawn (`.Figure.draw`). + +A solution to this is putting the adjustment logic in a draw callback. +This function is executed after the figure has been drawn. It can now check +if the subplot leaves enough room for the text. If not, the subplot parameters +are updated and second draw is triggered. + +.. redirect-from:: /gallery/pyplots/auto_subplots_adjust +""" + +import matplotlib.pyplot as plt +import matplotlib.transforms as mtransforms + +fig, ax = plt.subplots() +ax.plot(range(10)) +ax.set_yticks([2, 5, 7], labels=['really, really, really', 'long', 'labels']) + + +def on_draw(event): + bboxes = [] + for label in ax.get_yticklabels(): + # Bounding box in pixels + bbox_px = label.get_window_extent() + # Transform to relative figure coordinates. This is the inverse of + # transFigure. + bbox_fig = bbox_px.transformed(fig.transFigure.inverted()) + bboxes.append(bbox_fig) + # the bbox that bounds all the bboxes, again in relative figure coords + bbox = mtransforms.Bbox.union(bboxes) + if fig.subplotpars.left < bbox.width: + # Move the subplot left edge more to the right + fig.subplots_adjust(left=1.1*bbox.width) # pad a little + fig.canvas.draw() + + +fig.canvas.mpl_connect('draw_event', on_draw) + +plt.show() + +############################################################################# +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.artist.Artist.get_window_extent` +# - `matplotlib.transforms.Bbox` +# - `matplotlib.transforms.BboxBase.transformed` +# - `matplotlib.transforms.BboxBase.union` +# - `matplotlib.transforms.Transform.inverted` +# - `matplotlib.figure.Figure.subplots_adjust` +# - `matplotlib.figure.SubplotParams` +# - `matplotlib.backend_bases.FigureCanvasBase.mpl_connect` diff --git a/examples/subplots_axes_and_figures/axes_box_aspect.py b/examples/subplots_axes_and_figures/axes_box_aspect.py index 4444f0f28366..089f84c26eea 100644 --- a/examples/subplots_axes_and_figures/axes_box_aspect.py +++ b/examples/subplots_axes_and_figures/axes_box_aspect.py @@ -3,7 +3,7 @@ Axes box aspect =============== -This demo shows how to set the aspect of an axes box directly via +This demo shows how to set the aspect of an Axes box directly via `~.Axes.set_box_aspect`. The box aspect is the ratio between axes height and axes width in physical units, independent of the data limits. This is useful to e.g. produce a square plot, independent of the data it @@ -74,10 +74,10 @@ # height. `~.Axes.set_box_aspect` provides an easy solution to that by allowing # to have the normal plot's axes use the images dimensions as box aspect. # -# This example also shows that ``constrained_layout`` interplays nicely with +# This example also shows that *constrained layout* interplays nicely with # a fixed box aspect. -fig4, (ax, ax2) = plt.subplots(ncols=2, constrained_layout=True) +fig4, (ax, ax2) = plt.subplots(ncols=2, layout="constrained") np.random.seed(19680801) # Fixing random state for reproducibility im = np.random.rand(16, 27) @@ -119,7 +119,7 @@ # ~~~~~~~~~~~~~~~~~~~~~~~~~~ # # When setting the box aspect, one may still set the data aspect as well. -# Here we create an axes with a box twice as long as tall and use an "equal" +# Here we create an Axes with a box twice as long as tall and use an "equal" # data aspect for its contents, i.e. the circle actually stays circular. fig6, ax = plt.subplots() @@ -135,11 +135,11 @@ # Box aspect for many subplots # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # -# It is possible to pass the box aspect to an axes at initialization. The -# following creates a 2 by 3 subplot grid with all square axes. +# It is possible to pass the box aspect to an Axes at initialization. The +# following creates a 2 by 3 subplot grid with all square Axes. fig7, axs = plt.subplots(2, 3, subplot_kw=dict(box_aspect=1), - sharex=True, sharey=True, constrained_layout=True) + sharex=True, sharey=True, layout="constrained") for i, ax in enumerate(axs.flat): ax.scatter(i % 3, -((i // 3) - 0.5)*200, c=[plt.cm.hsv(i / 6)], s=300) diff --git a/examples/subplots_axes_and_figures/axes_margins.py b/examples/subplots_axes_and_figures/axes_margins.py index d532f6138fe8..e84a6e835924 100644 --- a/examples/subplots_axes_and_figures/axes_margins.py +++ b/examples/subplots_axes_and_figures/axes_margins.py @@ -69,7 +69,7 @@ def f(t): ) # not sticky ax.margins(x=0.1, y=0.05) ax.set_aspect('equal') - ax.set_title('{} Sticky'.format(status)) + ax.set_title(f'{status} Sticky') plt.show() diff --git a/examples/subplots_axes_and_figures/axes_zoom_effect.py b/examples/subplots_axes_and_figures/axes_zoom_effect.py index b525cd7ccd41..4b7ff6e91e30 100644 --- a/examples/subplots_axes_and_figures/axes_zoom_effect.py +++ b/examples/subplots_axes_and_figures/axes_zoom_effect.py @@ -5,6 +5,8 @@ """ +import matplotlib.pyplot as plt + from matplotlib.transforms import ( Bbox, TransformedBbox, blended_transform_factory) from mpl_toolkits.axes_grid1.inset_locator import ( @@ -30,7 +32,6 @@ def connect_bbox(bbox1, bbox2, bbox_patch2 = BboxPatch(bbox2, **prop_patches) p = BboxConnectorPatch(bbox1, bbox2, - # loc1a=3, loc2a=2, loc1b=4, loc2b=1, loc1a=loc1a, loc2a=loc2a, loc1b=loc1b, loc2b=loc2b, clip_on=False, **prop_patches) @@ -107,19 +108,14 @@ def zoom_effect02(ax1, ax2, **kwargs): return c1, c2, bbox_patch1, bbox_patch2, p -import matplotlib.pyplot as plt - -plt.figure(figsize=(5, 5)) -ax1 = plt.subplot(221) -ax2 = plt.subplot(212) -ax2.set_xlim(0, 1) -ax2.set_xlim(0, 5) -zoom_effect01(ax1, ax2, 0.2, 0.8) - +axs = plt.figure().subplot_mosaic([ + ["zoom1", "zoom2"], + ["main", "main"], +]) -ax1 = plt.subplot(222) -ax1.set_xlim(2, 3) -ax2.set_xlim(0, 5) -zoom_effect02(ax1, ax2) +axs["main"].set(xlim=(0, 5)) +zoom_effect01(axs["zoom1"], axs["main"], 0.2, 0.8) +axs["zoom2"].set(xlim=(2, 3)) +zoom_effect02(axs["zoom2"], axs["main"]) plt.show() diff --git a/examples/subplots_axes_and_figures/colorbar_placement.py b/examples/subplots_axes_and_figures/colorbar_placement.py index 6beed5035856..7bd952247637 100644 --- a/examples/subplots_axes_and_figures/colorbar_placement.py +++ b/examples/subplots_axes_and_figures/colorbar_placement.py @@ -23,7 +23,6 @@ pcm = ax.pcolormesh(np.random.random((20, 20)) * (col + 1), cmap=cmaps[col]) fig.colorbar(pcm, ax=ax) -plt.show() ###################################################################### # The first column has the same type of data in both rows, so it may @@ -38,14 +37,13 @@ pcm = ax.pcolormesh(np.random.random((20, 20)) * (col + 1), cmap=cmaps[col]) fig.colorbar(pcm, ax=axs[:, col], shrink=0.6) -plt.show() ###################################################################### # Relatively complicated colorbar layouts are possible using this # paradigm. Note that this example works far better with -# ``constrained_layout=True`` +# ``layout='constrained'`` -fig, axs = plt.subplots(3, 3, constrained_layout=True) +fig, axs = plt.subplots(3, 3, layout='constrained') for ax in axs.flat: pcm = ax.pcolormesh(np.random.random((20, 20))) @@ -53,7 +51,6 @@ fig.colorbar(pcm, ax=[axs[0, 2]], location='bottom') fig.colorbar(pcm, ax=axs[1:, :], location='right', shrink=0.6) fig.colorbar(pcm, ax=[axs[2, 1]], location='left') -plt.show() ###################################################################### # Colorbars with fixed-aspect-ratio axes @@ -62,7 +59,7 @@ # Placing colorbars for axes with a fixed aspect ratio pose a particular # challenge as the parent axes changes size depending on the data view. -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout='constrained') cmaps = ['RdBu_r', 'viridis'] for col in range(2): for row in range(2): @@ -75,14 +72,13 @@ ax.set_aspect(1/2) if row == 1: fig.colorbar(pcm, ax=ax, shrink=0.6) -plt.show() ###################################################################### # One way around this issue is to use an `.Axes.inset_axes` to locate the -# axes in axes co-ordinates. Note that if you zoom in on the axes, and +# axes in axes coordinates. Note that if you zoom in on the axes, and # change the shape of the axes, the colorbar will also change position. -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout='constrained') cmaps = ['RdBu_r', 'viridis'] for col in range(2): for row in range(2): @@ -94,6 +90,7 @@ else: ax.set_aspect(1/2) if row == 1: - cax = ax.inset_axes([1.04, 0.2, 0.05, 0.6], transform=ax.transAxes) + cax = ax.inset_axes([1.04, 0.2, 0.05, 0.6]) fig.colorbar(pcm, ax=ax, cax=cax) + plt.show() diff --git a/examples/subplots_axes_and_figures/demo_constrained_layout.py b/examples/subplots_axes_and_figures/demo_constrained_layout.py index 26109cbe3129..bf2acd797c76 100644 --- a/examples/subplots_axes_and_figures/demo_constrained_layout.py +++ b/examples/subplots_axes_and_figures/demo_constrained_layout.py @@ -3,7 +3,7 @@ Resizing axes with constrained layout ===================================== -Constrained layout attempts to resize subplots in +*Constrained layout* attempts to resize subplots in a figure so that there are no overlaps between axes objects and labels on the axes. @@ -23,17 +23,17 @@ def example_plot(ax): ############################################################################### -# If we don't use constrained_layout, then labels overlap the axes +# If we don't use *constrained layout*, then labels overlap the axes -fig, axs = plt.subplots(nrows=2, ncols=2, constrained_layout=False) +fig, axs = plt.subplots(nrows=2, ncols=2, layout=None) for ax in axs.flat: example_plot(ax) ############################################################################### -# adding ``constrained_layout=True`` automatically adjusts. +# adding ``layout='constrained'`` automatically adjusts. -fig, axs = plt.subplots(nrows=2, ncols=2, constrained_layout=True) +fig, axs = plt.subplots(nrows=2, ncols=2, layout='constrained') for ax in axs.flat: example_plot(ax) @@ -41,7 +41,7 @@ def example_plot(ax): ############################################################################### # Below is a more complicated example using nested gridspecs. -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout='constrained') import matplotlib.gridspec as gridspec diff --git a/examples/subplots_axes_and_figures/demo_tight_layout.py b/examples/subplots_axes_and_figures/demo_tight_layout.py index edf79910c590..e3f22618ede5 100644 --- a/examples/subplots_axes_and_figures/demo_tight_layout.py +++ b/examples/subplots_axes_and_figures/demo_tight_layout.py @@ -3,9 +3,8 @@ Resizing axes with tight layout =============================== -`~.figure.Figure.tight_layout` attempts to resize subplots in -a figure so that there are no overlaps between axes objects and labels -on the axes. +`~.Figure.tight_layout` attempts to resize subplots in a figure so that there +are no overlaps between axes objects and labels on the axes. See :doc:`/tutorials/intermediate/tight_layout_guide` for more details and :doc:`/tutorials/intermediate/constrainedlayout_guide` for an alternative. @@ -31,7 +30,7 @@ def example_plot(ax): fig, ax = plt.subplots() example_plot(ax) -plt.tight_layout() +fig.tight_layout() ############################################################################### @@ -40,61 +39,53 @@ def example_plot(ax): example_plot(ax2) example_plot(ax3) example_plot(ax4) -plt.tight_layout() +fig.tight_layout() ############################################################################### fig, (ax1, ax2) = plt.subplots(nrows=2, ncols=1) example_plot(ax1) example_plot(ax2) -plt.tight_layout() +fig.tight_layout() ############################################################################### fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2) example_plot(ax1) example_plot(ax2) -plt.tight_layout() +fig.tight_layout() ############################################################################### fig, axs = plt.subplots(nrows=3, ncols=3) for ax in axs.flat: example_plot(ax) -plt.tight_layout() +fig.tight_layout() ############################################################################### -fig = plt.figure() - +plt.figure() ax1 = plt.subplot(221) ax2 = plt.subplot(223) ax3 = plt.subplot(122) - example_plot(ax1) example_plot(ax2) example_plot(ax3) - plt.tight_layout() ############################################################################### -fig = plt.figure() - +plt.figure() ax1 = plt.subplot2grid((3, 3), (0, 0)) ax2 = plt.subplot2grid((3, 3), (0, 1), colspan=2) ax3 = plt.subplot2grid((3, 3), (1, 0), colspan=2, rowspan=2) ax4 = plt.subplot2grid((3, 3), (1, 2), rowspan=2) - example_plot(ax1) example_plot(ax2) example_plot(ax3) example_plot(ax4) - plt.tight_layout() -plt.show() - ############################################################################### fig = plt.figure() @@ -103,20 +94,16 @@ def example_plot(ax): ax1 = fig.add_subplot(gs1[0]) ax2 = fig.add_subplot(gs1[1]) ax3 = fig.add_subplot(gs1[2]) - example_plot(ax1) example_plot(ax2) example_plot(ax3) - gs1.tight_layout(fig, rect=[None, None, 0.45, None]) gs2 = fig.add_gridspec(2, 1) ax4 = fig.add_subplot(gs2[0]) ax5 = fig.add_subplot(gs2[1]) - example_plot(ax4) example_plot(ax5) - with warnings.catch_warnings(): # gs2.tight_layout cannot handle the subplots from the first gridspec # (gs1), so it will raise a warning. We are going to match the gridspecs @@ -140,7 +127,8 @@ def example_plot(ax): # The use of the following functions, methods, classes and modules is shown # in this example: # -# - `matplotlib.figure.Figure.tight_layout` / `matplotlib.pyplot.tight_layout` +# - `matplotlib.figure.Figure.tight_layout` / +# `matplotlib.pyplot.tight_layout` # - `matplotlib.figure.Figure.add_gridspec` # - `matplotlib.figure.Figure.add_subplot` # - `matplotlib.pyplot.subplot2grid` diff --git a/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py b/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py index 1212f5a37f78..07b447c97521 100644 --- a/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py +++ b/examples/subplots_axes_and_figures/fahrenheit_celsius_scales.py @@ -3,7 +3,7 @@ Different scales on the same axes ================================= -Demo of how to display two scales on the left and right y axis. +Demo of how to display two scales on the left and right y-axis. This example uses the Fahrenheit and Celsius scales. """ @@ -23,7 +23,7 @@ def make_plot(): # Define a closure function to register as a callback def convert_ax_c_to_celsius(ax_f): """ - Update second axis according with first axis. + Update second axis according to first axis. """ y1, y2 = ax_f.get_ylim() ax_c.set_ylim(fahrenheit2celsius(y1), fahrenheit2celsius(y2)) diff --git a/examples/subplots_axes_and_figures/figure_size_units.py b/examples/subplots_axes_and_figures/figure_size_units.py index 82063ad4577d..94de72c1554c 100644 --- a/examples/subplots_axes_and_figures/figure_size_units.py +++ b/examples/subplots_axes_and_figures/figure_size_units.py @@ -56,15 +56,15 @@ # tedious for quick iterations. # # Because of the default ``rcParams['figure.dpi'] = 100``, one can mentally -# divide the needed pixel value by 100 [*]_: +# divide the needed pixel value by 100 [#]_: # plt.subplots(figsize=(6, 2)) plt.text(0.5, 0.5, '600px x 200px', **text_kwargs) plt.show() ############################################################################# -# .. [*] Unfortunately, this does not work well for the ``matplotlib inline`` -# backend in jupyter because that backend uses a different default of +# .. [#] Unfortunately, this does not work well for the ``matplotlib inline`` +# backend in Jupyter because that backend uses a different default of # ``rcParams['figure.dpi'] = 72``. Additionally, it saves the figure # with ``bbox_inches='tight'``, which crops the figure and makes the # actual size unpredictable. diff --git a/examples/subplots_axes_and_figures/figure_title.py b/examples/subplots_axes_and_figures/figure_title.py index 0b8a7e2c5855..195bab34d6be 100644 --- a/examples/subplots_axes_and_figures/figure_title.py +++ b/examples/subplots_axes_and_figures/figure_title.py @@ -18,7 +18,7 @@ x = np.linspace(0.0, 5.0, 501) -fig, (ax1, ax2) = plt.subplots(1, 2, constrained_layout=True, sharey=True) +fig, (ax1, ax2) = plt.subplots(1, 2, layout='constrained', sharey=True) ax1.plot(x, np.cos(6*x) * np.exp(-x)) ax1.set_title('damped') ax1.set_xlabel('time (s)') @@ -34,7 +34,7 @@ # A global x- or y-label can be set using the `.FigureBase.supxlabel` and # `.FigureBase.supylabel` methods. -fig, axs = plt.subplots(3, 5, figsize=(8, 5), constrained_layout=True, +fig, axs = plt.subplots(3, 5, figsize=(8, 5), layout='constrained', sharex=True, sharey=True) fname = get_sample_data('percent_bachelors_degrees_women_usa.csv', diff --git a/examples/subplots_axes_and_figures/ganged_plots.py b/examples/subplots_axes_and_figures/ganged_plots.py index 4014dabdf046..b32d2aed83d6 100644 --- a/examples/subplots_axes_and_figures/ganged_plots.py +++ b/examples/subplots_axes_and_figures/ganged_plots.py @@ -8,8 +8,8 @@ will automatically turn off all x ticks and labels except those on the bottom axis. -In this example the plots share a common x axis but you can follow the same -logic to supply a common y axis. +In this example the plots share a common x-axis, but you can follow the same +logic to supply a common y-axis. """ import matplotlib.pyplot as plt import numpy as np @@ -21,7 +21,7 @@ s3 = s1 * s2 fig, axs = plt.subplots(3, 1, sharex=True) -# Remove horizontal space between axes +# Remove vertical space between axes fig.subplots_adjust(hspace=0) # Plot each graph, and manually set the y tick values diff --git a/examples/subplots_axes_and_figures/geo_demo.py b/examples/subplots_axes_and_figures/geo_demo.py index 562f9d710438..27b6f27ae6e4 100644 --- a/examples/subplots_axes_and_figures/geo_demo.py +++ b/examples/subplots_axes_and_figures/geo_demo.py @@ -6,7 +6,7 @@ This shows 4 possible geographic projections. Cartopy_ supports more projections. -.. _Cartopy: http://scitools.org.uk/cartopy +.. _Cartopy: https://scitools.org.uk/cartopy/ """ import matplotlib.pyplot as plt diff --git a/examples/subplots_axes_and_figures/gridspec_and_subplots.py b/examples/subplots_axes_and_figures/gridspec_and_subplots.py index fbb7ebd013a0..133fd37b5d71 100644 --- a/examples/subplots_axes_and_figures/gridspec_and_subplots.py +++ b/examples/subplots_axes_and_figures/gridspec_and_subplots.py @@ -8,7 +8,10 @@ and then remove the covered axes and fill the gap with a new bigger axes. Here we create a layout with the bottom two axes in the last column combined. -See also :doc:`/tutorials/intermediate/gridspec`. +To start with this layout (rather than removing the overlapping axes) use +`~.pyplot.subplot_mosaic`. + +See also :doc:`/tutorials/intermediate/arranging_axes`. """ import matplotlib.pyplot as plt diff --git a/examples/subplots_axes_and_figures/gridspec_multicolumn.py b/examples/subplots_axes_and_figures/gridspec_multicolumn.py index 5a22aa2d310c..533967b998cb 100644 --- a/examples/subplots_axes_and_figures/gridspec_multicolumn.py +++ b/examples/subplots_axes_and_figures/gridspec_multicolumn.py @@ -17,7 +17,7 @@ def format_axes(fig): ax.text(0.5, 0.5, "ax%d" % (i+1), va="center", ha="center") ax.tick_params(labelbottom=False, labelleft=False) -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout="constrained") gs = GridSpec(3, 3, figure=fig) ax1 = fig.add_subplot(gs[0, :]) diff --git a/examples/subplots_axes_and_figures/gridspec_nested.py b/examples/subplots_axes_and_figures/gridspec_nested.py index e47106e5d47c..2a73fc4b3c1c 100644 --- a/examples/subplots_axes_and_figures/gridspec_nested.py +++ b/examples/subplots_axes_and_figures/gridspec_nested.py @@ -7,7 +7,7 @@ set the position for a nested grid of subplots. Note that the same functionality can be achieved more directly with -`~.figure.FigureBase.subfigures`; see +`~.FigureBase.subfigures`; see :doc:`/gallery/subplots_axes_and_figures/subfigures`. """ diff --git a/tutorials/provisional/mosaic.py b/examples/subplots_axes_and_figures/mosaic.py similarity index 62% rename from tutorials/provisional/mosaic.py rename to examples/subplots_axes_and_figures/mosaic.py index 120e80d97d5d..3c7a333e22bd 100644 --- a/tutorials/provisional/mosaic.py +++ b/examples/subplots_axes_and_figures/mosaic.py @@ -1,20 +1,17 @@ """ -======================================= -Complex and semantic figure composition -======================================= +.. redirect-from:: /tutorials/provisional/mosaic -.. warning:: - This tutorial documents experimental / provisional API. - We are releasing this in v3.3 to get user feedback. We may - make breaking changes in future versions with no warning. +======================================================== +Complex and semantic figure composition (subplot_mosaic) +======================================================== -Laying out Axes in a Figure in a non uniform grid can be both tedious +Laying out Axes in a Figure in a non-uniform grid can be both tedious and verbose. For dense, even grids we have `.Figure.subplots` but for more complex layouts, such as Axes that span multiple columns / rows of the layout or leave some areas of the Figure blank, you can use -`.gridspec.GridSpec` (see :doc:`/tutorials/intermediate/gridspec`) or +`.gridspec.GridSpec` (see :doc:`/tutorials/intermediate/arranging_axes`) or manually place your axes. `.Figure.subplot_mosaic` aims to provide an interface to visually lay out your axes (as either ASCII art or nested lists) to streamline this process. @@ -63,7 +60,7 @@ def identify_axes(ax_dict, fontsize=48): hist_data = np.random.randn(1_500) -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout="constrained") ax_array = fig.subplots(2, 2, squeeze=False) ax_array[0, 0].bar(["a", "b", "c"], [5, 7, 9]) @@ -79,7 +76,7 @@ def identify_axes(ax_dict, fontsize=48): # Using `.Figure.subplot_mosaic` we can produce the same mosaic but give the # axes semantic names -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout="constrained") ax_dict = fig.subplot_mosaic( [ ["bar", "plot"], @@ -105,7 +102,7 @@ def identify_axes(ax_dict, fontsize=48): # String short-hand # ================= # -# By restricting our axes labels to single characters we can use Using we can +# By restricting our axes labels to single characters we can # "draw" the Axes we want as "ASCII art". The following @@ -119,20 +116,36 @@ def identify_axes(ax_dict, fontsize=48): # figure mosaic as above (but now labeled with ``{"A", "B", "C", # "D"}`` rather than ``{"bar", "plot", "hist", "image"}``). -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout="constrained") ax_dict = fig.subplot_mosaic(mosaic) identify_axes(ax_dict) +############################################################################### +# Alternatively, you can use the more compact string notation +mosaic = "AB;CD" ############################################################################### -# Something we can do with `.Figure.subplot_mosaic` that you can not -# do with `.Figure.subplots` is specify that an Axes should span -# several rows or columns. +# will give you the same composition, where the ``";"`` is used +# as the row separator instead of newline. + +fig = plt.figure(layout="constrained") +ax_dict = fig.subplot_mosaic(mosaic) +identify_axes(ax_dict) + +############################################################################### +# Axes spanning multiple rows/columns +# =================================== # -# If we want to re-arrange our four Axes to have C be a horizontal -# span on the bottom and D be a vertical span on the right we would do +# Something we can do with `.Figure.subplot_mosaic`, that we cannot +# do with `.Figure.subplots`, is to specify that an Axes should span +# several rows or columns. -axd = plt.figure(constrained_layout=True).subplot_mosaic( + +############################################################################### +# If we want to re-arrange our four Axes to have ``"C"`` be a horizontal +# span on the bottom and ``"D"`` be a vertical span on the right we would do + +axd = plt.figure(layout="constrained").subplot_mosaic( """ ABD CCD @@ -145,7 +158,7 @@ def identify_axes(ax_dict, fontsize=48): # we can specify some spaces in the grid to be blank -axd = plt.figure(constrained_layout=True).subplot_mosaic( +axd = plt.figure(layout="constrained").subplot_mosaic( """ A.C BBB @@ -160,7 +173,7 @@ def identify_axes(ax_dict, fontsize=48): # to mark the empty space, we can use *empty_sentinel* to specify the # character to use. -axd = plt.figure(constrained_layout=True).subplot_mosaic( +axd = plt.figure(layout="constrained").subplot_mosaic( """ aX Xb @@ -175,7 +188,7 @@ def identify_axes(ax_dict, fontsize=48): # Internally there is no meaning attached to the letters we use, any # Unicode code point is valid! -axd = plt.figure(constrained_layout=True).subplot_mosaic( +axd = plt.figure(layout="constrained").subplot_mosaic( """αб â„☢""" ) @@ -186,36 +199,37 @@ def identify_axes(ax_dict, fontsize=48): # empty sentinel with the string shorthand because it may be stripped # while processing the input. # -# Controlling mosaic and subplot creation -# ======================================= +# Controlling mosaic creation +# =========================== # # This feature is built on top of `.gridspec` and you can pass the # keyword arguments through to the underlying `.gridspec.GridSpec` # (the same as `.Figure.subplots`). # # In this case we want to use the input to specify the arrangement, -# but set the relative widths of the rows / columns via *gridspec_kw*. +# but set the relative widths of the rows / columns. For convenience, +# `.gridspec.GridSpec`'s *height_ratios* and *width_ratios* are exposed in the +# `.Figure.subplot_mosaic` calling sequence. -axd = plt.figure(constrained_layout=True).subplot_mosaic( +axd = plt.figure(layout="constrained").subplot_mosaic( """ .a. bAc .d. """, - gridspec_kw={ - # set the height ratios between the rows - "height_ratios": [1, 3.5, 1], - # set the width ratios between the columns - "width_ratios": [1, 3.5, 1], - }, + # set the height ratios between the rows + height_ratios=[1, 3.5, 1], + # set the width ratios between the columns + width_ratios=[1, 3.5, 1], ) identify_axes(axd) ############################################################################### -# Or use the {*left*, *right*, *bottom*, *top*} keyword arguments to +# Other `.gridspec.GridSpec` keywords can be passed via *gridspec_kw*. For +# example, use the {*left*, *right*, *bottom*, *top*} keyword arguments to # position the overall mosaic to put multiple versions of the same -# mosaic in a figure +# mosaic in a figure. mosaic = """AA BC""" @@ -251,7 +265,7 @@ def identify_axes(ax_dict, fontsize=48): mosaic = """AA BC""" -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout="constrained") left, right = fig.subfigures(nrows=1, ncols=2) axd = left.subplot_mosaic(mosaic) identify_axes(axd) @@ -261,31 +275,87 @@ def identify_axes(ax_dict, fontsize=48): ############################################################################### +# Controlling subplot creation +# ============================ +# # We can also pass through arguments used to create the subplots -# (again, the same as `.Figure.subplots`). +# (again, the same as `.Figure.subplots`) which will apply to all +# of the Axes created. -axd = plt.figure(constrained_layout=True).subplot_mosaic( +axd = plt.figure(layout="constrained").subplot_mosaic( "AB", subplot_kw={"projection": "polar"} ) identify_axes(axd) +############################################################################### +# Per-Axes subplot keyword arguments +# ---------------------------------- +# +# If you need to control the parameters passed to each subplot individually use +# *per_subplot_kw* to pass a mapping between the Axes identifiers (or +# tuples of Axes identifiers) to dictionaries of keywords to be passed. +# +# .. versionadded:: 3.7 +# + + +fig, axd = plt.subplot_mosaic( + "AB;CD", + per_subplot_kw={ + "A": {"projection": "polar"}, + ("C", "D"): {"xscale": "log"} + }, +) +identify_axes(axd) + +############################################################################### +# If the layout is specified with the string short-hand, then we know the +# Axes labels will be one character and can unambiguously interpret longer +# strings in *per_subplot_kw* to specify a set of Axes to apply the +# keywords to: + + +fig, axd = plt.subplot_mosaic( + "AB;CD", + per_subplot_kw={ + "AD": {"projection": "polar"}, + "BC": {"facecolor": ".9"} + }, +) +identify_axes(axd) + +############################################################################### +# If *subplot_kw* and *per_subplot_kw* are used together, then they are +# merged with *per_subplot_kw* taking priority: + + +axd = plt.figure(layout="constrained").subplot_mosaic( + "AB;CD", + subplot_kw={"facecolor": "xkcd:tangerine"}, + per_subplot_kw={ + "B": {"facecolor": "xkcd:water blue"}, + "D": {"projection": "polar", "facecolor": "w"}, + } +) +identify_axes(axd) + ############################################################################### -# Nested List input +# Nested list input # ================= # -# Everything we can do with the string short-hand we can also do when +# Everything we can do with the string shorthand we can also do when # passing in a list (internally we convert the string shorthand to a nested # list), for example using spans, blanks, and *gridspec_kw*: -axd = plt.figure(constrained_layout=True).subplot_mosaic( +axd = plt.figure(layout="constrained").subplot_mosaic( [ ["main", "zoom"], ["main", "BLANK"], ], empty_sentinel="BLANK", - gridspec_kw={"width_ratios": [2, 1]}, + width_ratios=[2, 1], ) identify_axes(axd) @@ -303,7 +373,7 @@ def identify_axes(ax_dict, fontsize=48): ["main", inner], ["bottom", "bottom"], ] -axd = plt.figure(constrained_layout=True).subplot_mosaic( +axd = plt.figure(layout="constrained").subplot_mosaic( outer_nested_mosaic, empty_sentinel=None ) identify_axes(axd, fontsize=36) @@ -314,7 +384,7 @@ def identify_axes(ax_dict, fontsize=48): mosaic = np.zeros((4, 4), dtype=int) for j in range(4): mosaic[j, j] = j + 1 -axd = plt.figure(constrained_layout=True).subplot_mosaic( +axd = plt.figure(layout="constrained").subplot_mosaic( mosaic, empty_sentinel=0, ) diff --git a/examples/subplots_axes_and_figures/multiple_figs_demo.py b/examples/subplots_axes_and_figures/multiple_figs_demo.py index 2d30768902c7..ee667e95565f 100644 --- a/examples/subplots_axes_and_figures/multiple_figs_demo.py +++ b/examples/subplots_axes_and_figures/multiple_figs_demo.py @@ -10,10 +10,11 @@ .. note:: - We discourage working with multiple figures in pyplot because managing - the *current figure* is cumbersome and error-prone. Instead, we recommend - to use the object-oriented approach and call methods on Figure and Axes - instances. + We discourage working with multiple figures through the implicit pyplot + interface because managing the *current figure* is cumbersome and + error-prone. Instead, we recommend using the explicit approach and call + methods on Figure and Axes instances. See :ref:`api_interfaces` for an + explanation of the trade-offs between the implicit and explicit interfaces. """ import matplotlib.pyplot as plt diff --git a/examples/subplots_axes_and_figures/secondary_axis.py b/examples/subplots_axes_and_figures/secondary_axis.py index b326a858871c..464a88aa1064 100644 --- a/examples/subplots_axes_and_figures/secondary_axis.py +++ b/examples/subplots_axes_and_figures/secondary_axis.py @@ -8,7 +8,7 @@ axes with only one axis visible via `.axes.Axes.secondary_xaxis` and `.axes.Axes.secondary_yaxis`. This secondary axis can have a different scale than the main axis by providing both a forward and an inverse conversion -function in a tuple to the ``functions`` kwarg: +function in a tuple to the *functions* keyword argument: """ import matplotlib.pyplot as plt @@ -17,7 +17,7 @@ import matplotlib.dates as mdates from matplotlib.ticker import AutoMinorLocator -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout='constrained') x = np.arange(0, 360, 1) y = np.sin(2 * x * np.pi / 180) ax.plot(x, y) @@ -47,7 +47,7 @@ def rad2deg(x): # In this case, the xscale of the parent is logarithmic, so the child is # made logarithmic as well. -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout='constrained') x = np.arange(0.02, 1, 0.02) np.random.seed(19680801) y = np.random.randn(len(x)) ** 2 @@ -59,7 +59,7 @@ def rad2deg(x): def one_over(x): """Vectorized 1/x, treating x==0 manually""" - x = np.array(x).astype(float) + x = np.array(x, float) near_zero = np.isclose(x, 0) x[near_zero] = np.inf x[~near_zero] = 1 / x[~near_zero] @@ -87,11 +87,11 @@ def one_over(x): # nominal plot limits. # # In the specific case of the numpy linear interpolation, `numpy.interp`, -# this condition can be arbitrarily enforced by providing optional kwargs -# *left*, *right* such that values outside the data range are mapped -# well outside the plot limits. +# this condition can be arbitrarily enforced by providing optional keyword +# arguments *left*, *right* such that values outside the data range are +# mapped well outside the plot limits. -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout='constrained') xdata = np.arange(1, 11, 0.4) ydata = np.random.randn(len(xdata)) ax.plot(xdata, ydata, label='Plotted data') @@ -129,7 +129,7 @@ def inverse(x): dates = [datetime.datetime(2018, 1, 1) + datetime.timedelta(hours=k * 6) for k in range(240)] temperature = np.random.randn(len(dates)) * 4 + 6.7 -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout='constrained') ax.plot(dates, temperature) ax.set_ylabel(r'$T\ [^oC]$') diff --git a/examples/subplots_axes_and_figures/shared_axis_demo.py b/examples/subplots_axes_and_figures/shared_axis_demo.py index f09e5c87759b..cfe2d68701f0 100644 --- a/examples/subplots_axes_and_figures/shared_axis_demo.py +++ b/examples/subplots_axes_and_figures/shared_axis_demo.py @@ -1,17 +1,17 @@ """ =========== -Shared Axis +Shared axis =========== -You can share the x or y axis limits for one axis with another by -passing an axes instance as a *sharex* or *sharey* keyword argument. +You can share the x- or y-axis limits for one axis with another by +passing an `~.axes.Axes` instance as a *sharex* or *sharey* keyword argument. Changing the axis limits on one axes will be reflected automatically in the other, and vice-versa, so when you navigate with the toolbar -the axes will follow each other on their shared axes. Ditto for +the Axes will follow each other on their shared axis. Ditto for changes in the axis scaling (e.g., log vs. linear). However, it is possible to have differences in tick labeling, e.g., you can selectively -turn off the tick labels on one axes. +turn off the tick labels on one Axes. The example below shows how to customize the tick labels on the various axes. Shared axes share the tick locator, tick formatter, @@ -20,13 +20,13 @@ because you may want to make the tick labels smaller on the upper axes, e.g., in the example below. -If you want to turn off the ticklabels for a given axes (e.g., on -subplot(211) or subplot(212), you cannot do the standard trick:: +If you want to turn off the ticklabels for a given Axes (e.g., on +subplot(211) or subplot(212)), you cannot do the standard trick:: setp(ax2, xticklabels=[]) because this changes the tick Formatter, which is shared among all -axes. But you can alter the visibility of the labels, which is a +Axes. But you can alter the visibility of the labels, which is a property:: setp(ax2.get_xticklabels(), visible=False) @@ -42,13 +42,13 @@ ax1 = plt.subplot(311) plt.plot(t, s1) -plt.setp(ax1.get_xticklabels(), fontsize=6) +plt.tick_params('x', labelsize=6) # share x only ax2 = plt.subplot(312, sharex=ax1) plt.plot(t, s2) # make these tick labels invisible -plt.setp(ax2.get_xticklabels(), visible=False) +plt.tick_params('x', labelbottom=False) # share x and y ax3 = plt.subplot(313, sharex=ax1, sharey=ax1) diff --git a/examples/subplots_axes_and_figures/subfigures.py b/examples/subplots_axes_and_figures/subfigures.py index 1e7ae401554b..4a95932fe7f5 100644 --- a/examples/subplots_axes_and_figures/subfigures.py +++ b/examples/subplots_axes_and_figures/subfigures.py @@ -31,7 +31,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): np.random.seed(19680808) # gridspec inside gridspec -fig = plt.figure(constrained_layout=True, figsize=(10, 4)) +fig = plt.figure(layout='constrained', figsize=(10, 4)) subfigs = fig.subfigures(1, 2, wspace=0.07) axsLeft = subfigs[0].subplots(1, 2, sharey=True) @@ -62,7 +62,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # `matplotlib.figure.Figure.add_subfigure`. This requires getting # the gridspec that the subplots are laid out on. -fig, axs = plt.subplots(2, 3, constrained_layout=True, figsize=(10, 4)) +fig, axs = plt.subplots(2, 3, layout='constrained', figsize=(10, 4)) gridspec = axs[0, 0].get_subplotspec().get_gridspec() # clear the left column for the subfigure: @@ -90,7 +90,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # Subfigures can have different widths and heights. This is exactly the # same example as the first example, but *width_ratios* has been changed: -fig = plt.figure(constrained_layout=True, figsize=(10, 4)) +fig = plt.figure(layout='constrained', figsize=(10, 4)) subfigs = fig.subfigures(1, 2, wspace=0.07, width_ratios=[2, 1]) axsLeft = subfigs[0].subplots(1, 2, sharey=True) @@ -119,7 +119,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): ############################################################################## # Subfigures can be also be nested: -fig = plt.figure(constrained_layout=True, figsize=(10, 8)) +fig = plt.figure(layout='constrained', figsize=(10, 8)) fig.suptitle('fig') diff --git a/examples/subplots_axes_and_figures/subplot.py b/examples/subplots_axes_and_figures/subplot.py index 5502d7c23f2b..c368746a8856 100644 --- a/examples/subplots_axes_and_figures/subplot.py +++ b/examples/subplots_axes_and_figures/subplot.py @@ -4,18 +4,25 @@ ================= Simple demo with multiple subplots. + +For more options, see :doc:`/gallery/subplots_axes_and_figures/subplots_demo`. + +.. redirect-from:: /gallery/subplots_axes_and_figures/subplot_demo """ + import numpy as np import matplotlib.pyplot as plt -############################################################################### - +# Create some fake data. x1 = np.linspace(0.0, 5.0) -x2 = np.linspace(0.0, 2.0) - y1 = np.cos(2 * np.pi * x1) * np.exp(-x1) +x2 = np.linspace(0.0, 2.0) y2 = np.cos(2 * np.pi * x2) +############################################################################### +# `~.pyplot.subplots()` is the recommended method to generate simple subplot +# arrangements: + fig, (ax1, ax2) = plt.subplots(2, 1) fig.suptitle('A tale of 2 subplots') @@ -28,22 +35,8 @@ plt.show() -############################################################################# -# -# -# Alternative Method For Creating Multiple Plots -# """""""""""""""""""""""""""""""""""""""""""""" -# -# Subplots can also be generated using `~.pyplot.subplot()` \ -# as in the following example: -# - - -x1 = np.linspace(0.0, 5.0) -x2 = np.linspace(0.0, 2.0) - -y1 = np.cos(2 * np.pi * x1) * np.exp(-x1) -y2 = np.cos(2 * np.pi * x2) +############################################################################### +# Subplots can also be generated one at a time using `~.pyplot.subplot()`: plt.subplot(2, 1, 1) plt.plot(x1, y1, 'o-') diff --git a/examples/subplots_axes_and_figures/subplot_demo.py b/examples/subplots_axes_and_figures/subplot_demo.py deleted file mode 100644 index 836476829a65..000000000000 --- a/examples/subplots_axes_and_figures/subplot_demo.py +++ /dev/null @@ -1,28 +0,0 @@ -""" -================== -Basic Subplot Demo -================== - -Demo with two subplots. -For more options, see -:doc:`/gallery/subplots_axes_and_figures/subplots_demo` -""" -import numpy as np -import matplotlib.pyplot as plt - -# Data for plotting -x1 = np.linspace(0.0, 5.0) -x2 = np.linspace(0.0, 2.0) -y1 = np.cos(2 * np.pi * x1) * np.exp(-x1) -y2 = np.cos(2 * np.pi * x2) - -# Create two subplots sharing y axis -fig, (ax1, ax2) = plt.subplots(2, sharey=True) - -ax1.plot(x1, y1, 'ko-') -ax1.set(title='A tale of 2 subplots', ylabel='Damped oscillation') - -ax2.plot(x2, y2, 'r.-') -ax2.set(xlabel='time (s)', ylabel='Undamped') - -plt.show() diff --git a/examples/subplots_axes_and_figures/subplot_toolbar.py b/examples/subplots_axes_and_figures/subplot_toolbar.py deleted file mode 100644 index e65b1f7091f0..000000000000 --- a/examples/subplots_axes_and_figures/subplot_toolbar.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -=============== -Subplot Toolbar -=============== - -Matplotlib has a toolbar available for adjusting subplot spacing. -""" -import matplotlib.pyplot as plt -import numpy as np - - -# Fixing random state for reproducibility -np.random.seed(19680801) - -fig, axs = plt.subplots(2, 2) - -axs[0, 0].imshow(np.random.random((100, 100))) - -axs[0, 1].imshow(np.random.random((100, 100))) - -axs[1, 0].imshow(np.random.random((100, 100))) - -axs[1, 1].imshow(np.random.random((100, 100))) - -plt.subplot_tool() -plt.show() diff --git a/examples/subplots_axes_and_figures/subplots_adjust.py b/examples/subplots_axes_and_figures/subplots_adjust.py index fecc41f6f5ff..3eb6d709a546 100644 --- a/examples/subplots_axes_and_figures/subplots_adjust.py +++ b/examples/subplots_axes_and_figures/subplots_adjust.py @@ -1,10 +1,16 @@ """ -=============== -Subplots Adjust -=============== +============================= +Subplots spacings and margins +============================= -Adjusting the spacing of margins and subplots using -`~matplotlib.pyplot.subplots_adjust`. +Adjusting the spacing of margins and subplots using `.pyplot.subplots_adjust`. + +.. note:: + There is also a tool window to adjust the margins and spacings of displayed + figures interactively. It can be opened via the toolbar or by calling + `.pyplot.subplot_tool`. + +.. redirect-from:: /gallery/subplots_axes_and_figures/subplot_toolbar """ import matplotlib.pyplot as plt @@ -14,11 +20,12 @@ np.random.seed(19680801) plt.subplot(211) -plt.imshow(np.random.random((100, 100)), cmap=plt.cm.BuPu_r) +plt.imshow(np.random.random((100, 100))) plt.subplot(212) -plt.imshow(np.random.random((100, 100)), cmap=plt.cm.BuPu_r) +plt.imshow(np.random.random((100, 100))) plt.subplots_adjust(bottom=0.1, right=0.8, top=0.9) cax = plt.axes([0.85, 0.1, 0.075, 0.8]) plt.colorbar(cax=cax) + plt.show() diff --git a/examples/subplots_axes_and_figures/subplots_demo.py b/examples/subplots_axes_and_figures/subplots_demo.py index bbc8afa469fa..41b14dadd620 100644 --- a/examples/subplots_axes_and_figures/subplots_demo.py +++ b/examples/subplots_axes_and_figures/subplots_demo.py @@ -176,7 +176,7 @@ ax3.plot(x + 1, -y, 'tab:green') ax4.plot(x + 2, -y**2, 'tab:red') -for ax in axs.flat: +for ax in fig.get_axes(): ax.label_outer() ############################################################################### diff --git a/examples/subplots_axes_and_figures/zoom_inset_axes.py b/examples/subplots_axes_and_figures/zoom_inset_axes.py index e3f99ce3aec9..db78aedb318f 100644 --- a/examples/subplots_axes_and_figures/zoom_inset_axes.py +++ b/examples/subplots_axes_and_figures/zoom_inset_axes.py @@ -29,12 +29,12 @@ def get_demo_image(): # inset axes.... axins = ax.inset_axes([0.5, 0.5, 0.47, 0.47]) axins.imshow(Z2, extent=extent, origin="lower") -# sub region of the original image +# subregion of the original image x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9 axins.set_xlim(x1, x2) axins.set_ylim(y1, y2) -axins.set_xticklabels('') -axins.set_yticklabels('') +axins.set_xticklabels([]) +axins.set_yticklabels([]) ax.indicate_inset_zoom(axins, edgecolor="black") diff --git a/examples/text_labels_and_annotations/accented_text.py b/examples/text_labels_and_annotations/accented_text.py index eb45fe8f276d..9e9ad65e9f15 100644 --- a/examples/text_labels_and_annotations/accented_text.py +++ b/examples/text_labels_and_annotations/accented_text.py @@ -1,9 +1,9 @@ r""" ================================= -Using accented text in matplotlib +Using accented text in Matplotlib ================================= -Matplotlib supports accented characters via TeX mathtext or unicode. +Matplotlib supports accented characters via TeX mathtext or Unicode. Using mathtext, the following accents are provided: \\hat, \\breve, \\grave, \\bar, \\acute, \\tilde, \\vec, \\dot, \\ddot. All of them have the same @@ -24,7 +24,8 @@ ax.text(4, 0.5, r"$F=m\ddot{x}$") fig.tight_layout() -# Unicode demo +############################################################################# +# You can also use Unicode characters directly in strings. fig, ax = plt.subplots() ax.set_title("GISCARD CHAHUTÉ À L'ASSEMBLÉE") ax.set_xlabel("LE COUP DE DÉ DE DE GAULLE") diff --git a/examples/pyplots/align_ylabels.py b/examples/text_labels_and_annotations/align_ylabels.py similarity index 97% rename from examples/pyplots/align_ylabels.py rename to examples/text_labels_and_annotations/align_ylabels.py index 3523e08f93fd..8e47909cc6c1 100644 --- a/examples/pyplots/align_ylabels.py +++ b/examples/text_labels_and_annotations/align_ylabels.py @@ -6,6 +6,7 @@ Two methods are shown here, one using a short call to `.Figure.align_ylabels` and the second a manual way to align the labels. +.. redirect-from:: /gallery/pyplots/align_ylabels """ import numpy as np import matplotlib.pyplot as plt diff --git a/examples/text_labels_and_annotations/angle_annotation.py b/examples/text_labels_and_annotations/angle_annotation.py index 065e4f51c9b8..8272dd1f3e06 100644 --- a/examples/text_labels_and_annotations/angle_annotation.py +++ b/examples/text_labels_and_annotations/angle_annotation.py @@ -134,8 +134,7 @@ def get_size(self): if self.unit == "points": factor = self.ax.figure.dpi / 72. elif self.unit[:4] == "axes": - b = TransformedBbox(Bbox.from_bounds(0, 0, 1, 1), - self.ax.transAxes) + b = TransformedBbox(Bbox.unit(), self.ax.transAxes) dic = {"max": max(b.width, b.height), "min": min(b.width, b.height), "width": b.width, "height": b.height} diff --git a/examples/text_labels_and_annotations/angles_on_bracket_arrows.py b/examples/text_labels_and_annotations/angles_on_bracket_arrows.py new file mode 100644 index 000000000000..f23fe8ff887b --- /dev/null +++ b/examples/text_labels_and_annotations/angles_on_bracket_arrows.py @@ -0,0 +1,66 @@ +""" +=================================== +Angle annotations on bracket arrows +=================================== + +This example shows how to add angle annotations to bracket arrow styles +created using `.FancyArrowPatch`. *angleA* and *angleB* are measured from a +vertical line as positive (to the left) or negative (to the right). Blue +`.FancyArrowPatch` arrows indicate the directions of *angleA* and *angleB* +from the vertical and axes text annotate the angle sizes. +""" + +import matplotlib.pyplot as plt +import numpy as np +from matplotlib.patches import FancyArrowPatch + + +def get_point_of_rotated_vertical(origin, line_length, degrees): + """Return xy coordinates of the vertical line end rotated by degrees.""" + rad = np.deg2rad(-degrees) + return [origin[0] + line_length * np.sin(rad), + origin[1] + line_length * np.cos(rad)] + + +fig, ax = plt.subplots() +ax.set(xlim=(0, 6), ylim=(-1, 5)) +ax.set_title("Orientation of the bracket arrows relative to angleA and angleB") + +style = ']-[' +for i, angle in enumerate([-40, 0, 60]): + y = 2*i + arrow_centers = ((1, y), (5, y)) + vlines = ((1, y + 0.5), (5, y + 0.5)) + anglesAB = (angle, -angle) + bracketstyle = f"{style}, angleA={anglesAB[0]}, angleB={anglesAB[1]}" + bracket = FancyArrowPatch(*arrow_centers, arrowstyle=bracketstyle, + mutation_scale=42) + ax.add_patch(bracket) + ax.text(3, y + 0.05, bracketstyle, ha="center", va="bottom", fontsize=14) + ax.vlines([line[0] for line in vlines], [y, y], [line[1] for line in vlines], + linestyles="--", color="C0") + # Get the top coordinates for the drawn patches at A and B + patch_tops = [get_point_of_rotated_vertical(center, 0.5, angle) + for center, angle in zip(arrow_centers, anglesAB)] + # Define the connection directions for the annotation arrows + connection_dirs = (1, -1) if angle > 0 else (-1, 1) + # Add arrows and annotation text + arrowstyle = "Simple, tail_width=0.5, head_width=4, head_length=8" + for vline, dir, patch_top, angle in zip(vlines, connection_dirs, + patch_tops, anglesAB): + kw = dict(connectionstyle=f"arc3,rad={dir * 0.5}", + arrowstyle=arrowstyle, color="C0") + ax.add_patch(FancyArrowPatch(vline, patch_top, **kw)) + ax.text(vline[0] - dir * 0.15, y + 0.7, f'{angle}°', ha="center", + va="center") + +plt.show() + +############################################################################# +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.patches.ArrowStyle` diff --git a/examples/pyplots/annotate_transform.py b/examples/text_labels_and_annotations/annotate_transform.py similarity index 96% rename from examples/pyplots/annotate_transform.py rename to examples/text_labels_and_annotations/annotate_transform.py index 9d3d09f5a0ba..1145f7fdb9a2 100644 --- a/examples/pyplots/annotate_transform.py +++ b/examples/text_labels_and_annotations/annotate_transform.py @@ -6,6 +6,8 @@ This example shows how to use different coordinate systems for annotations. For a complete overview of the annotation capabilities, also see the :doc:`annotation tutorial`. + +.. redirect-from:: /gallery/pyplots/annotate_transform """ import numpy as np diff --git a/examples/pyplots/annotation_basic.py b/examples/text_labels_and_annotations/annotation_basic.py similarity index 94% rename from examples/pyplots/annotation_basic.py rename to examples/text_labels_and_annotations/annotation_basic.py index 5a6327b48748..d9ea9748197f 100644 --- a/examples/pyplots/annotation_basic.py +++ b/examples/text_labels_and_annotations/annotation_basic.py @@ -8,6 +8,8 @@ For a complete overview of the annotation capabilities, also see the :doc:`annotation tutorial`. + +.. redirect-from:: /gallery/pyplots/annotation_basic """ import numpy as np import matplotlib.pyplot as plt diff --git a/examples/text_labels_and_annotations/annotation_demo.py b/examples/text_labels_and_annotations/annotation_demo.py index f04460b7698b..2008a8744675 100644 --- a/examples/text_labels_and_annotations/annotation_demo.py +++ b/examples/text_labels_and_annotations/annotation_demo.py @@ -107,7 +107,7 @@ # In the example below, the *xy* point is in native coordinates (*xycoords* # defaults to 'data'). For a polar axes, this is in (theta, radius) space. # The text in the example is placed in the fractional figure coordinate system. -# Text keyword args like horizontal and vertical alignment are respected. +# Text keyword arguments like horizontal and vertical alignment are respected. fig, ax = plt.subplots(subplot_kw=dict(projection='polar'), figsize=(3, 3)) r = np.arange(0, 1, 0.001) @@ -125,6 +125,7 @@ horizontalalignment='left', verticalalignment='bottom') +############################################################################# # You can also use polar notation on a cartesian axes. Here the native # coordinate system ('data') is cartesian, so you need to specify the # xycoords and textcoords as 'polar' if you want to use (theta, radius). @@ -230,6 +231,7 @@ ax.set(xlim=(-1, 5), ylim=(-4, 3)) +############################################################################# # We'll create another figure so that it doesn't get too cluttered fig, ax = plt.subplots() @@ -247,7 +249,6 @@ xy=(2., -1), xycoords='data', xytext=(-100, 60), textcoords='offset points', size=20, - # bbox=dict(boxstyle="round", fc="0.8"), arrowprops=dict(arrowstyle="fancy", fc="0.6", ec="none", patchB=el, @@ -256,7 +257,6 @@ xy=(2., -1), xycoords='data', xytext=(100, 60), textcoords='offset points', size=20, - # bbox=dict(boxstyle="round", fc="0.8"), arrowprops=dict(arrowstyle="simple", fc="0.6", ec="none", patchB=el, @@ -265,7 +265,6 @@ xy=(2., -1), xycoords='data', xytext=(-100, -100), textcoords='offset points', size=20, - # bbox=dict(boxstyle="round", fc="0.8"), arrowprops=dict(arrowstyle="wedge,tail_width=0.7", fc="0.6", ec="none", patchB=el, @@ -338,11 +337,8 @@ # It is also possible to generate draggable annotations an1 = ax1.annotate('Drag me 1', xy=(.5, .7), xycoords='data', - #xytext=(.5, .7), textcoords='data', ha="center", va="center", - bbox=bbox_args, - #arrowprops=arrow_args - ) + bbox=bbox_args) an2 = ax1.annotate('Drag me 2', xy=(.5, .5), xycoords=an1, xytext=(.5, .3), textcoords='axes fraction', diff --git a/examples/pyplots/annotation_polar.py b/examples/text_labels_and_annotations/annotation_polar.py similarity index 95% rename from examples/pyplots/annotation_polar.py rename to examples/text_labels_and_annotations/annotation_polar.py index 24a3a20bc3fe..a7f8b764d914 100644 --- a/examples/pyplots/annotation_polar.py +++ b/examples/text_labels_and_annotations/annotation_polar.py @@ -7,6 +7,8 @@ For a complete overview of the annotation capabilities, also see the :doc:`annotation tutorial`. + +.. redirect-from:: /gallery/pyplots/annotation_polar """ import numpy as np import matplotlib.pyplot as plt diff --git a/examples/text_labels_and_annotations/arrow_demo.py b/examples/text_labels_and_annotations/arrow_demo.py index 9e678249d1a1..9607818181dc 100644 --- a/examples/text_labels_and_annotations/arrow_demo.py +++ b/examples/text_labels_and_annotations/arrow_demo.py @@ -3,145 +3,74 @@ Arrow Demo ========== -Arrow drawing example for the new fancy_arrow facilities. - -Code contributed by: Rob Knight - -usage: - - python arrow_demo.py realistic|full|sample|extreme +Three ways of drawing arrows to encode arrow "strength" (e.g., transition +probabilities in a Markov model) using arrow length, width, or alpha (opacity). +""" +import itertools -""" import matplotlib.pyplot as plt import numpy as np -rates_to_bases = {'r1': 'AT', 'r2': 'TA', 'r3': 'GA', 'r4': 'AG', 'r5': 'CA', - 'r6': 'AC', 'r7': 'GT', 'r8': 'TG', 'r9': 'CT', 'r10': 'TC', - 'r11': 'GC', 'r12': 'CG'} -numbered_bases_to_rates = {v: k for k, v in rates_to_bases.items()} -lettered_bases_to_rates = {v: 'r' + v for k, v in rates_to_bases.items()} - -def make_arrow_plot(data, size=4, display='length', shape='right', - max_arrow_width=0.03, arrow_sep=0.02, alpha=0.5, - normalize_data=False, ec=None, labelcolor=None, - head_starts_at_zero=True, - rate_labels=lettered_bases_to_rates, - **kwargs): +def make_arrow_graph(ax, data, size=4, display='length', shape='right', + max_arrow_width=0.03, arrow_sep=0.02, alpha=0.5, + normalize_data=False, ec=None, labelcolor=None, + **kwargs): """ Makes an arrow plot. Parameters ---------- + ax + The axes where the graph is drawn. data Dict with probabilities for the bases and pair transitions. size - Size of the graph in inches. + Size of the plot, in inches. display : {'length', 'width', 'alpha'} The arrow property to change. shape : {'full', 'left', 'right'} For full or half arrows. max_arrow_width : float - Maximum width of an arrow, data coordinates. + Maximum width of an arrow, in data coordinates. arrow_sep : float - Separation between arrows in a pair, data coordinates. + Separation between arrows in a pair, in data coordinates. alpha : float Maximum opacity of arrows. **kwargs - Can be anything allowed by a Arrow object, e.g. *linewidth* or - *edgecolor*. + `.FancyArrow` properties, e.g. *linewidth* or *edgecolor*. """ - plt.xlim(-0.5, 1.5) - plt.ylim(-0.5, 1.5) - plt.gcf().set_size_inches(size, size) - plt.xticks([]) - plt.yticks([]) + ax.set(xlim=(-0.25, 1.25), ylim=(-0.25, 1.25), xticks=[], yticks=[], + title=f'flux encoded as arrow {display}') max_text_size = size * 12 min_text_size = size - label_text_size = size * 2.5 - text_params = {'ha': 'center', 'va': 'center', 'family': 'sans-serif', - 'fontweight': 'bold'} - r2 = np.sqrt(2) - - deltas = { - 'AT': (1, 0), - 'TA': (-1, 0), - 'GA': (0, 1), - 'AG': (0, -1), - 'CA': (-1 / r2, 1 / r2), - 'AC': (1 / r2, -1 / r2), - 'GT': (1 / r2, 1 / r2), - 'TG': (-1 / r2, -1 / r2), - 'CT': (0, 1), - 'TC': (0, -1), - 'GC': (1, 0), - 'CG': (-1, 0)} - - colors = { - 'AT': 'r', - 'TA': 'k', - 'GA': 'g', - 'AG': 'r', - 'CA': 'b', - 'AC': 'r', - 'GT': 'g', - 'TG': 'k', - 'CT': 'b', - 'TC': 'k', - 'GC': 'g', - 'CG': 'b'} - - label_positions = { - 'AT': 'center', - 'TA': 'center', - 'GA': 'center', - 'AG': 'center', - 'CA': 'left', - 'AC': 'left', - 'GT': 'left', - 'TG': 'left', - 'CT': 'center', - 'TC': 'center', - 'GC': 'center', - 'CG': 'center'} - - def do_fontsize(k): - return float(np.clip(max_text_size * np.sqrt(data[k]), - min_text_size, max_text_size)) - - plt.text(0, 1, '$A_3$', color='r', size=do_fontsize('A'), **text_params) - plt.text(1, 1, '$T_3$', color='k', size=do_fontsize('T'), **text_params) - plt.text(0, 0, '$G_3$', color='g', size=do_fontsize('G'), **text_params) - plt.text(1, 0, '$C_3$', color='b', size=do_fontsize('C'), **text_params) + label_text_size = size * 4 + + bases = 'ATGC' + coords = { + 'A': np.array([0, 1]), + 'T': np.array([1, 1]), + 'G': np.array([0, 0]), + 'C': np.array([1, 0]), + } + colors = {'A': 'r', 'T': 'k', 'G': 'g', 'C': 'b'} + + for base in bases: + fontsize = np.clip(max_text_size * data[base]**(1/2), + min_text_size, max_text_size) + ax.text(*coords[base], f'${base}_3$', + color=colors[base], size=fontsize, + horizontalalignment='center', verticalalignment='center', + weight='bold') arrow_h_offset = 0.25 # data coordinates, empirically determined max_arrow_length = 1 - 2 * arrow_h_offset max_head_width = 2.5 * max_arrow_width max_head_length = 2 * max_arrow_width - arrow_params = {'length_includes_head': True, 'shape': shape, - 'head_starts_at_zero': head_starts_at_zero} sf = 0.6 # max arrow size represents this in data coords - d = (r2 / 2 + arrow_h_offset - 0.5) / r2 # distance for diags - r2v = arrow_sep / r2 # offset for diags - - # tuple of x, y for start position - positions = { - 'AT': (arrow_h_offset, 1 + arrow_sep), - 'TA': (1 - arrow_h_offset, 1 - arrow_sep), - 'GA': (-arrow_sep, arrow_h_offset), - 'AG': (arrow_sep, 1 - arrow_h_offset), - 'CA': (1 - d - r2v, d - r2v), - 'AC': (d + r2v, 1 - d + r2v), - 'GT': (d - r2v, d + r2v), - 'TG': (1 - d + r2v, 1 - d - r2v), - 'CT': (1 - arrow_sep, arrow_h_offset), - 'TC': (1 + arrow_sep, 1 - arrow_h_offset), - 'GC': (arrow_h_offset, arrow_sep), - 'CG': (1 - arrow_h_offset, -arrow_sep)} - if normalize_data: # find maximum value for rates, i.e. where keys are 2 chars long max_val = max((v for k, v in data.items() if len(k) == 2), default=0) @@ -149,7 +78,8 @@ def do_fontsize(k): for k, v in data.items(): data[k] = v / max_val * sf - def draw_arrow(pair, alpha=alpha, ec=ec, labelcolor=labelcolor): + # iterate over strings 'AT', 'TA', 'AG', 'GA', etc. + for pair in map(''.join, itertools.permutations(bases, 2)): # set the length of the arrow if display == 'length': length = (max_head_length @@ -159,7 +89,6 @@ def draw_arrow(pair, alpha=alpha, ec=ec, labelcolor=labelcolor): # set the transparency of the arrow if display == 'alpha': alpha = min(data[pair] / sf, alpha) - # set the width of the arrow if display == 'width': scale = data[pair] / sf @@ -171,137 +100,60 @@ def draw_arrow(pair, alpha=alpha, ec=ec, labelcolor=labelcolor): head_width = max_head_width head_length = max_head_length - fc = colors[pair] - ec = ec or fc - - x_scale, y_scale = deltas[pair] - x_pos, y_pos = positions[pair] - plt.arrow(x_pos, y_pos, x_scale * length, y_scale * length, - fc=fc, ec=ec, alpha=alpha, width=width, - head_width=head_width, head_length=head_length, - **arrow_params) - - # figure out coordinates for text + fc = colors[pair[0]] + + cp0 = coords[pair[0]] + cp1 = coords[pair[1]] + # unit vector in arrow direction + delta = cos, sin = (cp1 - cp0) / np.hypot(*(cp1 - cp0)) + x_pos, y_pos = ( + (cp0 + cp1) / 2 # midpoint + - delta * length / 2 # half the arrow length + + np.array([-sin, cos]) * arrow_sep # shift outwards by arrow_sep + ) + ax.arrow( + x_pos, y_pos, cos * length, sin * length, + fc=fc, ec=ec or fc, alpha=alpha, width=width, + head_width=head_width, head_length=head_length, shape=shape, + length_includes_head=True, + **kwargs + ) + + # figure out coordinates for text: # if drawing relative to base: x and y are same as for arrow # dx and dy are one arrow width left and up - # need to rotate based on direction of arrow, use x_scale and y_scale - # as sin x and cos x? - sx, cx = y_scale, x_scale - - where = label_positions[pair] - if where == 'left': - orig_position = 3 * np.array([[max_arrow_width, max_arrow_width]]) - elif where == 'absolute': - orig_position = np.array([[max_arrow_length / 2.0, - 3 * max_arrow_width]]) - elif where == 'right': - orig_position = np.array([[length - 3 * max_arrow_width, - 3 * max_arrow_width]]) - elif where == 'center': - orig_position = np.array([[length / 2.0, 3 * max_arrow_width]]) - else: - raise ValueError("Got unknown position parameter %s" % where) - - M = np.array([[cx, sx], [-sx, cx]]) - coords = np.dot(orig_position, M) + [[x_pos, y_pos]] - x, y = np.ravel(coords) - orig_label = rate_labels[pair] - label = r'$%s_{_{\mathrm{%s}}}$' % (orig_label[0], orig_label[1:]) - - plt.text(x, y, label, size=label_text_size, ha='center', va='center', - color=labelcolor or fc) - - for p in sorted(positions): - draw_arrow(p) - - -# test data -all_on_max = dict([(i, 1) for i in 'TCAG'] + - [(i + j, 0.6) for i in 'TCAG' for j in 'TCAG']) - -realistic_data = { - 'A': 0.4, - 'T': 0.3, - 'G': 0.5, - 'C': 0.2, - 'AT': 0.4, - 'AC': 0.3, - 'AG': 0.2, - 'TA': 0.2, - 'TC': 0.3, - 'TG': 0.4, - 'CT': 0.2, - 'CG': 0.3, - 'CA': 0.2, - 'GA': 0.1, - 'GT': 0.4, - 'GC': 0.1} - -extreme_data = { - 'A': 0.75, - 'T': 0.10, - 'G': 0.10, - 'C': 0.05, - 'AT': 0.6, - 'AC': 0.3, - 'AG': 0.1, - 'TA': 0.02, - 'TC': 0.3, - 'TG': 0.01, - 'CT': 0.2, - 'CG': 0.5, - 'CA': 0.2, - 'GA': 0.1, - 'GT': 0.4, - 'GC': 0.2} - -sample_data = { - 'A': 0.2137, - 'T': 0.3541, - 'G': 0.1946, - 'C': 0.2376, - 'AT': 0.0228, - 'AC': 0.0684, - 'AG': 0.2056, - 'TA': 0.0315, - 'TC': 0.0629, - 'TG': 0.0315, - 'CT': 0.1355, - 'CG': 0.0401, - 'CA': 0.0703, - 'GA': 0.1824, - 'GT': 0.0387, - 'GC': 0.1106} + orig_positions = { + 'base': [3 * max_arrow_width, 3 * max_arrow_width], + 'center': [length / 2, 3 * max_arrow_width], + 'tip': [length - 3 * max_arrow_width, 3 * max_arrow_width], + } + # for diagonal arrows, put the label at the arrow base + # for vertical or horizontal arrows, center the label + where = 'base' if (cp0 != cp1).all() else 'center' + # rotate based on direction of arrow (cos, sin) + M = [[cos, -sin], [sin, cos]] + x, y = np.dot(M, orig_positions[where]) + [x_pos, y_pos] + label = r'$r_{_{\mathrm{%s}}}$' % (pair,) + ax.text(x, y, label, size=label_text_size, ha='center', va='center', + color=labelcolor or fc) if __name__ == '__main__': - from sys import argv - d = None - if len(argv) > 1: - if argv[1] == 'full': - d = all_on_max - scaled = False - elif argv[1] == 'extreme': - d = extreme_data - scaled = False - elif argv[1] == 'realistic': - d = realistic_data - scaled = False - elif argv[1] == 'sample': - d = sample_data - scaled = True - if d is None: - d = all_on_max - scaled = False - if len(argv) > 2: - display = argv[2] - else: - display = 'length' + data = { # test data + 'A': 0.4, 'T': 0.3, 'G': 0.6, 'C': 0.2, + 'AT': 0.4, 'AC': 0.3, 'AG': 0.2, + 'TA': 0.2, 'TC': 0.3, 'TG': 0.4, + 'CT': 0.2, 'CG': 0.3, 'CA': 0.2, + 'GA': 0.1, 'GT': 0.4, 'GC': 0.1, + } size = 4 - plt.figure(figsize=(size, size)) + fig = plt.figure(figsize=(3 * size, size), layout="constrained") + axs = fig.subplot_mosaic([["length", "width", "alpha"]]) - make_arrow_plot(d, display=display, linewidth=0.001, edgecolor=None, - normalize_data=scaled, head_starts_at_zero=True, size=size) + for display, ax in axs.items(): + make_arrow_graph( + ax, data, display=display, linewidth=0.001, edgecolor=None, + normalize_data=True, size=size) plt.show() diff --git a/examples/text_labels_and_annotations/arrow_simple_demo.py b/examples/text_labels_and_annotations/arrow_simple_demo.py deleted file mode 100644 index c8a07ee204d3..000000000000 --- a/examples/text_labels_and_annotations/arrow_simple_demo.py +++ /dev/null @@ -1,11 +0,0 @@ -""" -================= -Arrow Simple Demo -================= - -""" -import matplotlib.pyplot as plt - -ax = plt.axes() -ax.arrow(0, 0, 0.5, 0.5, head_width=0.05, head_length=0.1, fc='k', ec='k') -plt.show() diff --git a/examples/text_labels_and_annotations/custom_legends.py b/examples/text_labels_and_annotations/custom_legends.py index e9f840fe768f..7f4ca07398f4 100644 --- a/examples/text_labels_and_annotations/custom_legends.py +++ b/examples/text_labels_and_annotations/custom_legends.py @@ -19,7 +19,8 @@ and call ``ax.legend()``, you will get the following: """ # sphinx_gallery_thumbnail_number = 2 -from matplotlib import rcParams, cycler +import matplotlib as mpl +from matplotlib import cycler import matplotlib.pyplot as plt import numpy as np @@ -29,7 +30,7 @@ N = 10 data = (np.geomspace(1, 10, 100) + np.random.randn(N, 100)).T cmap = plt.cm.coolwarm -rcParams['axes.prop_cycle'] = cycler(color=cmap(np.linspace(0, 1, N))) +mpl.rcParams['axes.prop_cycle'] = cycler(color=cmap(np.linspace(0, 1, N))) fig, ax = plt.subplots() lines = ax.plot(data) diff --git a/examples/text_labels_and_annotations/date.py b/examples/text_labels_and_annotations/date.py index c8287fffcdc6..1bce29c2f5f1 100644 --- a/examples/text_labels_and_annotations/date.py +++ b/examples/text_labels_and_annotations/date.py @@ -3,66 +3,61 @@ Date tick labels ================ -Show how to make date plots in Matplotlib using date tick locators and -formatters. See :doc:`/gallery/ticks_and_spines/major_minor_demo` for more -information on controlling major and minor ticks. - Matplotlib date plotting is done by converting date instances into days since an epoch (by default 1970-01-01T00:00:00). The :mod:`matplotlib.dates` module provides the converter functions `.date2num` -and `.num2date`, which convert `datetime.datetime` and `numpy.datetime64` +and `.num2date` that convert `datetime.datetime` and `numpy.datetime64` objects to and from Matplotlib's internal representation. These data -types are registered with with the unit conversion mechanism described in +types are registered with the unit conversion mechanism described in :mod:`matplotlib.units`, so the conversion happens automatically for the user. The registration process also sets the default tick ``locator`` and ``formatter`` for the axis to be `~.matplotlib.dates.AutoDateLocator` and -`~.matplotlib.dates.AutoDateFormatter`. These can be changed manually with -`.Axis.set_major_locator` and `.Axis.set_major_formatter`; see for example -:doc:`/gallery/ticks_and_spines/date_demo_convert`. - -An alternative formatter is the `~.matplotlib.dates.ConciseDateFormatter` -as described at :doc:`/gallery/ticks_and_spines/date_concise_formatter`, -which often removes the need to rotate the tick labels. +`~.matplotlib.dates.AutoDateFormatter`. + +An alternative formatter is the `~.dates.ConciseDateFormatter`, +used in the second ``Axes`` below (see +:doc:`/gallery/ticks/date_concise_formatter`), which often removes the need to +rotate the tick labels. The last ``Axes`` formats the dates manually, using +`~.dates.DateFormatter` to format the dates using the format strings documented +at `datetime.date.strftime`. """ -import numpy as np import matplotlib.pyplot as plt import matplotlib.dates as mdates import matplotlib.cbook as cbook -# Load a numpy structured array from yahoo csv data with fields date, open, -# close, volume, adj_close from the mpl-data/example directory. This array -# stores the date as an np.datetime64 with a day unit ('D') in the 'date' -# column. +# Load a numpy record array from yahoo csv data with fields date, open, high, +# low, close, volume, adj_close from the mpl-data/sample_data directory. The +# record array stores the date as an np.datetime64 with a day unit ('D') in +# the date column. data = cbook.get_sample_data('goog.npz', np_load=True)['price_data'] -fig, ax = plt.subplots() -ax.plot('date', 'adj_close', data=data) - -# Major ticks every 6 months. -fmt_half_year = mdates.MonthLocator(interval=6) -ax.xaxis.set_major_locator(fmt_half_year) - -# Minor ticks every month. -fmt_month = mdates.MonthLocator() -ax.xaxis.set_minor_locator(fmt_month) - -# Text in the x axis will be displayed in 'YYYY-mm' format. -ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m')) - -# Round to nearest years. -datemin = np.datetime64(data['date'][0], 'Y') -datemax = np.datetime64(data['date'][-1], 'Y') + np.timedelta64(1, 'Y') -ax.set_xlim(datemin, datemax) - -# Format the coords message box, i.e. the numbers displayed as the cursor moves -# across the axes within the interactive GUI. -ax.format_xdata = mdates.DateFormatter('%Y-%m') -ax.format_ydata = lambda x: f'${x:.2f}' # Format the price. -ax.grid(True) - -# Rotates and right aligns the x labels, and moves the bottom of the -# axes up to make room for them. -fig.autofmt_xdate() +fig, axs = plt.subplots(3, 1, figsize=(6.4, 7), layout='constrained') +# common to all three: +for ax in axs: + ax.plot('date', 'adj_close', data=data) + # Major ticks every half year, minor ticks every month, + ax.xaxis.set_major_locator(mdates.MonthLocator(bymonth=(1, 7))) + ax.xaxis.set_minor_locator(mdates.MonthLocator()) + ax.grid(True) + ax.set_ylabel(r'Price [\$]') + +# different formats: +ax = axs[0] +ax.set_title('DefaultFormatter', loc='left', y=0.85, x=0.02, fontsize='medium') + +ax = axs[1] +ax.set_title('ConciseFormatter', loc='left', y=0.85, x=0.02, fontsize='medium') +ax.xaxis.set_major_formatter( + mdates.ConciseDateFormatter(ax.xaxis.get_major_locator())) + +ax = axs[2] +ax.set_title('Manual DateFormatter', loc='left', y=0.85, x=0.02, + fontsize='medium') +# Text in the x-axis will be displayed in 'YYYY-mm' format. +ax.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%b')) +# Rotates and right-aligns the x labels so they don't crowd each other. +for label in ax.get_xticklabels(which='major'): + label.set(rotation=30, horizontalalignment='right') plt.show() diff --git a/examples/text_labels_and_annotations/date_index_formatter.py b/examples/text_labels_and_annotations/date_index_formatter.py deleted file mode 100644 index 4da74ebb7687..000000000000 --- a/examples/text_labels_and_annotations/date_index_formatter.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -===================================== -Custom tick formatter for time series -===================================== - -When plotting time series, e.g., financial time series, one often wants -to leave out days on which there is no data, i.e. weekends. The example -below shows how to use an 'index formatter' to achieve the desired plot -""" -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.cbook as cbook - -# Load a numpy record array from yahoo csv data with fields date, open, close, -# volume, adj_close from the mpl-data/example directory. The record array -# stores the date as an np.datetime64 with a day unit ('D') in the date column. -r = (cbook.get_sample_data('goog.npz', np_load=True)['price_data'] - .view(np.recarray)) -r = r[-30:] # get the last 30 days - -# first we'll do it the default way, with gaps on weekends -fig, (ax1, ax2) = plt.subplots(ncols=2, figsize=(8, 4)) -ax1.plot(r.date, r.adj_close, 'o-') -ax1.set_title("Default") -fig.autofmt_xdate() - -# next we'll write a custom formatter -N = len(r) -ind = np.arange(N) # the evenly spaced plot indices - - -def format_date(x, pos=None): - thisind = np.clip(int(x + 0.5), 0, N - 1) - return r.date[thisind].item().strftime('%Y-%m-%d') - - -ax2.plot(ind, r.adj_close, 'o-') -# Use automatic FuncFormatter creation -ax2.xaxis.set_major_formatter(format_date) -ax2.set_title("Custom tick formatter") -fig.autofmt_xdate() - -plt.show() - - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.pyplot.subplots` -# - `matplotlib.axis.Axis.set_major_formatter` -# - `matplotlib.cbook.get_sample_data` -# - `matplotlib.ticker.FuncFormatter` diff --git a/examples/text_labels_and_annotations/demo_annotation_box.py b/examples/text_labels_and_annotations/demo_annotation_box.py index db808b543541..1f2f812649a4 100644 --- a/examples/text_labels_and_annotations/demo_annotation_box.py +++ b/examples/text_labels_and_annotations/demo_annotation_box.py @@ -1,13 +1,12 @@ """ =================== -Demo Annotation Box +AnnotationBbox demo =================== -The AnnotationBbox Artist creates an annotation using an OffsetBox. This -example demonstrates three different OffsetBoxes: TextArea, DrawingArea and -OffsetImage. AnnotationBbox gives more fine-grained control than using the axes -method annotate. - +`.AnnotationBbox` creates an annotation using an `.OffsetBox`, and +provides more fine-grained control than `.Axes.annotate`. This example +demonstrates the use of AnnotationBbox together with three different +OffsetBoxes: `.TextArea`, `.DrawingArea`, and `.OffsetImage`. """ import matplotlib.pyplot as plt @@ -32,7 +31,8 @@ xybox=(-20, 40), xycoords='data', boxcoords="offset points", - arrowprops=dict(arrowstyle="->")) + arrowprops=dict(arrowstyle="->"), + bboxprops=dict(boxstyle="sawtooth")) ax.add_artist(ab) # Annotate the 1st position with another text box ('Test') @@ -55,11 +55,12 @@ da.add_artist(p) ab = AnnotationBbox(da, xy, - xybox=(1.02, xy[1]), + xybox=(1., xy[1]), xycoords='data', boxcoords=("axes fraction", "data"), - box_alignment=(0., 0.5), - arrowprops=dict(arrowstyle="->")) + box_alignment=(0.2, 0.5), + arrowprops=dict(arrowstyle="->"), + bboxprops=dict(alpha=0.5)) ax.add_artist(ab) diff --git a/examples/text_labels_and_annotations/demo_text_path.py b/examples/text_labels_and_annotations/demo_text_path.py index 460670d79dae..1899f314f5b6 100644 --- a/examples/text_labels_and_annotations/demo_text_path.py +++ b/examples/text_labels_and_annotations/demo_text_path.py @@ -3,7 +3,7 @@ Using a text as a Path ====================== -`~matplotlib.textpath.TextPath` creates a `.Path` that is the outline of the +`~matplotlib.text.TextPath` creates a `.Path` that is the outline of the characters of a text. The resulting path can be employed e.g. as a clip path for an image. """ @@ -46,8 +46,6 @@ def draw(self, renderer=None): if __name__ == "__main__": - usetex = plt.rcParams["text.usetex"] - fig, (ax1, ax2) = plt.subplots(2) # EXAMPLE 1 @@ -68,30 +66,28 @@ def draw(self, renderer=None): ax1.add_artist(ao) # another text - from matplotlib.patches import PathPatch - if usetex: - r = r"\mbox{textpath supports mathtext \& \TeX}" - else: - r = r"textpath supports mathtext & TeX" - - text_path = TextPath((0, 0), r, size=20, usetex=usetex) - - p1 = PathPatch(text_path, ec="w", lw=3, fc="w", alpha=0.9, - transform=IdentityTransform()) - p2 = PathPatch(text_path, ec="none", fc="k", - transform=IdentityTransform()) - - offsetbox2 = AuxTransformBox(IdentityTransform()) - offsetbox2.add_artist(p1) - offsetbox2.add_artist(p2) - - ab = AnnotationBbox(offsetbox2, (0.95, 0.05), - xycoords='axes fraction', - boxcoords="offset points", - box_alignment=(1., 0.), - frameon=False - ) - ax1.add_artist(ab) + for usetex, ypos, string in [ + (False, 0.25, r"textpath supports mathtext"), + (True, 0.05, r"textpath supports \TeX"), + ]: + text_path = TextPath((0, 0), string, size=20, usetex=usetex) + + p1 = PathPatch(text_path, ec="w", lw=3, fc="w", alpha=0.9, + transform=IdentityTransform()) + p2 = PathPatch(text_path, ec="none", fc="k", + transform=IdentityTransform()) + + offsetbox2 = AuxTransformBox(IdentityTransform()) + offsetbox2.add_artist(p1) + offsetbox2.add_artist(p2) + + ab = AnnotationBbox(offsetbox2, (0.95, ypos), + xycoords='axes fraction', + boxcoords="offset points", + box_alignment=(1., 0.), + frameon=False, + ) + ax1.add_artist(ab) ax1.imshow([[0, 1, 2], [1, 2, 3]], cmap=plt.cm.gist_gray_r, interpolation="bilinear", aspect="auto") @@ -100,32 +96,34 @@ def draw(self, renderer=None): arr = np.arange(256).reshape(1, 256) / 256 - if usetex: - s = (r"$\displaystyle\left[\sum_{n=1}^\infty" - r"\frac{-e^{i\pi}}{2^n}\right]$!") - else: - s = r"$\left[\sum_{n=1}^\infty\frac{-e^{i\pi}}{2^n}\right]$!" - text_path = TextPath((0, 0), s, size=40, usetex=usetex) - text_patch = PathClippedImagePatch(text_path, arr, ec="none", - transform=IdentityTransform()) - - shadow1 = Shadow(text_patch, 1, -1, fc="none", ec="0.6", lw=3) - shadow2 = Shadow(text_patch, 1, -1, fc="0.3", ec="none") - - # make offset box - offsetbox = AuxTransformBox(IdentityTransform()) - offsetbox.add_artist(shadow1) - offsetbox.add_artist(shadow2) - offsetbox.add_artist(text_patch) - - # place the anchored offset box using AnnotationBbox - ab = AnnotationBbox(offsetbox, (0.5, 0.5), - xycoords='data', - boxcoords="offset points", - box_alignment=(0.5, 0.5), - ) - - ax2.add_artist(ab) + for usetex, xpos, string in [ + (False, 0.25, + r"$\left[\sum_{n=1}^\infty\frac{-e^{i\pi}}{2^n}\right]$!"), + (True, 0.75, + r"$\displaystyle\left[\sum_{n=1}^\infty" + r"\frac{-e^{i\pi}}{2^n}\right]$!"), + ]: + text_path = TextPath((0, 0), string, size=40, usetex=usetex) + text_patch = PathClippedImagePatch(text_path, arr, ec="none", + transform=IdentityTransform()) + + shadow1 = Shadow(text_patch, 1, -1, fc="none", ec="0.6", lw=3) + shadow2 = Shadow(text_patch, 1, -1, fc="0.3", ec="none") + + # make offset box + offsetbox = AuxTransformBox(IdentityTransform()) + offsetbox.add_artist(shadow1) + offsetbox.add_artist(shadow2) + offsetbox.add_artist(text_patch) + + # place the anchored offset box using AnnotationBbox + ab = AnnotationBbox(offsetbox, (xpos, 0.5), + xycoords='data', + boxcoords="offset points", + box_alignment=(0.5, 0.5), + ) + + ax2.add_artist(ab) ax2.set_xlim(0, 1) ax2.set_ylim(0, 1) diff --git a/examples/text_labels_and_annotations/demo_text_rotation_mode.py b/examples/text_labels_and_annotations/demo_text_rotation_mode.py index ded20e8abe8d..8920cb543162 100644 --- a/examples/text_labels_and_annotations/demo_text_rotation_mode.py +++ b/examples/text_labels_and_annotations/demo_text_rotation_mode.py @@ -18,61 +18,63 @@ - ``rotation_mode='anchor'`` aligns the unrotated text and then rotates the text around the point of alignment. +.. redirect-from:: /gallery/text_labels_and_annotations/text_rotation """ + import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1.axes_grid import ImageGrid -def test_rotation_mode(fig, mode, subplot_location): +def test_rotation_mode(fig, mode): ha_list = ["left", "center", "right"] va_list = ["top", "center", "baseline", "bottom"] - grid = ImageGrid(fig, subplot_location, - nrows_ncols=(len(va_list), len(ha_list)), - share_all=True, aspect=True, cbar_mode=None) + axs = fig.subplots(len(va_list), len(ha_list), sharex=True, sharey=True, + subplot_kw=dict(aspect=1), + gridspec_kw=dict(hspace=0, wspace=0)) # labels and title - for ha, ax in zip(ha_list, grid.axes_row[-1]): - ax.axis["bottom"].label.set_text(ha) - for va, ax in zip(va_list, grid.axes_column[0]): - ax.axis["left"].label.set_text(va) - grid.axes_row[0][1].set_title(f"rotation_mode='{mode}'", size="large") + for ha, ax in zip(ha_list, axs[-1, :]): + ax.set_xlabel(ha) + for va, ax in zip(va_list, axs[:, 0]): + ax.set_ylabel(va) + axs[0, 1].set_title(f"rotation_mode='{mode}'", size="large") - if mode == "default": - kw = dict() - else: - kw = dict( - bbox=dict(boxstyle="square,pad=0.", ec="none", fc="C1", alpha=0.3)) + kw = ( + {} if mode == "default" else + {"bbox": dict(boxstyle="square,pad=0.", ec="none", fc="C1", alpha=0.3)} + ) + + texts = {} # use a different text alignment in each axes - texts = [] - for (va, ha), ax in zip([(x, y) for x in va_list for y in ha_list], grid): - # prepare axes layout - for axis in ax.axis.values(): - axis.toggle(ticks=False, ticklabels=False) - ax.axvline(0.5, color="skyblue", zorder=0) - ax.axhline(0.5, color="skyblue", zorder=0) - ax.plot(0.5, 0.5, color="C0", marker="o", zorder=1) - - # add text with rotation and alignment settings - tx = ax.text(0.5, 0.5, "Tpg", - size="x-large", rotation=40, - horizontalalignment=ha, verticalalignment=va, - rotation_mode=mode, **kw) - texts.append(tx) + for i, va in enumerate(va_list): + for j, ha in enumerate(ha_list): + ax = axs[i, j] + # prepare axes layout + ax.set(xticks=[], yticks=[]) + ax.axvline(0.5, color="skyblue", zorder=0) + ax.axhline(0.5, color="skyblue", zorder=0) + ax.plot(0.5, 0.5, color="C0", marker="o", zorder=1) + # add text with rotation and alignment settings + tx = ax.text(0.5, 0.5, "Tpg", + size="x-large", rotation=40, + horizontalalignment=ha, verticalalignment=va, + rotation_mode=mode, **kw) + texts[ax] = tx if mode == "default": # highlight bbox fig.canvas.draw() - for ax, tx in zip(grid, texts): - bb = tx.get_window_extent().transformed(ax.transData.inverted()) + for ax, text in texts.items(): + bb = text.get_window_extent().transformed(ax.transData.inverted()) rect = plt.Rectangle((bb.x0, bb.y0), bb.width, bb.height, facecolor="C1", alpha=0.3, zorder=2) ax.add_patch(rect) -fig = plt.figure(figsize=(8, 6)) -test_rotation_mode(fig, "default", 121) -test_rotation_mode(fig, "anchor", 122) +fig = plt.figure(figsize=(8, 5)) +subfigs = fig.subfigures(1, 2) +test_rotation_mode(subfigs[0], "default") +test_rotation_mode(subfigs[1], "anchor") plt.show() diff --git a/examples/text_labels_and_annotations/fancyarrow_demo.py b/examples/text_labels_and_annotations/fancyarrow_demo.py index 89ce96c01083..12bf39ee57b6 100644 --- a/examples/text_labels_and_annotations/fancyarrow_demo.py +++ b/examples/text_labels_and_annotations/fancyarrow_demo.py @@ -4,54 +4,49 @@ ================================ Overview of the arrow styles available in `~.Axes.annotate`. - """ + +import inspect +import re +import itertools + import matplotlib.patches as mpatches import matplotlib.pyplot as plt styles = mpatches.ArrowStyle.get_styles() - ncol = 2 nrow = (len(styles) + 1) // ncol -figheight = (nrow + 0.5) -fig = plt.figure(figsize=(4 * ncol / 1.5, figheight / 1.5)) -fontsize = 0.2 * 70 - - -ax = fig.add_axes([0, 0, 1, 1], frameon=False, aspect=1.) - -ax.set_xlim(0, 4 * ncol) -ax.set_ylim(0, figheight) - - -def to_texstring(s): - s = s.replace("<", r"$<$") - s = s.replace(">", r"$>$") - s = s.replace("|", r"$|$") - return s - - -for i, (stylename, styleclass) in enumerate(sorted(styles.items())): - x = 3.2 + (i // nrow) * 4 - y = (figheight - 0.7 - i % nrow) # /figheight - p = mpatches.Circle((x, y), 0.2) - ax.add_patch(p) - - ax.annotate(to_texstring(stylename), (x, y), - (x - 1.2, y), - ha="right", va="center", - size=fontsize, - arrowprops=dict(arrowstyle=stylename, - patchB=p, - shrinkA=5, - shrinkB=5, - fc="k", ec="k", - connectionstyle="arc3,rad=-0.05", - ), +axs = (plt.figure(figsize=(4 * ncol, 1 + nrow)) + .add_gridspec(1 + nrow, ncol, + wspace=.7, left=.1, right=.9, bottom=0, top=1).subplots()) +for ax in axs.flat: + ax.set_axis_off() +for ax in axs[0, :]: + ax.text(0, .5, "arrowstyle", + transform=ax.transAxes, size="large", color="tab:blue", + horizontalalignment="center", verticalalignment="center") + ax.text(.35, .5, "default parameters", + transform=ax.transAxes, + horizontalalignment="left", verticalalignment="center") +for ax, (stylename, stylecls) in zip(axs[1:, :].T.flat, styles.items()): + l, = ax.plot(.25, .5, "ok", transform=ax.transAxes) + ax.annotate(stylename, (.25, .5), (-0.1, .5), + xycoords="axes fraction", textcoords="axes fraction", + size="large", color="tab:blue", + horizontalalignment="center", verticalalignment="center", + arrowprops=dict( + arrowstyle=stylename, connectionstyle="arc3,rad=-0.05", + color="k", shrinkA=5, shrinkB=5, patchB=l, + ), bbox=dict(boxstyle="square", fc="w")) - -ax.xaxis.set_visible(False) -ax.yaxis.set_visible(False) + # wrap at every nth comma (n = 1 or 2, depending on text length) + s = str(inspect.signature(stylecls))[1:-1] + n = 2 if s.count(',') > 3 else 1 + ax.text(.35, .5, + re.sub(', ', lambda m, c=itertools.count(1): m.group() + if next(c) % n else '\n', s), + transform=ax.transAxes, + horizontalalignment="left", verticalalignment="center") plt.show() diff --git a/examples/text_labels_and_annotations/figlegend_demo.py b/examples/text_labels_and_annotations/figlegend_demo.py index 674b7627f09f..e14d242bdd73 100644 --- a/examples/text_labels_and_annotations/figlegend_demo.py +++ b/examples/text_labels_and_annotations/figlegend_demo.py @@ -23,8 +23,31 @@ l3, = axs[1].plot(x, y3, color='tab:green') l4, = axs[1].plot(x, y4, color='tab:red', marker='^') -fig.legend((l1, l2), ('Line 1', 'Line 2'), 'upper left') -fig.legend((l3, l4), ('Line 3', 'Line 4'), 'upper right') +fig.legend((l1, l2), ('Line 1', 'Line 2'), loc='upper left') +fig.legend((l3, l4), ('Line 3', 'Line 4'), loc='upper right') plt.tight_layout() plt.show() + +############################################################################## +# Sometimes we do not want the legend to overlap the axes. If you use +# *constrained layout* you can specify "outside right upper", and +# *constrained layout* will make room for the legend. + +fig, axs = plt.subplots(1, 2, layout='constrained') + +x = np.arange(0.0, 2.0, 0.02) +y1 = np.sin(2 * np.pi * x) +y2 = np.exp(-x) +l1, = axs[0].plot(x, y1) +l2, = axs[0].plot(x, y2, marker='o') + +y3 = np.sin(4 * np.pi * x) +y4 = np.exp(-2 * x) +l3, = axs[1].plot(x, y3, color='tab:green') +l4, = axs[1].plot(x, y4, color='tab:red', marker='^') + +fig.legend((l1, l2), ('Line 1', 'Line 2'), loc='upper left') +fig.legend((l3, l4), ('Line 3', 'Line 4'), loc='outside right upper') + +plt.show() diff --git a/examples/text_labels_and_annotations/font_family_rc.py b/examples/text_labels_and_annotations/font_family_rc.py new file mode 100644 index 000000000000..d840a8575920 --- /dev/null +++ b/examples/text_labels_and_annotations/font_family_rc.py @@ -0,0 +1,73 @@ +""" +=========================== +Configuring the font family +=========================== + +You can explicitly set which font family is picked up, either by specifying +family names of fonts installed on user's system, or generic-families +(e.g., 'serif', 'sans-serif', 'monospace', 'fantasy' or 'cursive'), +or a combination of both. +(see :doc:`font tutorial `) + +In the example below, we are overriding the default sans-serif generic family +to include a specific (Tahoma) font. (Note that the best way to achieve this +would simply be to prepend 'Tahoma' in 'font.family') + +The default family is set with the font.family rcparam, +e.g. :: + + rcParams['font.family'] = 'sans-serif' + +and for the font.family you set a list of font styles to try to find +in order:: + + rcParams['font.sans-serif'] = ['Tahoma', 'DejaVu Sans', + 'Lucida Grande', 'Verdana'] + +.. redirect-from:: /gallery/font_family_rc_sgskip + + + +The font font.family defaults are OS dependent and can be viewed with +""" +import matplotlib.pyplot as plt + +print(plt.rcParams["font.sans-serif"][0]) +print(plt.rcParams["font.monospace"][0]) + + +################################################################# +# Choose default sans-serif font + +def print_text(text): + fig, ax = plt.subplots(figsize=(6, 1), facecolor="#eefade") + ax.text(0.5, 0.5, text, ha='center', va='center', size=40) + ax.axis("off") + plt.show() + + +plt.rcParams["font.family"] = "sans-serif" +print_text("Hello World! 01") + + +################################################################# +# Choose sans-serif font and specify to it to "Nimbus Sans" + +plt.rcParams["font.family"] = "sans-serif" +plt.rcParams["font.sans-serif"] = ["Nimbus Sans"] +print_text("Hello World! 02") + + +################################################################## +# Choose default monospace font + +plt.rcParams["font.family"] = "monospace" +print_text("Hello World! 03") + + +################################################################### +# Choose monospace font and specify to it to "FreeMono" + +plt.rcParams["font.family"] = "monospace" +plt.rcParams["font.monospace"] = ["FreeMono"] +print_text("Hello World! 04") diff --git a/examples/text_labels_and_annotations/font_family_rc_sgskip.py b/examples/text_labels_and_annotations/font_family_rc_sgskip.py deleted file mode 100644 index 1eada97fb7e2..000000000000 --- a/examples/text_labels_and_annotations/font_family_rc_sgskip.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -=========================== -Configuring the font family -=========================== - -You can explicitly set which font family is picked up for a given font -style (e.g., 'serif', 'sans-serif', or 'monospace'). - -In the example below, we only allow one font family (Tahoma) for the -sans-serif font style. The default family is set with the font.family rcparam, -e.g. :: - - rcParams['font.family'] = 'sans-serif' - -and for the font.family you set a list of font styles to try to find -in order:: - - rcParams['font.sans-serif'] = ['Tahoma', 'DejaVu Sans', - 'Lucida Grande', 'Verdana'] -""" - -from matplotlib import rcParams -rcParams['font.family'] = 'sans-serif' -rcParams['font.sans-serif'] = ['Tahoma'] -import matplotlib.pyplot as plt - -fig, ax = plt.subplots() -ax.plot([1, 2, 3], label='test') - -ax.legend() -plt.show() diff --git a/examples/text_labels_and_annotations/font_file.py b/examples/text_labels_and_annotations/font_file.py index 51752e6f4c5b..7d808a122231 100644 --- a/examples/text_labels_and_annotations/font_file.py +++ b/examples/text_labels_and_annotations/font_file.py @@ -12,7 +12,7 @@ Matplotlib. For a more flexible solution, see -:doc:`/gallery/text_labels_and_annotations/font_family_rc_sgskip` and +:doc:`/gallery/text_labels_and_annotations/font_family_rc` and :doc:`/gallery/text_labels_and_annotations/fonts_demo`. """ diff --git a/examples/text_labels_and_annotations/font_table.py b/examples/text_labels_and_annotations/font_table.py index e9296430ac13..6b03a602fbe6 100644 --- a/examples/text_labels_and_annotations/font_table.py +++ b/examples/text_labels_and_annotations/font_table.py @@ -75,8 +75,8 @@ def draw_font_table(path): # we have indeed selected a Unicode charmap. codes = font.get_charmap().items() - labelc = ["{:X}".format(i) for i in range(16)] - labelr = ["{:02X}".format(16 * i) for i in range(16)] + labelc = [f"{i:X}" for i in range(16)] + labelr = [f"{i:02X}" for i in range(0, 16*16, 16)] chars = [["" for c in range(16)] for r in range(16)] for char_code, glyph_index in codes: diff --git a/examples/text_labels_and_annotations/fonts_demo.py b/examples/text_labels_and_annotations/fonts_demo.py index 1aa38dcbcf38..112c6b0b4ce9 100644 --- a/examples/text_labels_and_annotations/fonts_demo.py +++ b/examples/text_labels_and_annotations/fonts_demo.py @@ -5,95 +5,66 @@ Set font properties using setters. -See :doc:`fonts_demo_kw` to achieve the same effect using kwargs. +See :doc:`fonts_demo_kw` to achieve the same effect using keyword arguments. """ from matplotlib.font_manager import FontProperties import matplotlib.pyplot as plt -font0 = FontProperties() +fig = plt.figure() alignment = {'horizontalalignment': 'center', 'verticalalignment': 'baseline'} -# Show family options - -families = ['serif', 'sans-serif', 'cursive', 'fantasy', 'monospace'] - -font1 = font0.copy() -font1.set_size('large') - -t = plt.figtext(0.1, 0.9, 'family', fontproperties=font1, **alignment) - yp = [0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2] +heading_font = FontProperties(size='large') +# Show family options +fig.text(0.1, 0.9, 'family', fontproperties=heading_font, **alignment) +families = ['serif', 'sans-serif', 'cursive', 'fantasy', 'monospace'] for k, family in enumerate(families): - font = font0.copy() + font = FontProperties() font.set_family(family) - t = plt.figtext(0.1, yp[k], family, fontproperties=font, **alignment) + fig.text(0.1, yp[k], family, fontproperties=font, **alignment) # Show style options - styles = ['normal', 'italic', 'oblique'] - -t = plt.figtext(0.3, 0.9, 'style', fontproperties=font1, **alignment) - +fig.text(0.3, 0.9, 'style', fontproperties=heading_font, **alignment) for k, style in enumerate(styles): - font = font0.copy() + font = FontProperties() font.set_family('sans-serif') font.set_style(style) - t = plt.figtext(0.3, yp[k], style, fontproperties=font, **alignment) + fig.text(0.3, yp[k], style, fontproperties=font, **alignment) # Show variant options - variants = ['normal', 'small-caps'] - -t = plt.figtext(0.5, 0.9, 'variant', fontproperties=font1, **alignment) - +fig.text(0.5, 0.9, 'variant', fontproperties=heading_font, **alignment) for k, variant in enumerate(variants): - font = font0.copy() + font = FontProperties() font.set_family('serif') font.set_variant(variant) - t = plt.figtext(0.5, yp[k], variant, fontproperties=font, **alignment) + fig.text(0.5, yp[k], variant, fontproperties=font, **alignment) # Show weight options - weights = ['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black'] - -t = plt.figtext(0.7, 0.9, 'weight', fontproperties=font1, **alignment) - +fig.text(0.7, 0.9, 'weight', fontproperties=heading_font, **alignment) for k, weight in enumerate(weights): - font = font0.copy() + font = FontProperties() font.set_weight(weight) - t = plt.figtext(0.7, yp[k], weight, fontproperties=font, **alignment) + fig.text(0.7, yp[k], weight, fontproperties=font, **alignment) # Show size options - -sizes = ['xx-small', 'x-small', 'small', 'medium', 'large', - 'x-large', 'xx-large'] - -t = plt.figtext(0.9, 0.9, 'size', fontproperties=font1, **alignment) - +sizes = [ + 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'] +fig.text(0.9, 0.9, 'size', fontproperties=heading_font, **alignment) for k, size in enumerate(sizes): - font = font0.copy() + font = FontProperties() font.set_size(size) - t = plt.figtext(0.9, yp[k], size, fontproperties=font, **alignment) + fig.text(0.9, yp[k], size, fontproperties=font, **alignment) # Show bold italic - -font = font0.copy() -font.set_style('italic') -font.set_weight('bold') -font.set_size('x-small') -t = plt.figtext(0.3, 0.1, 'bold italic', fontproperties=font, **alignment) - -font = font0.copy() -font.set_style('italic') -font.set_weight('bold') -font.set_size('medium') -t = plt.figtext(0.3, 0.2, 'bold italic', fontproperties=font, **alignment) - -font = font0.copy() -font.set_style('italic') -font.set_weight('bold') -font.set_size('x-large') -t = plt.figtext(-0.4, 0.3, 'bold italic', fontproperties=font, **alignment) +font = FontProperties(style='italic', weight='bold', size='x-small') +fig.text(0.3, 0.1, 'bold italic', fontproperties=font, **alignment) +font = FontProperties(style='italic', weight='bold', size='medium') +fig.text(0.3, 0.2, 'bold italic', fontproperties=font, **alignment) +font = FontProperties(style='italic', weight='bold', size='x-large') +fig.text(0.3, 0.3, 'bold italic', fontproperties=font, **alignment) plt.show() diff --git a/examples/text_labels_and_annotations/fonts_demo_kw.py b/examples/text_labels_and_annotations/fonts_demo_kw.py index afbdf1504431..ca19222f878d 100644 --- a/examples/text_labels_and_annotations/fonts_demo_kw.py +++ b/examples/text_labels_and_annotations/fonts_demo_kw.py @@ -1,76 +1,56 @@ """ -=================== -Fonts demo (kwargs) -=================== +============================== +Fonts demo (keyword arguments) +============================== -Set font properties using kwargs. +Set font properties using keyword arguments. See :doc:`fonts_demo` to achieve the same effect using setters. """ import matplotlib.pyplot as plt +fig = plt.figure() alignment = {'horizontalalignment': 'center', 'verticalalignment': 'baseline'} +yp = [0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2] # Show family options - +fig.text(0.1, 0.9, 'family', size='large', **alignment) families = ['serif', 'sans-serif', 'cursive', 'fantasy', 'monospace'] - -t = plt.figtext(0.1, 0.9, 'family', size='large', **alignment) - -yp = [0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2] - for k, family in enumerate(families): - t = plt.figtext(0.1, yp[k], family, family=family, **alignment) + fig.text(0.1, yp[k], family, family=family, **alignment) # Show style options - +fig.text(0.3, 0.9, 'style', **alignment) styles = ['normal', 'italic', 'oblique'] - -t = plt.figtext(0.3, 0.9, 'style', **alignment) - for k, style in enumerate(styles): - t = plt.figtext(0.3, yp[k], style, family='sans-serif', style=style, - **alignment) + fig.text(0.3, yp[k], style, family='sans-serif', style=style, **alignment) # Show variant options - +fig.text(0.5, 0.9, 'variant', **alignment) variants = ['normal', 'small-caps'] - -t = plt.figtext(0.5, 0.9, 'variant', **alignment) - for k, variant in enumerate(variants): - t = plt.figtext(0.5, yp[k], variant, family='serif', variant=variant, - **alignment) + fig.text(0.5, yp[k], variant, family='serif', variant=variant, **alignment) # Show weight options - +fig.text(0.7, 0.9, 'weight', **alignment) weights = ['light', 'normal', 'medium', 'semibold', 'bold', 'heavy', 'black'] - -t = plt.figtext(0.7, 0.9, 'weight', **alignment) - for k, weight in enumerate(weights): - t = plt.figtext(0.7, yp[k], weight, weight=weight, **alignment) + fig.text(0.7, yp[k], weight, weight=weight, **alignment) # Show size options - -sizes = ['xx-small', 'x-small', 'small', 'medium', 'large', - 'x-large', 'xx-large'] - -t = plt.figtext(0.9, 0.9, 'size', **alignment) - +fig.text(0.9, 0.9, 'size', **alignment) +sizes = [ + 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'] for k, size in enumerate(sizes): - t = plt.figtext(0.9, yp[k], size, size=size, **alignment) + fig.text(0.9, yp[k], size, size=size, **alignment) # Show bold italic -t = plt.figtext(0.3, 0.1, 'bold italic', style='italic', - weight='bold', size='x-small', - **alignment) -t = plt.figtext(0.3, 0.2, 'bold italic', - style='italic', weight='bold', size='medium', - **alignment) -t = plt.figtext(0.3, 0.3, 'bold italic', - style='italic', weight='bold', size='x-large', - **alignment) +fig.text(0.3, 0.1, 'bold italic', + style='italic', weight='bold', size='x-small', **alignment) +fig.text(0.3, 0.2, 'bold italic', + style='italic', weight='bold', size='medium', **alignment) +fig.text(0.3, 0.3, 'bold italic', + style='italic', weight='bold', size='x-large', **alignment) plt.show() diff --git a/examples/text_labels_and_annotations/label_subplots.py b/examples/text_labels_and_annotations/label_subplots.py index 974b775a9a97..b74e9a9d7437 100644 --- a/examples/text_labels_and_annotations/label_subplots.py +++ b/examples/text_labels_and_annotations/label_subplots.py @@ -17,7 +17,7 @@ import matplotlib.transforms as mtransforms fig, axs = plt.subplot_mosaic([['a)', 'c)'], ['b)', 'c)'], ['d)', 'd)']], - constrained_layout=True) + layout='constrained') for label, ax in axs.items(): # label physical distance in and down: @@ -33,7 +33,7 @@ # with each other, in which case we use a slightly different transform: fig, axs = plt.subplot_mosaic([['a)', 'c)'], ['b)', 'c)'], ['d)', 'd)']], - constrained_layout=True) + layout='constrained') for label, ax in axs.items(): # label physical distance to the left and up: @@ -48,7 +48,7 @@ # use the *loc* keyword argument: fig, axs = plt.subplot_mosaic([['a)', 'c)'], ['b)', 'c)'], ['d)', 'd)']], - constrained_layout=True) + layout='constrained') for label, ax in axs.items(): ax.set_title('Normal Title', fontstyle='italic') diff --git a/examples/text_labels_and_annotations/legend_demo.py b/examples/text_labels_and_annotations/legend_demo.py index 74bdd245df80..13b846389280 100644 --- a/examples/text_labels_and_annotations/legend_demo.py +++ b/examples/text_labels_and_annotations/legend_demo.py @@ -45,9 +45,9 @@ # Plot the lines y=x**n for n=1..4. for n in range(1, 5): - ax0.plot(x, x**n, label="n={0}".format(n)) + ax0.plot(x, x**n, label=f"{n=}") leg = ax0.legend(loc="upper left", bbox_to_anchor=[0, 1], - ncol=2, shadow=True, title="Legend", fancybox=True) + ncols=2, shadow=True, title="Legend", fancybox=True) leg.get_title().set_color("red") # Demonstrate some more complex labels. @@ -63,7 +63,7 @@ ############################################################################### # Here we attach legends to more complex plots. -fig, axs = plt.subplots(3, 1, constrained_layout=True) +fig, axs = plt.subplots(3, 1, layout="constrained") top_ax, middle_ax, bottom_ax = axs top_ax.bar([0, 1, 2], [0.2, 0.3, 0.1], width=0.4, label="Bar 1", @@ -86,7 +86,7 @@ ############################################################################### # Now we'll showcase legend entries with more than one legend key. -fig, (ax1, ax2) = plt.subplots(2, 1, constrained_layout=True) +fig, (ax1, ax2) = plt.subplots(2, 1, layout='constrained') # First plot: two legend keys for a single entry p1 = ax1.scatter([1], [5], c='r', marker='s', s=100) @@ -116,7 +116,7 @@ plt.show() ############################################################################### -# Finally, it is also possible to write custom objects that define +# Finally, it is also possible to write custom classes that define # how to stylize legends. @@ -166,7 +166,6 @@ def create_artists(self, legend, orig_handle, fig, ax = plt.subplots() colors = plt.rcParams['axes.prop_cycle'].by_key()['color'][:5] styles = ['solid', 'dashed', 'dashed', 'dashed', 'solid'] -lines = [] for i, color, style in zip(range(5), colors, styles): ax.plot(x, np.sin(x) - .1 * i, c=color, ls=style) diff --git a/examples/text_labels_and_annotations/line_with_text.py b/examples/text_labels_and_annotations/line_with_text.py index 81854bffd4fc..51f1a27d98ec 100644 --- a/examples/text_labels_and_annotations/line_with_text.py +++ b/examples/text_labels_and_annotations/line_with_text.py @@ -55,7 +55,6 @@ def draw(self, renderer): fig, ax = plt.subplots() x, y = np.random.rand(2, 20) line = MyLine(x, y, mfc='red', ms=12, label='line label') -#line.text.set_text('line label') line.text.set_color('red') line.text.set_fontsize(16) diff --git a/examples/text_labels_and_annotations/mathtext_asarray.py b/examples/text_labels_and_annotations/mathtext_asarray.py index 550b4aa090d8..224634046c68 100644 --- a/examples/text_labels_and_annotations/mathtext_asarray.py +++ b/examples/text_labels_and_annotations/mathtext_asarray.py @@ -21,10 +21,11 @@ def text_to_rgba(s, *, dpi, **kwargs): # (If desired, one can also directly save the image to the filesystem.) fig = Figure(facecolor="none") fig.text(0, 0, s, **kwargs) - buf = BytesIO() - fig.savefig(buf, dpi=dpi, format="png", bbox_inches="tight", pad_inches=0) - buf.seek(0) - rgba = plt.imread(buf) + with BytesIO() as buf: + fig.savefig(buf, dpi=dpi, format="png", bbox_inches="tight", + pad_inches=0) + buf.seek(0) + rgba = plt.imread(buf) return rgba diff --git a/examples/text_labels_and_annotations/mathtext_examples.py b/examples/text_labels_and_annotations/mathtext_examples.py index 6d8be49599cf..838b68bb2ca2 100644 --- a/examples/text_labels_and_annotations/mathtext_examples.py +++ b/examples/text_labels_and_annotations/mathtext_examples.py @@ -5,121 +5,116 @@ Selected features of Matplotlib's math rendering engine. """ -import matplotlib.pyplot as plt +import re import subprocess import sys -import re -# Selection of features following "Writing mathematical expressions" tutorial -mathtext_titles = { - 0: "Header demo", - 1: "Subscripts and superscripts", - 2: "Fractions, binomials and stacked numbers", - 3: "Radicals", - 4: "Fonts", - 5: "Accents", - 6: "Greek, Hebrew", - 7: "Delimiters, functions and Symbols"} -n_lines = len(mathtext_titles) +import matplotlib.pyplot as plt -# Randomly picked examples -mathext_demos = { - 0: r"$W^{3\beta}_{\delta_1 \rho_1 \sigma_2} = " - r"U^{3\beta}_{\delta_1 \rho_1} + \frac{1}{8 \pi 2} " - r"\int^{\alpha_2}_{\alpha_2} d \alpha^\prime_2 \left[\frac{ " - r"U^{2\beta}_{\delta_1 \rho_1} - \alpha^\prime_2U^{1\beta}_" - r"{\rho_1 \sigma_2} }{U^{0\beta}_{\rho_1 \sigma_2}}\right]$", - 1: r"$\alpha_i > \beta_i,\ " - r"\alpha_{i+1}^j = {\rm sin}(2\pi f_j t_i) e^{-5 t_i/\tau},\ " - r"\ldots$", +# Selection of features following "Writing mathematical expressions" tutorial, +# with randomly picked examples. +mathtext_demos = { + "Header demo": + r"$W^{3\beta}_{\delta_1 \rho_1 \sigma_2} = " + r"U^{3\beta}_{\delta_1 \rho_1} + \frac{1}{8 \pi 2} " + r"\int^{\alpha_2}_{\alpha_2} d \alpha^\prime_2 \left[\frac{ " + r"U^{2\beta}_{\delta_1 \rho_1} - \alpha^\prime_2U^{1\beta}_" + r"{\rho_1 \sigma_2} }{U^{0\beta}_{\rho_1 \sigma_2}}\right]$", - 2: r"$\frac{3}{4},\ \binom{3}{4},\ \genfrac{}{}{0}{}{3}{4},\ " - r"\left(\frac{5 - \frac{1}{x}}{4}\right),\ \ldots$", + "Subscripts and superscripts": + r"$\alpha_i > \beta_i,\ " + r"\alpha_{i+1}^j = {\rm sin}(2\pi f_j t_i) e^{-5 t_i/\tau},\ " + r"\ldots$", - 3: r"$\sqrt{2},\ \sqrt[3]{x},\ \ldots$", + "Fractions, binomials and stacked numbers": + r"$\frac{3}{4},\ \binom{3}{4},\ \genfrac{}{}{0}{}{3}{4},\ " + r"\left(\frac{5 - \frac{1}{x}}{4}\right),\ \ldots$", - 4: r"$\mathrm{Roman}\ , \ \mathit{Italic}\ , \ \mathtt{Typewriter} \ " - r"\mathrm{or}\ \mathcal{CALLIGRAPHY}$", + "Radicals": + r"$\sqrt{2},\ \sqrt[3]{x},\ \ldots$", - 5: r"$\acute a,\ \bar a,\ \breve a,\ \dot a,\ \ddot a, \ \grave a, \ " - r"\hat a,\ \tilde a,\ \vec a,\ \widehat{xyz},\ \widetilde{xyz},\ " - r"\ldots$", + "Fonts": + r"$\mathrm{Roman}\ , \ \mathit{Italic}\ , \ \mathtt{Typewriter} \ " + r"\mathrm{or}\ \mathcal{CALLIGRAPHY}$", - 6: r"$\alpha,\ \beta,\ \chi,\ \delta,\ \lambda,\ \mu,\ " - r"\Delta,\ \Gamma,\ \Omega,\ \Phi,\ \Pi,\ \Upsilon,\ \nabla,\ " - r"\aleph,\ \beth,\ \daleth,\ \gimel,\ \ldots$", + "Accents": + r"$\acute a,\ \bar a,\ \breve a,\ \dot a,\ \ddot a, \ \grave a, \ " + r"\hat a,\ \tilde a,\ \vec a,\ \widehat{xyz},\ \widetilde{xyz},\ " + r"\ldots$", - 7: r"$\coprod,\ \int,\ \oint,\ \prod,\ \sum,\ " - r"\log,\ \sin,\ \approx,\ \oplus,\ \star,\ \varpropto,\ " - r"\infty,\ \partial,\ \Re,\ \leftrightsquigarrow, \ \ldots$"} + "Greek, Hebrew": + r"$\alpha,\ \beta,\ \chi,\ \delta,\ \lambda,\ \mu,\ " + r"\Delta,\ \Gamma,\ \Omega,\ \Phi,\ \Pi,\ \Upsilon,\ \nabla,\ " + r"\aleph,\ \beth,\ \daleth,\ \gimel,\ \ldots$", + + "Delimiters, functions and Symbols": + r"$\coprod,\ \int,\ \oint,\ \prod,\ \sum,\ " + r"\log,\ \sin,\ \approx,\ \oplus,\ \star,\ \varpropto,\ " + r"\infty,\ \partial,\ \Re,\ \leftrightsquigarrow, \ \ldots$", +} +n_lines = len(mathtext_demos) def doall(): # Colors used in Matplotlib online documentation. - mpl_blue_rvb = (191. / 255., 209. / 256., 212. / 255.) - mpl_orange_rvb = (202. / 255., 121. / 256., 0. / 255.) - mpl_grey_rvb = (51. / 255., 51. / 255., 51. / 255.) + mpl_grey_rgb = (51 / 255, 51 / 255, 51 / 255) # Creating figure and axis. - plt.figure(figsize=(6, 7)) - plt.axes([0.01, 0.01, 0.98, 0.90], facecolor="white", frameon=True) - plt.gca().set_xlim(0., 1.) - plt.gca().set_ylim(0., 1.) - plt.gca().set_title("Matplotlib's math rendering engine", - color=mpl_grey_rvb, fontsize=14, weight='bold') - plt.gca().set_xticklabels("", visible=False) - plt.gca().set_yticklabels("", visible=False) + fig = plt.figure(figsize=(7, 7)) + ax = fig.add_axes([0.01, 0.01, 0.98, 0.90], + facecolor="white", frameon=True) + ax.set_xlim(0, 1) + ax.set_ylim(0, 1) + ax.set_title("Matplotlib's math rendering engine", + color=mpl_grey_rgb, fontsize=14, weight='bold') + ax.set_xticks([]) + ax.set_yticks([]) # Gap between lines in axes coords line_axesfrac = 1 / n_lines - # Plotting header demonstration formula - full_demo = mathext_demos[0] - plt.annotate(full_demo, - xy=(0.5, 1. - 0.59 * line_axesfrac), - color=mpl_orange_rvb, ha='center', fontsize=20) + # Plot header demonstration formula. + full_demo = mathtext_demos['Header demo'] + ax.annotate(full_demo, + xy=(0.5, 1. - 0.59 * line_axesfrac), + color='tab:orange', ha='center', fontsize=20) + + # Plot feature demonstration formulae. + for i_line, (title, demo) in enumerate(mathtext_demos.items()): + print(i_line, demo) + if i_line == 0: + continue - # Plotting features demonstration formulae - for i_line in range(1, n_lines): baseline = 1 - i_line * line_axesfrac baseline_next = baseline - line_axesfrac - title = mathtext_titles[i_line] + ":" - fill_color = ['white', mpl_blue_rvb][i_line % 2] - plt.fill_between([0., 1.], [baseline, baseline], - [baseline_next, baseline_next], - color=fill_color, alpha=0.5) - plt.annotate(title, - xy=(0.07, baseline - 0.3 * line_axesfrac), - color=mpl_grey_rvb, weight='bold') - demo = mathext_demos[i_line] - plt.annotate(demo, - xy=(0.05, baseline - 0.75 * line_axesfrac), - color=mpl_grey_rvb, fontsize=16) - - for i in range(n_lines): - s = mathext_demos[i] - print(i, s) + fill_color = ['white', 'tab:blue'][i_line % 2] + ax.axhspan(baseline, baseline_next, color=fill_color, alpha=0.2) + ax.annotate(f'{title}:', + xy=(0.06, baseline - 0.3 * line_axesfrac), + color=mpl_grey_rgb, weight='bold') + ax.annotate(demo, + xy=(0.04, baseline - 0.75 * line_axesfrac), + color=mpl_grey_rgb, fontsize=16) + plt.show() if '--latex' in sys.argv: # Run: python mathtext_examples.py --latex # Need amsmath and amssymb packages. - fd = open("mathtext_examples.ltx", "w") - fd.write("\\documentclass{article}\n") - fd.write("\\usepackage{amsmath, amssymb}\n") - fd.write("\\begin{document}\n") - fd.write("\\begin{enumerate}\n") - - for i in range(n_lines): - s = mathext_demos[i] - s = re.sub(r"(? np.timedelta64(1, 'D')) +for gap in r[['date', 'adj_close']][np.stack((gaps, gaps + 1)).T]: + ax1.plot(gap.date, gap.adj_close, 'w--', lw=2) +ax1.legend(handles=[ml.Line2D([], [], ls='--', label='Gaps in daily data')]) + +ax1.set_title("Plot y at x Coordinates") +ax1.xaxis.set_major_locator(DayLocator()) +ax1.xaxis.set_major_formatter(DateFormatter('%a')) + + +# Next we'll write a custom index formatter. Below we will plot +# the data against an index that goes from 0, 1, ... len(data). Instead of +# formatting the tick marks as integers, we format as times. +def format_date(x, _): + try: + # convert datetime64 to datetime, and use datetime's strftime: + return r.date[round(x)].item().strftime('%a') + except IndexError: + pass + +# Create an index plot (x defaults to range(len(y)) if omitted) +ax2.plot(r.adj_close, 'o-') + +ax2.set_title("Plot y at Index Coordinates Using Custom Formatter") +ax2.xaxis.set_major_formatter(format_date) # internally creates FuncFormatter + +############################################################################# +# Instead of passing a function into `.Axis.set_major_formatter` you can use +# any other callable, e.g. an instance of a class that implements __call__: + + +class MyFormatter(Formatter): + def __init__(self, dates, fmt='%a'): + self.dates = dates + self.fmt = fmt + + def __call__(self, x, pos=0): + """Return the label for time x at position pos.""" + try: + return self.dates[round(x)].item().strftime(self.fmt) + except IndexError: + pass + + +ax2.xaxis.set_major_formatter(MyFormatter(r.date, '%a')) diff --git a/examples/ticks_and_spines/date_precision_and_epochs.py b/examples/ticks/date_precision_and_epochs.py similarity index 95% rename from examples/ticks_and_spines/date_precision_and_epochs.py rename to examples/ticks/date_precision_and_epochs.py index 54e6b2e26b12..0f4f993f6f04 100644 --- a/examples/ticks_and_spines/date_precision_and_epochs.py +++ b/examples/ticks/date_precision_and_epochs.py @@ -66,7 +66,7 @@ def _reset_epoch_for_tutorial(): ############################################################################# # If a user wants to use modern dates at microsecond precision, they -# can change the epoch using `~.set_epoch`. However, the epoch has to be +# can change the epoch using `.set_epoch`. However, the epoch has to be # set before any date operations to prevent confusion between different # epochs. Trying to change the epoch later will raise a `RuntimeError`. @@ -128,19 +128,19 @@ def _reset_epoch_for_tutorial(): _reset_epoch_for_tutorial() # Don't do this. Just for this tutorial. mdates.set_epoch(new_epoch) -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout='constrained') ax.plot(xold, y) ax.set_title('Epoch: ' + mdates.get_epoch()) -plt.setp(ax.xaxis.get_majorticklabels(), rotation=40) +ax.xaxis.set_tick_params(rotation=40) plt.show() ############################################################################# # For dates plotted using the more recent epoch, the plot is smooth: -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout='constrained') ax.plot(x, y) ax.set_title('Epoch: ' + mdates.get_epoch()) -plt.setp(ax.xaxis.get_majorticklabels(), rotation=40) +ax.xaxis.set_tick_params(rotation=40) plt.show() _reset_epoch_for_tutorial() # Don't do this. Just for this tutorial. diff --git a/examples/pyplots/dollar_ticks.py b/examples/ticks/dollar_ticks.py similarity index 92% rename from examples/pyplots/dollar_ticks.py rename to examples/ticks/dollar_ticks.py index 1f99ccdd674d..b8fe95e7acd1 100644 --- a/examples/pyplots/dollar_ticks.py +++ b/examples/ticks/dollar_ticks.py @@ -1,9 +1,11 @@ """ ============ -Dollar Ticks +Dollar ticks ============ -Use a `~.ticker.FormatStrFormatter` to prepend dollar signs on y axis labels. +Use a `~.ticker.FormatStrFormatter` to prepend dollar signs on y-axis labels. + +.. redirect-from:: /gallery/pyplots/dollar_ticks """ import numpy as np import matplotlib.pyplot as plt diff --git a/examples/pyplots/fig_axes_customize_simple.py b/examples/ticks/fig_axes_customize_simple.py similarity index 76% rename from examples/pyplots/fig_axes_customize_simple.py rename to examples/ticks/fig_axes_customize_simple.py index 691a9cef26ed..74742f718939 100644 --- a/examples/pyplots/fig_axes_customize_simple.py +++ b/examples/ticks/fig_axes_customize_simple.py @@ -4,6 +4,8 @@ ========================= Customize the background, labels and ticks of a simple plot. + +.. redirect-from:: /gallery/pyplots/fig_axes_customize_simple """ import matplotlib.pyplot as plt @@ -19,18 +21,8 @@ rect = ax1.patch rect.set_facecolor('lightslategray') - -for label in ax1.xaxis.get_ticklabels(): - # label is a Text instance - label.set_color('tab:red') - label.set_rotation(45) - label.set_fontsize(16) - -for line in ax1.yaxis.get_ticklines(): - # line is a Line2D instance - line.set_color('tab:green') - line.set_markersize(25) - line.set_markeredgewidth(3) +ax1.tick_params(axis='x', labelcolor='tab:red', labelrotation=45, labelsize=16) +ax1.tick_params(axis='y', color='tab:green', size=25, width=3) plt.show() @@ -47,7 +39,7 @@ # - `matplotlib.text.Text.set_fontsize` # - `matplotlib.text.Text.set_color` # - `matplotlib.lines.Line2D` -# - `matplotlib.lines.Line2D.set_color` +# - `matplotlib.lines.Line2D.set_markeredgecolor` # - `matplotlib.lines.Line2D.set_markersize` # - `matplotlib.lines.Line2D.set_markeredgewidth` # - `matplotlib.patches.Patch.set_facecolor` diff --git a/examples/ticks_and_spines/major_minor_demo.py b/examples/ticks/major_minor_demo.py similarity index 96% rename from examples/ticks_and_spines/major_minor_demo.py rename to examples/ticks/major_minor_demo.py index 26d9a2ce5844..c139ff4741e4 100644 --- a/examples/ticks_and_spines/major_minor_demo.py +++ b/examples/ticks/major_minor_demo.py @@ -21,9 +21,9 @@ `.Axis.set_minor_formatter`. An appropriate `.StrMethodFormatter` will be created and used automatically. -`.pyplot.grid` changes the grid settings of the major ticks of the y and y axis -together. If you want to control the grid of the minor ticks for a given axis, -use for example :: +`.pyplot.grid` changes the grid settings of the major ticks of the x- and +y-axis together. If you want to control the grid of the minor ticks for a +given axis, use for example :: ax.xaxis.grid(True, which='minor') diff --git a/examples/ticks_and_spines/scalarformatter.py b/examples/ticks/scalarformatter.py similarity index 100% rename from examples/ticks_and_spines/scalarformatter.py rename to examples/ticks/scalarformatter.py diff --git a/examples/ticks_and_spines/tick-formatters.py b/examples/ticks/tick-formatters.py similarity index 93% rename from examples/ticks_and_spines/tick-formatters.py rename to examples/ticks/tick-formatters.py index 286d248e6d5b..0f5c8f38d411 100644 --- a/examples/ticks_and_spines/tick-formatters.py +++ b/examples/ticks/tick-formatters.py @@ -7,6 +7,7 @@ is formatted as a string. This example illustrates the usage and effect of the most common formatters. + """ import matplotlib.pyplot as plt @@ -17,9 +18,7 @@ def setup(ax, title): """Set up common parameters for the Axes in the example.""" # only show the bottom spine ax.yaxis.set_major_locator(ticker.NullLocator()) - ax.spines.right.set_color('none') - ax.spines.left.set_color('none') - ax.spines.top.set_color('none') + ax.spines[['left', 'right', 'top']].set_visible(False) # define tick positions ax.xaxis.set_major_locator(ticker.MultipleLocator(1.00)) @@ -34,12 +33,13 @@ def setup(ax, title): fontsize=14, fontname='Monospace', color='tab:blue') +############################################################################# # Tick formatters can be set in one of two ways, either by passing a ``str`` # or function to `~.Axis.set_major_formatter` or `~.Axis.set_minor_formatter`, # or by creating an instance of one of the various `~.ticker.Formatter` classes # and providing that to `~.Axis.set_major_formatter` or # `~.Axis.set_minor_formatter`. - +# # The first two examples directly pass a ``str`` or function. fig0, axs0 = plt.subplots(2, 1, figsize=(8, 2)) @@ -53,14 +53,15 @@ def setup(ax, title): # A function can also be used directly as a formatter. The function must take # two arguments: ``x`` for the tick value and ``pos`` for the tick position, -# and must return a ``str`` This creates a FuncFormatter automatically. +# and must return a ``str``. This creates a FuncFormatter automatically. setup(axs0[1], title="lambda x, pos: str(x-5)") axs0[1].xaxis.set_major_formatter(lambda x, pos: str(x-5)) fig0.tight_layout() -# The remaining examples use Formatter objects. +############################################################################# +# The remaining examples use `.Formatter` objects. fig1, axs1 = plt.subplots(7, 1, figsize=(8, 6)) fig1.suptitle('Formatter Object Formatting') diff --git a/examples/ticks_and_spines/tick-locators.py b/examples/ticks/tick-locators.py similarity index 95% rename from examples/ticks_and_spines/tick-locators.py rename to examples/ticks/tick-locators.py index eeac29a07ad2..1832f382529e 100644 --- a/examples/ticks_and_spines/tick-locators.py +++ b/examples/ticks/tick-locators.py @@ -17,9 +17,7 @@ def setup(ax, title): """Set up common parameters for the Axes in the example.""" # only show the bottom spine ax.yaxis.set_major_locator(ticker.NullLocator()) - ax.spines.right.set_color('none') - ax.spines.left.set_color('none') - ax.spines.top.set_color('none') + ax.spines[['left', 'right', 'top']].set_visible(False) ax.xaxis.set_ticks_position('bottom') ax.tick_params(which='major', width=1.00, length=5) diff --git a/examples/ticks_and_spines/tick_label_right.py b/examples/ticks/tick_label_right.py similarity index 64% rename from examples/ticks_and_spines/tick_label_right.py rename to examples/ticks/tick_label_right.py index f49492e93bf1..79eccd8777e7 100644 --- a/examples/ticks_and_spines/tick_label_right.py +++ b/examples/ticks/tick_label_right.py @@ -3,10 +3,9 @@ Set default y-axis tick labels on the right ============================================ -We can use :rc:`ytick.labelright` (default False) and :rc:`ytick.right` -(default False) and :rc:`ytick.labelleft` (default True) and :rc:`ytick.left` -(default True) to control where on the axes ticks and their labels appear. -These properties can also be set in the ``.matplotlib/matplotlibrc``. +We can use :rc:`ytick.labelright`, :rc:`ytick.right`, :rc:`ytick.labelleft`, +and :rc:`ytick.left` to control where on the axes ticks and their labels +appear. These properties can also be set in ``.matplotlib/matplotlibrc``. """ import matplotlib.pyplot as plt diff --git a/examples/ticks_and_spines/tick_labels_from_values.py b/examples/ticks/tick_labels_from_values.py similarity index 100% rename from examples/ticks_and_spines/tick_labels_from_values.py rename to examples/ticks/tick_labels_from_values.py diff --git a/examples/ticks/tick_xlabel_top.py b/examples/ticks/tick_xlabel_top.py new file mode 100644 index 000000000000..de2ca569851a --- /dev/null +++ b/examples/ticks/tick_xlabel_top.py @@ -0,0 +1,32 @@ +""" +================================== +Move x-axis tick labels to the top +================================== + +`~.axes.Axes.tick_params` can be used to configure the ticks. *top* and +*labeltop* control the visibility tick lines and labels at the top x-axis. +To move x-axis ticks from bottom to top, we have to activate the top ticks +and deactivate the bottom ticks:: + + ax.tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) + +.. note:: + + If the change should be made for all future plots and not only the current + Axes, you can adapt the respective config parameters + + - :rc:`xtick.top` + - :rc:`xtick.labeltop` + - :rc:`xtick.bottom` + - :rc:`xtick.labelbottom` + +""" + +import matplotlib.pyplot as plt + +fig, ax = plt.subplots() +ax.plot(range(10)) +ax.tick_params(top=True, labeltop=True, bottom=False, labelbottom=False) +ax.set_title('x-ticks moved to the top') + +plt.show() diff --git a/examples/ticks_and_spines/ticklabels_rotation.py b/examples/ticks/ticklabels_rotation.py similarity index 100% rename from examples/ticks_and_spines/ticklabels_rotation.py rename to examples/ticks/ticklabels_rotation.py diff --git a/examples/ticks/ticks_too_many.py b/examples/ticks/ticks_too_many.py new file mode 100644 index 000000000000..29bde86a4582 --- /dev/null +++ b/examples/ticks/ticks_too_many.py @@ -0,0 +1,76 @@ +""" +===================== +Fixing too many ticks +===================== + +One common cause for unexpected tick behavior is passing a list of strings +instead of numbers or datetime objects. This can easily happen without notice +when reading in a comma-delimited text file. Matplotlib treats lists of strings +as *categorical* variables +(:doc:`/gallery/lines_bars_and_markers/categorical_variables`), and by default +puts one tick per category, and plots them in the order in which they are +supplied. If this is not desired, the solution is to convert the strings to +a numeric type as in the following examples. + +""" + +############################################################################ +# Example 1: Strings can lead to an unexpected order of number ticks +# ------------------------------------------------------------------ + +import matplotlib.pyplot as plt +import numpy as np + +fig, ax = plt.subplots(1, 2, layout='constrained', figsize=(6, 2.5)) +x = ['1', '5', '2', '3'] +y = [1, 4, 2, 3] +ax[0].plot(x, y, 'd') +ax[0].tick_params(axis='x', color='r', labelcolor='r') +ax[0].set_xlabel('Categories') +ax[0].set_title('Ticks seem out of order / misplaced') + +# convert to numbers: +x = np.asarray(x, dtype='float') +ax[1].plot(x, y, 'd') +ax[1].set_xlabel('Floats') +ax[1].set_title('Ticks as expected') + +############################################################################ +# Example 2: Strings can lead to very many ticks +# ---------------------------------------------- +# If *x* has 100 elements, all strings, then we would have 100 (unreadable) +# ticks, and again the solution is to convert the strings to floats: + +fig, ax = plt.subplots(1, 2, figsize=(6, 2.5)) +x = [f'{xx}' for xx in np.arange(100)] +y = np.arange(100) +ax[0].plot(x, y) +ax[0].tick_params(axis='x', color='r', labelcolor='r') +ax[0].set_title('Too many ticks') +ax[0].set_xlabel('Categories') + +ax[1].plot(np.asarray(x, float), y) +ax[1].set_title('x converted to numbers') +ax[1].set_xlabel('Floats') + +############################################################################ +# Example 3: Strings can lead to an unexpected order of datetime ticks +# -------------------------------------------------------------------- +# A common case is when dates are read from a CSV file, they need to be +# converted from strings to datetime objects to get the proper date locators +# and formatters. + +fig, ax = plt.subplots(1, 2, layout='constrained', figsize=(6, 2.75)) +x = ['2021-10-01', '2021-11-02', '2021-12-03', '2021-09-01'] +y = [0, 2, 3, 1] +ax[0].plot(x, y, 'd') +ax[0].tick_params(axis='x', labelrotation=90, color='r', labelcolor='r') +ax[0].set_title('Dates out of order') + +# convert to datetime64 +x = np.asarray(x, dtype='datetime64[s]') +ax[1].plot(x, y, 'd') +ax[1].tick_params(axis='x', labelrotation=90) +ax[1].set_title('x converted to datetimes') + +plt.show() diff --git a/examples/ticks_and_spines/README.txt b/examples/ticks_and_spines/README.txt deleted file mode 100644 index e7869c5a08d1..000000000000 --- a/examples/ticks_and_spines/README.txt +++ /dev/null @@ -1,4 +0,0 @@ -.. _ticks_and_spines_examples: - -Ticks and spines -================ diff --git a/examples/ticks_and_spines/auto_ticks.py b/examples/ticks_and_spines/auto_ticks.py deleted file mode 100644 index 5e06b8cf02ab..000000000000 --- a/examples/ticks_and_spines/auto_ticks.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -================================= -Automatically setting tick labels -================================= - -Setting the behavior of tick auto-placement. - -If you don't explicitly set tick positions / labels, Matplotlib will attempt -to choose them both automatically based on the displayed data and its limits. - -By default, this attempts to choose tick positions that are distributed -along the axis: -""" - -import matplotlib.pyplot as plt -import numpy as np -np.random.seed(19680801) - -fig, ax = plt.subplots() -dots = np.arange(10) / 100. + .03 -x, y = np.meshgrid(dots, dots) -data = [x.ravel(), y.ravel()] -ax.scatter(*data, c=data[1]) - -############################################################################### -# Sometimes choosing evenly-distributed ticks results in strange tick numbers. -# If you'd like Matplotlib to keep ticks located at round numbers, you can -# change this behavior with the following rcParams value: - -print(plt.rcParams['axes.autolimit_mode']) - -# Now change this value and see the results -with plt.rc_context({'axes.autolimit_mode': 'round_numbers'}): - fig, ax = plt.subplots() - ax.scatter(*data, c=data[1]) - -############################################################################### -# You can also alter the margins of the axes around the data by -# with ``axes.(x,y)margin``: - -with plt.rc_context({'axes.autolimit_mode': 'round_numbers', - 'axes.xmargin': .8, - 'axes.ymargin': .8}): - fig, ax = plt.subplots() - ax.scatter(*data, c=data[1]) - -plt.show() diff --git a/examples/ticks_and_spines/custom_ticker1.py b/examples/ticks_and_spines/custom_ticker1.py deleted file mode 100644 index fc4e2bbb11dd..000000000000 --- a/examples/ticks_and_spines/custom_ticker1.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -============== -Custom Ticker1 -============== - -The new ticker code was designed to explicitly support user customized -ticking. The documentation of :mod:`matplotlib.ticker` details this -process. That code defines a lot of preset tickers but was primarily -designed to be user extensible. - -In this example a user defined function is used to format the ticks in -millions of dollars on the y axis. -""" -import matplotlib.pyplot as plt - -money = [1.5e5, 2.5e6, 5.5e6, 2.0e7] - - -def millions(x, pos): - """The two args are the value and tick position.""" - return '${:1.1f}M'.format(x*1e-6) - -fig, ax = plt.subplots() -# Use automatic FuncFormatter creation -ax.yaxis.set_major_formatter(millions) -ax.bar(['Bill', 'Fred', 'Mary', 'Sue'], money) -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.pyplot.subplots` -# - `matplotlib.axis.Axis.set_major_formatter` -# - `matplotlib.ticker.FuncFormatter` diff --git a/examples/ticks_and_spines/date_index_formatter2.py b/examples/ticks_and_spines/date_index_formatter2.py deleted file mode 100644 index 38c9038812ae..000000000000 --- a/examples/ticks_and_spines/date_index_formatter2.py +++ /dev/null @@ -1,45 +0,0 @@ -""" -==================== -Date Index Formatter -==================== - -When plotting daily data, a frequent request is to plot the data -ignoring skips, e.g., no extra spaces for weekends. This is particularly -common in financial time series, when you may have data for M-F and -not Sat, Sun and you don't want gaps in the x axis. The approach is -to simply use the integer index for the xdata and a custom tick -Formatter to get the appropriate date string for a given index. -""" - -import dateutil.parser -from matplotlib import cbook, dates -import matplotlib.pyplot as plt -from matplotlib.ticker import Formatter -import numpy as np - - -datafile = cbook.get_sample_data('msft.csv', asfileobj=False) -print('loading %s' % datafile) -msft_data = np.genfromtxt( - datafile, delimiter=',', names=True, - converters={0: lambda s: dates.date2num(dateutil.parser.parse(s))}) - - -class MyFormatter(Formatter): - def __init__(self, dates, fmt='%Y-%m-%d'): - self.dates = dates - self.fmt = fmt - - def __call__(self, x, pos=0): - """Return the label for time x at position pos.""" - ind = int(round(x)) - if ind >= len(self.dates) or ind < 0: - return '' - return dates.num2date(self.dates[ind]).strftime(self.fmt) - - -fig, ax = plt.subplots() -ax.xaxis.set_major_formatter(MyFormatter(msft_data['Date'])) -ax.plot(msft_data['Close'], 'o-') -fig.autofmt_xdate() -plt.show() diff --git a/examples/ticks_and_spines/multiple_yaxis_with_spines.py b/examples/ticks_and_spines/multiple_yaxis_with_spines.py deleted file mode 100644 index 54eebdae11ba..000000000000 --- a/examples/ticks_and_spines/multiple_yaxis_with_spines.py +++ /dev/null @@ -1,55 +0,0 @@ -r""" -========================== -Multiple Yaxis With Spines -========================== - -Create multiple y axes with a shared x axis. This is done by creating -a `~.axes.Axes.twinx` axes, turning all spines but the right one invisible -and offset its position using `~.spines.Spine.set_position`. - -Note that this approach uses `matplotlib.axes.Axes` and their -`~matplotlib.spines.Spine`\s. An alternative approach for parasite -axes is shown in the :doc:`/gallery/axisartist/demo_parasite_axes` and -:doc:`/gallery/axisartist/demo_parasite_axes2` examples. -""" - -import matplotlib.pyplot as plt - - -fig, ax = plt.subplots() -fig.subplots_adjust(right=0.75) - -twin1 = ax.twinx() -twin2 = ax.twinx() - -# Offset the right spine of twin2. The ticks and label have already been -# placed on the right by twinx above. -twin2.spines.right.set_position(("axes", 1.2)) - -p1, = ax.plot([0, 1, 2], [0, 1, 2], "b-", label="Density") -p2, = twin1.plot([0, 1, 2], [0, 3, 2], "r-", label="Temperature") -p3, = twin2.plot([0, 1, 2], [50, 30, 15], "g-", label="Velocity") - -ax.set_xlim(0, 2) -ax.set_ylim(0, 2) -twin1.set_ylim(0, 4) -twin2.set_ylim(1, 65) - -ax.set_xlabel("Distance") -ax.set_ylabel("Density") -twin1.set_ylabel("Temperature") -twin2.set_ylabel("Velocity") - -ax.yaxis.label.set_color(p1.get_color()) -twin1.yaxis.label.set_color(p2.get_color()) -twin2.yaxis.label.set_color(p3.get_color()) - -tkw = dict(size=4, width=1.5) -ax.tick_params(axis='y', colors=p1.get_color(), **tkw) -twin1.tick_params(axis='y', colors=p2.get_color(), **tkw) -twin2.tick_params(axis='y', colors=p3.get_color(), **tkw) -ax.tick_params(axis='x', **tkw) - -ax.legend(handles=[p1, p2, p3]) - -plt.show() diff --git a/examples/ticks_and_spines/spines.py b/examples/ticks_and_spines/spines.py deleted file mode 100644 index 08e3700d1387..000000000000 --- a/examples/ticks_and_spines/spines.py +++ /dev/null @@ -1,46 +0,0 @@ -""" -====== -Spines -====== - -This demo compares: - -- normal axes, with spines on all four sides; -- an axes with spines only on the left and bottom; -- an axes using custom bounds to limit the extent of the spine. -""" -import numpy as np -import matplotlib.pyplot as plt - - -x = np.linspace(0, 2 * np.pi, 100) -y = 2 * np.sin(x) - -# Constrained layout makes sure the labels don't overlap the axes. -fig, (ax0, ax1, ax2) = plt.subplots(nrows=3, constrained_layout=True) - -ax0.plot(x, y) -ax0.set_title('normal spines') - -ax1.plot(x, y) -ax1.set_title('bottom-left spines') - -# Hide the right and top spines -ax1.spines.right.set_visible(False) -ax1.spines.top.set_visible(False) -# Only show ticks on the left and bottom spines -ax1.yaxis.set_ticks_position('left') -ax1.xaxis.set_ticks_position('bottom') - -ax2.plot(x, y) - -# Only draw spine between the y-ticks -ax2.spines.left.set_bounds(-1, 1) -# Hide the right and top spines -ax2.spines.right.set_visible(False) -ax2.spines.top.set_visible(False) -# Only show ticks on the left and bottom spines -ax2.yaxis.set_ticks_position('left') -ax2.xaxis.set_ticks_position('bottom') - -plt.show() diff --git a/examples/ticks_and_spines/spines_bounds.py b/examples/ticks_and_spines/spines_bounds.py deleted file mode 100644 index 96205a43d38e..000000000000 --- a/examples/ticks_and_spines/spines_bounds.py +++ /dev/null @@ -1,38 +0,0 @@ -""" -=================== -Custom spine bounds -=================== - -Demo of spines using custom bounds to limit the extent of the spine. -""" -import numpy as np -import matplotlib.pyplot as plt - -# Fixing random state for reproducibility -np.random.seed(19680801) - -x = np.linspace(0, 2*np.pi, 50) -y = np.sin(x) -y2 = y + 0.1 * np.random.normal(size=x.shape) - -fig, ax = plt.subplots() -ax.plot(x, y) -ax.plot(x, y2) - -# set ticks and tick labels -ax.set_xlim((0, 2*np.pi)) -ax.set_xticks([0, np.pi, 2*np.pi]) -ax.set_xticklabels(['0', r'$\pi$', r'2$\pi$']) -ax.set_ylim((-1.5, 1.5)) -ax.set_yticks([-1, 0, 1]) - -# Only draw spine between the y-ticks -ax.spines.left.set_bounds((-1, 1)) -# Hide the right and top spines -ax.spines.right.set_visible(False) -ax.spines.top.set_visible(False) -# Only show ticks on the left and bottom spines -ax.yaxis.set_ticks_position('left') -ax.xaxis.set_ticks_position('bottom') - -plt.show() diff --git a/examples/ticks_and_spines/tick_xlabel_top.py b/examples/ticks_and_spines/tick_xlabel_top.py deleted file mode 100644 index 5ed2f81c23ce..000000000000 --- a/examples/ticks_and_spines/tick_xlabel_top.py +++ /dev/null @@ -1,27 +0,0 @@ -""" -========================================== -Set default x-axis tick labels on the top -========================================== - -We can use :rc:`xtick.labeltop` and :rc:`xtick.top` and :rc:`xtick.labelbottom` -and :rc:`xtick.bottom` to control where on the axes ticks and their labels -appear. - -These properties can also be set in ``.matplotlib/matplotlibrc``. -""" - -import matplotlib.pyplot as plt -import numpy as np - - -plt.rcParams['xtick.bottom'] = plt.rcParams['xtick.labelbottom'] = False -plt.rcParams['xtick.top'] = plt.rcParams['xtick.labeltop'] = True - -x = np.arange(10) - -fig, ax = plt.subplots() - -ax.plot(x) -ax.set_title('xlabel top') # Note title moves to make room for ticks - -plt.show() diff --git a/examples/units/bar_unit_demo.py b/examples/units/bar_unit_demo.py index da963e19f734..b64784cc5c07 100644 --- a/examples/units/bar_unit_demo.py +++ b/examples/units/bar_unit_demo.py @@ -18,23 +18,22 @@ N = 5 -men_means = [150*cm, 160*cm, 146*cm, 172*cm, 155*cm] -men_std = [20*cm, 30*cm, 32*cm, 10*cm, 20*cm] +tea_means = [15*cm, 10*cm, 8*cm, 12*cm, 5*cm] +tea_std = [2*cm, 1*cm, 1*cm, 4*cm, 2*cm] fig, ax = plt.subplots() ind = np.arange(N) # the x locations for the groups width = 0.35 # the width of the bars -ax.bar(ind, men_means, width, bottom=0*cm, yerr=men_std, label='Men') +ax.bar(ind, tea_means, width, bottom=0*cm, yerr=tea_std, label='Tea') -women_means = (145*cm, 149*cm, 172*cm, 165*cm, 200*cm) -women_std = (30*cm, 25*cm, 20*cm, 31*cm, 22*cm) -ax.bar(ind + width, women_means, width, bottom=0*cm, yerr=women_std, - label='Women') +coffee_means = (14*cm, 19*cm, 7*cm, 5*cm, 10*cm) +coffee_std = (3*cm, 5*cm, 2*cm, 1*cm, 2*cm) +ax.bar(ind + width, coffee_means, width, bottom=0*cm, yerr=coffee_std, + label='Coffee') -ax.set_title('Scores by group and gender') -ax.set_xticks(ind + width / 2) -ax.set_xticklabels(('G1', 'G2', 'G3', 'G4', 'G5')) +ax.set_title('Cup height by group and beverage choice') +ax.set_xticks(ind + width / 2, labels=['G1', 'G2', 'G3', 'G4', 'G5']) ax.legend() ax.yaxis.set_units(inch) diff --git a/examples/units/basic_units.py b/examples/units/basic_units.py index 727b538ec183..95df58ccf0c4 100644 --- a/examples/units/basic_units.py +++ b/examples/units/basic_units.py @@ -5,10 +5,10 @@ """ -from distutils.version import LooseVersion import math import numpy as np +from packaging.version import parse as parse_version import matplotlib.units as units import matplotlib.ticker as ticker @@ -79,8 +79,8 @@ def __call__(self, *args): arg_units = [self.unit] for a in args: if hasattr(a, 'get_unit') and not hasattr(a, 'convert_to'): - # if this arg has a unit type but no conversion ability, - # this operation is prohibited + # If this argument has a unit type but no conversion ability, + # this operation is prohibited. return NotImplemented if hasattr(a, 'convert_to'): @@ -132,6 +132,9 @@ def __init__(self, value, unit): self.unit = unit self.proxy_target = self.value + def __copy__(self): + return TaggedValue(self.value, self.unit) + def __getattribute__(self, name): if name.startswith('__'): return object.__getattribute__(self, name) @@ -141,21 +144,21 @@ def __getattribute__(self, name): return object.__getattribute__(self, name) def __array__(self, dtype=object): - return np.asarray(self.value).astype(dtype) + return np.asarray(self.value, dtype) def __array_wrap__(self, array, context): return TaggedValue(array, self.unit) def __repr__(self): - return 'TaggedValue({!r}, {!r})'.format(self.value, self.unit) + return f'TaggedValue({self.value!r}, {self.unit!r})' def __str__(self): - return str(self.value) + ' in ' + str(self.unit) + return f"{self.value} in {self.unit}" def __len__(self): return len(self.value) - if LooseVersion(np.__version__) >= '1.20': + if parse_version(np.__version__) >= parse_version('1.20'): def __getitem__(self, key): return TaggedValue(self.value[key], self.unit) @@ -343,8 +346,6 @@ def axisinfo(unit, axis): @staticmethod def convert(val, unit, axis): - if units.ConversionInterface.is_numlike(val): - return val if np.iterable(val): if isinstance(val, np.ma.MaskedArray): val = val.astype(float).filled(np.nan) diff --git a/examples/units/ellipse_with_units.py b/examples/units/ellipse_with_units.py index 2a2242cde30c..369ad5cf4e68 100644 --- a/examples/units/ellipse_with_units.py +++ b/examples/units/ellipse_with_units.py @@ -1,14 +1,15 @@ """ ================== -Ellipse With Units +Ellipse with units ================== -Compare the ellipse generated with arcs versus a polygonal approximation +Compare the ellipse generated with arcs versus a polygonal approximation. .. only:: builder_html This example requires :download:`basic_units.py ` """ + from basic_units import cm import numpy as np from matplotlib import patches diff --git a/examples/units/evans_test.py b/examples/units/evans_test.py index 8e6c363eaaf9..3a4ac2fe7f55 100644 --- a/examples/units/evans_test.py +++ b/examples/units/evans_test.py @@ -48,9 +48,6 @@ def convert(obj, unit, axis): If *obj* is a sequence, return the converted sequence. """ - if units.ConversionInterface.is_numlike(obj): - return obj - if np.iterable(obj): return [o.value(unit) for o in obj] else: diff --git a/examples/units/units_sample.py b/examples/units/units_sample.py index db39ee73dbe9..13448a30883e 100644 --- a/examples/units/units_sample.py +++ b/examples/units/units_sample.py @@ -19,14 +19,14 @@ cms = cm * np.arange(0, 10, 2) -fig, axs = plt.subplots(2, 2) +fig, axs = plt.subplots(2, 2, layout='constrained') axs[0, 0].plot(cms, cms) axs[0, 1].plot(cms, cms, xunits=cm, yunits=inch) axs[1, 0].plot(cms, cms, xunits=inch, yunits=cm) -axs[1, 0].set_xlim(3, 6) # scalars are interpreted in current units +axs[1, 0].set_xlim(-1, 4) # scalars are interpreted in current units axs[1, 1].plot(cms, cms, xunits=inch, yunits=inch) axs[1, 1].set_xlim(3*cm, 6*cm) # cm are converted to inches diff --git a/examples/user_interfaces/README.txt b/examples/user_interfaces/README.txt index d526adc9d65d..75b469da7cf6 100644 --- a/examples/user_interfaces/README.txt +++ b/examples/user_interfaces/README.txt @@ -8,6 +8,6 @@ following the embedding_in_SOMEGUI.py examples here. Currently Matplotlib supports PyQt/PySide, PyGObject, Tkinter, and wxPython. When embedding Matplotlib in a GUI, you must use the Matplotlib API -directly rather than the pylab/pyplot proceedural interface, so take a +directly rather than the pylab/pyplot procedural interface, so take a look at the examples/api directory for some example code working with the API. diff --git a/examples/user_interfaces/canvasagg.py b/examples/user_interfaces/canvasagg.py index c9ab14218b69..afb1c3acbf7f 100644 --- a/examples/user_interfaces/canvasagg.py +++ b/examples/user_interfaces/canvasagg.py @@ -14,14 +14,21 @@ the backend to "Agg" would be sufficient. In this example, we show how to save the contents of the agg canvas to a file, -and how to extract them to a string, which can in turn be passed off to PIL or -put in a numpy array. The latter functionality allows e.g. to use Matplotlib -inside a cgi-script *without* needing to write a figure to disk. +and how to extract them to a numpy array, which can in turn be passed off +to Pillow_. The latter functionality allows e.g. to use Matplotlib inside a +cgi-script *without* needing to write a figure to disk, and to write images in +any format supported by Pillow. + +.. _Pillow: https://pillow.readthedocs.io/ +.. redirect-from:: /gallery/misc/agg_buffer +.. redirect-from:: /gallery/misc/agg_buffer_to_array """ from matplotlib.backends.backend_agg import FigureCanvasAgg from matplotlib.figure import Figure import numpy as np +from PIL import Image + fig = Figure(figsize=(5, 4), dpi=100) # A canvas must be manually attached to the figure (pyplot would automatically @@ -37,14 +44,14 @@ # etc.). fig.savefig("test.png") -# Option 2: Retrieve a view on the renderer buffer... +# Option 2: Retrieve a memoryview on the renderer buffer, and convert it to a +# numpy array. canvas.draw() -buf = canvas.buffer_rgba() -# ... convert to a NumPy array ... -X = np.asarray(buf) +rgba = np.asarray(canvas.buffer_rgba()) # ... and pass it to PIL. -from PIL import Image -im = Image.fromarray(X) +im = Image.fromarray(rgba) +# This image can then be saved to any format supported by Pillow, e.g.: +im.save("test.bmp") # Uncomment this line to display the image using ImageMagick's `display` tool. # im.show() diff --git a/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py b/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py index 2f0833f09511..7009b55bada7 100644 --- a/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py +++ b/examples/user_interfaces/embedding_in_gtk3_panzoom_sgskip.py @@ -20,7 +20,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK") +win.set_title("Embedding in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot(1, 1, 1) @@ -36,7 +36,7 @@ vbox.pack_start(canvas, True, True, 0) # Create toolbar -toolbar = NavigationToolbar(canvas, win) +toolbar = NavigationToolbar(canvas) vbox.pack_start(toolbar, False, False, 0) win.show_all() diff --git a/examples/user_interfaces/embedding_in_gtk3_sgskip.py b/examples/user_interfaces/embedding_in_gtk3_sgskip.py index f5872304964d..b672ba8d9ff0 100644 --- a/examples/user_interfaces/embedding_in_gtk3_sgskip.py +++ b/examples/user_interfaces/embedding_in_gtk3_sgskip.py @@ -19,7 +19,7 @@ win = Gtk.Window() win.connect("delete-event", Gtk.main_quit) win.set_default_size(400, 300) -win.set_title("Embedding in GTK") +win.set_title("Embedding in GTK3") fig = Figure(figsize=(5, 4), dpi=100) ax = fig.add_subplot() diff --git a/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py b/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py new file mode 100644 index 000000000000..b132aff1074f --- /dev/null +++ b/examples/user_interfaces/embedding_in_gtk4_panzoom_sgskip.py @@ -0,0 +1,51 @@ +""" +=========================================== +Embedding in GTK4 with a navigation toolbar +=========================================== + +Demonstrate NavigationToolbar with GTK4 accessed via pygobject. +""" + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + +from matplotlib.backends.backend_gtk4 import ( + NavigationToolbar2GTK4 as NavigationToolbar) +from matplotlib.backends.backend_gtk4agg import ( + FigureCanvasGTK4Agg as FigureCanvas) +from matplotlib.figure import Figure +import numpy as np + + +def on_activate(app): + win = Gtk.ApplicationWindow(application=app) + win.set_default_size(400, 300) + win.set_title("Embedding in GTK4") + + fig = Figure(figsize=(5, 4), dpi=100) + ax = fig.add_subplot(1, 1, 1) + t = np.arange(0.0, 3.0, 0.01) + s = np.sin(2*np.pi*t) + ax.plot(t, s) + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) + win.set_child(vbox) + + # Add canvas to vbox + canvas = FigureCanvas(fig) # a Gtk.DrawingArea + canvas.set_hexpand(True) + canvas.set_vexpand(True) + vbox.append(canvas) + + # Create toolbar + toolbar = NavigationToolbar(canvas) + vbox.append(toolbar) + + win.show() + + +app = Gtk.Application( + application_id='org.matplotlib.examples.EmbeddingInGTK4PanZoom') +app.connect('activate', on_activate) +app.run(None) diff --git a/examples/user_interfaces/embedding_in_gtk4_sgskip.py b/examples/user_interfaces/embedding_in_gtk4_sgskip.py new file mode 100644 index 000000000000..c92e139de25f --- /dev/null +++ b/examples/user_interfaces/embedding_in_gtk4_sgskip.py @@ -0,0 +1,45 @@ +""" +================= +Embedding in GTK4 +================= + +Demonstrate adding a FigureCanvasGTK4Agg widget to a Gtk.ScrolledWindow using +GTK4 accessed via pygobject. +""" + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + +from matplotlib.backends.backend_gtk4agg import ( + FigureCanvasGTK4Agg as FigureCanvas) +from matplotlib.figure import Figure +import numpy as np + + +def on_activate(app): + win = Gtk.ApplicationWindow(application=app) + win.set_default_size(400, 300) + win.set_title("Embedding in GTK4") + + fig = Figure(figsize=(5, 4), dpi=100) + ax = fig.add_subplot() + t = np.arange(0.0, 3.0, 0.01) + s = np.sin(2*np.pi*t) + ax.plot(t, s) + + # A scrolled margin goes outside the scrollbars and viewport. + sw = Gtk.ScrolledWindow(margin_top=10, margin_bottom=10, + margin_start=10, margin_end=10) + win.set_child(sw) + + canvas = FigureCanvas(fig) # a Gtk.DrawingArea + canvas.set_size_request(800, 600) + sw.set_child(canvas) + + win.show() + + +app = Gtk.Application(application_id='org.matplotlib.examples.EmbeddingInGTK4') +app.connect('activate', on_activate) +app.run(None) diff --git a/examples/user_interfaces/embedding_in_qt_sgskip.py b/examples/user_interfaces/embedding_in_qt_sgskip.py index 8994c079943d..32529739ac14 100644 --- a/examples/user_interfaces/embedding_in_qt_sgskip.py +++ b/examples/user_interfaces/embedding_in_qt_sgskip.py @@ -4,9 +4,9 @@ =============== Simple Qt application embedding Matplotlib canvases. This program will work -equally well using Qt4 and Qt5. Either version of Qt can be selected (for -example) by setting the ``MPLBACKEND`` environment variable to "Qt4Agg" or -"Qt5Agg", or by first importing the desired version of PyQt. +equally well using any Qt binding (PyQt6, PySide6, PyQt5, PySide2). The +binding can be selected by setting the :envvar:`QT_API` environment variable to +the binding name, or by first importing it. """ import sys @@ -14,13 +14,9 @@ import numpy as np -from matplotlib.backends.qt_compat import QtCore, QtWidgets -if QtCore.qVersion() >= "5.": - from matplotlib.backends.backend_qt5agg import ( - FigureCanvas, NavigationToolbar2QT as NavigationToolbar) -else: - from matplotlib.backends.backend_qt4agg import ( - FigureCanvas, NavigationToolbar2QT as NavigationToolbar) +from matplotlib.backends.qt_compat import QtWidgets +from matplotlib.backends.backend_qtagg import ( + FigureCanvas, NavigationToolbar2QT as NavigationToolbar) from matplotlib.figure import Figure @@ -32,13 +28,15 @@ def __init__(self): layout = QtWidgets.QVBoxLayout(self._main) static_canvas = FigureCanvas(Figure(figsize=(5, 3))) + # Ideally one would use self.addToolBar here, but it is slightly + # incompatible between PyQt6 and other bindings, so we just add the + # toolbar as a plain widget instead. + layout.addWidget(NavigationToolbar(static_canvas, self)) layout.addWidget(static_canvas) - self.addToolBar(NavigationToolbar(static_canvas, self)) dynamic_canvas = FigureCanvas(Figure(figsize=(5, 3))) layout.addWidget(dynamic_canvas) - self.addToolBar(QtCore.Qt.BottomToolBarArea, - NavigationToolbar(dynamic_canvas, self)) + layout.addWidget(NavigationToolbar(dynamic_canvas, self)) self._static_ax = static_canvas.figure.subplots() t = np.linspace(0, 10, 501) @@ -70,4 +68,4 @@ def _update_canvas(self): app.show() app.activateWindow() app.raise_() - qapp.exec_() + qapp.exec() diff --git a/examples/user_interfaces/embedding_in_tk_sgskip.py b/examples/user_interfaces/embedding_in_tk_sgskip.py index e7342f3dcbf3..55fb2b48b5d6 100644 --- a/examples/user_interfaces/embedding_in_tk_sgskip.py +++ b/examples/user_interfaces/embedding_in_tk_sgskip.py @@ -21,7 +21,10 @@ fig = Figure(figsize=(5, 4), dpi=100) t = np.arange(0, 3, .01) -fig.add_subplot().plot(t, 2 * np.sin(2 * np.pi * t)) +ax = fig.add_subplot() +line, = ax.plot(t, 2 * np.sin(2 * np.pi * t)) +ax.set_xlabel("time [s]") +ax.set_ylabel("f(t)") canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea. canvas.draw() @@ -30,19 +33,35 @@ toolbar = NavigationToolbar2Tk(canvas, root, pack_toolbar=False) toolbar.update() - 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) +button_quit = tkinter.Button(master=root, text="Quit", command=root.destroy) + + +def update_frequency(new_val): + # retrieve frequency + f = float(new_val) + + # update data + y = 2 * np.sin(2 * np.pi * f * t) + line.set_data(t, y) + + # required to update canvas and attached toolbar! + canvas.draw() + + +slider_update = tkinter.Scale(root, from_=1, to=5, orient=tkinter.HORIZONTAL, + command=update_frequency, label="Frequency [Hz]") # Packing order is important. Widgets are processed sequentially and if there # is no space left, because the window is too small, they are not displayed. # The canvas is rather flexible in its size, so we pack it last which makes # sure the UI controls are displayed as long as possible. -button.pack(side=tkinter.BOTTOM) +button_quit.pack(side=tkinter.BOTTOM) +slider_update.pack(side=tkinter.BOTTOM) toolbar.pack(side=tkinter.BOTTOM, fill=tkinter.X) -canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1) +canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True) tkinter.mainloop() diff --git a/examples/user_interfaces/embedding_in_wx2_sgskip.py b/examples/user_interfaces/embedding_in_wx2_sgskip.py index 19580517e9e1..fcebeeee8cd6 100644 --- a/examples/user_interfaces/embedding_in_wx2_sgskip.py +++ b/examples/user_interfaces/embedding_in_wx2_sgskip.py @@ -47,8 +47,8 @@ def add_toolbar(self): self.toolbar.update() -# alternatively you could use -#class App(wx.App): +# Alternatively you could use: +# class App(wx.App): class App(WIT.InspectableApp): def OnInit(self): """Create the main window and insert the custom frame.""" diff --git a/examples/user_interfaces/embedding_in_wx5_sgskip.py b/examples/user_interfaces/embedding_in_wx5_sgskip.py index 2a97089697bc..c3c3d91f7368 100644 --- a/examples/user_interfaces/embedding_in_wx5_sgskip.py +++ b/examples/user_interfaces/embedding_in_wx5_sgskip.py @@ -9,7 +9,7 @@ import wx.lib.agw.aui as aui import wx.lib.mixins.inspection as wit -import matplotlib as mpl +from matplotlib.figure import Figure from matplotlib.backends.backend_wxagg import ( FigureCanvasWxAgg as FigureCanvas, NavigationToolbar2WxAgg as NavigationToolbar) @@ -18,7 +18,7 @@ class Plot(wx.Panel): def __init__(self, parent, id=-1, dpi=None, **kwargs): super().__init__(parent, id=id, **kwargs) - self.figure = mpl.figure.Figure(dpi=dpi, figsize=(2, 2)) + self.figure = Figure(dpi=dpi, figsize=(2, 2)) self.canvas = FigureCanvas(self, -1, self.figure) self.toolbar = NavigationToolbar(self.canvas) self.toolbar.Realize() @@ -44,16 +44,16 @@ def add(self, name="plot"): def demo(): - # alternatively you could use - #app = wx.App() + # Alternatively you could use: + # app = wx.App() # InspectableApp is a great debug tool, see: # http://wiki.wxpython.org/Widget%20Inspection%20Tool app = wit.InspectableApp() frame = wx.Frame(None, -1, 'Plotter') plotter = PlotNotebook(frame) - axes1 = plotter.add('figure 1').gca() + axes1 = plotter.add('figure 1').add_subplot() axes1.plot([1, 2, 3], [2, 1, 4]) - axes2 = plotter.add('figure 2').gca() + axes2 = plotter.add('figure 2').add_subplot() axes2.plot([1, 2, 3, 4, 5], [2, 1, 4, 2, 3]) frame.Show() app.MainLoop() diff --git a/examples/user_interfaces/embedding_webagg_sgskip.py b/examples/user_interfaces/embedding_webagg_sgskip.py index d3a313c6b9ef..15a2dd95b3ba 100644 --- a/examples/user_interfaces/embedding_webagg_sgskip.py +++ b/examples/user_interfaces/embedding_webagg_sgskip.py @@ -11,10 +11,13 @@ The framework being used must support web sockets. """ +import argparse import io import json import mimetypes from pathlib import Path +import signal +import socket try: import tornado @@ -27,7 +30,7 @@ import matplotlib as mpl -from matplotlib.backends.backend_webagg_core import ( +from matplotlib.backends.backend_webagg import ( FigureManagerWebAgg, new_figure_manager_given_figure) from matplotlib.figure import Figure @@ -49,16 +52,15 @@ def create_figure(): # The following is the content of the web page. You would normally # generate this using some sort of template facility in your web # framework, but here we just use Python string formatting. -html_content = """ - +html_content = """ + - - + + @@ -120,7 +122,7 @@ class MainPage(tornado.web.RequestHandler): def get(self): manager = self.application.manager - ws_uri = "ws://{req.host}/".format(req=self.request) + ws_uri = f"ws://{self.request.host}/" content = html_content % { "ws_uri": ws_uri, "fig_id": manager.num} self.write(content) @@ -204,8 +206,8 @@ def send_binary(self, blob): if self.supports_binary: self.write_message(blob, binary=True) else: - data_uri = "data:image/png;base64,{0}".format( - blob.encode('base64').replace('\n', '')) + data_uri = ("data:image/png;base64," + + blob.encode('base64').replace('\n', '')) self.write_message(data_uri) def __init__(self, figure): @@ -238,13 +240,36 @@ def __init__(self, figure): if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('-p', '--port', type=int, default=8080, + help='Port to listen on (0 for a random port).') + args = parser.parse_args() + figure = create_figure() application = MyApplication(figure) http_server = tornado.httpserver.HTTPServer(application) - http_server.listen(8080) - - print("http://127.0.0.1:8080/") + sockets = tornado.netutil.bind_sockets(args.port, '') + http_server.add_sockets(sockets) + + for s in sockets: + addr, port = s.getsockname()[:2] + if s.family is socket.AF_INET6: + addr = f'[{addr}]' + print(f"Listening on http://{addr}:{port}/") print("Press Ctrl+C to quit") - tornado.ioloop.IOLoop.instance().start() + ioloop = tornado.ioloop.IOLoop.instance() + + def shutdown(): + ioloop.stop() + print("Server stopped") + + old_handler = signal.signal( + signal.SIGINT, + lambda sig, frame: ioloop.add_callback_from_signal(shutdown)) + + try: + ioloop.start() + finally: + signal.signal(signal.SIGINT, old_handler) diff --git a/examples/user_interfaces/fourier_demo_wx_sgskip.py b/examples/user_interfaces/fourier_demo_wx_sgskip.py index 86441ed0c838..392fbb10f9a3 100644 --- a/examples/user_interfaces/fourier_demo_wx_sgskip.py +++ b/examples/user_interfaces/fourier_demo_wx_sgskip.py @@ -75,10 +75,10 @@ def __init__(self, parent, label, param): sizer = wx.BoxSizer(wx.HORIZONTAL) sizer.Add(self.sliderLabel, 0, - wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, + wx.EXPAND | wx.ALL, border=2) sizer.Add(self.sliderText, 0, - wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, + wx.EXPAND | wx.ALL, border=2) sizer.Add(self.slider, 1, wx.EXPAND) self.sizer = sizer @@ -98,7 +98,7 @@ def sliderTextHandler(self, event): self.param.set(value) def setKnob(self, value): - self.sliderText.SetValue('%g' % value) + self.sliderText.SetValue(f'{value:g}') self.slider.SetValue(int(value * 1000)) @@ -115,9 +115,9 @@ def __init__(self, *args, **kwargs): sizer = wx.BoxSizer(wx.VERTICAL) sizer.Add(self.canvas, 1, wx.EXPAND) sizer.Add(self.frequencySliderGroup.sizer, 0, - wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, border=5) + wx.EXPAND | wx.ALL, border=5) sizer.Add(self.amplitudeSliderGroup.sizer, 0, - wx.EXPAND | wx.ALIGN_CENTER | wx.ALL, border=5) + wx.EXPAND | wx.ALL, border=5) panel.SetSizer(sizer) def createCanvas(self, parent): diff --git a/examples/user_interfaces/gtk_spreadsheet_sgskip.py b/examples/user_interfaces/gtk3_spreadsheet_sgskip.py similarity index 95% rename from examples/user_interfaces/gtk_spreadsheet_sgskip.py rename to examples/user_interfaces/gtk3_spreadsheet_sgskip.py index 1f0f6e702240..4261d9ac1e89 100644 --- a/examples/user_interfaces/gtk_spreadsheet_sgskip.py +++ b/examples/user_interfaces/gtk3_spreadsheet_sgskip.py @@ -1,10 +1,10 @@ """ -=============== -GTK Spreadsheet -=============== +================ +GTK3 spreadsheet +================ Example of embedding Matplotlib in an application and interacting with a -treeview to store data. Double click on an entry to update plot data. +treeview to store data. Double-click on an entry to update plot data. """ import gi diff --git a/examples/user_interfaces/gtk4_spreadsheet_sgskip.py b/examples/user_interfaces/gtk4_spreadsheet_sgskip.py new file mode 100644 index 000000000000..c3f053480a71 --- /dev/null +++ b/examples/user_interfaces/gtk4_spreadsheet_sgskip.py @@ -0,0 +1,91 @@ +""" +================ +GTK4 spreadsheet +================ + +Example of embedding Matplotlib in an application and interacting with a +treeview to store data. Double-click on an entry to update plot data. +""" + +import gi +gi.require_version('Gtk', '4.0') +gi.require_version('Gdk', '4.0') +from gi.repository import Gtk + +from matplotlib.backends.backend_gtk4agg import FigureCanvas # or gtk4cairo. + +from numpy.random import random +from matplotlib.figure import Figure + + +class DataManager(Gtk.ApplicationWindow): + num_rows, num_cols = 20, 10 + + data = random((num_rows, num_cols)) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.set_default_size(600, 600) + + self.set_title('GtkListStore demo') + + vbox = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, homogeneous=False, + spacing=8) + self.set_child(vbox) + + label = Gtk.Label(label='Double click a row to plot the data') + vbox.append(label) + + sw = Gtk.ScrolledWindow() + sw.set_has_frame(True) + sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) + sw.set_hexpand(True) + sw.set_vexpand(True) + vbox.append(sw) + + model = self.create_model() + self.treeview = Gtk.TreeView(model=model) + self.treeview.connect('row-activated', self.plot_row) + sw.set_child(self.treeview) + + # Matplotlib stuff + fig = Figure(figsize=(6, 4), layout='constrained') + + self.canvas = FigureCanvas(fig) # a Gtk.DrawingArea + self.canvas.set_hexpand(True) + self.canvas.set_vexpand(True) + vbox.append(self.canvas) + ax = fig.add_subplot() + self.line, = ax.plot(self.data[0, :], 'go') # plot the first row + + self.add_columns() + + def plot_row(self, treeview, path, view_column): + ind, = path # get the index into data + points = self.data[ind, :] + self.line.set_ydata(points) + self.canvas.draw() + + def add_columns(self): + for i in range(self.num_cols): + column = Gtk.TreeViewColumn(str(i), Gtk.CellRendererText(), text=i) + self.treeview.append_column(column) + + def create_model(self): + types = [float] * self.num_cols + store = Gtk.ListStore(*types) + for row in self.data: + # Gtk.ListStore.append is broken in PyGObject, so insert manually. + it = store.insert(-1) + store.set(it, {i: val for i, val in enumerate(row)}) + return store + + +def on_activate(app): + manager = DataManager(application=app) + manager.show() + + +app = Gtk.Application(application_id='org.matplotlib.examples.GTK4Spreadsheet') +app.connect('activate', on_activate) +app.run() diff --git a/examples/user_interfaces/images/eye-symbolic.svg b/examples/user_interfaces/images/eye-symbolic.svg new file mode 100644 index 000000000000..20d5db230405 --- /dev/null +++ b/examples/user_interfaces/images/eye-symbolic.svg @@ -0,0 +1,70 @@ + + + + + + + + 2021-07-14T19:48:07.525592 + image/svg+xml + + + Matplotlib v3.4.2.post1357+gf1afce77c6.d20210714, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/user_interfaces/images/eye.pdf b/examples/user_interfaces/images/eye.pdf new file mode 100644 index 000000000000..52f18e8342f8 Binary files /dev/null and b/examples/user_interfaces/images/eye.pdf differ diff --git a/examples/user_interfaces/images/eye.png b/examples/user_interfaces/images/eye.png new file mode 100644 index 000000000000..365f6fbcde5d Binary files /dev/null and b/examples/user_interfaces/images/eye.png differ diff --git a/examples/user_interfaces/images/eye.svg b/examples/user_interfaces/images/eye.svg new file mode 100644 index 000000000000..20d5db230405 --- /dev/null +++ b/examples/user_interfaces/images/eye.svg @@ -0,0 +1,70 @@ + + + + + + + + 2021-07-14T19:48:07.525592 + image/svg+xml + + + Matplotlib v3.4.2.post1357+gf1afce77c6.d20210714, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/user_interfaces/images/eye_large.png b/examples/user_interfaces/images/eye_large.png new file mode 100644 index 000000000000..f8a2911032a4 Binary files /dev/null and b/examples/user_interfaces/images/eye_large.png differ diff --git a/examples/user_interfaces/mathtext_wx_sgskip.py b/examples/user_interfaces/mathtext_wx_sgskip.py index ca5a869bef5b..f9af7a89c416 100644 --- a/examples/user_interfaces/mathtext_wx_sgskip.py +++ b/examples/user_interfaces/mathtext_wx_sgskip.py @@ -1,7 +1,7 @@ """ -=========== -MathText WX -=========== +====================== +Display mathtext in WX +====================== Demonstrates how to convert (math)text to a wx.Bitmap for display in various controls on wxPython. @@ -15,13 +15,12 @@ import numpy as np import wx -IS_GTK = 'wxGTK' in wx.PlatformInfo IS_WIN = 'wxMSW' in wx.PlatformInfo def mathtext_to_wxbitmap(s): # We draw the text at position (0, 0) but then rely on - # ``facecolor="none"`` and ``bbox_inches="tight", pad_inches=0`` to get an + # ``facecolor="none"`` and ``bbox_inches="tight", pad_inches=0`` to get a # transparent mask that is then loaded into a wx.Bitmap. fig = Figure(facecolor="none") text_color = ( @@ -66,7 +65,7 @@ def __init__(self, parent, title): menuBar.Append(menu, "&File") self.Bind(wx.EVT_MENU, self.OnClose, m_exit) - if IS_GTK or IS_WIN: + if IS_WIN: # Equation Menu menu = wx.Menu() for i, (mt, func) in enumerate(functions): diff --git a/examples/user_interfaces/mpl_with_glade3_sgskip.py b/examples/user_interfaces/mpl_with_glade3_sgskip.py index 2464c5e04c41..5e6a43caca61 100644 --- a/examples/user_interfaces/mpl_with_glade3_sgskip.py +++ b/examples/user_interfaces/mpl_with_glade3_sgskip.py @@ -1,8 +1,7 @@ """ ======================= -Matplotlib With Glade 3 +Matplotlib with Glade 3 ======================= - """ from pathlib import Path diff --git a/examples/user_interfaces/mplcvd.py b/examples/user_interfaces/mplcvd.py new file mode 100644 index 000000000000..8eaa22a8977a --- /dev/null +++ b/examples/user_interfaces/mplcvd.py @@ -0,0 +1,299 @@ +""" +mplcvd -- an example of figure hook +=================================== + +To use this hook, ensure that this module is in your ``PYTHONPATH``, and set +``rcParams["figure.hooks"] = ["mplcvd:setup"]``. This hook depends on +the ``colorspacious`` third-party module. +""" + +import functools +from pathlib import Path + +import colorspacious +import numpy as np + + +_BUTTON_NAME = "Filter" +_BUTTON_HELP = "Simulate color vision deficiencies" +_MENU_ENTRIES = { + "None": None, + "Greyscale": "greyscale", + "Deuteranopia": "deuteranomaly", + "Protanopia": "protanomaly", + "Tritanopia": "tritanomaly", +} + + +def _get_color_filter(name): + """ + Given a color filter name, create a color filter function. + + Parameters + ---------- + name : str + The color filter name, one of the following: + + - ``"none"``: ... + - ``"greyscale"``: Convert the input to luminosity. + - ``"deuteranopia"``: Simulate the most common form of red-green + colorblindness. + - ``"protanopia"``: Simulate a rarer form of red-green colorblindness. + - ``"tritanopia"``: Simulate the rare form of blue-yellow + colorblindness. + + Color conversions use `colorspacious`_. + + Returns + ------- + callable + A color filter function that has the form: + + def filter(input: np.ndarray[M, N, D])-> np.ndarray[M, N, D] + + where (M, N) are the image dimensions, and D is the color depth (3 for + RGB, 4 for RGBA). Alpha is passed through unchanged and otherwise + ignored. + """ + if name not in _MENU_ENTRIES: + raise ValueError(f"Unsupported filter name: {name!r}") + name = _MENU_ENTRIES[name] + + if name is None: + return None + + elif name == "greyscale": + rgb_to_jch = colorspacious.cspace_converter("sRGB1", "JCh") + jch_to_rgb = colorspacious.cspace_converter("JCh", "sRGB1") + + def convert(im): + greyscale_JCh = rgb_to_jch(im) + greyscale_JCh[..., 1] = 0 + im = jch_to_rgb(greyscale_JCh) + return im + + else: + cvd_space = {"name": "sRGB1+CVD", "cvd_type": name, "severity": 100} + convert = colorspacious.cspace_converter(cvd_space, "sRGB1") + + def filter_func(im, dpi): + alpha = None + if im.shape[-1] == 4: + im, alpha = im[..., :3], im[..., 3] + im = convert(im) + if alpha is not None: + im = np.dstack((im, alpha)) + return np.clip(im, 0, 1), 0, 0 + + return filter_func + + +def _set_menu_entry(tb, name): + tb.canvas.figure.set_agg_filter(_get_color_filter(name)) + tb.canvas.draw_idle() + + +def setup(figure): + tb = figure.canvas.toolbar + if tb is None: + return + for cls in type(tb).__mro__: + pkg = cls.__module__.split(".")[0] + if pkg != "matplotlib": + break + if pkg == "gi": + _setup_gtk(tb) + elif pkg in ("PyQt5", "PySide2", "PyQt6", "PySide6"): + _setup_qt(tb) + elif pkg == "tkinter": + _setup_tk(tb) + elif pkg == "wx": + _setup_wx(tb) + else: + raise NotImplementedError("The current backend is not supported") + + +def _setup_gtk(tb): + from gi.repository import Gio, GLib, Gtk + + for idx in range(tb.get_n_items()): + children = tb.get_nth_item(idx).get_children() + if children and isinstance(children[0], Gtk.Label): + break + + toolitem = Gtk.SeparatorToolItem() + tb.insert(toolitem, idx) + + image = Gtk.Image.new_from_gicon( + Gio.Icon.new_for_string( + str(Path(__file__).parent / "images/eye-symbolic.svg")), + Gtk.IconSize.LARGE_TOOLBAR) + + # The type of menu is progressively downgraded depending on GTK version. + if Gtk.check_version(3, 6, 0) is None: + + group = Gio.SimpleActionGroup.new() + action = Gio.SimpleAction.new_stateful("cvdsim", + GLib.VariantType("s"), + GLib.Variant("s", "none")) + group.add_action(action) + + @functools.partial(action.connect, "activate") + def set_filter(action, parameter): + _set_menu_entry(tb, parameter.get_string()) + action.set_state(parameter) + + menu = Gio.Menu() + for name in _MENU_ENTRIES: + menu.append(name, f"local.cvdsim::{name}") + + button = Gtk.MenuButton.new() + button.remove(button.get_children()[0]) + button.add(image) + button.insert_action_group("local", group) + button.set_menu_model(menu) + button.get_style_context().add_class("flat") + + item = Gtk.ToolItem() + item.add(button) + tb.insert(item, idx + 1) + + else: + + menu = Gtk.Menu() + group = [] + for name in _MENU_ENTRIES: + item = Gtk.RadioMenuItem.new_with_label(group, name) + item.set_active(name == "None") + item.connect( + "activate", lambda item: _set_menu_entry(tb, item.get_label())) + group.append(item) + menu.append(item) + menu.show_all() + + tbutton = Gtk.MenuToolButton.new(image, _BUTTON_NAME) + tbutton.set_menu(menu) + tb.insert(tbutton, idx + 1) + + tb.show_all() + + +def _setup_qt(tb): + from matplotlib.backends.qt_compat import QtGui, QtWidgets + + menu = QtWidgets.QMenu() + try: + QActionGroup = QtGui.QActionGroup # Qt6 + except AttributeError: + QActionGroup = QtWidgets.QActionGroup # Qt5 + group = QActionGroup(menu) + group.triggered.connect(lambda action: _set_menu_entry(tb, action.text())) + + for name in _MENU_ENTRIES: + action = menu.addAction(name) + action.setCheckable(True) + action.setActionGroup(group) + action.setChecked(name == "None") + + actions = tb.actions() + before = next( + (action for action in actions + if isinstance(tb.widgetForAction(action), QtWidgets.QLabel)), None) + + tb.insertSeparator(before) + button = QtWidgets.QToolButton() + # FIXME: _icon needs public API. + button.setIcon(tb._icon(str(Path(__file__).parent / "images/eye.png"))) + button.setText(_BUTTON_NAME) + button.setToolTip(_BUTTON_HELP) + button.setPopupMode(QtWidgets.QToolButton.ToolButtonPopupMode.InstantPopup) + button.setMenu(menu) + tb.insertWidget(before, button) + + +def _setup_tk(tb): + import tkinter as tk + + tb._Spacer() # FIXME: _Spacer needs public API. + + button = tk.Menubutton(master=tb, relief="raised") + button._image_file = str(Path(__file__).parent / "images/eye.png") + # FIXME: _set_image_for_button needs public API (perhaps like _icon). + tb._set_image_for_button(button) + button.pack(side=tk.LEFT) + + menu = tk.Menu(master=button, tearoff=False) + for name in _MENU_ENTRIES: + menu.add("radiobutton", label=name, + command=lambda _name=name: _set_menu_entry(tb, _name)) + menu.invoke(0) + button.config(menu=menu) + + +def _setup_wx(tb): + import wx + + idx = next(idx for idx in range(tb.ToolsCount) + if tb.GetToolByPos(idx).IsStretchableSpace()) + tb.InsertSeparator(idx) + tool = tb.InsertTool( + idx + 1, -1, _BUTTON_NAME, + # FIXME: _icon needs public API. + tb._icon(str(Path(__file__).parent / "images/eye.png")), + # FIXME: ITEM_DROPDOWN is not supported on macOS. + kind=wx.ITEM_DROPDOWN, shortHelp=_BUTTON_HELP) + + menu = wx.Menu() + for name in _MENU_ENTRIES: + item = menu.AppendRadioItem(-1, name) + menu.Bind( + wx.EVT_MENU, + lambda event, _name=name: _set_menu_entry(tb, _name), + id=item.Id, + ) + tb.SetDropdownMenu(tool.Id, menu) + + +if __name__ == '__main__': + import matplotlib.pyplot as plt + from matplotlib import cbook + + plt.rcParams['figure.hooks'].append('mplcvd:setup') + + fig, axd = plt.subplot_mosaic( + [ + ['viridis', 'turbo'], + ['photo', 'lines'] + ] + ) + + delta = 0.025 + x = y = np.arange(-3.0, 3.0, delta) + X, Y = np.meshgrid(x, y) + Z1 = np.exp(-X**2 - Y**2) + Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) + Z = (Z1 - Z2) * 2 + + imv = axd['viridis'].imshow( + Z, interpolation='bilinear', + origin='lower', extent=[-3, 3, -3, 3], + vmax=abs(Z).max(), vmin=-abs(Z).max() + ) + fig.colorbar(imv) + imt = axd['turbo'].imshow( + Z, interpolation='bilinear', cmap='turbo', + origin='lower', extent=[-3, 3, -3, 3], + vmax=abs(Z).max(), vmin=-abs(Z).max() + ) + fig.colorbar(imt) + + # A sample image + with cbook.get_sample_data('grace_hopper.jpg') as image_file: + photo = plt.imread(image_file) + axd['photo'].imshow(photo) + + th = np.linspace(0, 2*np.pi, 1024) + for j in [1, 2, 4, 6]: + axd['lines'].plot(th, np.sin(th * j), label=f'$\\omega={j}$') + axd['lines'].legend(ncols=2, loc='upper right') + plt.show() diff --git a/examples/user_interfaces/pylab_with_gtk_sgskip.py b/examples/user_interfaces/pylab_with_gtk3_sgskip.py similarity index 96% rename from examples/user_interfaces/pylab_with_gtk_sgskip.py rename to examples/user_interfaces/pylab_with_gtk3_sgskip.py index 561bdbad341c..4d943032df5a 100644 --- a/examples/user_interfaces/pylab_with_gtk_sgskip.py +++ b/examples/user_interfaces/pylab_with_gtk3_sgskip.py @@ -1,7 +1,7 @@ """ -=============== -pyplot with GTK -=============== +================ +pyplot with GTK3 +================ An example of how to use pyplot to manage your figure windows, but modify the GUI by accessing the underlying GTK widgets. @@ -46,6 +46,7 @@ vbox.pack_start(label, False, False, 0) vbox.reorder_child(toolbar, -1) + def update(event): if event.xdata is None: label.set_markup('Drag mouse over axes for position') @@ -53,6 +54,7 @@ def update(event): label.set_markup( f'x,y=({event.xdata}, {event.ydata})') + fig.canvas.mpl_connect('motion_notify_event', update) plt.show() diff --git a/examples/user_interfaces/pylab_with_gtk4_sgskip.py b/examples/user_interfaces/pylab_with_gtk4_sgskip.py new file mode 100644 index 000000000000..6e0cebcce23c --- /dev/null +++ b/examples/user_interfaces/pylab_with_gtk4_sgskip.py @@ -0,0 +1,51 @@ +""" +================ +pyplot with GTK4 +================ + +An example of how to use pyplot to manage your figure windows, but modify the +GUI by accessing the underlying GTK widgets. +""" + +import matplotlib +matplotlib.use('GTK4Agg') # or 'GTK4Cairo' +import matplotlib.pyplot as plt + +import gi +gi.require_version('Gtk', '4.0') +from gi.repository import Gtk + + +fig, ax = plt.subplots() +ax.plot([1, 2, 3], 'ro-', label='easy as 1 2 3') +ax.plot([1, 4, 9], 'gs--', label='easy as 1 2 3 squared') +ax.legend() + +manager = fig.canvas.manager +# you can access the window or vbox attributes this way +toolbar = manager.toolbar +vbox = manager.vbox + +# now let's add a button to the toolbar +button = Gtk.Button(label='Click me') +button.connect('clicked', lambda button: print('hi mom')) +button.set_tooltip_text('Click me for fun and profit') +toolbar.append(button) + +# now let's add a widget to the vbox +label = Gtk.Label() +label.set_markup('Drag mouse over axes for position') +vbox.insert_child_after(label, fig.canvas) + + +def update(event): + if event.xdata is None: + label.set_markup('Drag mouse over axes for position') + else: + label.set_markup( + f'x,y=({event.xdata}, {event.ydata})') + + +fig.canvas.mpl_connect('motion_notify_event', update) + +plt.show() diff --git a/examples/user_interfaces/svg_histogram_sgskip.py b/examples/user_interfaces/svg_histogram_sgskip.py index 4311399fcae6..eb4a9db25cfc 100644 --- a/examples/user_interfaces/svg_histogram_sgskip.py +++ b/examples/user_interfaces/svg_histogram_sgskip.py @@ -66,18 +66,18 @@ hist_patches = {} for ic, c in enumerate(containers): - hist_patches['hist_%d' % ic] = [] + hist_patches[f'hist_{ic}'] = [] for il, element in enumerate(c): - element.set_gid('hist_%d_patch_%d' % (ic, il)) - hist_patches['hist_%d' % ic].append('hist_%d_patch_%d' % (ic, il)) + element.set_gid(f'hist_{ic}_patch_{il}') + hist_patches[f'hist_{ic}'].append(f'hist_{ic}_patch_{il}') # Set ids for the legend patches for i, t in enumerate(leg.get_patches()): - t.set_gid('leg_patch_%d' % i) + t.set_gid(f'leg_patch_{i}') # Set ids for the text patches for i, t in enumerate(leg.get_texts()): - t.set_gid('leg_text_%d' % i) + t.set_gid(f'leg_text_{i}') # Save SVG in a fake file object. f = BytesIO() @@ -91,13 +91,13 @@ # Add attributes to the patch objects. for i, t in enumerate(leg.get_patches()): - el = xmlid['leg_patch_%d' % i] + el = xmlid[f'leg_patch_{i}'] el.set('cursor', 'pointer') el.set('onclick', "toggle_hist(this)") # Add attributes to the text objects. for i, t in enumerate(leg.get_texts()): - el = xmlid['leg_text_%d' % i] + el = xmlid[f'leg_text_{i}'] el.set('cursor', 'pointer') el.set('onclick', "toggle_hist(this)") diff --git a/examples/user_interfaces/svg_tooltip_sgskip.py b/examples/user_interfaces/svg_tooltip_sgskip.py index 04827d163bd8..a709428374c0 100644 --- a/examples/user_interfaces/svg_tooltip_sgskip.py +++ b/examples/user_interfaces/svg_tooltip_sgskip.py @@ -48,8 +48,8 @@ zorder=1)) ax.add_patch(patch) - patch.set_gid('mypatch_{:03d}'.format(i)) - annotate.set_gid('mytooltip_{:03d}'.format(i)) + patch.set_gid(f'mypatch_{i:03d}') + annotate.set_gid(f'mytooltip_{i:03d}') # Save the figure in a fake file object ax.set_xlim(-30, 30) @@ -69,10 +69,10 @@ # Get the index of the shape index = shapes.index(i) # Hide the tooltips - tooltip = xmlid['mytooltip_{:03d}'.format(index)] + tooltip = xmlid[f'mytooltip_{index:03d}'] tooltip.set('visibility', 'hidden') # Assign onmouseover and onmouseout callbacks to patches. - mypatch = xmlid['mypatch_{:03d}'.format(index)] + mypatch = xmlid[f'mypatch_{index:03d}'] mypatch.set('onmouseover', "ShowTooltip(this)") mypatch.set('onmouseout', "HideTooltip(this)") diff --git a/examples/user_interfaces/toolmanager_sgskip.py b/examples/user_interfaces/toolmanager_sgskip.py index cec5e577051f..a96e3876b040 100644 --- a/examples/user_interfaces/toolmanager_sgskip.py +++ b/examples/user_interfaces/toolmanager_sgskip.py @@ -3,45 +3,46 @@ Tool Manager ============ -This example demonstrates how to: +This example demonstrates how to -* Modify the Toolbar -* Create tools -* Add tools -* Remove tools +* modify the Toolbar +* create tools +* add tools +* remove tools -Using `matplotlib.backend_managers.ToolManager` +using `matplotlib.backend_managers.ToolManager`. """ import matplotlib.pyplot as plt -plt.rcParams['toolbar'] = 'toolmanager' from matplotlib.backend_tools import ToolBase, ToolToggleBase +plt.rcParams['toolbar'] = 'toolmanager' + + class ListTools(ToolBase): """List all the tools controlled by the `ToolManager`.""" - # keyboard shortcut - default_keymap = 'm' + default_keymap = 'm' # keyboard shortcut description = 'List Tools' def trigger(self, *args, **kwargs): print('_' * 80) - print("{0:12} {1:45} {2}".format( - 'Name (id)', 'Tool description', 'Keymap')) + fmt_tool = "{:12} {:45} {}".format + print(fmt_tool('Name (id)', 'Tool description', 'Keymap')) print('-' * 80) tools = self.toolmanager.tools for name in sorted(tools): if not tools[name].description: continue keys = ', '.join(sorted(self.toolmanager.get_tool_keymap(name))) - print("{0:12} {1:45} {2}".format( - name, tools[name].description, keys)) + print(fmt_tool(name, tools[name].description, keys)) print('_' * 80) + fmt_active_toggle = "{0!s:12} {1!s:45}".format print("Active Toggle tools") - print("{0:12} {1:45}".format("Group", "Active")) + print(fmt_active_toggle("Group", "Active")) print('-' * 80) for group, active in self.toolmanager.active_toggle.items(): - print("{0:12} {1:45}".format(str(group), str(active))) + print(fmt_active_toggle(group, active)) class GroupHideTool(ToolToggleBase): @@ -77,7 +78,6 @@ def set_lines_visibility(self, state): fig.canvas.manager.toolmanager.add_tool('List', ListTools) fig.canvas.manager.toolmanager.add_tool('Show', GroupHideTool, gid='mygroup') - # Add an existing tool to new group `foo`. # It can be added as many times as we want fig.canvas.manager.toolbar.add_tool('zoom', 'foo') diff --git a/examples/user_interfaces/web_application_server_sgskip.py b/examples/user_interfaces/web_application_server_sgskip.py index ff16277cc752..c430c24b70ed 100644 --- a/examples/user_interfaces/web_application_server_sgskip.py +++ b/examples/user_interfaces/web_application_server_sgskip.py @@ -44,7 +44,7 @@ def hello(): ############################################################################# # # Since the above code is a Flask application, it should be run using the -# `flask command-line tool `_ +# `flask command-line tool `_ # Assuming that the working directory contains this script: # # Unix-like systems diff --git a/examples/user_interfaces/wxcursor_demo_sgskip.py b/examples/user_interfaces/wxcursor_demo_sgskip.py index c909a3d8baad..ae1e06c81a01 100644 --- a/examples/user_interfaces/wxcursor_demo_sgskip.py +++ b/examples/user_interfaces/wxcursor_demo_sgskip.py @@ -50,8 +50,7 @@ def ChangeCursor(self, event): def UpdateStatusBar(self, event): if event.inaxes: - self.statusBar.SetStatusText( - "x={} y={}".format(event.xdata, event.ydata)) + self.statusBar.SetStatusText(f"x={event.xdata} y={event.ydata}") class App(wx.App): diff --git a/examples/userdemo/anchored_box01.py b/examples/userdemo/anchored_box01.py deleted file mode 100644 index 00f75c7f5518..000000000000 --- a/examples/userdemo/anchored_box01.py +++ /dev/null @@ -1,18 +0,0 @@ -""" -============== -Anchored Box01 -============== - -""" -import matplotlib.pyplot as plt -from matplotlib.offsetbox import AnchoredText - - -fig, ax = plt.subplots(figsize=(3, 3)) - -at = AnchoredText("Figure 1a", - prop=dict(size=15), frameon=True, loc='upper left') -at.patch.set_boxstyle("round,pad=0.,rounding_size=0.2") -ax.add_artist(at) - -plt.show() diff --git a/examples/userdemo/anchored_box02.py b/examples/userdemo/anchored_box02.py deleted file mode 100644 index 59db0a4180a8..000000000000 --- a/examples/userdemo/anchored_box02.py +++ /dev/null @@ -1,23 +0,0 @@ -""" -============== -Anchored Box02 -============== - -""" -from matplotlib.patches import Circle -import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1.anchored_artists import AnchoredDrawingArea - - -fig, ax = plt.subplots(figsize=(3, 3)) - -ada = AnchoredDrawingArea(40, 20, 0, 0, - loc='upper right', pad=0., frameon=False) -p1 = Circle((10, 10), 10) -ada.drawing_area.add_artist(p1) -p2 = Circle((30, 10), 5, fc="r") -ada.drawing_area.add_artist(p2) - -ax.add_artist(ada) - -plt.show() diff --git a/examples/userdemo/anchored_box03.py b/examples/userdemo/anchored_box03.py deleted file mode 100644 index ba673d8471a5..000000000000 --- a/examples/userdemo/anchored_box03.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -============== -Anchored Box03 -============== - -""" -from matplotlib.patches import Ellipse -import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1.anchored_artists import AnchoredAuxTransformBox - - -fig, ax = plt.subplots(figsize=(3, 3)) - -box = AnchoredAuxTransformBox(ax.transData, loc='upper left') -el = Ellipse((0, 0), width=0.1, height=0.4, angle=30) # in data coordinates! -box.drawing_area.add_artist(el) - -ax.add_artist(box) - -plt.show() diff --git a/examples/userdemo/anchored_box04.py b/examples/userdemo/anchored_box04.py index d641e7a18ac4..2a4a0f4cea07 100644 --- a/examples/userdemo/anchored_box04.py +++ b/examples/userdemo/anchored_box04.py @@ -12,7 +12,7 @@ fig, ax = plt.subplots(figsize=(3, 3)) -box1 = TextArea(" Test : ", textprops=dict(color="k")) +box1 = TextArea(" Test: ", textprops=dict(color="k")) box2 = DrawingArea(60, 20, 0, 0) el1 = Ellipse((10, 10), width=16, height=5, angle=30, fc="r") diff --git a/examples/userdemo/colormap_normalizations_symlognorm.py b/examples/userdemo/colormap_normalizations_symlognorm.py deleted file mode 100644 index ff0d4872f75e..000000000000 --- a/examples/userdemo/colormap_normalizations_symlognorm.py +++ /dev/null @@ -1,40 +0,0 @@ -""" -================================== -Colormap Normalizations Symlognorm -================================== - -Demonstration of using norm to map colormaps onto data in non-linear ways. -""" - -import numpy as np -import matplotlib.pyplot as plt -import matplotlib.colors as colors - -""" -SymLogNorm: two humps, one negative and one positive, The positive -with 5-times the amplitude. Linearly, you cannot see detail in the -negative hump. Here we logarithmically scale the positive and -negative data separately. - -Note that colorbar labels do not come out looking very good. -""" - -N = 100 -X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)] -Z1 = np.exp(-X**2 - Y**2) -Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) -Z = (Z1 - Z2) * 2 - -fig, ax = plt.subplots(2, 1) - -pcm = ax[0].pcolormesh(X, Y, Z, - norm=colors.SymLogNorm(linthresh=0.03, linscale=0.03, - vmin=-1.0, vmax=1.0, base=10), - cmap='RdBu_r', shading='nearest') -fig.colorbar(pcm, ax=ax[0], extend='both') - -pcm = ax[1].pcolormesh(X, Y, Z, cmap='RdBu_r', vmin=-np.max(Z), - shading='nearest') -fig.colorbar(pcm, ax=ax[1], extend='both') - -plt.show() diff --git a/examples/userdemo/connectionstyle_demo.py b/examples/userdemo/connectionstyle_demo.py index 38655675f870..45e1e605e3e4 100644 --- a/examples/userdemo/connectionstyle_demo.py +++ b/examples/userdemo/connectionstyle_demo.py @@ -30,7 +30,7 @@ def demo_con_style(ax, connectionstyle): transform=ax.transAxes, ha="left", va="top") -fig, axs = plt.subplots(3, 5, figsize=(8, 4.8)) +fig, axs = plt.subplots(3, 5, figsize=(7, 6.3), layout="constrained") demo_con_style(axs[0, 0], "angle3,angleA=90,angleB=0") demo_con_style(axs[1, 0], "angle3,angleA=0,angleB=90") demo_con_style(axs[0, 1], "arc3,rad=0.") @@ -47,8 +47,8 @@ def demo_con_style(ax, connectionstyle): demo_con_style(axs[2, 4], "bar,angle=180,fraction=-0.2") for ax in axs.flat: - ax.set(xlim=(0, 1), ylim=(0, 1), xticks=[], yticks=[], aspect=1) -fig.tight_layout(pad=0.2) + ax.set(xlim=(0, 1), ylim=(0, 1.25), xticks=[], yticks=[], aspect=1.25) +fig.set_constrained_layout_pads(wspace=0, hspace=0, w_pad=0, h_pad=0) plt.show() diff --git a/examples/userdemo/demo_gridspec03.py b/examples/userdemo/demo_gridspec03.py index 2fa3fa58c0fc..55f424a808f8 100644 --- a/examples/userdemo/demo_gridspec03.py +++ b/examples/userdemo/demo_gridspec03.py @@ -31,6 +31,7 @@ def annotate_axes(fig): annotate_axes(fig) +############################################################################# fig = plt.figure() fig.suptitle("Controlling spacing around and between subplots") diff --git a/examples/userdemo/pgf_fonts.py b/examples/userdemo/pgf_fonts.py index 1f9d49853bcf..112c249752c2 100644 --- a/examples/userdemo/pgf_fonts.py +++ b/examples/userdemo/pgf_fonts.py @@ -1,8 +1,7 @@ """ ========= -Pgf Fonts +PGF fonts ========= - """ import matplotlib.pyplot as plt diff --git a/examples/userdemo/pgf_preamble_sgskip.py b/examples/userdemo/pgf_preamble_sgskip.py index e16ee0f1bbf5..ca193a0aecc0 100644 --- a/examples/userdemo/pgf_preamble_sgskip.py +++ b/examples/userdemo/pgf_preamble_sgskip.py @@ -1,8 +1,7 @@ """ ============ -Pgf Preamble +PGF preamble ============ - """ import matplotlib as mpl diff --git a/examples/userdemo/pgf_texsystem.py b/examples/userdemo/pgf_texsystem.py index dc44e8c12298..20f35b5cc906 100644 --- a/examples/userdemo/pgf_texsystem.py +++ b/examples/userdemo/pgf_texsystem.py @@ -1,8 +1,7 @@ """ ============= -Pgf Texsystem +PGF texsystem ============= - """ import matplotlib.pyplot as plt diff --git a/examples/userdemo/simple_legend01.py b/examples/userdemo/simple_legend01.py index c80488d1ad2d..2aaac1424558 100644 --- a/examples/userdemo/simple_legend01.py +++ b/examples/userdemo/simple_legend01.py @@ -15,7 +15,7 @@ # Place a legend above this subplot, expanding itself to # fully use the given bounding box. ax.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc='lower left', - ncol=2, mode="expand", borderaxespad=0.) + ncols=2, mode="expand", borderaxespad=0.) ax = fig.add_subplot(223) ax.plot([1, 2, 3], label="test1") diff --git a/examples/widgets/annotated_cursor.py b/examples/widgets/annotated_cursor.py new file mode 100644 index 000000000000..3fb21b1f2302 --- /dev/null +++ b/examples/widgets/annotated_cursor.py @@ -0,0 +1,356 @@ +""" +================ +Annotated cursor +================ + +Display a data cursor including a text box, which shows the plot point close +to the mouse pointer. + +The new cursor inherits from `~matplotlib.widgets.Cursor` and demonstrates the +creation of new widgets and their event callbacks. + +See also the :doc:`cross hair cursor +
`, which implements a cursor tracking the +plotted data, but without using inheritance and without displaying the +currently tracked coordinates. + +.. note:: + The figure related to this example does not show the cursor, because that + figure is automatically created in a build queue, where the first mouse + movement, which triggers the cursor creation, is missing. + +""" +from matplotlib.widgets import Cursor +import numpy as np +import matplotlib.pyplot as plt + +from matplotlib.backend_bases import MouseEvent + + +class AnnotatedCursor(Cursor): + """ + A crosshair cursor like `~matplotlib.widgets.Cursor` with a text showing \ + the current coordinates. + + For the cursor to remain responsive you must keep a reference to it. + The data of the axis specified as *dataaxis* must be in ascending + order. Otherwise, the `numpy.searchsorted` call might fail and the text + disappears. You can satisfy the requirement by sorting the data you plot. + Usually the data is already sorted (if it was created e.g. using + `numpy.linspace`), but e.g. scatter plots might cause this problem. + The cursor sticks to the plotted line. + + Parameters + ---------- + line : `matplotlib.lines.Line2D` + The plot line from which the data coordinates are displayed. + + numberformat : `python format string `_, optional, default: "{0:.4g};{1:.4g}" + The displayed text is created by calling *format()* on this string + with the two coordinates. + + offset : (float, float) default: (5, 5) + The offset in display (pixel) coordinates of the text position + relative to the cross-hair. + + dataaxis : {"x", "y"}, optional, default: "x" + If "x" is specified, the vertical cursor line sticks to the mouse + pointer. The horizontal cursor line sticks to *line* + at that x value. The text shows the data coordinates of *line* + at the pointed x value. If you specify "y", it works in the opposite + manner. But: For the "y" value, where the mouse points to, there might + be multiple matching x values, if the plotted function is not biunique. + Cursor and text coordinate will always refer to only one x value. + So if you use the parameter value "y", ensure that your function is + biunique. + + Other Parameters + ---------------- + textprops : `matplotlib.text` properties as dictionary + Specifies the appearance of the rendered text object. + + **cursorargs : `matplotlib.widgets.Cursor` properties + Arguments passed to the internal `~matplotlib.widgets.Cursor` instance. + The `matplotlib.axes.Axes` argument is mandatory! The parameter + *useblit* can be set to *True* in order to achieve faster rendering. + + """ + + def __init__(self, line, numberformat="{0:.4g};{1:.4g}", offset=(5, 5), + dataaxis='x', textprops=None, **cursorargs): + if textprops is None: + textprops = {} + # The line object, for which the coordinates are displayed + self.line = line + # The format string, on which .format() is called for creating the text + self.numberformat = numberformat + # Text position offset + self.offset = np.array(offset) + # The axis in which the cursor position is looked up + self.dataaxis = dataaxis + + # First call baseclass constructor. + # Draws cursor and remembers background for blitting. + # Saves ax as class attribute. + super().__init__(**cursorargs) + + # Default value for position of text. + self.set_position(self.line.get_xdata()[0], self.line.get_ydata()[0]) + # Create invisible animated text + self.text = self.ax.text( + self.ax.get_xbound()[0], + self.ax.get_ybound()[0], + "0, 0", + animated=bool(self.useblit), + visible=False, **textprops) + # The position at which the cursor was last drawn + self.lastdrawnplotpoint = None + + def onmove(self, event): + """ + Overridden draw callback for cursor. Called when moving the mouse. + """ + + # Leave method under the same conditions as in overridden method + if self.ignore(event): + self.lastdrawnplotpoint = None + return + if not self.canvas.widgetlock.available(self): + self.lastdrawnplotpoint = None + return + + # If the mouse left drawable area, we now make the text invisible. + # Baseclass will redraw complete canvas after, which makes both text + # and cursor disappear. + if event.inaxes != self.ax: + self.lastdrawnplotpoint = None + self.text.set_visible(False) + super().onmove(event) + return + + # Get the coordinates, which should be displayed as text, + # if the event coordinates are valid. + plotpoint = None + if event.xdata is not None and event.ydata is not None: + # Get plot point related to current x position. + # These coordinates are displayed in text. + plotpoint = self.set_position(event.xdata, event.ydata) + # Modify event, such that the cursor is displayed on the + # plotted line, not at the mouse pointer, + # if the returned plot point is valid + if plotpoint is not None: + event.xdata = plotpoint[0] + event.ydata = plotpoint[1] + + # If the plotpoint is given, compare to last drawn plotpoint and + # return if they are the same. + # Skip even the call of the base class, because this would restore the + # background, draw the cursor lines and would leave us the job to + # re-draw the text. + if plotpoint is not None and plotpoint == self.lastdrawnplotpoint: + return + + # Baseclass redraws canvas and cursor. Due to blitting, + # the added text is removed in this call, because the + # background is redrawn. + super().onmove(event) + + # Check if the display of text is still necessary. + # If not, just return. + # This behaviour is also cloned from the base class. + if not self.get_active() or not self.visible: + return + + # Draw the widget, if event coordinates are valid. + if plotpoint is not None: + # Update position and displayed text. + # Position: Where the event occurred. + # Text: Determined by set_position() method earlier + # Position is transformed to pixel coordinates, + # an offset is added there and this is transformed back. + temp = [event.xdata, event.ydata] + temp = self.ax.transData.transform(temp) + temp = temp + self.offset + temp = self.ax.transData.inverted().transform(temp) + self.text.set_position(temp) + self.text.set_text(self.numberformat.format(*plotpoint)) + self.text.set_visible(self.visible) + + # Tell base class, that we have drawn something. + # Baseclass needs to know, that it needs to restore a clean + # background, if the cursor leaves our figure context. + self.needclear = True + + # Remember the recently drawn cursor position, so events for the + # same position (mouse moves slightly between two plot points) + # can be skipped + self.lastdrawnplotpoint = plotpoint + # otherwise, make text invisible + else: + self.text.set_visible(False) + + # Draw changes. Cannot use _update method of baseclass, + # because it would first restore the background, which + # is done already and is not necessary. + if self.useblit: + self.ax.draw_artist(self.text) + self.canvas.blit(self.ax.bbox) + else: + # If blitting is deactivated, the overridden _update call made + # by the base class immediately returned. + # We still have to draw the changes. + self.canvas.draw_idle() + + def set_position(self, xpos, ypos): + """ + Finds the coordinates, which have to be shown in text. + + The behaviour depends on the *dataaxis* attribute. Function looks + up the matching plot coordinate for the given mouse position. + + Parameters + ---------- + xpos : float + The current x position of the cursor in data coordinates. + Important if *dataaxis* is set to 'x'. + ypos : float + The current y position of the cursor in data coordinates. + Important if *dataaxis* is set to 'y'. + + Returns + ------- + ret : {2D array-like, None} + The coordinates which should be displayed. + *None* is the fallback value. + """ + + # Get plot line data + xdata = self.line.get_xdata() + ydata = self.line.get_ydata() + + # The dataaxis attribute decides, in which axis we look up which cursor + # coordinate. + if self.dataaxis == 'x': + pos = xpos + data = xdata + lim = self.ax.get_xlim() + elif self.dataaxis == 'y': + pos = ypos + data = ydata + lim = self.ax.get_ylim() + else: + raise ValueError(f"The data axis specifier {self.dataaxis} should " + f"be 'x' or 'y'") + + # If position is valid and in valid plot data range. + if pos is not None and lim[0] <= pos <= lim[-1]: + # Find closest x value in sorted x vector. + # This requires the plotted data to be sorted. + index = np.searchsorted(data, pos) + # Return none, if this index is out of range. + if index < 0 or index >= len(data): + return None + # Return plot point as tuple. + return (xdata[index], ydata[index]) + + # Return none if there is no good related point for this x position. + return None + + def clear(self, event): + """ + Overridden clear callback for cursor, called before drawing the figure. + """ + + # The base class saves the clean background for blitting. + # Text and cursor are invisible, + # until the first mouse move event occurs. + super().clear(event) + if self.ignore(event): + return + self.text.set_visible(False) + + def _update(self): + """ + Overridden method for either blitting or drawing the widget canvas. + + Passes call to base class if blitting is activated, only. + In other cases, one draw_idle call is enough, which is placed + explicitly in this class (see *onmove()*). + In that case, `~matplotlib.widgets.Cursor` is not supposed to draw + something using this method. + """ + + if self.useblit: + super()._update() + + +fig, ax = plt.subplots(figsize=(8, 6)) +ax.set_title("Cursor Tracking x Position") + +x = np.linspace(-5, 5, 1000) +y = x**2 + +line, = ax.plot(x, y) +ax.set_xlim(-5, 5) +ax.set_ylim(0, 25) + +# A minimum call +# Set useblit=True on most backends for enhanced performance +# and pass the ax parameter to the Cursor base class. +# cursor = AnnotatedCursor(line=lin[0], ax=ax, useblit=True) + +# A more advanced call. Properties for text and lines are passed. +# Watch the passed color names and the color of cursor line and text, to +# relate the passed options to graphical elements. +# The dataaxis parameter is still the default. +cursor = AnnotatedCursor( + line=line, + numberformat="{0:.2f}\n{1:.2f}", + dataaxis='x', offset=[10, 10], + textprops={'color': 'blue', 'fontweight': 'bold'}, + ax=ax, + useblit=True, + color='red', + linewidth=2) + +# Simulate a mouse move to (-2, 10), needed for online docs +t = ax.transData +MouseEvent( + "motion_notify_event", ax.figure.canvas, *t.transform((-2, 10)) +)._process() + +plt.show() + +############################################################################### +# Trouble with non-biunique functions +# ----------------------------------- +# A call demonstrating problems with the *dataaxis=y* parameter. +# The text now looks up the matching x value for the current cursor y position +# instead of vice versa. Hover your cursor to y=4. There are two x values +# producing this y value: -2 and 2. The function is only unique, +# but not biunique. Only one value is shown in the text. + +fig, ax = plt.subplots(figsize=(8, 6)) +ax.set_title("Cursor Tracking y Position") + +line, = ax.plot(x, y) +ax.set_xlim(-5, 5) +ax.set_ylim(0, 25) + +cursor = AnnotatedCursor( + line=line, + numberformat="{0:.2f}\n{1:.2f}", + dataaxis='y', offset=[10, 10], + textprops={'color': 'blue', 'fontweight': 'bold'}, + ax=ax, + useblit=True, + color='red', linewidth=2) + +# Simulate a mouse move to (-2, 10), needed for online docs +t = ax.transData +MouseEvent( + "motion_notify_event", ax.figure.canvas, *t.transform((-2, 10)) +)._process() + +plt.show() diff --git a/examples/widgets/buttons.py b/examples/widgets/buttons.py index 80d77c463266..24331a4acd00 100644 --- a/examples/widgets/buttons.py +++ b/examples/widgets/buttons.py @@ -16,10 +16,10 @@ freqs = np.arange(2, 20, 3) fig, ax = plt.subplots() -plt.subplots_adjust(bottom=0.2) +fig.subplots_adjust(bottom=0.2) t = np.arange(0.0, 1.0, 0.001) s = np.sin(2*np.pi*freqs[0]*t) -l, = plt.plot(t, s, lw=2) +l, = ax.plot(t, s, lw=2) class Index: @@ -40,8 +40,8 @@ def prev(self, event): plt.draw() callback = Index() -axprev = plt.axes([0.7, 0.05, 0.1, 0.075]) -axnext = plt.axes([0.81, 0.05, 0.1, 0.075]) +axprev = fig.add_axes([0.7, 0.05, 0.1, 0.075]) +axnext = fig.add_axes([0.81, 0.05, 0.1, 0.075]) bnext = Button(axnext, 'Next') bnext.on_clicked(callback.next) bprev = Button(axprev, 'Previous') diff --git a/examples/widgets/check_buttons.py b/examples/widgets/check_buttons.py index 97e879cb22e8..b96ffb51c0af 100644 --- a/examples/widgets/check_buttons.py +++ b/examples/widgets/check_buttons.py @@ -1,14 +1,15 @@ """ ============= -Check Buttons +Check buttons ============= Turning visual elements on and off with check buttons. -This program shows the use of 'Check Buttons' which is similar to -check boxes. There are 3 different sine waves shown and we can choose which +This program shows the use of `.CheckButtons` which is similar to +check boxes. There are 3 different sine waves shown, and we can choose which waves are displayed with the check buttons. """ + import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import CheckButtons @@ -19,26 +20,32 @@ s2 = np.sin(6*np.pi*t) fig, ax = plt.subplots() -l0, = ax.plot(t, s0, visible=False, lw=2, color='k', label='2 Hz') -l1, = ax.plot(t, s1, lw=2, color='r', label='4 Hz') -l2, = ax.plot(t, s2, lw=2, color='g', label='6 Hz') -plt.subplots_adjust(left=0.2) +l0, = ax.plot(t, s0, visible=False, lw=2, color='black', label='1 Hz') +l1, = ax.plot(t, s1, lw=2, color='red', label='2 Hz') +l2, = ax.plot(t, s2, lw=2, color='green', label='3 Hz') +fig.subplots_adjust(left=0.2) -lines = [l0, l1, l2] +lines_by_label = {l.get_label(): l for l in [l0, l1, l2]} +line_colors = [l.get_color() for l in lines_by_label.values()] # Make checkbuttons with all plotted lines with correct visibility -rax = plt.axes([0.05, 0.4, 0.1, 0.15]) -labels = [str(line.get_label()) for line in lines] -visibility = [line.get_visible() for line in lines] -check = CheckButtons(rax, labels, visibility) - - -def func(label): - index = labels.index(label) - lines[index].set_visible(not lines[index].get_visible()) - plt.draw() - -check.on_clicked(func) +rax = fig.add_axes([0.05, 0.4, 0.1, 0.15]) +check = CheckButtons( + ax=rax, + labels=lines_by_label.keys(), + actives=[l.get_visible() for l in lines_by_label.values()], + label_props={'color': line_colors}, + frame_props={'edgecolor': line_colors}, + check_props={'facecolor': line_colors}, +) + + +def callback(label): + ln = lines_by_label[label] + ln.set_visible(not ln.get_visible()) + ln.figure.canvas.draw_idle() + +check.on_clicked(callback) plt.show() diff --git a/examples/widgets/mouse_cursor.py b/examples/widgets/mouse_cursor.py new file mode 100644 index 000000000000..1b0a1b2c57c3 --- /dev/null +++ b/examples/widgets/mouse_cursor.py @@ -0,0 +1,46 @@ +""" +============ +Mouse Cursor +============ + +This example sets an alternative cursor on a figure canvas. + +Note, this is an interactive example, and must be run to see the effect. +""" + +import matplotlib.pyplot as plt +from matplotlib.backend_tools import Cursors + + +fig, axs = plt.subplots(len(Cursors), figsize=(6, len(Cursors) + 0.5), + gridspec_kw={'hspace': 0}) +fig.suptitle('Hover over an Axes to see alternate Cursors') + +for cursor, ax in zip(Cursors, axs): + ax.cursor_to_use = cursor + ax.text(0.5, 0.5, cursor.name, + horizontalalignment='center', verticalalignment='center') + ax.set(xticks=[], yticks=[]) + + +def hover(event): + if fig.canvas.widgetlock.locked(): + # Don't do anything if the zoom/pan tools have been enabled. + return + + fig.canvas.set_cursor( + event.inaxes.cursor_to_use if event.inaxes else Cursors.POINTER) + + +fig.canvas.mpl_connect('motion_notify_event', hover) + +plt.show() + +############################################################################# +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.backend_bases.FigureCanvasBase.set_cursor` diff --git a/examples/widgets/multicursor.py b/examples/widgets/multicursor.py index 6e0775dd85e7..618fa17c5ad6 100644 --- a/examples/widgets/multicursor.py +++ b/examples/widgets/multicursor.py @@ -5,22 +5,27 @@ Showing a cursor on multiple plots simultaneously. -This example generates two subplots and on hovering the cursor over data in one -subplot, the values of that datapoint are shown in both respectively. +This example generates three axes split over two different figures. On +hovering the cursor over data in one subplot, the values of that datapoint are +shown in all axes. """ + import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import MultiCursor t = np.arange(0.0, 2.0, 0.01) s1 = np.sin(2*np.pi*t) -s2 = np.sin(4*np.pi*t) +s2 = np.sin(3*np.pi*t) +s3 = np.sin(4*np.pi*t) fig, (ax1, ax2) = plt.subplots(2, sharex=True) ax1.plot(t, s1) ax2.plot(t, s2) +fig, ax3 = plt.subplots() +ax3.plot(t, s3) -multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1) +multi = MultiCursor(None, (ax1, ax2, ax3), color='r', lw=1) plt.show() ############################################################################# diff --git a/examples/widgets/polygon_selector_demo.py b/examples/widgets/polygon_selector_demo.py index 3bea63565045..da9e65c39268 100644 --- a/examples/widgets/polygon_selector_demo.py +++ b/examples/widgets/polygon_selector_demo.py @@ -1,7 +1,7 @@ """ -================ -Polygon Selector -================ +======================================================= +Select indices from a collection using polygon selector +======================================================= Shows how one can select indices of a polygon interactively. """ @@ -50,7 +50,7 @@ def __init__(self, ax, collection, alpha_other=0.3): elif len(self.fc) == 1: self.fc = np.tile(self.fc, (self.Npts, 1)) - self.poly = PolygonSelector(ax, self.onselect) + self.poly = PolygonSelector(ax, self.onselect, draw_bounding_box=True) self.ind = [] def onselect(self, verts): diff --git a/examples/widgets/polygon_selector_simple.py b/examples/widgets/polygon_selector_simple.py new file mode 100644 index 000000000000..cdc84b07eaf6 --- /dev/null +++ b/examples/widgets/polygon_selector_simple.py @@ -0,0 +1,46 @@ +""" +================ +Polygon Selector +================ + +Shows how to create a polygon programmatically or interactively +""" + +import matplotlib.pyplot as plt +from matplotlib.widgets import PolygonSelector + +############################################################################### +# +# To create the polygon programmatically +fig, ax = plt.subplots() +fig.show() + +selector = PolygonSelector(ax, lambda *args: None) + +# Add three vertices +selector.verts = [(0.1, 0.4), (0.5, 0.9), (0.3, 0.2)] + + +############################################################################### +# +# To create the polygon interactively + +fig2, ax2 = plt.subplots() +fig2.show() + +selector2 = PolygonSelector(ax2, lambda *args: None) + +print("Click on the figure to create a polygon.") +print("Press the 'esc' key to start a new polygon.") +print("Try holding the 'shift' key to move all of the vertices.") +print("Try holding the 'ctrl' key to move a single vertex.") + + +############################################################################# +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.widgets.PolygonSelector` diff --git a/examples/widgets/radio_buttons.py b/examples/widgets/radio_buttons.py index 1421fc90b1bc..4d3c93e579b6 100644 --- a/examples/widgets/radio_buttons.py +++ b/examples/widgets/radio_buttons.py @@ -9,6 +9,7 @@ In this case, the buttons let the user choose one of the three different sine waves to be shown in the plot. """ + import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import RadioButtons @@ -20,36 +21,44 @@ fig, ax = plt.subplots() l, = ax.plot(t, s0, lw=2, color='red') -plt.subplots_adjust(left=0.3) +fig.subplots_adjust(left=0.3) axcolor = 'lightgoldenrodyellow' -rax = plt.axes([0.05, 0.7, 0.15, 0.15], facecolor=axcolor) -radio = RadioButtons(rax, ('2 Hz', '4 Hz', '8 Hz')) +rax = fig.add_axes([0.05, 0.7, 0.15, 0.15], facecolor=axcolor) +radio = RadioButtons(rax, ('1 Hz', '2 Hz', '4 Hz'), + label_props={'color': 'cmy', 'fontsize': [12, 14, 16]}, + radio_props={'s': [16, 32, 64]}) def hzfunc(label): - hzdict = {'2 Hz': s0, '4 Hz': s1, '8 Hz': s2} + hzdict = {'1 Hz': s0, '2 Hz': s1, '4 Hz': s2} ydata = hzdict[label] l.set_ydata(ydata) - plt.draw() + fig.canvas.draw() radio.on_clicked(hzfunc) -rax = plt.axes([0.05, 0.4, 0.15, 0.15], facecolor=axcolor) -radio2 = RadioButtons(rax, ('red', 'blue', 'green')) +rax = fig.add_axes([0.05, 0.4, 0.15, 0.15], facecolor=axcolor) +radio2 = RadioButtons( + rax, ('red', 'blue', 'green'), + label_props={'color': ['red', 'blue', 'green']}, + radio_props={ + 'facecolor': ['red', 'blue', 'green'], + 'edgecolor': ['darkred', 'darkblue', 'darkgreen'], + }) def colorfunc(label): l.set_color(label) - plt.draw() + fig.canvas.draw() radio2.on_clicked(colorfunc) -rax = plt.axes([0.05, 0.1, 0.15, 0.15], facecolor=axcolor) -radio3 = RadioButtons(rax, ('-', '--', '-.', 'steps', ':')) +rax = fig.add_axes([0.05, 0.1, 0.15, 0.15], facecolor=axcolor) +radio3 = RadioButtons(rax, ('-', '--', '-.', ':')) def stylefunc(label): l.set_linestyle(label) - plt.draw() + fig.canvas.draw() radio3.on_clicked(stylefunc) plt.show() diff --git a/examples/widgets/range_slider.py b/examples/widgets/range_slider.py index 789df59d9324..bdb69fa80859 100644 --- a/examples/widgets/range_slider.py +++ b/examples/widgets/range_slider.py @@ -26,14 +26,14 @@ img = np.random.randn(N, N) fig, axs = plt.subplots(1, 2, figsize=(10, 5)) -plt.subplots_adjust(bottom=0.25) +fig.subplots_adjust(bottom=0.25) im = axs[0].imshow(img) axs[1].hist(img.flatten(), bins='auto') axs[1].set_title('Histogram of pixel intensities') # Create the RangeSlider -slider_ax = plt.axes([0.20, 0.1, 0.60, 0.03]) +slider_ax = fig.add_axes([0.20, 0.1, 0.60, 0.03]) slider = RangeSlider(slider_ax, "Threshold", img.min(), img.max()) # Create the Vertical lines on the histogram diff --git a/examples/widgets/rectangle_selector.py b/examples/widgets/rectangle_selector.py index 4d27fadd32d2..f43f9e56fca3 100644 --- a/examples/widgets/rectangle_selector.py +++ b/examples/widgets/rectangle_selector.py @@ -1,22 +1,21 @@ """ -================== -Rectangle Selector -================== +=============================== +Rectangle and ellipse selectors +=============================== -Do a mouseclick somewhere, move the mouse to some destination, release -the button. This class gives click- and release-events and also draws -a line or a box from the click-point to the actual mouseposition -(within the same axes) until the button is released. Within the -method ``self.ignore()`` it is checked whether the button from eventpress -and eventrelease are the same. +Click somewhere, move the mouse, and release the mouse button. +`.RectangleSelector` and `.EllipseSelector` draw a rectangle or an ellipse +from the initial click position to the current mouse position (within the same +axes) until the button is released. A connected callback receives the click- +and release-events. """ -from matplotlib.widgets import RectangleSelector +from matplotlib.widgets import EllipseSelector, RectangleSelector import numpy as np import matplotlib.pyplot as plt -def line_select_callback(eclick, erelease): +def select_callback(eclick, erelease): """ Callback for line selection. @@ -25,37 +24,42 @@ def line_select_callback(eclick, erelease): x1, y1 = eclick.xdata, eclick.ydata x2, y2 = erelease.xdata, erelease.ydata print(f"({x1:3.2f}, {y1:3.2f}) --> ({x2:3.2f}, {y2:3.2f})") - print(f" The buttons you used were: {eclick.button} {erelease.button}") + print(f"The buttons you used were: {eclick.button} {erelease.button}") def toggle_selector(event): - print(' Key pressed.') + print('Key pressed.') if event.key == 't': - if toggle_selector.RS.active: - print(' RectangleSelector deactivated.') - toggle_selector.RS.set_active(False) - else: - print(' RectangleSelector activated.') - toggle_selector.RS.set_active(True) + for selector in selectors: + name = type(selector).__name__ + if selector.active: + print(f'{name} deactivated.') + selector.set_active(False) + else: + print(f'{name} activated.') + selector.set_active(True) -fig, ax = plt.subplots() +fig = plt.figure(layout='constrained') +axs = fig.subplots(2) + N = 100000 # If N is large one can see improvement by using blitting. x = np.linspace(0, 10, N) -ax.plot(x, np.sin(2*np.pi*x)) # plot something -ax.set_title( - "Click and drag to draw a rectangle.\n" - "Press 't' to toggle the selector on and off.") - -# drawtype is 'box' or 'line' or 'none' -toggle_selector.RS = RectangleSelector(ax, line_select_callback, - drawtype='box', useblit=True, - button=[1, 3], # disable middle button - minspanx=5, minspany=5, - spancoords='pixels', - interactive=True) -fig.canvas.mpl_connect('key_press_event', toggle_selector) +selectors = [] +for ax, selector_class in zip(axs, [RectangleSelector, EllipseSelector]): + ax.plot(x, np.sin(2*np.pi*x)) # plot something + ax.set_title(f"Click and drag to draw a {selector_class.__name__}.") + selectors.append(selector_class( + ax, select_callback, + useblit=True, + button=[1, 3], # disable middle button + minspanx=5, minspany=5, + spancoords='pixels', + interactive=True)) + fig.canvas.mpl_connect('key_press_event', toggle_selector) +axs[0].set_title("Press 't' to toggle the selectors on and off.\n" + + axs[0].get_title()) plt.show() ############################################################################# @@ -66,3 +70,4 @@ def toggle_selector(event): # in this example: # # - `matplotlib.widgets.RectangleSelector` +# - `matplotlib.widgets.EllipseSelector` diff --git a/examples/widgets/slider_demo.py b/examples/widgets/slider_demo.py index aa33315d3db4..6f849645f9bf 100644 --- a/examples/widgets/slider_demo.py +++ b/examples/widgets/slider_demo.py @@ -12,6 +12,7 @@ See :doc:`/gallery/widgets/range_slider` for an example of using a ``RangeSlider`` to define a range of values. """ + import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import Slider, Button @@ -29,17 +30,14 @@ def f(t, amplitude, frequency): # Create the figure and the line that we will manipulate fig, ax = plt.subplots() -line, = plt.plot(t, f(t, init_amplitude, init_frequency), lw=2) +line, = ax.plot(t, f(t, init_amplitude, init_frequency), lw=2) ax.set_xlabel('Time [s]') -axcolor = 'lightgoldenrodyellow' -ax.margins(x=0) - # adjust the main plot to make room for the sliders -plt.subplots_adjust(left=0.25, bottom=0.25) +fig.subplots_adjust(left=0.25, bottom=0.25) # Make a horizontal slider to control the frequency. -axfreq = plt.axes([0.25, 0.1, 0.65, 0.03], facecolor=axcolor) +axfreq = fig.add_axes([0.25, 0.1, 0.65, 0.03]) freq_slider = Slider( ax=axfreq, label='Frequency [Hz]', @@ -49,7 +47,7 @@ def f(t, amplitude, frequency): ) # Make a vertically oriented slider to control the amplitude -axamp = plt.axes([0.1, 0.25, 0.0225, 0.63], facecolor=axcolor) +axamp = fig.add_axes([0.1, 0.25, 0.0225, 0.63]) amp_slider = Slider( ax=axamp, label="Amplitude", @@ -71,8 +69,8 @@ def update(val): amp_slider.on_changed(update) # Create a `matplotlib.widgets.Button` to reset the sliders to initial values. -resetax = plt.axes([0.8, 0.025, 0.1, 0.04]) -button = Button(resetax, 'Reset', color=axcolor, hovercolor='0.975') +resetax = fig.add_axes([0.8, 0.025, 0.1, 0.04]) +button = Button(resetax, 'Reset', hovercolor='0.975') def reset(event): diff --git a/examples/widgets/slider_snap_demo.py b/examples/widgets/slider_snap_demo.py index e985f84ca7ac..5ace232c12c5 100644 --- a/examples/widgets/slider_snap_demo.py +++ b/examples/widgets/slider_snap_demo.py @@ -15,6 +15,7 @@ See :doc:`/gallery/widgets/range_slider` for an example of using a ``RangeSlider`` to define a range of values. """ + import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import Slider, Button @@ -25,12 +26,11 @@ s = a0 * np.sin(2 * np.pi * f0 * t) fig, ax = plt.subplots() -plt.subplots_adjust(bottom=0.25) -l, = plt.plot(t, s, lw=2) +fig.subplots_adjust(bottom=0.25) +l, = ax.plot(t, s, lw=2) -slider_bkd_color = 'lightgoldenrodyellow' -ax_freq = plt.axes([0.25, 0.1, 0.65, 0.03], facecolor=slider_bkd_color) -ax_amp = plt.axes([0.25, 0.15, 0.65, 0.03], facecolor=slider_bkd_color) +ax_freq = fig.add_axes([0.25, 0.1, 0.65, 0.03]) +ax_amp = fig.add_axes([0.25, 0.15, 0.65, 0.03]) # define the values to use for snapping allowed_amplitudes = np.concatenate([np.linspace(.1, 5, 100), [6, 7, 8, 9]]) @@ -59,8 +59,8 @@ def update(val): sfreq.on_changed(update) samp.on_changed(update) -ax_reset = plt.axes([0.8, 0.025, 0.1, 0.04]) -button = Button(ax_reset, 'Reset', color=slider_bkd_color, hovercolor='0.975') +ax_reset = fig.add_axes([0.8, 0.025, 0.1, 0.04]) +button = Button(ax_reset, 'Reset', hovercolor='0.975') def reset(event): diff --git a/examples/widgets/span_selector.py b/examples/widgets/span_selector.py index 8392be667cfd..fa75a70d2863 100644 --- a/examples/widgets/span_selector.py +++ b/examples/widgets/span_selector.py @@ -3,9 +3,18 @@ Span Selector ============= -The SpanSelector is a mouse widget to select a xmin/xmax range and plot the -detail view of the selected region in the lower axes +The `.SpanSelector` is a mouse widget that enables selecting a range on an +axis. + +Here, an x-range can be selected on the upper axis; a detailed view of the +selected range is then plotted on the lower axis. + +.. note:: + + If the SpanSelector object is garbage collected you will lose the + interactivity. You must keep a hard reference to it to prevent this. """ + import numpy as np import matplotlib.pyplot as plt from matplotlib.widgets import SpanSelector @@ -16,7 +25,7 @@ fig, (ax1, ax2) = plt.subplots(2, figsize=(8, 6)) x = np.arange(0.0, 5.0, 0.01) -y = np.sin(2*np.pi*x) + 0.5*np.random.randn(len(x)) +y = np.sin(2 * np.pi * x) + 0.5 * np.random.randn(len(x)) ax1.plot(x, y) ax1.set_ylim(-2, 2) @@ -37,18 +46,18 @@ def onselect(xmin, xmax): line2.set_data(region_x, region_y) ax2.set_xlim(region_x[0], region_x[-1]) ax2.set_ylim(region_y.min(), region_y.max()) - fig.canvas.draw() - -############################################################################# -# .. note:: -# -# If the SpanSelector object is garbage collected you will lose the -# interactivity. You must keep a hard reference to it to prevent this. -# - - -span = SpanSelector(ax1, onselect, 'horizontal', useblit=True, - rectprops=dict(alpha=0.5, facecolor='tab:blue')) + fig.canvas.draw_idle() + + +span = SpanSelector( + ax1, + onselect, + "horizontal", + useblit=True, + props=dict(alpha=0.5, facecolor="tab:blue"), + interactive=True, + drag_from_anywhere=True +) # Set useblit=True on most backends for enhanced performance. diff --git a/examples/widgets/textbox.py b/examples/widgets/textbox.py index 70402595e2e2..33a996ae73c2 100644 --- a/examples/widgets/textbox.py +++ b/examples/widgets/textbox.py @@ -40,7 +40,7 @@ def submit(expression): axbox = fig.add_axes([0.1, 0.05, 0.8, 0.075]) -text_box = TextBox(axbox, "Evaluate") +text_box = TextBox(axbox, "Evaluate", textalignment="center") text_box.on_submit(submit) text_box.set_val("t ** 2") # Trigger `submit` with the initial string. diff --git a/extern/ttconv/pprdrv.h b/extern/ttconv/pprdrv.h index 39e81fee7f0c..8c0b6c195564 100644 --- a/extern/ttconv/pprdrv.h +++ b/extern/ttconv/pprdrv.h @@ -48,20 +48,6 @@ class TTStreamWriter virtual void putline(const char* a); }; -class TTDictionaryCallback -{ -private: - // Private copy and assignment - TTDictionaryCallback& operator=(const TTStreamWriter& other); - TTDictionaryCallback(const TTStreamWriter& other); - -public: - TTDictionaryCallback() { } - virtual ~TTDictionaryCallback() { } - - virtual void add_pair(const char* key, const char* value) = 0; -}; - void replace_newlines_with_spaces(char* a); /* @@ -95,6 +81,12 @@ class TTException #define DEBUG_TRUETYPE /* truetype fonts, conversion to Postscript */ #endif +#if DEBUG_TRUETYPE +#define debug(...) printf(__VA_ARGS__) +#else +#define debug(...) +#endif + /* Do not change anything below this line. */ enum font_type_enum @@ -102,12 +94,9 @@ enum font_type_enum PS_TYPE_3 = 3, PS_TYPE_42 = 42, PS_TYPE_42_3_HYBRID = 43, - PDF_TYPE_3 = -3 }; /* routines in pprdrv_tt.c */ void insert_ttfont(const char *filename, TTStreamWriter& stream, font_type_enum target_type, std::vector& glyph_ids); -void get_pdf_charprocs(const char *filename, std::vector& glyph_ids, TTDictionaryCallback& dict); - /* end of file */ diff --git a/extern/ttconv/pprdrv_tt.cpp b/extern/ttconv/pprdrv_tt.cpp index abe209856ed9..a0c724c8aa11 100644 --- a/extern/ttconv/pprdrv_tt.cpp +++ b/extern/ttconv/pprdrv_tt.cpp @@ -121,10 +121,7 @@ BYTE *GetTable(struct TTFONT *font, const char *name) { BYTE *ptr; ULONG x; - -#ifdef DEBUG_TRUETYPE debug("GetTable(file,font,\"%s\")",name); -#endif /* We must search the table directory. */ ptr = font->offset_table + 12; @@ -142,9 +139,7 @@ BYTE *GetTable(struct TTFONT *font, const char *name) try { -#ifdef DEBUG_TRUETYPE debug("Loading table \"%s\" from offset %d, %d bytes",name,offset,length); -#endif if ( fseek( font->file, (long)offset, SEEK_SET ) ) { @@ -200,10 +195,7 @@ void Read_name(struct TTFONT *font) int platform; /* Current platform id */ int nameid; /* name id, */ int offset,length; /* offset and length of string. */ - -#ifdef DEBUG_TRUETYPE debug("Read_name()"); -#endif table_ptr = NULL; @@ -235,11 +227,8 @@ void Read_name(struct TTFONT *font) nameid = getUSHORT(ptr2+6); length = getUSHORT(ptr2+8); offset = getUSHORT(ptr2+10); - -#ifdef DEBUG_TRUETYPE debug("platform %d, encoding %d, language 0x%x, name %d, offset %d, length %d", platform,encoding,language,nameid,offset,length); -#endif /* Copyright notice */ if ( platform == 1 && nameid == 0 ) @@ -248,10 +237,7 @@ void Read_name(struct TTFONT *font) strncpy(font->Copyright,(const char*)strings+offset,length); font->Copyright[length]='\0'; replace_newlines_with_spaces(font->Copyright); - -#ifdef DEBUG_TRUETYPE debug("font->Copyright=\"%s\"",font->Copyright); -#endif continue; } @@ -264,10 +250,7 @@ void Read_name(struct TTFONT *font) strncpy(font->FamilyName,(const char*)strings+offset,length); font->FamilyName[length]='\0'; replace_newlines_with_spaces(font->FamilyName); - -#ifdef DEBUG_TRUETYPE debug("font->FamilyName=\"%s\"",font->FamilyName); -#endif continue; } @@ -280,10 +263,7 @@ void Read_name(struct TTFONT *font) strncpy(font->Style,(const char*)strings+offset,length); font->Style[length]='\0'; replace_newlines_with_spaces(font->Style); - -#ifdef DEBUG_TRUETYPE debug("font->Style=\"%s\"",font->Style); -#endif continue; } @@ -296,10 +276,7 @@ void Read_name(struct TTFONT *font) strncpy(font->FullName,(const char*)strings+offset,length); font->FullName[length]='\0'; replace_newlines_with_spaces(font->FullName); - -#ifdef DEBUG_TRUETYPE debug("font->FullName=\"%s\"",font->FullName); -#endif continue; } @@ -312,10 +289,7 @@ void Read_name(struct TTFONT *font) strncpy(font->Version,(const char*)strings+offset,length); font->Version[length]='\0'; replace_newlines_with_spaces(font->Version); - -#ifdef DEBUG_TRUETYPE debug("font->Version=\"%s\"",font->Version); -#endif continue; } @@ -328,10 +302,7 @@ void Read_name(struct TTFONT *font) strncpy(font->PostName,(const char*)strings+offset,length); font->PostName[length]='\0'; replace_newlines_with_spaces(font->PostName); - -#ifdef DEBUG_TRUETYPE debug("font->PostName=\"%s\"",font->PostName); -#endif continue; } @@ -343,10 +314,7 @@ void Read_name(struct TTFONT *font) utf16be_to_ascii(font->PostName, (char *)strings+offset, length); font->PostName[length/2]='\0'; replace_newlines_with_spaces(font->PostName); - -#ifdef DEBUG_TRUETYPE debug("font->PostName=\"%s\"",font->PostName); -#endif continue; } @@ -358,10 +326,7 @@ void Read_name(struct TTFONT *font) strncpy(font->Trademark,(const char*)strings+offset,length); font->Trademark[length]='\0'; replace_newlines_with_spaces(font->Trademark); - -#ifdef DEBUG_TRUETYPE debug("font->Trademark=\"%s\"",font->Trademark); -#endif continue; } } @@ -677,10 +642,7 @@ void sfnts_glyf_table(TTStreamWriter& stream, struct TTFONT *font, ULONG oldoffs ULONG total=0; /* running total of bytes written to table */ int x; bool loca_is_local=false; - -#ifdef DEBUG_TRUETYPE debug("sfnts_glyf_table(font,%d)", (int)correct_total_length); -#endif if (font->loca_table == NULL) { @@ -709,10 +671,7 @@ void sfnts_glyf_table(TTStreamWriter& stream, struct TTFONT *font, ULONG oldoffs length = getULONG( font->loca_table + ((x+1) * 4) ); length -= off; } - -#ifdef DEBUG_TRUETYPE debug("glyph length=%d",(int)length); -#endif /* Start new string if necessary. */ sfnts_new_table( stream, (int)length ); @@ -860,9 +819,7 @@ void ttfont_sfnts(TTStreamWriter& stream, struct TTFONT *font) sfnts_pputUSHORT(stream, entry_sel); /* entrySelector */ sfnts_pputUSHORT(stream, range_shift); /* rangeShift */ -#ifdef DEBUG_TRUETYPE debug("only %d tables selected",count); -#endif /* Now, emmit the table directory. */ for (x=0; x < 9; x++) @@ -895,10 +852,7 @@ void ttfont_sfnts(TTStreamWriter& stream, struct TTFONT *font) { continue; } - -#ifdef DEBUG_TRUETYPE debug("emmiting table '%s'",table_names[x]); -#endif /* 'glyf' table gets special treatment */ if ( strcmp(table_names[x],"glyf")==0 ) @@ -1288,9 +1242,7 @@ void read_font(const char *filename, font_type_enum target_type, std::vector& glyph_ids, TTDictionaryCallback& dict) -{ - struct TTFONT font; - - read_font(filename, PDF_TYPE_3, glyph_ids, font); - - for (std::vector::const_iterator i = glyph_ids.begin(); - i != glyph_ids.end(); ++i) - { - StringStreamWriter writer; - tt_type3_charproc(writer, &font, *i); - const char* name = ttfont_CharStrings_getname(&font, *i); - dict.add_pair(name, writer.str().c_str()); - } -} - TTFONT::TTFONT() : file(NULL), PostName(NULL), diff --git a/extern/ttconv/pprdrv_tt2.cpp b/extern/ttconv/pprdrv_tt2.cpp index 058bc005348b..ec2298c8c42b 100644 --- a/extern/ttconv/pprdrv_tt2.cpp +++ b/extern/ttconv/pprdrv_tt2.cpp @@ -60,8 +60,6 @@ class GlyphToType3 int stack_depth; /* A book-keeping variable for keeping track of the depth of the PS stack */ - bool pdf_mode; - void load_char(TTFONT* font, BYTE *glyph); void stack(TTStreamWriter& stream, int new_elem); void stack_end(TTStreamWriter& stream); @@ -91,12 +89,6 @@ struct FlaggedPoint FlaggedPoint(Flag flag_, FWord x_, FWord y_): flag(flag_), x(x_), y(y_) {}; }; -double area(FWord *x, FWord *y, int n); -#define sqr(x) ((x)*(x)) - -#define NOMOREINCTR -1 -#define NOMOREOUTCTR -1 - /* ** This routine is used to break the character ** procedure up into a number of smaller @@ -111,9 +103,8 @@ double area(FWord *x, FWord *y, int n); */ void GlyphToType3::stack(TTStreamWriter& stream, int new_elem) { - if ( !pdf_mode && num_pts > 25 ) /* Only do something of we will */ + if ( num_pts > 25 ) /* Only do something of we will have a log of points. */ { - /* have a log of points. */ if (stack_depth == 0) { stream.put_char('{'); @@ -132,7 +123,7 @@ void GlyphToType3::stack(TTStreamWriter& stream, int new_elem) void GlyphToType3::stack_end(TTStreamWriter& stream) /* called at end */ { - if ( !pdf_mode && stack_depth ) + if ( stack_depth ) { stream.puts("}_e"); stack_depth=0; @@ -238,19 +229,17 @@ void GlyphToType3::PSConvert(TTStreamWriter& stream) /* Now, we can fill the whole thing. */ stack(stream, 1); - stream.puts( pdf_mode ? "f" : "_cl" ); + stream.puts("_cl"); } /* end of PSConvert() */ void GlyphToType3::PSMoveto(TTStreamWriter& stream, int x, int y) { - stream.printf(pdf_mode ? "%d %d m\n" : "%d %d _m\n", - x, y); + stream.printf("%d %d _m\n", x, y); } void GlyphToType3::PSLineto(TTStreamWriter& stream, int x, int y) { - stream.printf(pdf_mode ? "%d %d l\n" : "%d %d _l\n", - x, y); + stream.printf("%d %d _l\n", x, y); } /* @@ -278,9 +267,9 @@ void GlyphToType3::PSCurveto(TTStreamWriter& stream, cy[1] = (sy[2]+2*sy[1])/3; cx[2] = sx[2]; cy[2] = sy[2]; - stream.printf("%d %d %d %d %d %d %s\n", + stream.printf("%d %d %d %d %d %d _c\n", (int)cx[0], (int)cy[0], (int)cx[1], (int)cy[1], - (int)cx[2], (int)cy[2], pdf_mode ? "c" : "_c"); + (int)cx[2], (int)cy[2]); } /* @@ -464,50 +453,27 @@ void GlyphToType3::do_composite(TTStreamWriter& stream, struct TTFONT *font, BYT (int)flags,arg1,arg2); #endif - if (pdf_mode) + /* If we have an (X,Y) shift and it is non-zero, */ + /* translate the coordinate system. */ + if ( flags & ARGS_ARE_XY_VALUES ) { - if ( flags & ARGS_ARE_XY_VALUES ) - { - /* We should have been able to use 'Do' to reference the - subglyph here. However, that doesn't seem to work with - xpdf or gs (only acrobat), so instead, this just includes - the subglyph here inline. */ - stream.printf("q 1 0 0 1 %d %d cm\n", topost(arg1), topost(arg2)); - } - else - { - stream.printf("%% unimplemented shift, arg1=%d, arg2=%d\n",arg1,arg2); - } - GlyphToType3(stream, font, glyphIndex, true); - if ( flags & ARGS_ARE_XY_VALUES ) - { - stream.printf("\nQ\n"); - } + if ( arg1 != 0 || arg2 != 0 ) + stream.printf("gsave %d %d translate\n", topost(arg1), topost(arg2) ); } else { - /* If we have an (X,Y) shif and it is non-zero, */ - /* translate the coordinate system. */ - if ( flags & ARGS_ARE_XY_VALUES ) - { - if ( arg1 != 0 || arg2 != 0 ) - stream.printf("gsave %d %d translate\n", topost(arg1), topost(arg2) ); - } - else - { - stream.printf("%% unimplemented shift, arg1=%d, arg2=%d\n",arg1,arg2); - } + stream.printf("%% unimplemented shift, arg1=%d, arg2=%d\n",arg1,arg2); + } - /* Invoke the CharStrings procedure to print the component. */ - stream.printf("false CharStrings /%s get exec\n", - ttfont_CharStrings_getname(font,glyphIndex)); + /* Invoke the CharStrings procedure to print the component. */ + stream.printf("false CharStrings /%s get exec\n", + ttfont_CharStrings_getname(font, glyphIndex)); - /* If we translated the coordinate system, */ - /* put it back the way it was. */ - if ( flags & ARGS_ARE_XY_VALUES && (arg1 != 0 || arg2 != 0) ) - { - stream.puts("grestore "); - } + /* If we translated the coordinate system, */ + /* put it back the way it was. */ + if ( flags & ARGS_ARE_XY_VALUES && (arg1 != 0 || arg2 != 0) ) + { + stream.puts("grestore "); } } @@ -559,7 +525,6 @@ GlyphToType3::GlyphToType3(TTStreamWriter& stream, struct TTFONT *font, int char ycoor = NULL; epts_ctr = NULL; stack_depth = 0; - pdf_mode = font->target_type < 0; /* Get a pointer to the data. */ glyph = find_glyph_data( font, charindex ); @@ -610,15 +575,7 @@ GlyphToType3::GlyphToType3(TTStreamWriter& stream, struct TTFONT *font, int char /* Execute setcachedevice in order to inform the font machinery */ /* of the character bounding box and advance width. */ stack(stream, 7); - if (pdf_mode) - { - if (!embedded) { - stream.printf("%d 0 %d %d %d %d d1\n", - topost(advance_width), - topost(llx), topost(lly), topost(urx), topost(ury) ); - } - } - else if (font->target_type == PS_TYPE_42_3_HYBRID) + if (font->target_type == PS_TYPE_42_3_HYBRID) { stream.printf("pop gsave .001 .001 scale %d 0 %d %d %d %d setcachedevice\n", topost(advance_width), diff --git a/extern/ttconv/ttutil.cpp b/extern/ttconv/ttutil.cpp index 85e7e23c4f07..6028e1d45d4a 100644 --- a/extern/ttconv/ttutil.cpp +++ b/extern/ttconv/ttutil.cpp @@ -14,18 +14,6 @@ #include #include "pprdrv.h" -#if DEBUG_TRUETYPE -void debug(const char *format, ... ) -{ - va_list arg_list; - va_start(arg_list, format); - - printf(format, arg_list); - - va_end(arg_list); -} -#endif - #define PRINTF_BUFFER_SIZE 512 void TTStreamWriter::printf(const char* format, ...) { @@ -45,6 +33,7 @@ void TTStreamWriter::printf(const char* format, ...) #else vsnprintf(buffer2, size, format, arg_list); #endif + this->write(buffer2); free(buffer2); } else { this->write(buffer); diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 71c68a3d6ba3..b279c465163e 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -17,10 +17,13 @@ at the ipython shell prompt. -For the most part, direct use of the object-oriented library is encouraged when -programming; pyplot is primarily for working interactively. The exceptions are -the pyplot functions `.pyplot.figure`, `.pyplot.subplot`, `.pyplot.subplots`, -and `.pyplot.savefig`, which can greatly simplify scripting. +For the most part, direct use of the explicit object-oriented library is +encouraged when programming; the implicit pyplot interface is primarily for +working interactively. The exceptions to this suggestion are the pyplot +functions `.pyplot.figure`, `.pyplot.subplot`, `.pyplot.subplots`, and +`.pyplot.savefig`, which can greatly simplify scripting. See +:ref:`api_interfaces` for an explanation of the tradeoffs between the implicit +and explicit interfaces. Modules include: @@ -74,18 +77,36 @@ figure is created, because it is not possible to switch between different GUI backends after that. +The following environment variables can be used to customize the behavior:: + + .. envvar:: MPLBACKEND + + This optional variable can be set to choose the Matplotlib backend. See + :ref:`what-is-a-backend`. + + .. envvar:: MPLCONFIGDIR + + This is the directory used to store user customizations to + Matplotlib, as well as some caches to improve performance. If + :envvar:`MPLCONFIGDIR` is not defined, :file:`{HOME}/.config/matplotlib` + and :file:`{HOME}/.cache/matplotlib` are used on Linux, and + :file:`{HOME}/.matplotlib` on other platforms, if they are + writable. Otherwise, the Python standard library's `tempfile.gettempdir` + is used to find a base directory in which the :file:`matplotlib` + subdirectory is created. + Matplotlib was initially written by John D. Hunter (1968-2012) and is now developed and maintained by a host of others. Occasionally the internal documentation (python docstrings) will refer -to MATLAB®, a registered trademark of The MathWorks, Inc. +to MATLAB®, a registered trademark of The MathWorks, Inc. + """ import atexit from collections import namedtuple from collections.abc import MutableMapping import contextlib -from distutils.version import LooseVersion import functools import importlib import inspect @@ -102,20 +123,16 @@ import tempfile import warnings +import numpy +from packaging.version import parse as parse_version + # cbook must import matplotlib only within function # definitions, so it is safe to import from it here. -from . import _api, cbook, docstring, rcsetup -from matplotlib.cbook import MatplotlibDeprecationWarning, sanitize_sequence -from matplotlib.cbook import mplDeprecation # deprecated +from . import _api, _version, cbook, _docstring, rcsetup +from matplotlib.cbook import sanitize_sequence +from matplotlib._api import MatplotlibDeprecationWarning from matplotlib.rcsetup import validate_backend, cycler -import numpy - -# Get the version from the _version.py versioneer file. For a git checkout, -# this is computed based on the number of commits since the last tag. -from ._version import get_versions -__version__ = str(get_versions()['version']) -del get_versions _log = logging.getLogger(__name__) @@ -134,6 +151,61 @@ year = 2007 }""" +# modelled after sys.version_info +_VersionInfo = namedtuple('_VersionInfo', + 'major, minor, micro, releaselevel, serial') + + +def _parse_to_version_info(version_str): + """ + Parse a version string to a namedtuple analogous to sys.version_info. + + See: + https://packaging.pypa.io/en/latest/version.html#packaging.version.parse + https://docs.python.org/3/library/sys.html#sys.version_info + """ + v = parse_version(version_str) + if v.pre is None and v.post is None and v.dev is None: + return _VersionInfo(v.major, v.minor, v.micro, 'final', 0) + elif v.dev is not None: + return _VersionInfo(v.major, v.minor, v.micro, 'alpha', v.dev) + elif v.pre is not None: + releaselevel = { + 'a': 'alpha', + 'b': 'beta', + 'rc': 'candidate'}.get(v.pre[0], 'alpha') + return _VersionInfo(v.major, v.minor, v.micro, releaselevel, v.pre[1]) + else: + # fallback for v.post: guess-next-dev scheme from setuptools_scm + return _VersionInfo(v.major, v.minor, v.micro + 1, 'alpha', v.post) + + +def _get_version(): + """Return the version string used for __version__.""" + # Only shell out to a git subprocess if really needed, i.e. when we are in + # a matplotlib git repo but not in a shallow clone, such as those used by + # CI, as the latter would trigger a warning from setuptools_scm. + root = Path(__file__).resolve().parents[2] + if ((root / ".matplotlib-repo").exists() + and (root / ".git").exists() + and not (root / ".git/shallow").exists()): + import setuptools_scm + return setuptools_scm.get_version( + root=root, + version_scheme="release-branch-semver", + local_scheme="node-and-date", + fallback_version=_version.version, + ) + else: # Get the version from the _version.py setuptools_scm file. + return _version.version + + +@_api.caching_module_getattr +class __getattr__: + __version__ = property(lambda self: _get_version()) + __version_info__ = property( + lambda self: _parse_to_version_info(self.__version__)) + def _check_versions(): @@ -145,13 +217,13 @@ def _check_versions(): ("cycler", "0.10"), ("dateutil", "2.7"), ("kiwisolver", "1.0.1"), - ("numpy", "1.16"), - ("pyparsing", "2.2.1"), + ("numpy", "1.20"), + ("pyparsing", "2.3.1"), ]: module = importlib.import_module(modname) - if LooseVersion(module.__version__) < minver: - raise ImportError("Matplotlib requires {}>={}; you have {}" - .format(modname, minver, module.__version__)) + if parse_version(module.__version__) < parse_version(minver): + raise ImportError(f"Matplotlib requires {modname}>={minver}; " + f"you have {module.__version__}") _check_versions() @@ -175,12 +247,22 @@ def _ensure_handler(): def set_loglevel(level): """ - Set Matplotlib's root logger and root logger handler level, creating - the handler if it does not exist yet. + Configure Matplotlib's logging levels. + + Matplotlib uses the standard library `logging` framework under the root + logger 'matplotlib'. This is a helper function to: + + - set Matplotlib's root logger level + - set the root logger handler's level, creating the handler + if it does not exist yet Typically, one should call ``set_loglevel("info")`` or ``set_loglevel("debug")`` to get additional debugging information. + Users or applications that are installing their own logging handlers + may want to directly manipulate ``logging.getLogger('matplotlib')`` rather + than use this function. + Parameters ---------- level : {"notset", "debug", "info", "warning", "error", "critical"} @@ -191,6 +273,7 @@ def set_loglevel(level): The first time this function is called, an additional handler is attached to Matplotlib's root handler; this handler is reused every time and this function simply manipulates the logger and handler's level. + """ _log.setLevel(level.upper()) _ensure_handler().setLevel(level.upper()) @@ -227,7 +310,7 @@ def wrapper(**kwargs): return wrapper -_ExecInfo = namedtuple("_ExecInfo", "executable version") +_ExecInfo = namedtuple("_ExecInfo", "executable raw_version version") class ExecutableNotFoundError(FileNotFoundError): @@ -251,21 +334,23 @@ def _get_executable_info(name): ---------- name : str The executable to query. The following values are currently supported: - "dvipng", "gs", "inkscape", "magick", "pdftops". This list is subject - to change without notice. + "dvipng", "gs", "inkscape", "magick", "pdftocairo", "pdftops". This + list is subject to change without notice. Returns ------- tuple A namedtuple with fields ``executable`` (`str`) and ``version`` - (`distutils.version.LooseVersion`, or ``None`` if the version cannot be - determined). + (`packaging.Version`, or ``None`` if the version cannot be determined). Raises ------ ExecutableNotFoundError If the executable is not found or older than the oldest version - supported by Matplotlib. + supported by Matplotlib. For debugging purposes, it is also + possible to "hide" an executable from Matplotlib by adding it to the + :envvar:`_MPLHIDEEXECUTABLES` environment variable (a comma-separated + list), which must be set prior to any calls to this function. ValueError If the executable is not one that we know how to query. """ @@ -289,17 +374,21 @@ def impl(args, regex, min_ver=None, ignore_exit_code=False): raise ExecutableNotFoundError(str(_ose)) from _ose match = re.search(regex, output) if match: - version = LooseVersion(match.group(1)) - if min_ver is not None and version < min_ver: + raw_version = match.group(1) + version = parse_version(raw_version) + if min_ver is not None and version < parse_version(min_ver): raise ExecutableNotFoundError( f"You have {args[0]} version {version} but the minimum " f"version supported by Matplotlib is {min_ver}") - return _ExecInfo(args[0], version) + return _ExecInfo(args[0], raw_version, version) else: raise ExecutableNotFoundError( f"Failed to determine the version of {args[0]} from " f"{' '.join(args)}, which output {output}") + if name in os.environ.get("_MPLHIDEEXECUTABLES", "").split(","): + raise ExecutableNotFoundError(f"{name} was hidden") + if name == "dvipng": return impl(["dvipng", "-version"], "(?m)^dvipng(?: .*)? (.+)", "1.6") elif name == "gs": @@ -351,17 +440,20 @@ def impl(args, regex, min_ver=None, ignore_exit_code=False): else: path = "convert" info = impl([path, "--version"], r"^Version: ImageMagick (\S*)") - if info.version == "7.0.10-34": + if info.raw_version == "7.0.10-34": # https://github.com/ImageMagick/ImageMagick/issues/2720 raise ExecutableNotFoundError( f"You have ImageMagick {info.version}, which is unsupported") return info + elif name == "pdftocairo": + return impl(["pdftocairo", "-v"], "pdftocairo version (.*)") elif name == "pdftops": info = impl(["pdftops", "-v"], "^pdftops version (.*)", ignore_exit_code=True) - if info and not ("3.0" <= info.version - # poppler version numbers. - or "0.9" <= info.version <= "1.0"): + if info and not ( + 3 <= info.version.major or + # poppler version numbers. + parse_version("0.9") <= info.version < parse_version("1.0")): raise ExecutableNotFoundError( f"You have pdftops version {info.version} but the minimum " f"version supported by Matplotlib is 3.0") @@ -370,6 +462,7 @@ def impl(args, regex, min_ver=None, ignore_exit_code=False): raise ValueError("Unknown executable: {!r}".format(name)) +@_api.deprecated("3.6", alternative="a vendored copy of this function") def checkdep_usetex(s): if not s: return False @@ -394,7 +487,7 @@ def _get_xdg_config_dir(): Return the XDG configuration directory, according to the XDG base directory spec: - https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html """ return os.environ.get('XDG_CONFIG_HOME') or str(Path.home() / ".config") @@ -403,7 +496,7 @@ def _get_xdg_cache_dir(): """ Return the XDG cache directory, according to the XDG base directory spec: - https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html """ return os.environ.get('XDG_CACHE_HOME') or str(Path.home() / ".cache") @@ -443,7 +536,7 @@ def _get_config_or_cache_dir(xdg_base_getter): @_logged_cached('CONFIGDIR=%s') def get_configdir(): """ - Return the string path of the the configuration directory. + Return the string path of the configuration directory. The directory is chosen as follows: @@ -524,34 +617,21 @@ def gen_candidates(): # rcParams deprecated and automatically mapped to another key. # Values are tuples of (version, new_name, f_old2new, f_new2old). _deprecated_map = {} - # rcParams deprecated; some can manually be mapped to another key. # Values are tuples of (version, new_name_or_None). -_deprecated_ignore_map = { - 'mpl_toolkits.legacy_colorbar': ('3.4', None), -} - +_deprecated_ignore_map = {} # rcParams deprecated; can use None to suppress warnings; remain actually -# listed in the rcParams (not included in _all_deprecated). +# listed in the rcParams. # Values are tuples of (version,) -_deprecated_remain_as_none = { - 'animation.avconv_path': ('3.3',), - 'animation.avconv_args': ('3.3',), - 'animation.html_args': ('3.3',), - 'mathtext.fallback_to_cm': ('3.3',), - 'keymap.all_axes': ('3.3',), - 'savefig.jpeg_quality': ('3.3',), - 'text.latex.preview': ('3.3',), -} - +_deprecated_remain_as_none = {} -_all_deprecated = {*_deprecated_map, *_deprecated_ignore_map} - -@docstring.Substitution("\n".join(map("- {}".format, rcsetup._validators))) +@_docstring.Substitution( + "\n".join(map("- {}".format, sorted(rcsetup._validators, key=str.lower))) +) class RcParams(MutableMapping, dict): """ - A dictionary object including validation. + A dict-like key-value store for config parameters, including validation. Validating functions are defined and associated with rc parameters in :mod:`matplotlib.rcsetup`. @@ -571,6 +651,47 @@ class RcParams(MutableMapping, dict): def __init__(self, *args, **kwargs): self.update(*args, **kwargs) + def _set(self, key, val): + """ + Directly write data bypassing deprecation and validation logic. + + Notes + ----- + As end user or downstream library you almost always should use + ``rcParams[key] = val`` and not ``_set()``. + + There are only very few special cases that need direct data access. + These cases previously used ``dict.__setitem__(rcParams, key, val)``, + which is now deprecated and replaced by ``rcParams._set(key, val)``. + + Even though private, we guarantee API stability for ``rcParams._set``, + i.e. it is subject to Matplotlib's API and deprecation policy. + + :meta public: + """ + dict.__setitem__(self, key, val) + + def _get(self, key): + """ + Directly read data bypassing deprecation, backend and validation + logic. + + Notes + ----- + As end user or downstream library you almost always should use + ``val = rcParams[key]`` and not ``_get()``. + + There are only very few special cases that need direct data access. + These cases previously used ``dict.__getitem__(rcParams, key, val)``, + which is now deprecated and replaced by ``rcParams._get(key)``. + + Even though private, we guarantee API stability for ``rcParams._get``, + i.e. it is subject to Matplotlib's API and deprecation policy. + + :meta public: + """ + return dict.__getitem__(self, key) + def __setitem__(self, key, val): try: if key in _deprecated_map: @@ -595,7 +716,7 @@ def __setitem__(self, key, val): cval = self.validate[key](val) except ValueError as ve: raise ValueError(f"Key {key}: {ve}") from None - dict.__setitem__(self, key, cval) + self._set(key, cval) except KeyError as err: raise KeyError( f"{key} is not a valid rc parameter (see rcParams.keys() for " @@ -606,21 +727,28 @@ def __getitem__(self, key): version, alt_key, alt_val, inverse_alt = _deprecated_map[key] _api.warn_deprecated( version, name=key, obj_type="rcparam", alternative=alt_key) - return inverse_alt(dict.__getitem__(self, alt_key)) + return inverse_alt(self._get(alt_key)) elif key in _deprecated_ignore_map: version, alt_key = _deprecated_ignore_map[key] _api.warn_deprecated( version, name=key, obj_type="rcparam", alternative=alt_key) - return dict.__getitem__(self, alt_key) if alt_key else None + return self._get(alt_key) if alt_key else None - elif key == "backend": - val = dict.__getitem__(self, key) + # In theory, this should only ever be used after the global rcParams + # has been set up, but better be safe e.g. in presence of breakpoints. + elif key == "backend" and self is globals().get("rcParams"): + val = self._get(key) if val is rcsetup._auto_backend_sentinel: from matplotlib import pyplot as plt plt.switch_backend(rcsetup._auto_backend_sentinel) - return dict.__getitem__(self, key) + return self._get(key) + + def _get_backend_or_none(self): + """Get the requested backend, if any, without triggering resolution.""" + backend = self._get("backend") + return None if backend is rcsetup._auto_backend_sentinel else backend def __repr__(self): class_name = self.__class__.__name__ @@ -659,7 +787,11 @@ def find_all(self, pattern): if pattern_re.search(key)) def copy(self): - return {k: dict.__getitem__(self, k) for k in self} + """Copy this RcParams instance.""" + rccopy = RcParams() + for k in self: # Skip deprecations and revalidation. + rccopy._set(k, self._get(k)) + return rccopy def rc_params(fail_on_error=False): @@ -667,14 +799,6 @@ def rc_params(fail_on_error=False): return rc_params_from_file(matplotlib_fname(), fail_on_error) -URL_REGEX = re.compile(r'^http://|^https://|^ftp://|^file:') - - -def is_url(filename): - """Return whether *filename* is an http, https, ftp, or file URL path.""" - return URL_REGEX.match(filename) is not None - - @functools.lru_cache() def _get_ssl_context(): try: @@ -688,7 +812,8 @@ def _get_ssl_context(): @contextlib.contextmanager def _open_file_or_url(fname): - if not isinstance(fname, Path) and is_url(fname): + if (isinstance(fname, str) + and fname.startswith(('http://', 'https://', 'ftp://', 'file:'))): import urllib.request ssl_ctx = _get_ssl_context() if ssl_ctx is None: @@ -699,10 +824,7 @@ def _open_file_or_url(fname): yield (line.decode('utf-8') for line in f) else: fname = os.path.expanduser(fname) - encoding = locale.getpreferredencoding(do_setlocale=False) - if encoding is None: - encoding = "utf-8" - with open(fname, encoding=encoding) as f: + with open(fname, encoding='utf-8') as f: yield f @@ -723,12 +845,13 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False): fail_on_error : bool, default: False Whether invalid entries should result in an exception or a warning. """ + import matplotlib as mpl rc_temp = {} with _open_file_or_url(fname) as fd: try: for line_no, line in enumerate(fd, 1): line = transform(line) - strippedline = line.split('#', 1)[0].strip() + strippedline = cbook._strip_comment(line) if not strippedline: continue tup = strippedline.split(':', 1) @@ -739,16 +862,15 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False): key, val = tup key = key.strip() val = val.strip() + if val.startswith('"') and val.endswith('"'): + val = val[1:-1] # strip double quotes if key in rc_temp: _log.warning('Duplicate key in file %r, line %d (%r)', fname, line_no, line.rstrip('\n')) rc_temp[key] = (val, line, line_no) except UnicodeDecodeError: - _log.warning('Cannot decode configuration file %s with encoding ' - '%s, check LANG and LC_* variables.', - fname, - locale.getpreferredencoding(do_setlocale=False) - or 'utf-8 (default)') + _log.warning('Cannot decode configuration file %r as utf-8.', + fname) raise config = RcParams() @@ -769,7 +891,10 @@ def _rc_params_in_file(fname, transform=lambda x: x, fail_on_error=False): version, name=key, alternative=alt_key, obj_type='rcparam', addendum="Please update your matplotlibrc.") else: - version = 'master' if '.post' in __version__ else f'v{__version__}' + # __version__ must be looked up as an attribute to trigger the + # module-level __getattr__. + version = ('main' if '.post' in mpl.__version__ + else f'v{mpl.__version__}') _log.warning(""" Bad key %(key)s in file %(fname)s, line %(line_no)s (%(line)r) You probably need to get an updated matplotlibrc file from @@ -825,11 +950,17 @@ def rc_params_from_file(fname, fail_on_error=False, use_default_template=True): transform=lambda line: line[1:] if line.startswith("#") else line, fail_on_error=True) dict.update(rcParamsDefault, rcsetup._hardcoded_defaults) +# Normally, the default matplotlibrc file contains *no* entry for backend (the +# corresponding line starts with ##, not #; we fill on _auto_backend_sentinel +# in that case. However, packagers can set a different default backend +# (resulting in a normal `#backend: foo` line) in which case we should *not* +# fill in _auto_backend_sentinel. +dict.setdefault(rcParamsDefault, "backend", rcsetup._auto_backend_sentinel) rcParams = RcParams() # The global instance. dict.update(rcParams, dict.items(rcParamsDefault)) dict.update(rcParams, _rc_params_in_file(matplotlib_fname())) +rcParamsOrig = rcParams.copy() with _api.suppress_matplotlib_deprecation_warning(): - rcParamsOrig = RcParams(rcParams.copy()) # This also checks that all rcParams are indeed listed in the template. # Assigning to rcsetup.defaultParams is left only for backcompat. defaultParams = rcsetup.defaultParams = { @@ -923,7 +1054,7 @@ def rcdefaults(): Restore the `.rcParams` from Matplotlib's internal default style. Style-blacklisted `.rcParams` (defined in - `matplotlib.style.core.STYLE_BLACKLIST`) are not updated. + ``matplotlib.style.core.STYLE_BLACKLIST``) are not updated. See Also -------- @@ -948,7 +1079,7 @@ def rc_file_defaults(): Restore the `.rcParams` from the original rc file loaded by Matplotlib. Style-blacklisted `.rcParams` (defined in - `matplotlib.style.core.STYLE_BLACKLIST`) are not updated. + ``matplotlib.style.core.STYLE_BLACKLIST``) are not updated. """ # Deprecation warnings were already handled when creating rcParamsOrig, no # need to reemit them here. @@ -963,7 +1094,7 @@ def rc_file(fname, *, use_default_template=True): Update `.rcParams` from file. Style-blacklisted `.rcParams` (defined in - `matplotlib.style.core.STYLE_BLACKLIST`) are not updated. + ``matplotlib.style.core.STYLE_BLACKLIST``) are not updated. Parameters ---------- @@ -990,6 +1121,11 @@ def rc_context(rc=None, fname=None): """ Return a context manager for temporarily changing rcParams. + The :rc:`backend` will not be reset by the context manager. + + rcParams changed both through the context manager invocation and + in the body of the context will be reset on context exit. + Parameters ---------- rc : dict @@ -1017,8 +1153,16 @@ def rc_context(rc=None, fname=None): with mpl.rc_context(fname='print.rc'): plt.plot(x, y) # uses 'print.rc' + Setting in the context body:: + + with mpl.rc_context(): + # will be reset + mpl.rcParams['lines.linewidth'] = 5 + plt.plot(x, y) + """ - orig = rcParams.copy() + orig = dict(rcParams.copy()) + del orig['backend'] try: if fname: rc_file(fname) @@ -1033,6 +1177,10 @@ def use(backend, *, force=True): """ Select the backend used for rendering and GUI integration. + If pyplot is already imported, `~matplotlib.pyplot.switch_backend` is used + and if the new backend is different than the current backend, all Figures + will be closed. + Parameters ---------- backend : str @@ -1040,30 +1188,35 @@ def use(backend, *, force=True): backend names, which are case-insensitive: - interactive backends: - GTK3Agg, GTK3Cairo, MacOSX, nbAgg, - Qt4Agg, Qt4Cairo, Qt5Agg, Qt5Cairo, - TkAgg, TkCairo, WebAgg, WX, WXAgg, WXCairo + GTK3Agg, GTK3Cairo, GTK4Agg, GTK4Cairo, MacOSX, nbAgg, QtAgg, + QtCairo, TkAgg, TkCairo, WebAgg, WX, WXAgg, WXCairo, Qt5Agg, Qt5Cairo - non-interactive backends: agg, cairo, pdf, pgf, ps, svg, template or a string of the form: ``module://my.module.name``. + Switching to an interactive backend is not possible if an unrelated + event loop has already been started (e.g., switching to GTK3Agg if a + TkAgg window has already been opened). Switching to a non-interactive + backend is always possible. + force : bool, default: True If True (the default), raise an `ImportError` if the backend cannot be set up (either because it fails to import, or because an incompatible - GUI interactive framework is already running); if False, ignore the - failure. + GUI interactive framework is already running); if False, silently + ignore the failure. See Also -------- :ref:`backends` matplotlib.get_backend + matplotlib.pyplot.switch_backend + """ name = validate_backend(backend) - # we need to use the base-class method here to avoid (prematurely) - # resolving the "auto" backend setting - if dict.__getitem__(rcParams, 'backend') == name: + # don't (prematurely) resolve the "auto" backend setting + if rcParams._get_backend_or_none() == name: # Nothing to do if the requested backend is already set pass else: @@ -1125,12 +1278,6 @@ def is_interactive(): return rcParams['interactive'] -default_test_modules = [ - 'matplotlib.tests', - 'mpl_toolkits.tests', -] - - def _init_tests(): # The version of FreeType to install locally for running the # tests. This must match the value in `setupext.py` @@ -1142,70 +1289,13 @@ def _init_tests(): _log.warning( f"Matplotlib is not built with the correct FreeType version to " f"run tests. Rebuild without setting system_freetype=1 in " - f"setup.cfg. Expect many image comparison failures below. " + f"mplsetup.cfg. Expect many image comparison failures below. " f"Expected freetype version {LOCAL_FREETYPE_VERSION}. " f"Found freetype version {ft2font.__freetype_version__}. " "Freetype build type is {}local".format( "" if ft2font.__freetype_build_type__ == 'local' else "not ")) -@_api.delete_parameter("3.3", "recursionlimit") -def test(verbosity=None, coverage=False, *, recursionlimit=0, **kwargs): - """Run the matplotlib test suite.""" - - try: - import pytest - except ImportError: - print("matplotlib.test requires pytest to run.") - return -1 - - if not os.path.isdir(os.path.join(os.path.dirname(__file__), 'tests')): - print("Matplotlib test data is not installed") - return -1 - - old_backend = get_backend() - old_recursionlimit = sys.getrecursionlimit() - try: - use('agg') - if recursionlimit: - sys.setrecursionlimit(recursionlimit) - - args = kwargs.pop('argv', []) - provide_default_modules = True - use_pyargs = True - for arg in args: - if any(arg.startswith(module_path) - for module_path in default_test_modules): - provide_default_modules = False - break - if os.path.exists(arg): - provide_default_modules = False - use_pyargs = False - break - if use_pyargs: - args += ['--pyargs'] - if provide_default_modules: - args += default_test_modules - - if coverage: - args += ['--cov'] - - if verbosity: - args += ['-' + 'v' * verbosity] - - retcode = pytest.main(args, **kwargs) - finally: - if old_backend.lower() != 'agg': - use(old_backend) - if recursionlimit: - sys.setrecursionlimit(old_recursionlimit) - - return retcode - - -test.__test__ = False # pytest: this function is not a test - - def _replacer(data, value): """ Either returns ``data[value]`` or passes ``data`` back, converts either to @@ -1231,24 +1321,6 @@ def _label_from_arg(y, default_name): return None -_DATA_DOC_TITLE = """ - -Notes ------ -""" - -_DATA_DOC_APPENDIX = """ - -.. note:: - In addition to the above described arguments, this function can take - a *data* keyword argument. If such a *data* argument is given, -{replaced} - - Objects passed as **data** must support item access (``data[s]``) and - membership test (``s in data``). -""" - - def _add_data_doc(docstring, replace_names): """ Add documentation for a *data* field to the given docstring. @@ -1271,17 +1343,26 @@ def _add_data_doc(docstring, replace_names): or replace_names is not None and len(replace_names) == 0): return docstring docstring = inspect.cleandoc(docstring) - repl = ( - (" every other argument can also be string ``s``, which is\n" - " interpreted as ``data[s]`` (unless this raises an exception).") - if replace_names is None else - (" the following arguments can also be string ``s``, which is\n" - " interpreted as ``data[s]`` (unless this raises an exception):\n" - " " + ", ".join(map("*{}*".format, replace_names))) + ".") - addendum = _DATA_DOC_APPENDIX.format(replaced=repl) - if _DATA_DOC_TITLE not in docstring: - addendum = _DATA_DOC_TITLE + addendum - return docstring + addendum + + data_doc = ("""\ + If given, all parameters also accept a string ``s``, which is + interpreted as ``data[s]`` (unless this raises an exception).""" + if replace_names is None else f"""\ + If given, the following parameters also accept a string ``s``, which is + interpreted as ``data[s]`` (unless this raises an exception): + + {', '.join(map('*{}*'.format, replace_names))}""") + # using string replacement instead of formatting has the advantages + # 1) simpler indent handling + # 2) prevent problems with formatting characters '{', '%' in the docstring + if _log.level <= logging.DEBUG: + # test_data_parameter_replacement() tests against these log messages + # make sure to keep message and test in sync + if "data : indexable object, optional" not in docstring: + _log.debug("data parameter docstring error: no data parameter") + if 'DATA_PARAMETER_PLACEHOLDER' not in docstring: + _log.debug("data parameter docstring error: missing placeholder") + return docstring.replace(' DATA_PARAMETER_PLACEHOLDER', data_doc) def _preprocess_data(func=None, *, replace_names=None, label_namer=None): @@ -1391,7 +1472,11 @@ def inner(ax, *args, data=None, **kwargs): return inner -_log.debug('matplotlib version %s', __version__) _log.debug('interactive is %s', is_interactive()) _log.debug('platform is %s', sys.platform) -_log.debug('loaded modules: %s', list(sys.modules)) + + +# workaround: we must defer colormaps import to after loading rcParams, because +# colormap creation depends on rcParams +from matplotlib.cm import _colormaps as colormaps +from matplotlib.colors import _color_sequences as color_sequences diff --git a/lib/matplotlib/_afm.py b/lib/matplotlib/_afm.py new file mode 100644 index 000000000000..3d02d7f9c1d6 --- /dev/null +++ b/lib/matplotlib/_afm.py @@ -0,0 +1,532 @@ +""" +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 has no external dependencies: + +>>> import matplotlib as mpl +>>> from pathlib import Path +>>> afm_path = Path(mpl.get_data_path(), 'fonts', 'afm', 'ptmr8a.afm') +>>> +>>> from matplotlib.afm import AFM +>>> with afm_path.open('rb') as fh: +... afm = AFM(fh) +>>> afm.string_width_height('What the heck?') +(6220.0, 694) +>>> afm.get_fontname() +'Times-Roman' +>>> afm.get_kern_dist('A', 'f') +0 +>>> afm.get_kern_dist('A', 'y') +-92.0 +>>> afm.get_bbox_char('!') +[130, -9, 238, 676] + +As in the Adobe Font Metrics File Format Specification, all dimensions +are given in units of 1/1000 of the scale factor (point size) of the font +being used. +""" + +from collections import namedtuple +import logging +import re + +from ._mathtext_data import uni2type1 + + +_log = logging.getLogger(__name__) + + +def _to_int(x): + # Some AFM files have floats where we are expecting ints -- there is + # probably a better way to handle this (support floats, round rather than + # truncate). But I don't know what the best approach is now and this + # change to _to_int should at least prevent Matplotlib from crashing on + # these. JDH (2009-11-06) + return int(float(x)) + + +def _to_float(x): + # Some AFM files use "," instead of "." as decimal separator -- this + # shouldn't be ambiguous (unless someone is wicked enough to use "," as + # thousands separator...). + if isinstance(x, bytes): + # Encoding doesn't really matter -- if we have codepoints >127 the call + # to float() will error anyways. + x = x.decode('latin-1') + return float(x.replace(',', '.')) + + +def _to_str(x): + return x.decode('utf8') + + +def _to_list_of_ints(s): + s = s.replace(b',', b' ') + return [_to_int(val) for val in s.split()] + + +def _to_list_of_floats(s): + return [_to_float(val) for val in s.split()] + + +def _to_bool(s): + if s.lower().strip() in (b'false', b'0', b'no'): + return False + else: + return True + + +def _parse_header(fh): + """ + 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.: + + * 'False'->False + * '0'->0 + * '-168 -218 1000 898'-> [-168, -218, 1000, 898] + + Dictionary keys are + + StartFontMetrics, FontName, FullName, FamilyName, Weight, + ItalicAngle, IsFixedPitch, FontBBox, UnderlinePosition, + UnderlineThickness, Version, Notice, EncodingScheme, CapHeight, + XHeight, Ascender, Descender, StartCharMetrics + """ + header_converters = { + b'StartFontMetrics': _to_float, + b'FontName': _to_str, + b'FullName': _to_str, + b'FamilyName': _to_str, + b'Weight': _to_str, + b'ItalicAngle': _to_float, + b'IsFixedPitch': _to_bool, + b'FontBBox': _to_list_of_ints, + b'UnderlinePosition': _to_float, + b'UnderlineThickness': _to_float, + b'Version': _to_str, + # Some AFM files have non-ASCII characters (which are not allowed by + # the spec). Given that there is actually no public API to even access + # this field, just return it as straight bytes. + b'Notice': lambda x: x, + b'EncodingScheme': _to_str, + b'CapHeight': _to_float, # Is the second version a mistake, or + b'Capheight': _to_float, # do some AFM files contain 'Capheight'? -JKS + b'XHeight': _to_float, + b'Ascender': _to_float, + b'Descender': _to_float, + b'StdHW': _to_float, + b'StdVW': _to_float, + b'StartCharMetrics': _to_int, + b'CharacterSet': _to_str, + b'Characters': _to_int, + } + d = {} + first_line = True + for line in fh: + line = line.rstrip() + if line.startswith(b'Comment'): + continue + lst = line.split(b' ', 1) + key = lst[0] + if first_line: + # AFM spec, Section 4: The StartFontMetrics keyword + # [followed by a version number] must be the first line in + # the file, and the EndFontMetrics keyword must be the + # last non-empty line in the file. We just check the + # first header entry. + if key != b'StartFontMetrics': + raise RuntimeError('Not an AFM file') + first_line = False + if len(lst) == 2: + val = lst[1] + else: + val = b'' + try: + converter = header_converters[key] + except KeyError: + _log.error('Found an unknown keyword in AFM header (was %r)' % key) + continue + try: + d[key] = converter(val) + except ValueError: + _log.error('Value error parsing header in AFM: %s, %s', key, val) + continue + if key == b'StartCharMetrics': + break + else: + raise RuntimeError('Bad parse') + return d + + +CharMetrics = namedtuple('CharMetrics', 'width, name, bbox') +CharMetrics.__doc__ = """ + Represents the character metrics of a single character. + + Notes + ----- + The fields do currently only describe a subset of character metrics + information defined in the AFM standard. + """ +CharMetrics.width.__doc__ = """The character width (WX).""" +CharMetrics.name.__doc__ = """The character name (N).""" +CharMetrics.bbox.__doc__ = """ + The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*).""" + + +def _parse_char_metrics(fh): + """ + Parse the given filehandle for character metrics information and return + the information as dicts. + + It is assumed that the file cursor is on the line behind + 'StartCharMetrics'. + + Returns + ------- + ascii_d : dict + A mapping "ASCII num of the character" to `.CharMetrics`. + name_d : dict + A mapping "character name" to `.CharMetrics`. + + Notes + ----- + This function is incomplete per the standard, but thus far parses + all the sample afm files tried. + """ + required_keys = {'C', 'WX', 'N', 'B'} + + ascii_d = {} + name_d = {} + for line in fh: + # We are defensively letting values be utf8. The spec requires + # ascii, but there are non-compliant fonts in circulation + line = _to_str(line.rstrip()) # Convert from byte-literal + if line.startswith('EndCharMetrics'): + return ascii_d, name_d + # Split the metric line into a dictionary, keyed by metric identifiers + vals = dict(s.strip().split(' ', 1) for s in line.split(';') if s) + # There may be other metrics present, but only these are needed + if not required_keys.issubset(vals): + raise RuntimeError('Bad char metrics line: %s' % line) + num = _to_int(vals['C']) + wx = _to_float(vals['WX']) + name = vals['N'] + bbox = _to_list_of_floats(vals['B']) + bbox = list(map(int, bbox)) + metrics = CharMetrics(wx, name, bbox) + # Workaround: If the character name is 'Euro', give it the + # corresponding character code, according to WinAnsiEncoding (see PDF + # Reference). + if name == 'Euro': + num = 128 + elif name == 'minus': + num = ord("\N{MINUS SIGN}") # 0x2212 + if num != -1: + ascii_d[num] = metrics + name_d[name] = metrics + raise RuntimeError('Bad parse') + + +def _parse_kern_pairs(fh): + """ + Return a kern pairs dictionary; keys are (*char1*, *char2*) tuples and + values are the kern pair value. For example, a kern pairs line like + ``KPX A y -50`` + + will be represented as:: + + d[ ('A', 'y') ] = -50 + + """ + + line = next(fh) + if not line.startswith(b'StartKernPairs'): + raise RuntimeError('Bad start of kern pairs data: %s' % line) + + d = {} + for line in fh: + line = line.rstrip() + if not line: + continue + if line.startswith(b'EndKernPairs'): + next(fh) # EndKernData + return d + vals = line.split() + if len(vals) != 4 or vals[0] != b'KPX': + raise RuntimeError('Bad kern pairs line: %s' % line) + c1, c2, val = _to_str(vals[1]), _to_str(vals[2]), _to_float(vals[3]) + d[(c1, c2)] = val + raise RuntimeError('Bad kern pairs parse') + + +CompositePart = namedtuple('CompositePart', 'name, dx, dy') +CompositePart.__doc__ = """ + Represents the information on a composite element of a composite char.""" +CompositePart.name.__doc__ = """Name of the part, e.g. 'acute'.""" +CompositePart.dx.__doc__ = """x-displacement of the part from the origin.""" +CompositePart.dy.__doc__ = """y-displacement of the part from the origin.""" + + +def _parse_composites(fh): + """ + Parse the given filehandle for composites information return them as a + dict. + + It is assumed that the file cursor is on the line behind 'StartComposites'. + + Returns + ------- + dict + A dict mapping composite character names to a parts list. The parts + list is a list of `.CompositePart` entries describing the parts of + the composite. + + Examples + -------- + A composite definition line:: + + CC Aacute 2 ; PCC A 0 0 ; PCC acute 160 170 ; + + will be represented as:: + + composites['Aacute'] = [CompositePart(name='A', dx=0, dy=0), + CompositePart(name='acute', dx=160, dy=170)] + + """ + composites = {} + for line in fh: + line = line.rstrip() + if not line: + continue + if line.startswith(b'EndComposites'): + return composites + vals = line.split(b';') + cc = vals[0].split() + name, _num_parts = cc[1], _to_int(cc[2]) + pccParts = [] + for s in vals[1:-1]: + pcc = s.split() + part = CompositePart(pcc[1], _to_float(pcc[2]), _to_float(pcc[3])) + pccParts.append(part) + composites[name] = pccParts + + raise RuntimeError('Bad composites parse') + + +def _parse_optional(fh): + """ + Parse the optional fields for kern pair data and composites. + + Returns + ------- + kern_data : dict + A dict containing kerning information. May be empty. + See `._parse_kern_pairs`. + composites : dict + A dict containing composite information. May be empty. + See `._parse_composites`. + """ + optional = { + b'StartKernData': _parse_kern_pairs, + b'StartComposites': _parse_composites, + } + + d = {b'StartKernData': {}, + b'StartComposites': {}} + for line in fh: + line = line.rstrip() + if not line: + continue + key = line.split()[0] + + if key in optional: + d[key] = optional[key](fh) + + return d[b'StartKernData'], d[b'StartComposites'] + + +class AFM: + + def __init__(self, fh): + """Parse the AFM file in file object *fh*.""" + self._header = _parse_header(fh) + self._metrics, self._metrics_by_name = _parse_char_metrics(fh) + self._kern, self._composite = _parse_optional(fh) + + def get_bbox_char(self, c, isord=False): + if not isord: + c = ord(c) + return self._metrics[c].bbox + + def string_width_height(self, s): + """ + Return the string width (including kerning) and string height + as a (*w*, *h*) tuple. + """ + if not len(s): + return 0, 0 + total_width = 0 + namelast = None + miny = 1e9 + maxy = 0 + for c in s: + if c == '\n': + continue + wx, name, bbox = self._metrics[ord(c)] + + total_width += wx + self._kern.get((namelast, name), 0) + l, b, w, h = bbox + miny = min(miny, b) + maxy = max(maxy, b + h) + + namelast = name + + return total_width, maxy - miny + + def get_str_bbox_and_descent(self, s): + """Return the string bounding box and the maximal descent.""" + if not len(s): + return 0, 0, 0, 0, 0 + total_width = 0 + namelast = None + miny = 1e9 + maxy = 0 + left = 0 + if not isinstance(s, str): + s = _to_str(s) + for c in s: + if c == '\n': + continue + name = uni2type1.get(ord(c), f"uni{ord(c):04X}") + try: + wx, _, bbox = self._metrics_by_name[name] + except KeyError: + name = 'question' + wx, _, bbox = self._metrics_by_name[name] + total_width += wx + self._kern.get((namelast, name), 0) + l, b, w, h = bbox + left = min(left, l) + miny = min(miny, b) + maxy = max(maxy, b + h) + + namelast = name + + return left, miny, total_width, maxy - miny, -miny + + def get_str_bbox(self, s): + """Return the string bounding box.""" + return self.get_str_bbox_and_descent(s)[:4] + + def get_name_char(self, c, isord=False): + """Get the name of the character, i.e., ';' is 'semicolon'.""" + if not isord: + c = ord(c) + return self._metrics[c].name + + def get_width_char(self, c, isord=False): + """ + Get the width of the character from the character metric WX field. + """ + if not isord: + c = ord(c) + return self._metrics[c].width + + def get_width_from_char_name(self, name): + """Get the width of the character from a type1 character name.""" + return self._metrics_by_name[name].width + + def get_height_char(self, c, isord=False): + """Get the bounding box (ink) height of character *c* (space is 0).""" + if not isord: + c = ord(c) + return self._metrics[c].bbox[-1] + + def get_kern_dist(self, c1, c2): + """ + Return the kerning pair distance (possibly 0) for chars *c1* and *c2*. + """ + name1, name2 = self.get_name_char(c1), self.get_name_char(c2) + return self.get_kern_dist_from_name(name1, name2) + + def get_kern_dist_from_name(self, name1, name2): + """ + Return the kerning pair distance (possibly 0) for chars + *name1* and *name2*. + """ + return self._kern.get((name1, name2), 0) + + def get_fontname(self): + """Return the font name, e.g., 'Times-Roman'.""" + return self._header[b'FontName'] + + @property + def postscript_name(self): # For consistency with FT2Font. + return self.get_fontname() + + def get_fullname(self): + """Return the font full name, e.g., 'Times-Roman'.""" + name = self._header.get(b'FullName') + if name is None: # use FontName as a substitute + name = self._header[b'FontName'] + return name + + def get_familyname(self): + """Return the font family name, e.g., 'Times'.""" + name = self._header.get(b'FamilyName') + if name is not None: + return name + + # FamilyName not specified so we'll make a guess + name = self.get_fullname() + extras = (r'(?i)([ -](regular|plain|italic|oblique|bold|semibold|' + r'light|ultralight|extra|condensed))+$') + return re.sub(extras, '', name) + + @property + def family_name(self): + """The font family name, e.g., 'Times'.""" + return self.get_familyname() + + def get_weight(self): + """Return the font weight, e.g., 'Bold' or 'Roman'.""" + return self._header[b'Weight'] + + def get_angle(self): + """Return the fontangle as float.""" + return self._header[b'ItalicAngle'] + + def get_capheight(self): + """Return the cap height as float.""" + return self._header[b'CapHeight'] + + def get_xheight(self): + """Return the xheight as float.""" + return self._header[b'XHeight'] + + def get_underline_thickness(self): + """Return the underline thickness as float.""" + return self._header[b'UnderlineThickness'] + + def get_horizontal_stem_width(self): + """ + Return the standard horizontal stem width as float, or *None* if + not specified in AFM file. + """ + return self._header.get(b'StdHW', None) + + def get_vertical_stem_width(self): + """ + Return the standard vertical stem width as float, or *None* if + not specified in AFM file. + """ + return self._header.get(b'StdVW', None) diff --git a/lib/matplotlib/_animation_data.py b/lib/matplotlib/_animation_data.py index 257f0c4a7f6f..4bf2ae3148d2 100644 --- a/lib/matplotlib/_animation_data.py +++ b/lib/matplotlib/_animation_data.py @@ -1,4 +1,4 @@ -# Javascript template for HTMLWriter +# JavaScript template for HTMLWriter JS_INCLUDE = """ @@ -196,7 +196,7 @@
+ oninput="anim{id}.set_frame(parseInt(this.value));">
diff --git a/lib/matplotlib/_api/__init__.py b/lib/matplotlib/_api/__init__.py index f251e07bab59..068abb945181 100644 --- a/lib/matplotlib/_api/__init__.py +++ b/lib/matplotlib/_api/__init__.py @@ -3,13 +3,14 @@ This documentation is only relevant for Matplotlib developers, not for users. -.. warning: +.. warning:: This module and its submodules are for internal use only. Do not use them in your own code. We may change the API at any time with no warning. """ +import functools import itertools import re import sys @@ -119,15 +120,15 @@ def check_in_list(_values, *, _print_supported_values=True, **kwargs): -------- >>> _api.check_in_list(["foo", "bar"], arg=arg, other_arg=other_arg) """ + if not kwargs: + raise TypeError("No argument to check!") values = _values for key, val in kwargs.items(): if val not in values: + msg = f"{val!r} is not a valid value for {key}" if _print_supported_values: - raise ValueError( - f"{val!r} is not a valid value for {key}; " - f"supported values are {', '.join(map(repr, values))}") - else: - raise ValueError(f"{val!r} is not a valid value for {key}") + msg += f"; supported values are {', '.join(map(repr, values))}" + raise ValueError(msg) def check_shape(_shape, **kwargs): @@ -161,6 +162,8 @@ def check_shape(_shape, **kwargs): if n is not None else next(dim_labels) for n in target_shape)) + if len(target_shape) == 1: + text_shape += "," raise ValueError( f"{k!r} must be {len(target_shape)}D " @@ -191,6 +194,178 @@ def check_getitem(_mapping, **kwargs): .format(v, k, ', '.join(map(repr, mapping)))) from None +def caching_module_getattr(cls): + """ + Helper decorator for implementing module-level ``__getattr__`` as a class. + + This decorator must be used at the module toplevel as follows:: + + @caching_module_getattr + class __getattr__: # The class *must* be named ``__getattr__``. + @property # Only properties are taken into account. + def name(self): ... + + The ``__getattr__`` class will be replaced by a ``__getattr__`` + function such that trying to access ``name`` on the module will + resolve the corresponding property (which may be decorated e.g. with + ``_api.deprecated`` for deprecating module globals). The properties are + all implicitly cached. Moreover, a suitable AttributeError is generated + and raised if no property with the given name exists. + """ + + assert cls.__name__ == "__getattr__" + # Don't accidentally export cls dunders. + props = {name: prop for name, prop in vars(cls).items() + if isinstance(prop, property)} + instance = cls() + + @functools.lru_cache(None) + def __getattr__(name): + if name in props: + return props[name].__get__(instance) + raise AttributeError( + f"module {cls.__module__!r} has no attribute {name!r}") + + return __getattr__ + + +def define_aliases(alias_d, cls=None): + """ + Class decorator for defining property aliases. + + Use as :: + + @_api.define_aliases({"property": ["alias", ...], ...}) + class C: ... + + For each property, if the corresponding ``get_property`` is defined in the + class so far, an alias named ``get_alias`` will be defined; the same will + be done for setters. If neither the getter nor the setter exists, an + exception will be raised. + + The alias map is stored as the ``_alias_map`` attribute on the class and + can be used by `.normalize_kwargs` (which assumes that higher priority + aliases come last). + """ + if cls is None: # Return the actual class decorator. + return functools.partial(define_aliases, alias_d) + + def make_alias(name): # Enforce a closure over *name*. + @functools.wraps(getattr(cls, name)) + def method(self, *args, **kwargs): + return getattr(self, name)(*args, **kwargs) + return method + + for prop, aliases in alias_d.items(): + exists = False + for prefix in ["get_", "set_"]: + if prefix + prop in vars(cls): + exists = True + for alias in aliases: + method = make_alias(prefix + prop) + method.__name__ = prefix + alias + method.__doc__ = "Alias for `{}`.".format(prefix + prop) + setattr(cls, prefix + alias, method) + if not exists: + raise ValueError( + "Neither getter nor setter exists for {!r}".format(prop)) + + def get_aliased_and_aliases(d): + return {*d, *(alias for aliases in d.values() for alias in aliases)} + + preexisting_aliases = getattr(cls, "_alias_map", {}) + conflicting = (get_aliased_and_aliases(preexisting_aliases) + & get_aliased_and_aliases(alias_d)) + if conflicting: + # Need to decide on conflict resolution policy. + raise NotImplementedError( + f"Parent class already defines conflicting aliases: {conflicting}") + cls._alias_map = {**preexisting_aliases, **alias_d} + return cls + + +def select_matching_signature(funcs, *args, **kwargs): + """ + Select and call the function that accepts ``*args, **kwargs``. + + *funcs* is a list of functions which should not raise any exception (other + than `TypeError` if the arguments passed do not match their signature). + + `select_matching_signature` tries to call each of the functions in *funcs* + with ``*args, **kwargs`` (in the order in which they are given). Calls + that fail with a `TypeError` are silently skipped. As soon as a call + succeeds, `select_matching_signature` returns its return value. If no + function accepts ``*args, **kwargs``, then the `TypeError` raised by the + last failing call is re-raised. + + Callers should normally make sure that any ``*args, **kwargs`` can only + bind a single *func* (to avoid any ambiguity), although this is not checked + by `select_matching_signature`. + + Notes + ----- + `select_matching_signature` is intended to help implementing + signature-overloaded functions. In general, such functions should be + avoided, except for back-compatibility concerns. A typical use pattern is + :: + + def my_func(*args, **kwargs): + params = select_matching_signature( + [lambda old1, old2: locals(), lambda new: locals()], + *args, **kwargs) + if "old1" in params: + warn_deprecated(...) + old1, old2 = params.values() # note that locals() is ordered. + else: + new, = params.values() + # do things with params + + which allows *my_func* to be called either with two parameters (*old1* and + *old2*) or a single one (*new*). Note that the new signature is given + last, so that callers get a `TypeError` corresponding to the new signature + if the arguments they passed in do not match any signature. + """ + # Rather than relying on locals() ordering, one could have just used func's + # signature (``bound = inspect.signature(func).bind(*args, **kwargs); + # bound.apply_defaults(); return bound``) but that is significantly slower. + for i, func in enumerate(funcs): + try: + return func(*args, **kwargs) + except TypeError: + if i == len(funcs) - 1: + raise + + +def nargs_error(name, takes, given): + """Generate a TypeError to be raised by function calls with wrong arity.""" + return TypeError(f"{name}() takes {takes} positional arguments but " + f"{given} were given") + + +def kwarg_error(name, kw): + """ + Generate a TypeError to be raised by function calls with wrong kwarg. + + Parameters + ---------- + name : str + The name of the calling function. + kw : str or Iterable[str] + Either the invalid keyword argument name, or an iterable yielding + invalid keyword arguments (e.g., a ``kwargs`` dict). + """ + if not isinstance(kw, str): + kw = next(iter(kw)) + return TypeError(f"{name}() got an unexpected keyword argument '{kw}'") + + +def recursive_subclasses(cls): + """Yield *cls* and direct and indirect subclasses of *cls*.""" + yield cls + for subcls in cls.__subclasses__(): + yield from recursive_subclasses(subcls) + + def warn_external(message, category=None): """ `warnings.warn` wrapper that sets *stacklevel* to "outside Matplotlib". diff --git a/lib/matplotlib/_api/deprecation.py b/lib/matplotlib/_api/deprecation.py index 7c3a17965928..7c304173b2e5 100644 --- a/lib/matplotlib/_api/deprecation.py +++ b/lib/matplotlib/_api/deprecation.py @@ -3,7 +3,7 @@ This documentation is only relevant for Matplotlib developers, not for users. -.. warning: +.. warning:: This module is for internal use only. Do not use it in your own code. We may change the API at any time with no warning. @@ -13,25 +13,12 @@ import contextlib import functools import inspect +import math import warnings -class MatplotlibDeprecationWarning(UserWarning): - """ - A class for issuing deprecation warnings for Matplotlib users. - - In light of the fact that Python builtin DeprecationWarnings are ignored - by default as of Python 2.7 (see link below), this class was put in to - allow for the signaling of deprecation, but via UserWarnings which are not - ignored by default. - - https://docs.python.org/dev/whatsnew/2.7.html#the-future-for-python-2-x - """ - - -# mplDeprecation is deprecated. Use MatplotlibDeprecationWarning instead. -# remove when removing the re-import from cbook -mplDeprecation = MatplotlibDeprecationWarning +class MatplotlibDeprecationWarning(DeprecationWarning): + """A class for issuing deprecation warnings for Matplotlib users.""" def _generate_deprecation_warning( @@ -45,13 +32,11 @@ def _generate_deprecation_warning( removal = f"in {removal}" if removal else "two minor releases later" if not message: message = ( - "\nThe %(name)s %(obj_type)s" + ("The %(name)s %(obj_type)s" if obj_type else "%(name)s") + (" will be deprecated in a future version" if pending else (" was deprecated in Matplotlib %(since)s" - + (" and will be removed %(removal)s" - if removal else - ""))) + + (" and will be removed %(removal)s" if removal else ""))) + "." + (" Use %(alternative)s instead." if alternative else "") + (" %(addendum)s" if addendum else "")) @@ -72,31 +57,24 @@ def warn_deprecated( ---------- since : str The release at which this API became deprecated. - message : str, optional Override the default deprecation message. The ``%(since)s``, ``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``, and ``%(removal)s`` format specifiers will be replaced by the values of the respective arguments passed to this function. - name : str, optional The name of the deprecated object. - alternative : str, optional An alternative API that the user may use in place of the deprecated API. The deprecation warning will tell the user about this alternative if provided. - pending : bool, optional If True, uses a PendingDeprecationWarning instead of a DeprecationWarning. Cannot be used together with *removal*. - obj_type : str, optional The object type being deprecated. - addendum : str, optional Additional text appended directly to the final message. - removal : str, optional The expected removal version. With the default (an empty string), a removal version is automatically computed from *since*. Set to other @@ -105,7 +83,7 @@ def warn_deprecated( Examples -------- - Basic example:: + :: # To warn of the deprecation of "matplotlib.name_of_module" warn_deprecated('1.4.0', name='matplotlib.name_of_module', @@ -134,46 +112,13 @@ def deprecated(since, *, message='', name='', alternative='', pending=False, ``@deprecated`` would mess up ``__init__`` inheritance when installing its own (deprecation-emitting) ``C.__init__``). - Parameters - ---------- - since : str - The release at which this API became deprecated. - - message : str, optional - Override the default deprecation message. The ``%(since)s``, - ``%(name)s``, ``%(alternative)s``, ``%(obj_type)s``, ``%(addendum)s``, - and ``%(removal)s`` format specifiers will be replaced by the values - of the respective arguments passed to this function. - - name : str, optional - The name used in the deprecation message; if not provided, the name - is automatically determined from the deprecated object. - - alternative : str, optional - An alternative API that the user may use in place of the deprecated - API. The deprecation warning will tell the user about this alternative - if provided. - - pending : bool, optional - If True, uses a PendingDeprecationWarning instead of a - DeprecationWarning. Cannot be used together with *removal*. - - obj_type : str, optional - The object type being deprecated; by default, 'class' if decorating - a class, 'attribute' if decorating a property, 'function' otherwise. - - addendum : str, optional - Additional text appended directly to the final message. - - removal : str, optional - The expected removal version. With the default (an empty string), a - removal version is automatically computed from *since*. Set to other - Falsy values to not schedule a removal date. Cannot be used together - with *pending*. + Parameters are the same as for `warn_deprecated`, except that *obj_type* + defaults to 'class' if decorating a class, 'attribute' if decorating a + property, and 'function' otherwise. Examples -------- - Basic example:: + :: @deprecated('1.4.0') def the_function_to_deprecate(): @@ -200,13 +145,14 @@ def finalize(wrapper, new_doc): return obj elif isinstance(obj, (property, classproperty)): - obj_type = "attribute" + if obj_type is None: + obj_type = "attribute" func = None name = name or obj.fget.__name__ old_doc = obj.__doc__ class _deprecated_property(type(obj)): - def __get__(self, instance, owner): + def __get__(self, instance, owner=None): if instance is not None or owner is not None \ and isinstance(self, classproperty): emit_warning() @@ -256,10 +202,13 @@ def wrapper(*args, **kwargs): old_doc = inspect.cleandoc(old_doc or '').strip('\n') notes_header = '\nNotes\n-----' + second_arg = ' '.join([t.strip() for t in + (message, f"Use {alternative} instead." + if alternative else "", addendum) if t]) new_doc = (f"[*Deprecated*] {old_doc}\n" f"{notes_header if notes_header not in old_doc else ''}\n" f".. deprecated:: {since}\n" - f" {message.strip()}") + f" {second_arg}") if not old_doc: # This is to prevent a spurious 'unexpected unindent' warning from @@ -273,7 +222,7 @@ def wrapper(*args, **kwargs): class deprecate_privatize_attribute: """ - Helper to deprecate public access to an attribute. + Helper to deprecate public access to an attribute (or method). This helper should only be used at class scope, as follows:: @@ -281,9 +230,10 @@ class Foo: attr = _deprecate_privatize_attribute(*args, **kwargs) where *all* parameters are forwarded to `deprecated`. This form makes - ``attr`` a property which forwards access to ``self._attr`` (same name but - with a leading underscore), with a deprecation warning. Note that the - attribute name is derived from *the name this helper is assigned to*. + ``attr`` a property which forwards read and write access to ``self._attr`` + (same name but with a leading underscore), with a deprecation warning. + Note that the attribute name is derived from *the name this helper is + assigned to*. This helper also works for deprecating methods. """ def __init__(self, *args, **kwargs): @@ -291,7 +241,17 @@ def __init__(self, *args, **kwargs): def __set_name__(self, owner, name): setattr(owner, name, self.deprecator( - property(lambda self: getattr(self, f"_{name}")), name=name)) + property(lambda self: getattr(self, f"_{name}"), + lambda self, value: setattr(self, f"_{name}", value)), + name=name)) + + +# Used by _copy_docstring_and_deprecators to redecorate pyplot wrappers and +# boilerplate.py to retrieve original signatures. It may seem natural to store +# this information as an attribute on the wrapper, but if the wrapper gets +# itself functools.wraps()ed, then such attributes are silently propagated to +# the outer wrapper, which is not desired. +DECORATORS = {} def rename_parameter(since, old, new, func=None): @@ -313,8 +273,10 @@ def rename_parameter(since, old, new, func=None): def func(good_name): ... """ + decorator = functools.partial(rename_parameter, since, old, new) + if func is None: - return functools.partial(rename_parameter, since, old, new) + return decorator signature = inspect.signature(func) assert old not in signature.parameters, ( @@ -339,6 +301,7 @@ def wrapper(*args, **kwargs): # would both show up in the pyplot function for an Axes method as well and # pyplot would explicitly pass both arguments to the Axes method. + DECORATORS[wrapper] = decorator return wrapper @@ -375,8 +338,10 @@ def delete_parameter(since, name, func=None, **kwargs): def func(used_arg, other_arg, unused, more_args): ... """ + decorator = functools.partial(delete_parameter, since, name, **kwargs) + if func is None: - return functools.partial(delete_parameter, since, name, **kwargs) + return decorator signature = inspect.signature(func) # Name of `**kwargs` parameter of the decorated function, typically @@ -389,12 +354,22 @@ def func(used_arg, other_arg, unused, more_args): ... is_varargs = kind is inspect.Parameter.VAR_POSITIONAL is_varkwargs = kind is inspect.Parameter.VAR_KEYWORD if not is_varargs and not is_varkwargs: + name_idx = ( + # Deprecated parameter can't be passed positionally. + math.inf if kind is inspect.Parameter.KEYWORD_ONLY + # If call site has no more than this number of parameters, the + # deprecated parameter can't have been passed positionally. + else [*signature.parameters].index(name)) func.__signature__ = signature = signature.replace(parameters=[ param.replace(default=_deprecated_parameter) if param.name == name else param for param in signature.parameters.values()]) + else: + name_idx = -1 # Deprecated parameter can always have been passed. else: is_varargs = is_varkwargs = False + # Deprecated parameter can't be passed positionally. + name_idx = math.inf assert kwargs_name, ( f"Matplotlib internal error: {name!r} must be a parameter for " f"{func.__name__}()") @@ -403,6 +378,10 @@ def func(used_arg, other_arg, unused, more_args): ... @functools.wraps(func) def wrapper(*inner_args, **inner_kwargs): + if len(inner_args) <= name_idx and name not in inner_kwargs: + # Early return in the simple, non-deprecated case (much faster than + # calling bind()). + return func(*inner_args, **inner_kwargs) arguments = signature.bind(*inner_args, **inner_kwargs).arguments if is_varargs and arguments.get(name): warn_deprecated( @@ -430,6 +409,7 @@ def wrapper(*inner_args, **inner_kwargs): **kwargs) return func(*inner_args, **inner_kwargs) + DECORATORS[wrapper] = decorator return wrapper @@ -437,10 +417,16 @@ def make_keyword_only(since, name, func=None): """ Decorator indicating that passing parameter *name* (or any of the following ones) positionally to *func* is being deprecated. + + When used on a method that has a pyplot wrapper, this should be the + outermost decorator, so that :file:`boilerplate.py` can access the original + signature. """ + decorator = functools.partial(make_keyword_only, since, name) + if func is None: - return functools.partial(make_keyword_only, since, name) + return decorator signature = inspect.signature(func) POK = inspect.Parameter.POSITIONAL_OR_KEYWORD @@ -450,19 +436,16 @@ def make_keyword_only(since, name, func=None): f"Matplotlib internal error: {name!r} must be a positional-or-keyword " f"parameter for {func.__name__}()") names = [*signature.parameters] - kwonly = [name for name in names[names.index(name):] + name_idx = names.index(name) + kwonly = [name for name in names[name_idx:] if signature.parameters[name].kind == POK] - func.__signature__ = signature.replace(parameters=[ - param.replace(kind=KWO) if param.name in kwonly else param - for param in signature.parameters.values()]) @functools.wraps(func) def wrapper(*args, **kwargs): # Don't use signature.bind here, as it would fail when stacked with # rename_parameter and an "old" argument name is passed in # (signature.bind would fail, but the actual call would succeed). - idx = [*func.__signature__.parameters].index(name) - if len(args) > idx: + if len(args) > name_idx: warn_deprecated( since, message="Passing the %(name)s %(obj_type)s " "positionally is deprecated since Matplotlib %(since)s; the " @@ -470,6 +453,11 @@ def wrapper(*args, **kwargs): name=name, obj_type=f"parameter of {func.__name__}()") return func(*args, **kwargs) + # Don't modify *func*'s signature, as boilerplate.py needs it. + wrapper.__signature__ = signature.replace(parameters=[ + param.replace(kind=KWO) if param.name in kwonly else param + for param in signature.parameters.values()]) + DECORATORS[wrapper] = decorator return wrapper diff --git a/lib/matplotlib/_blocking_input.py b/lib/matplotlib/_blocking_input.py new file mode 100644 index 000000000000..45f077571443 --- /dev/null +++ b/lib/matplotlib/_blocking_input.py @@ -0,0 +1,30 @@ +def blocking_input_loop(figure, event_names, timeout, handler): + """ + Run *figure*'s event loop while listening to interactive events. + + The events listed in *event_names* are passed to *handler*. + + This function is used to implement `.Figure.waitforbuttonpress`, + `.Figure.ginput`, and `.Axes.clabel`. + + Parameters + ---------- + figure : `~matplotlib.figure.Figure` + event_names : list of str + The names of the events passed to *handler*. + timeout : float + If positive, the event loop is stopped after *timeout* seconds. + handler : Callable[[Event], Any] + Function called for each event; it can force an early exit of the event + loop by calling ``canvas.stop_event_loop()``. + """ + if figure.canvas.manager: + figure.show() # Ensure that the figure is shown if we are managing it. + # Connect the events to the on_event function call. + cids = [figure.canvas.mpl_connect(name, handler) for name in event_names] + try: + figure.canvas.start_event_loop(timeout) # Start event loop. + finally: # Run even on exception like ctrl-c. + # Disconnect the callbacks. + for cid in cids: + figure.canvas.mpl_disconnect(cid) diff --git a/lib/matplotlib/_cm.py b/lib/matplotlib/_cm.py index 84b2fd3799f2..586417d53954 100644 --- a/lib/matplotlib/_cm.py +++ b/lib/matplotlib/_cm.py @@ -459,44 +459,50 @@ def _g36(x): return 2 * x - 1 'blue': ((0., 1., 1.), (1.0, 0.5, 0.5))} _nipy_spectral_data = { - 'red': [(0.0, 0.0, 0.0), (0.05, 0.4667, 0.4667), - (0.10, 0.5333, 0.5333), (0.15, 0.0, 0.0), - (0.20, 0.0, 0.0), (0.25, 0.0, 0.0), - (0.30, 0.0, 0.0), (0.35, 0.0, 0.0), - (0.40, 0.0, 0.0), (0.45, 0.0, 0.0), - (0.50, 0.0, 0.0), (0.55, 0.0, 0.0), - (0.60, 0.0, 0.0), (0.65, 0.7333, 0.7333), - (0.70, 0.9333, 0.9333), (0.75, 1.0, 1.0), - (0.80, 1.0, 1.0), (0.85, 1.0, 1.0), - (0.90, 0.8667, 0.8667), (0.95, 0.80, 0.80), - (1.0, 0.80, 0.80)], - 'green': [(0.0, 0.0, 0.0), (0.05, 0.0, 0.0), - (0.10, 0.0, 0.0), (0.15, 0.0, 0.0), - (0.20, 0.0, 0.0), (0.25, 0.4667, 0.4667), - (0.30, 0.6000, 0.6000), (0.35, 0.6667, 0.6667), - (0.40, 0.6667, 0.6667), (0.45, 0.6000, 0.6000), - (0.50, 0.7333, 0.7333), (0.55, 0.8667, 0.8667), - (0.60, 1.0, 1.0), (0.65, 1.0, 1.0), - (0.70, 0.9333, 0.9333), (0.75, 0.8000, 0.8000), - (0.80, 0.6000, 0.6000), (0.85, 0.0, 0.0), - (0.90, 0.0, 0.0), (0.95, 0.0, 0.0), - (1.0, 0.80, 0.80)], - 'blue': [(0.0, 0.0, 0.0), (0.05, 0.5333, 0.5333), - (0.10, 0.6000, 0.6000), (0.15, 0.6667, 0.6667), - (0.20, 0.8667, 0.8667), (0.25, 0.8667, 0.8667), - (0.30, 0.8667, 0.8667), (0.35, 0.6667, 0.6667), - (0.40, 0.5333, 0.5333), (0.45, 0.0, 0.0), - (0.5, 0.0, 0.0), (0.55, 0.0, 0.0), - (0.60, 0.0, 0.0), (0.65, 0.0, 0.0), - (0.70, 0.0, 0.0), (0.75, 0.0, 0.0), - (0.80, 0.0, 0.0), (0.85, 0.0, 0.0), - (0.90, 0.0, 0.0), (0.95, 0.0, 0.0), - (1.0, 0.80, 0.80)], + 'red': [ + (0.0, 0.0, 0.0), (0.05, 0.4667, 0.4667), + (0.10, 0.5333, 0.5333), (0.15, 0.0, 0.0), + (0.20, 0.0, 0.0), (0.25, 0.0, 0.0), + (0.30, 0.0, 0.0), (0.35, 0.0, 0.0), + (0.40, 0.0, 0.0), (0.45, 0.0, 0.0), + (0.50, 0.0, 0.0), (0.55, 0.0, 0.0), + (0.60, 0.0, 0.0), (0.65, 0.7333, 0.7333), + (0.70, 0.9333, 0.9333), (0.75, 1.0, 1.0), + (0.80, 1.0, 1.0), (0.85, 1.0, 1.0), + (0.90, 0.8667, 0.8667), (0.95, 0.80, 0.80), + (1.0, 0.80, 0.80), + ], + 'green': [ + (0.0, 0.0, 0.0), (0.05, 0.0, 0.0), + (0.10, 0.0, 0.0), (0.15, 0.0, 0.0), + (0.20, 0.0, 0.0), (0.25, 0.4667, 0.4667), + (0.30, 0.6000, 0.6000), (0.35, 0.6667, 0.6667), + (0.40, 0.6667, 0.6667), (0.45, 0.6000, 0.6000), + (0.50, 0.7333, 0.7333), (0.55, 0.8667, 0.8667), + (0.60, 1.0, 1.0), (0.65, 1.0, 1.0), + (0.70, 0.9333, 0.9333), (0.75, 0.8000, 0.8000), + (0.80, 0.6000, 0.6000), (0.85, 0.0, 0.0), + (0.90, 0.0, 0.0), (0.95, 0.0, 0.0), + (1.0, 0.80, 0.80), + ], + 'blue': [ + (0.0, 0.0, 0.0), (0.05, 0.5333, 0.5333), + (0.10, 0.6000, 0.6000), (0.15, 0.6667, 0.6667), + (0.20, 0.8667, 0.8667), (0.25, 0.8667, 0.8667), + (0.30, 0.8667, 0.8667), (0.35, 0.6667, 0.6667), + (0.40, 0.5333, 0.5333), (0.45, 0.0, 0.0), + (0.5, 0.0, 0.0), (0.55, 0.0, 0.0), + (0.60, 0.0, 0.0), (0.65, 0.0, 0.0), + (0.70, 0.0, 0.0), (0.75, 0.0, 0.0), + (0.80, 0.0, 0.0), (0.85, 0.0, 0.0), + (0.90, 0.0, 0.0), (0.95, 0.0, 0.0), + (1.0, 0.80, 0.80), + ], } # 34 colormaps based on color specifications and designs -# developed by Cynthia Brewer (http://colorbrewer.org). +# developed by Cynthia Brewer (https://colorbrewer2.org/). # The ColorBrewer palettes have been included under the terms # of an Apache-stype license (for details, see the file # LICENSE_COLORBREWER in the license directory of the matplotlib @@ -1207,7 +1213,7 @@ def _gist_yarg(x): return 1 - x # Implementation of Carey Rappaport's CMRmap. # See `A Color Map for Effective Black-and-White Rendering of Color-Scale # Images' by Carey Rappaport -# http://www.mathworks.com/matlabcentral/fileexchange/2662-cmrmap-m +# https://www.mathworks.com/matlabcentral/fileexchange/2662-cmrmap-m _CMRmap_data = {'red': ((0.000, 0.00, 0.00), (0.125, 0.15, 0.15), (0.250, 0.30, 0.30), @@ -1239,7 +1245,7 @@ def _gist_yarg(x): return 1 - x # An MIT licensed, colorblind-friendly heatmap from Wistia: # https://github.com/wistia/heatmap-palette -# http://wistia.com/blog/heatmaps-for-colorblindness +# https://wistia.com/learn/culture/heatmaps-for-colorblindness # # >>> import matplotlib.colors as c # >>> colors = ["#e4ff7a", "#ffe81a", "#ffbd00", "#ffa000", "#fc7f00"] diff --git a/lib/matplotlib/_color_data.py b/lib/matplotlib/_color_data.py index e50998b18fd5..44f97adbb76a 100644 --- a/lib/matplotlib/_color_data.py +++ b/lib/matplotlib/_color_data.py @@ -1,6 +1,3 @@ -from collections import OrderedDict - - BASE_COLORS = { 'b': (0, 0, 1), # blue 'g': (0, 0.5, 0), # green @@ -14,32 +11,29 @@ # These colors are from Tableau -TABLEAU_COLORS = ( - ('blue', '#1f77b4'), - ('orange', '#ff7f0e'), - ('green', '#2ca02c'), - ('red', '#d62728'), - ('purple', '#9467bd'), - ('brown', '#8c564b'), - ('pink', '#e377c2'), - ('gray', '#7f7f7f'), - ('olive', '#bcbd22'), - ('cyan', '#17becf'), -) +TABLEAU_COLORS = { + 'tab:blue': '#1f77b4', + 'tab:orange': '#ff7f0e', + 'tab:green': '#2ca02c', + 'tab:red': '#d62728', + 'tab:purple': '#9467bd', + 'tab:brown': '#8c564b', + 'tab:pink': '#e377c2', + 'tab:gray': '#7f7f7f', + 'tab:olive': '#bcbd22', + 'tab:cyan': '#17becf', +} -# Normalize name to "tab:" to avoid name collisions. -TABLEAU_COLORS = OrderedDict( - ('tab:' + name, value) for name, value in TABLEAU_COLORS) # This mapping of color names -> hex values is taken from # a survey run by Randall Munroe see: # https://blog.xkcd.com/2010/05/03/color-survey-results/ # for more details. The results are hosted at -# https://xkcd.com/color/rgb +# https://xkcd.com/color/rgb/ # and also available as a text file at # https://xkcd.com/color/rgb.txt # -# License: http://creativecommons.org/publicdomain/zero/1.0/ +# License: https://creativecommons.org/publicdomain/zero/1.0/ XKCD_COLORS = { 'cloudy blue': '#acc2d9', 'dark pastel green': '#56ae57', diff --git a/lib/matplotlib/_constrained_layout.py b/lib/matplotlib/_constrained_layout.py index 27393ce0bfcd..9554a156f1ec 100644 --- a/lib/matplotlib/_constrained_layout.py +++ b/lib/matplotlib/_constrained_layout.py @@ -11,18 +11,7 @@ layout. Axes manually placed via ``figure.add_axes()`` will not. See Tutorial: :doc:`/tutorials/intermediate/constrainedlayout_guide` -""" - -import logging - -import numpy as np - -from matplotlib import _api -import matplotlib.transforms as mtransforms -_log = logging.getLogger(__name__) - -""" General idea: ------------- @@ -31,8 +20,8 @@ often just set to 1 for an equal grid. Subplotspecs that are derived from this gridspec can contain either a -``SubPanel``, a ``GridSpecFromSubplotSpec``, or an axes. The ``SubPanel`` and -``GridSpecFromSubplotSpec`` are dealt with recursively and each contain an +``SubPanel``, a ``GridSpecFromSubplotSpec``, or an ``Axes``. The ``SubPanel`` +and ``GridSpecFromSubplotSpec`` are dealt with recursively and each contain an analogous layout. Each ``GridSpec`` has a ``_layoutgrid`` attached to it. The ``_layoutgrid`` @@ -58,10 +47,22 @@ for more discussion of the algorithm with examples. """ +import logging + +import numpy as np + +from matplotlib import _api, artist as martist +import matplotlib.transforms as mtransforms +import matplotlib._layoutgrid as mlayoutgrid + + +_log = logging.getLogger(__name__) + ###################################################### -def do_constrained_layout(fig, renderer, h_pad, w_pad, - hspace=None, wspace=None): +def do_constrained_layout(fig, h_pad, w_pad, + hspace=None, wspace=None, rect=(0, 0, 1, 1), + compress=False): """ Do the constrained_layout. Called at draw time in ``figure.constrained_layout()`` @@ -83,20 +84,29 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, A value of 0.2 for a three-column layout would have a space of 0.1 of the figure width between each column. If h/wspace < h/w_pad, then the pads are used instead. + + rect : tuple of 4 floats + Rectangle in figure coordinates to perform constrained layout in + [left, bottom, width, height], each from 0-1. + + compress : bool + Whether to shift Axes so that white space in between them is + removed. This is useful for simple grids of fixed-aspect Axes (e.g. + a grid of images). + + Returns + ------- + layoutgrid : private debugging structure """ - # list of unique gridspecs that contain child axes: - gss = set() - for ax in fig.axes: - if hasattr(ax, 'get_subplotspec'): - gs = ax.get_subplotspec().get_gridspec() - if gs._layoutgrid is not None: - gss.add(gs) - gss = list(gss) - if len(gss) == 0: + renderer = fig._get_renderer() + # make layoutgrid tree... + layoutgrids = make_layoutgrids(fig, None, rect=rect) + if not layoutgrids['hasgrids']: _api.warn_external('There are no gridspecs with layoutgrids. ' 'Possibly did not call parent GridSpec with the' ' "figure" keyword') + return for _ in range(2): # do the algorithm twice. This has to be done because decorations @@ -106,56 +116,192 @@ def do_constrained_layout(fig, renderer, h_pad, w_pad, # make margins for all the axes and subfigures in the # figure. Add margins for colorbars... - _make_layout_margins(fig, renderer, h_pad=h_pad, w_pad=w_pad, - hspace=hspace, wspace=wspace) - _make_margin_suptitles(fig, renderer, h_pad=h_pad, w_pad=w_pad) + make_layout_margins(layoutgrids, fig, renderer, h_pad=h_pad, + w_pad=w_pad, hspace=hspace, wspace=wspace) + make_margin_suptitles(layoutgrids, fig, renderer, h_pad=h_pad, + w_pad=w_pad) # if a layout is such that a columns (or rows) margin has no # constraints, we need to make all such instances in the grid # match in margin size. - _match_submerged_margins(fig) + match_submerged_margins(layoutgrids, fig) # update all the variables in the layout. - fig._layoutgrid.update_variables() - - if _check_no_collapsed_axes(fig): - _reposition_axes(fig, renderer, h_pad=h_pad, w_pad=w_pad, - hspace=hspace, wspace=wspace) + layoutgrids[fig].update_variables() + + warn_collapsed = ('constrained_layout not applied because ' + 'axes sizes collapsed to zero. Try making ' + 'figure larger or axes decorations smaller.') + if check_no_collapsed_axes(layoutgrids, fig): + reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad, + w_pad=w_pad, hspace=hspace, wspace=wspace) + if compress: + layoutgrids = compress_fixed_aspect(layoutgrids, fig) + layoutgrids[fig].update_variables() + if check_no_collapsed_axes(layoutgrids, fig): + reposition_axes(layoutgrids, fig, renderer, h_pad=h_pad, + w_pad=w_pad, hspace=hspace, wspace=wspace) + else: + _api.warn_external(warn_collapsed) else: - _api.warn_external('constrained_layout not applied because ' - 'axes sizes collapsed to zero. Try making ' - 'figure larger or axes decorations smaller.') - _reset_margins(fig) + _api.warn_external(warn_collapsed) + reset_margins(layoutgrids, fig) + return layoutgrids + + +def make_layoutgrids(fig, layoutgrids, rect=(0, 0, 1, 1)): + """ + Make the layoutgrid tree. + + (Sub)Figures get a layoutgrid so we can have figure margins. + + Gridspecs that are attached to axes get a layoutgrid so axes + can have margins. + """ + + if layoutgrids is None: + layoutgrids = dict() + layoutgrids['hasgrids'] = False + if not hasattr(fig, '_parent'): + # top figure; pass rect as parent to allow user-specified + # margins + layoutgrids[fig] = mlayoutgrid.LayoutGrid(parent=rect, name='figlb') + else: + # subfigure + gs = fig._subplotspec.get_gridspec() + # it is possible the gridspec containing this subfigure hasn't + # been added to the tree yet: + layoutgrids = make_layoutgrids_gs(layoutgrids, gs) + # add the layoutgrid for the subfigure: + parentlb = layoutgrids[gs] + layoutgrids[fig] = mlayoutgrid.LayoutGrid( + parent=parentlb, + name='panellb', + parent_inner=True, + nrows=1, ncols=1, + parent_pos=(fig._subplotspec.rowspan, + fig._subplotspec.colspan)) + # recursively do all subfigures in this figure... + for sfig in fig.subfigs: + layoutgrids = make_layoutgrids(sfig, layoutgrids) + # for each axes at the local level add its gridspec: + for ax in fig._localaxes: + gs = ax.get_gridspec() + if gs is not None: + layoutgrids = make_layoutgrids_gs(layoutgrids, gs) -def _check_no_collapsed_axes(fig): + return layoutgrids + + +def make_layoutgrids_gs(layoutgrids, gs): + """ + Make the layoutgrid for a gridspec (and anything nested in the gridspec) + """ + + if gs in layoutgrids or gs.figure is None: + return layoutgrids + # in order to do constrained_layout there has to be at least *one* + # gridspec in the tree: + layoutgrids['hasgrids'] = True + if not hasattr(gs, '_subplot_spec'): + # normal gridspec + parent = layoutgrids[gs.figure] + layoutgrids[gs] = mlayoutgrid.LayoutGrid( + parent=parent, + parent_inner=True, + name='gridspec', + ncols=gs._ncols, nrows=gs._nrows, + width_ratios=gs.get_width_ratios(), + height_ratios=gs.get_height_ratios()) + else: + # this is a gridspecfromsubplotspec: + subplot_spec = gs._subplot_spec + parentgs = subplot_spec.get_gridspec() + # if a nested gridspec it is possible the parent is not in there yet: + if parentgs not in layoutgrids: + layoutgrids = make_layoutgrids_gs(layoutgrids, parentgs) + subspeclb = layoutgrids[parentgs] + # gridspecfromsubplotspec need an outer container: + # get a unique representation: + rep = (gs, 'top') + if rep not in layoutgrids: + layoutgrids[rep] = mlayoutgrid.LayoutGrid( + parent=subspeclb, + name='top', + nrows=1, ncols=1, + parent_pos=(subplot_spec.rowspan, subplot_spec.colspan)) + layoutgrids[gs] = mlayoutgrid.LayoutGrid( + parent=layoutgrids[rep], + name='gridspec', + nrows=gs._nrows, ncols=gs._ncols, + width_ratios=gs.get_width_ratios(), + height_ratios=gs.get_height_ratios()) + return layoutgrids + + +def check_no_collapsed_axes(layoutgrids, fig): """ Check that no axes have collapsed to zero size. """ - for panel in fig.subfigs: - ok = _check_no_collapsed_axes(panel) + for sfig in fig.subfigs: + ok = check_no_collapsed_axes(layoutgrids, sfig) if not ok: return False - for ax in fig.axes: - if hasattr(ax, 'get_subplotspec'): - gs = ax.get_subplotspec().get_gridspec() - lg = gs._layoutgrid - if lg is not None: - for i in range(gs.nrows): - for j in range(gs.ncols): - bb = lg.get_inner_bbox(i, j) - if bb.width <= 0 or bb.height <= 0: - return False + gs = ax.get_gridspec() + if gs in layoutgrids: # also implies gs is not None. + lg = layoutgrids[gs] + for i in range(gs.nrows): + for j in range(gs.ncols): + bb = lg.get_inner_bbox(i, j) + if bb.width <= 0 or bb.height <= 0: + return False return True -def _get_margin_from_padding(object, *, w_pad=0, h_pad=0, - hspace=0, wspace=0): - - ss = object._subplotspec +def compress_fixed_aspect(layoutgrids, fig): + gs = None + for ax in fig.axes: + if ax.get_subplotspec() is None: + continue + ax.apply_aspect() + sub = ax.get_subplotspec() + _gs = sub.get_gridspec() + if gs is None: + gs = _gs + extraw = np.zeros(gs.ncols) + extrah = np.zeros(gs.nrows) + elif _gs != gs: + raise ValueError('Cannot do compressed layout if axes are not' + 'all from the same gridspec') + orig = ax.get_position(original=True) + actual = ax.get_position(original=False) + dw = orig.width - actual.width + if dw > 0: + extraw[sub.colspan] = np.maximum(extraw[sub.colspan], dw) + dh = orig.height - actual.height + if dh > 0: + extrah[sub.rowspan] = np.maximum(extrah[sub.rowspan], dh) + + if gs is None: + raise ValueError('Cannot do compressed layout if no axes ' + 'are part of a gridspec.') + w = np.sum(extraw) / 2 + layoutgrids[fig].edit_margin_min('left', w) + layoutgrids[fig].edit_margin_min('right', w) + + h = np.sum(extrah) / 2 + layoutgrids[fig].edit_margin_min('top', h) + layoutgrids[fig].edit_margin_min('bottom', h) + return layoutgrids + + +def get_margin_from_padding(obj, *, w_pad=0, h_pad=0, + hspace=0, wspace=0): + + ss = obj._subplotspec gs = ss.get_gridspec() - lg = gs._layoutgrid if hasattr(gs, 'hspace'): _hspace = (gs.hspace if gs.hspace is not None else hspace) @@ -189,8 +335,8 @@ def _get_margin_from_padding(object, *, w_pad=0, h_pad=0, return margin -def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0, - hspace=0, wspace=0): +def make_layout_margins(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0, + hspace=0, wspace=0): """ For each axes, make a margin between the *pos* layoutbox and the *axes* layoutbox be a minimum size that can accommodate the @@ -198,30 +344,29 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0, Then make room for colorbars. """ - for panel in fig.subfigs: # recursively make child panel margins - ss = panel._subplotspec - _make_layout_margins(panel, renderer, w_pad=w_pad, h_pad=h_pad, - hspace=hspace, wspace=wspace) - - margins = _get_margin_from_padding(panel, w_pad=0, h_pad=0, - hspace=hspace, wspace=wspace) - panel._layoutgrid.parent.edit_outer_margin_mins(margins, ss) - - for ax in fig._localaxes.as_list(): - if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout(): + for sfig in fig.subfigs: # recursively make child panel margins + ss = sfig._subplotspec + make_layout_margins(layoutgrids, sfig, renderer, + w_pad=w_pad, h_pad=h_pad, + hspace=hspace, wspace=wspace) + + margins = get_margin_from_padding(sfig, w_pad=0, h_pad=0, + hspace=hspace, wspace=wspace) + layoutgrids[sfig].parent.edit_outer_margin_mins(margins, ss) + + for ax in fig._localaxes: + if not ax.get_subplotspec() or not ax.get_in_layout(): continue ss = ax.get_subplotspec() gs = ss.get_gridspec() - nrows, ncols = gs.get_geometry() - if gs._layoutgrid is None: + if gs not in layoutgrids: return - margin = _get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad, - hspace=hspace, wspace=wspace) - margin0 = margin.copy() - pos, bbox = _get_pos_and_bbox(ax, renderer) + margin = get_margin_from_padding(ax, w_pad=w_pad, h_pad=h_pad, + hspace=hspace, wspace=wspace) + pos, bbox = get_pos_and_bbox(ax, renderer) # the margin is the distance between the bounding box of the axes # and its position (plus the padding from above) margin['left'] += pos.x0 - bbox.x0 @@ -234,11 +379,11 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0, # padding margin, versus the margin for axes decorators. for cbax in ax._colorbars: # note pad is a fraction of the parent width... - pad = _colorbar_get_pad(cbax) + pad = colorbar_get_pad(layoutgrids, cbax) # colorbars can be child of more than one subplot spec: - cbp_rspan, cbp_cspan = _get_cb_parent_spans(cbax) + cbp_rspan, cbp_cspan = get_cb_parent_spans(cbax) loc = cbax._colorbar_info['location'] - cbpos, cbbbox = _get_pos_and_bbox(cbax, renderer) + cbpos, cbbbox = get_pos_and_bbox(cbax, renderer) if loc == 'right': if cbp_cspan.stop == ss.colspan.stop: # only increase if the colorbar is on the right edge @@ -271,10 +416,29 @@ def _make_layout_margins(fig, renderer, *, w_pad=0, h_pad=0, cbbbox.y1 > bbox.y1): margin['top'] += cbbbox.y1 - bbox.y1 # pass the new margins down to the layout grid for the solution... - gs._layoutgrid.edit_outer_margin_mins(margin, ss) - - -def _make_margin_suptitles(fig, renderer, *, w_pad=0, h_pad=0): + layoutgrids[gs].edit_outer_margin_mins(margin, ss) + + # make margins for figure-level legends: + for leg in fig.legends: + inv_trans_fig = None + if leg._outside_loc and leg._bbox_to_anchor is None: + if inv_trans_fig is None: + inv_trans_fig = fig.transFigure.inverted().transform_bbox + bbox = inv_trans_fig(leg.get_tightbbox(renderer)) + w = bbox.width + 2 * w_pad + h = bbox.height + 2 * h_pad + legendloc = leg._outside_loc + if legendloc == 'lower': + layoutgrids[fig].edit_margin_min('bottom', h) + elif legendloc == 'upper': + layoutgrids[fig].edit_margin_min('top', h) + if legendloc == 'right': + layoutgrids[fig].edit_margin_min('right', w) + elif legendloc == 'left': + layoutgrids[fig].edit_margin_min('left', w) + + +def make_margin_suptitles(layoutgrids, fig, renderer, *, w_pad=0, h_pad=0): # Figure out how large the suptitle is and make the # top level figure margin larger. @@ -286,32 +450,34 @@ def _make_margin_suptitles(fig, renderer, *, w_pad=0, h_pad=0): h_pad_local = padbox.height w_pad_local = padbox.width - for panel in fig.subfigs: - _make_margin_suptitles(panel, renderer, w_pad=w_pad, h_pad=h_pad) + for sfig in fig.subfigs: + make_margin_suptitles(layoutgrids, sfig, renderer, + w_pad=w_pad, h_pad=h_pad) if fig._suptitle is not None and fig._suptitle.get_in_layout(): p = fig._suptitle.get_position() if getattr(fig._suptitle, '_autopos', False): fig._suptitle.set_position((p[0], 1 - h_pad_local)) bbox = inv_trans_fig(fig._suptitle.get_tightbbox(renderer)) - fig._layoutgrid.edit_margin_min('top', bbox.height + 2 * h_pad) + layoutgrids[fig].edit_margin_min('top', bbox.height + 2 * h_pad) if fig._supxlabel is not None and fig._supxlabel.get_in_layout(): p = fig._supxlabel.get_position() if getattr(fig._supxlabel, '_autopos', False): fig._supxlabel.set_position((p[0], h_pad_local)) bbox = inv_trans_fig(fig._supxlabel.get_tightbbox(renderer)) - fig._layoutgrid.edit_margin_min('bottom', bbox.height + 2 * h_pad) + layoutgrids[fig].edit_margin_min('bottom', + bbox.height + 2 * h_pad) if fig._supylabel is not None and fig._supylabel.get_in_layout(): p = fig._supylabel.get_position() if getattr(fig._supylabel, '_autopos', False): fig._supylabel.set_position((w_pad_local, p[1])) bbox = inv_trans_fig(fig._supylabel.get_tightbbox(renderer)) - fig._layoutgrid.edit_margin_min('left', bbox.width + 2 * w_pad) + layoutgrids[fig].edit_margin_min('left', bbox.width + 2 * w_pad) -def _match_submerged_margins(fig): +def match_submerged_margins(layoutgrids, fig): """ Make the margins that are submerged inside an Axes the same size. @@ -336,18 +502,18 @@ def _match_submerged_margins(fig): See test_constrained_layout::test_constrained_layout12 for an example. """ - for panel in fig.subfigs: - _match_submerged_margins(panel) + for sfig in fig.subfigs: + match_submerged_margins(layoutgrids, sfig) - axs = [a for a in fig.get_axes() if (hasattr(a, 'get_subplotspec') - and a.get_in_layout())] + axs = [a for a in fig.get_axes() + if a.get_subplotspec() is not None and a.get_in_layout()] for ax1 in axs: ss1 = ax1.get_subplotspec() - lg1 = ss1.get_gridspec()._layoutgrid - if lg1 is None: + if ss1.get_gridspec() not in layoutgrids: axs.remove(ax1) continue + lg1 = layoutgrids[ss1.get_gridspec()] # interior columns: if len(ss1.colspan) > 1: @@ -361,7 +527,7 @@ def _match_submerged_margins(fig): ) for ax2 in axs: ss2 = ax2.get_subplotspec() - lg2 = ss2.get_gridspec()._layoutgrid + lg2 = layoutgrids[ss2.get_gridspec()] if lg2 is not None and len(ss2.colspan) > 1: maxsubl2 = np.max( lg2.margin_vals['left'][ss2.colspan[1:]] + @@ -391,7 +557,7 @@ def _match_submerged_margins(fig): for ax2 in axs: ss2 = ax2.get_subplotspec() - lg2 = ss2.get_gridspec()._layoutgrid + lg2 = layoutgrids[ss2.get_gridspec()] if lg2 is not None: if len(ss2.rowspan) > 1: maxsubt = np.max([np.max( @@ -408,7 +574,7 @@ def _match_submerged_margins(fig): lg1.edit_margin_min('bottom', maxsubb, cell=i) -def _get_cb_parent_spans(cbax): +def get_cb_parent_spans(cbax): """ Figure out which subplotspecs this colorbar belongs to: """ @@ -428,7 +594,7 @@ def _get_cb_parent_spans(cbax): return rowspan, colspan -def _get_pos_and_bbox(ax, renderer): +def get_pos_and_bbox(ax, renderer): """ Get the position and the bbox for the axes. @@ -443,17 +609,12 @@ def _get_pos_and_bbox(ax, renderer): Position in figure coordinates. bbox : Bbox Tight bounding box in figure coordinates. - """ fig = ax.figure pos = ax.get_position(original=True) # pos is in panel co-ords, but we need in figure for the layout pos = pos.transformed(fig.transSubfigure - fig.transFigure) - try: - tightbbox = ax.get_tightbbox(renderer=renderer, for_layout_only=True) - except TypeError: - tightbbox = ax.get_tightbbox(renderer=renderer) - + tightbbox = martist._get_tightbbox_for_layout_only(ax, renderer) if tightbbox is None: bbox = pos else: @@ -461,35 +622,33 @@ def _get_pos_and_bbox(ax, renderer): return pos, bbox -def _reposition_axes(fig, renderer, *, w_pad=0, h_pad=0, hspace=0, wspace=0): +def reposition_axes(layoutgrids, fig, renderer, *, + w_pad=0, h_pad=0, hspace=0, wspace=0): """ Reposition all the axes based on the new inner bounding box. """ trans_fig_to_subfig = fig.transFigure - fig.transSubfigure for sfig in fig.subfigs: - bbox = sfig._layoutgrid.get_outer_bbox() + bbox = layoutgrids[sfig].get_outer_bbox() sfig._redo_transform_rel_fig( bbox=bbox.transformed(trans_fig_to_subfig)) - _reposition_axes(sfig, renderer, - w_pad=w_pad, h_pad=h_pad, - wspace=wspace, hspace=hspace) + reposition_axes(layoutgrids, sfig, renderer, + w_pad=w_pad, h_pad=h_pad, + wspace=wspace, hspace=hspace) - for ax in fig._localaxes.as_list(): - if not hasattr(ax, 'get_subplotspec') or not ax.get_in_layout(): + for ax in fig._localaxes: + if ax.get_subplotspec() is None or not ax.get_in_layout(): continue # grid bbox is in Figure coordinates, but we specify in panel # coordinates... ss = ax.get_subplotspec() gs = ss.get_gridspec() - nrows, ncols = gs.get_geometry() - if gs._layoutgrid is None: + if gs not in layoutgrids: return - bbox = gs._layoutgrid.get_inner_bbox(rows=ss.rowspan, cols=ss.colspan) - - bboxouter = gs._layoutgrid.get_outer_bbox(rows=ss.rowspan, - cols=ss.colspan) + bbox = layoutgrids[gs].get_inner_bbox(rows=ss.rowspan, + cols=ss.colspan) # transform from figure to panel for set_position: newbbox = trans_fig_to_subfig.transform_bbox(bbox) @@ -501,11 +660,11 @@ def _reposition_axes(fig, renderer, *, w_pad=0, h_pad=0, hspace=0, wspace=0): offset = {'left': 0, 'right': 0, 'bottom': 0, 'top': 0} for nn, cbax in enumerate(ax._colorbars[::-1]): if ax == cbax._colorbar_info['parents'][0]: - margin = _reposition_colorbar( - cbax, renderer, offset=offset) + reposition_colorbar(layoutgrids, cbax, renderer, + offset=offset) -def _reposition_colorbar(cbax, renderer, *, offset=None): +def reposition_colorbar(layoutgrids, cbax, renderer, *, offset=None): """ Place the colorbar in its new place. @@ -527,13 +686,13 @@ def _reposition_colorbar(cbax, renderer, *, offset=None): parents = cbax._colorbar_info['parents'] gs = parents[0].get_gridspec() - ncols, nrows = gs.ncols, gs.nrows fig = cbax.figure trans_fig_to_subfig = fig.transFigure - fig.transSubfigure - cb_rspans, cb_cspans = _get_cb_parent_spans(cbax) - bboxparent = gs._layoutgrid.get_bbox_for_cb(rows=cb_rspans, cols=cb_cspans) - pb = gs._layoutgrid.get_inner_bbox(rows=cb_rspans, cols=cb_cspans) + cb_rspans, cb_cspans = get_cb_parent_spans(cbax) + bboxparent = layoutgrids[gs].get_bbox_for_cb(rows=cb_rspans, + cols=cb_cspans) + pb = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans) location = cbax._colorbar_info['location'] anchor = cbax._colorbar_info['anchor'] @@ -541,12 +700,12 @@ def _reposition_colorbar(cbax, renderer, *, offset=None): aspect = cbax._colorbar_info['aspect'] shrink = cbax._colorbar_info['shrink'] - cbpos, cbbbox = _get_pos_and_bbox(cbax, renderer) + cbpos, cbbbox = get_pos_and_bbox(cbax, renderer) # Colorbar gets put at extreme edge of outer bbox of the subplotspec # It needs to be moved in by: 1) a pad 2) its "margin" 3) by # any colorbars already added at this location: - cbpad = _colorbar_get_pad(cbax) + cbpad = colorbar_get_pad(layoutgrids, cbax) if location in ('left', 'right'): # fraction and shrink are fractions of parent pbcb = pb.shrunk(fraction, shrink).anchored(anchor, pb) @@ -582,34 +741,37 @@ def _reposition_colorbar(cbax, renderer, *, offset=None): pbcb = trans_fig_to_subfig.transform_bbox(pbcb) cbax.set_transform(fig.transSubfigure) cbax._set_position(pbcb) - cbax.set_aspect(aspect, anchor=anchor, adjustable='box') + cbax.set_anchor(anchor) + if location in ['bottom', 'top']: + aspect = 1 / aspect + cbax.set_box_aspect(aspect) + cbax.set_aspect('auto') return offset -def _reset_margins(fig): +def reset_margins(layoutgrids, fig): """ Reset the margins in the layoutboxes of fig. Margins are usually set as a minimum, so if the figure gets smaller the minimum needs to be zero in order for it to grow again. """ - for span in fig.subfigs: - _reset_margins(span) + for sfig in fig.subfigs: + reset_margins(layoutgrids, sfig) for ax in fig.axes: - if hasattr(ax, 'get_subplotspec') and ax.get_in_layout(): - ss = ax.get_subplotspec() - gs = ss.get_gridspec() - if gs._layoutgrid is not None: - gs._layoutgrid.reset_margins() - fig._layoutgrid.reset_margins() + if ax.get_in_layout(): + gs = ax.get_gridspec() + if gs in layoutgrids: # also implies gs is not None. + layoutgrids[gs].reset_margins() + layoutgrids[fig].reset_margins() -def _colorbar_get_pad(cax): +def colorbar_get_pad(layoutgrids, cax): parents = cax._colorbar_info['parents'] gs = parents[0].get_gridspec() - cb_rspans, cb_cspans = _get_cb_parent_spans(cax) - bboxouter = gs._layoutgrid.get_inner_bbox(rows=cb_rspans, cols=cb_cspans) + cb_rspans, cb_cspans = get_cb_parent_spans(cax) + bboxouter = layoutgrids[gs].get_inner_bbox(rows=cb_rspans, cols=cb_cspans) if cax._colorbar_info['location'] in ['right', 'left']: size = bboxouter.width diff --git a/lib/matplotlib/_docstring.py b/lib/matplotlib/_docstring.py new file mode 100644 index 000000000000..ecd209ca0853 --- /dev/null +++ b/lib/matplotlib/_docstring.py @@ -0,0 +1,97 @@ +import inspect + +from . import _api + + +class Substitution: + """ + A decorator that performs %-substitution on an object's docstring. + + This decorator should be robust even if ``obj.__doc__`` is None (for + example, if -OO was passed to the interpreter). + + Usage: construct a docstring.Substitution with a sequence or dictionary + suitable for performing substitution; then decorate a suitable function + with the constructed object, e.g.:: + + sub_author_name = Substitution(author='Jason') + + @sub_author_name + def some_function(x): + "%(author)s wrote this function" + + # note that some_function.__doc__ is now "Jason wrote this function" + + One can also use positional arguments:: + + sub_first_last_names = Substitution('Edgar Allen', 'Poe') + + @sub_first_last_names + def some_function(x): + "%s %s wrote the Raven" + """ + def __init__(self, *args, **kwargs): + if args and kwargs: + raise TypeError("Only positional or keyword args are allowed") + self.params = args or kwargs + + def __call__(self, func): + if func.__doc__: + func.__doc__ = inspect.cleandoc(func.__doc__) % self.params + return func + + def update(self, *args, **kwargs): + """ + Update ``self.params`` (which must be a dict) with the supplied args. + """ + self.params.update(*args, **kwargs) + + +class _ArtistKwdocLoader(dict): + def __missing__(self, key): + if not key.endswith(":kwdoc"): + raise KeyError(key) + name = key[:-len(":kwdoc")] + from matplotlib.artist import Artist, kwdoc + try: + cls, = [cls for cls in _api.recursive_subclasses(Artist) + if cls.__name__ == name] + except ValueError as e: + raise KeyError(key) from e + return self.setdefault(key, kwdoc(cls)) + + +class _ArtistPropertiesSubstitution(Substitution): + """ + A `.Substitution` with two additional features: + + - Substitutions of the form ``%(classname:kwdoc)s`` (ending with the + literal ":kwdoc" suffix) trigger lookup of an Artist subclass with the + given *classname*, and are substituted with the `.kwdoc` of that class. + - Decorating a class triggers substitution both on the class docstring and + on the class' ``__init__`` docstring (which is a commonly required + pattern for Artist subclasses). + """ + + def __init__(self): + self.params = _ArtistKwdocLoader() + + def __call__(self, obj): + super().__call__(obj) + if isinstance(obj, type) and obj.__init__ != object.__init__: + self(obj.__init__) + return obj + + +def copy(source): + """Copy a docstring from another source function (if present).""" + def do_copy(target): + if source.__doc__: + target.__doc__ = source.__doc__ + return target + return do_copy + + +# Create a decorator that will house the various docstring snippets reused +# throughout Matplotlib. +dedent_interpd = interpd = _ArtistPropertiesSubstitution() diff --git a/lib/matplotlib/_enums.py b/lib/matplotlib/_enums.py index 35fe82482869..c8c50f7c3028 100644 --- a/lib/matplotlib/_enums.py +++ b/lib/matplotlib/_enums.py @@ -11,7 +11,7 @@ """ from enum import Enum, auto -from matplotlib import cbook, docstring +from matplotlib import _docstring class _AutoStringNameEnum(Enum): @@ -24,23 +24,6 @@ def __hash__(self): return str(self).__hash__() -def _deprecate_case_insensitive_join_cap(s): - s_low = s.lower() - if s != s_low: - if s_low in ['miter', 'round', 'bevel']: - cbook.warn_deprecated( - "3.3", message="Case-insensitive capstyles are deprecated " - "since %(since)s and support for them will be removed " - "%(removal)s; please pass them in lowercase.") - elif s_low in ['butt', 'round', 'projecting']: - cbook.warn_deprecated( - "3.3", message="Case-insensitive joinstyles are deprecated " - "since %(since)s and support for them will be removed " - "%(removal)s; please pass them in lowercase.") - # Else, error out at the check_in_list stage. - return s_low - - class JoinStyle(str, _AutoStringNameEnum): """ Define how the connection between two line segments is drawn. @@ -100,10 +83,6 @@ class JoinStyle(str, _AutoStringNameEnum): round = auto() bevel = auto() - def __init__(self, s): - s = _deprecate_case_insensitive_join_cap(s) - Enum.__init__(self) - @staticmethod def demo(): """Demonstrate how each JoinStyle looks for various join angles.""" @@ -149,6 +128,9 @@ class CapStyle(str, _AutoStringNameEnum): For a visual impression of each *CapStyle*, `view these docs online ` or run `CapStyle.demo`. + By default, `~.backend_bases.GraphicsContextBase` draws a stroked line as + squared off at its endpoints. + **Supported values:** .. rst-class:: value-list @@ -169,13 +151,9 @@ class CapStyle(str, _AutoStringNameEnum): CapStyle.demo() """ - butt = 'butt' - projecting = 'projecting' - round = 'round' - - def __init__(self, s): - s = _deprecate_case_insensitive_join_cap(s) - Enum.__init__(self) + butt = auto() + projecting = auto() + round = auto() @staticmethod def demo(): @@ -193,7 +171,6 @@ def demo(): ax.plot(xx, yy, lw=12, color='tab:blue', solid_capstyle=style) ax.plot(xx, yy, lw=1, color='black') ax.plot(xx, yy, 'o', color='tab:red', markersize=3) - ax.text(2.25, 0.55, '(default)', ha='center') ax.set_ylim(-.5, 1.5) ax.set_axis_off() @@ -204,5 +181,5 @@ def demo(): + ", ".join([f"'{cs.name}'" for cs in CapStyle]) \ + "}" -docstring.interpd.update({'JoinStyle': JoinStyle.input_description, +_docstring.interpd.update({'JoinStyle': JoinStyle.input_description, 'CapStyle': CapStyle.input_description}) diff --git a/lib/matplotlib/_fontconfig_pattern.py b/lib/matplotlib/_fontconfig_pattern.py new file mode 100644 index 000000000000..f0ed155c2d62 --- /dev/null +++ b/lib/matplotlib/_fontconfig_pattern.py @@ -0,0 +1,120 @@ +""" +A module for parsing and generating `fontconfig patterns`_. + +.. _fontconfig patterns: + https://www.freedesktop.org/software/fontconfig/fontconfig-user.html +""" + +# This class logically belongs in `matplotlib.font_manager`, but placing it +# there would have created cyclical dependency problems, because it also needs +# to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files). + +from functools import lru_cache, partial +import re + +from pyparsing import ( + Group, Optional, ParseException, Regex, StringEnd, Suppress, ZeroOrMore) + +from matplotlib import _api + + +_family_punc = r'\\\-:,' +_family_unescape = partial(re.compile(r'\\(?=[%s])' % _family_punc).sub, '') +_family_escape = partial(re.compile(r'(?=[%s])' % _family_punc).sub, r'\\') +_value_punc = r'\\=_:,' +_value_unescape = partial(re.compile(r'\\(?=[%s])' % _value_punc).sub, '') +_value_escape = partial(re.compile(r'(?=[%s])' % _value_punc).sub, r'\\') + + +_CONSTANTS = { + 'thin': ('weight', 'light'), + 'extralight': ('weight', 'light'), + 'ultralight': ('weight', 'light'), + 'light': ('weight', 'light'), + 'book': ('weight', 'book'), + 'regular': ('weight', 'regular'), + 'normal': ('weight', 'normal'), + 'medium': ('weight', 'medium'), + 'demibold': ('weight', 'demibold'), + 'semibold': ('weight', 'semibold'), + 'bold': ('weight', 'bold'), + 'extrabold': ('weight', 'extra bold'), + 'black': ('weight', 'black'), + 'heavy': ('weight', 'heavy'), + 'roman': ('slant', 'normal'), + 'italic': ('slant', 'italic'), + 'oblique': ('slant', 'oblique'), + 'ultracondensed': ('width', 'ultra-condensed'), + 'extracondensed': ('width', 'extra-condensed'), + 'condensed': ('width', 'condensed'), + 'semicondensed': ('width', 'semi-condensed'), + 'expanded': ('width', 'expanded'), + 'extraexpanded': ('width', 'extra-expanded'), + 'ultraexpanded': ('width', 'ultra-expanded'), +} + + +@lru_cache # The parser instance is a singleton. +def _make_fontconfig_parser(): + def comma_separated(elem): + return elem + ZeroOrMore(Suppress(",") + elem) + + family = Regex(r"([^%s]|(\\[%s]))*" % (_family_punc, _family_punc)) + size = Regex(r"([0-9]+\.?[0-9]*|\.[0-9]+)") + name = Regex(r"[a-z]+") + value = Regex(r"([^%s]|(\\[%s]))*" % (_value_punc, _value_punc)) + # replace trailing `| name` by oneOf(_CONSTANTS) in mpl 3.9. + prop = Group((name + Suppress("=") + comma_separated(value)) | name) + return ( + Optional(comma_separated(family)("families")) + + Optional("-" + comma_separated(size)("sizes")) + + ZeroOrMore(":" + prop("properties*")) + + StringEnd() + ) + + +# `parse_fontconfig_pattern` is a bottleneck during the tests because it is +# repeatedly called when the rcParams are reset (to validate the default +# fonts). In practice, the cache size doesn't grow beyond a few dozen entries +# during the test suite. +@lru_cache +def parse_fontconfig_pattern(pattern): + """ + Parse a fontconfig *pattern* into a dict that can initialize a + `.font_manager.FontProperties` object. + """ + parser = _make_fontconfig_parser() + try: + parse = parser.parseString(pattern) + except ParseException as err: + # explain becomes a plain method on pyparsing 3 (err.explain(0)). + raise ValueError("\n" + ParseException.explain(err, 0)) from None + parser.resetCache() + props = {} + if "families" in parse: + props["family"] = [*map(_family_unescape, parse["families"])] + if "sizes" in parse: + props["size"] = [*parse["sizes"]] + for prop in parse.get("properties", []): + if len(prop) == 1: + if prop[0] not in _CONSTANTS: + _api.warn_deprecated( + "3.7", message=f"Support for unknown constants " + f"({prop[0]!r}) is deprecated since %(since)s and " + f"will be removed %(removal)s.") + continue + prop = _CONSTANTS[prop[0]] + k, *v = prop + props.setdefault(k, []).extend(map(_value_unescape, v)) + return props + + +def generate_fontconfig_pattern(d): + """Convert a `.FontProperties` to a fontconfig pattern string.""" + kvs = [(k, getattr(d, f"get_{k}")()) + for k in ["style", "variant", "weight", "stretch", "file", "size"]] + # Families is given first without a leading keyword. Other entries (which + # are necessarily scalar) are given as key=value, skipping Nones. + return (",".join(_family_escape(f) for f in d.get_family()) + + "".join(f":{k}={_value_escape(str(v))}" + for k, v in kvs if v is not None)) diff --git a/lib/matplotlib/_layoutgrid.py b/lib/matplotlib/_layoutgrid.py index e46b3fe8c062..12eec6f2b2d6 100644 --- a/lib/matplotlib/_layoutgrid.py +++ b/lib/matplotlib/_layoutgrid.py @@ -20,8 +20,10 @@ import kiwisolver as kiwi import logging import numpy as np -from matplotlib.transforms import Bbox +import matplotlib as mpl +import matplotlib.patches as mpatches +from matplotlib.transforms import Bbox _log = logging.getLogger(__name__) @@ -39,7 +41,9 @@ def __init__(self, parent=None, parent_pos=(0, 0), self.parent = parent self.parent_pos = parent_pos self.parent_inner = parent_inner - self.name = name + self.name = name + seq_id() + if isinstance(parent, LayoutGrid): + self.name = f'{parent.name}.{self.name}' self.nrows = nrows self.ncols = ncols self.height_ratios = np.atleast_1d(height_ratios) @@ -50,8 +54,10 @@ def __init__(self, parent=None, parent_pos=(0, 0), self.width_ratios = np.ones(ncols) sn = self.name + '_' - if parent is None: - self.parent = None + if not isinstance(parent, LayoutGrid): + # parent can be a rect if not a LayoutGrid + # allows specifying a rectangle to contain the layout. + self.parent = parent self.solver = kiwi.Solver() else: self.parent = parent @@ -168,7 +174,8 @@ def hard_constraints(self): self.solver.addConstraint(c | 'required') def add_child(self, child, i=0, j=0): - self.children[i, j] = child + # np.ix_ returns the cross product of i and j indices + self.children[np.ix_(np.atleast_1d(i), np.atleast_1d(j))] = child def parent_constraints(self): # constraints that are due to the parent... @@ -176,12 +183,13 @@ def parent_constraints(self): # parent's left, the last column right equal to the # parent's right... parent = self.parent - if parent is None: - hc = [self.lefts[0] == 0, - self.rights[-1] == 1, + if not isinstance(parent, LayoutGrid): + # specify a rectangle in figure coordinates + hc = [self.lefts[0] == parent[0], + self.rights[-1] == parent[0] + parent[2], # top and bottom reversed order... - self.tops[0] == 1, - self.bottoms[-1] == 0] + self.tops[0] == parent[1] + parent[3], + self.bottoms[-1] == parent[1]] else: rows, cols = self.parent_pos rows = np.atleast_1d(rows) @@ -502,20 +510,12 @@ def seq_id(): return '%06d' % next(_layoutboxobjnum) -def print_children(lb): - """Print the children of the layoutbox.""" - for child in lb.children: - print_children(child) - - -def plot_children(fig, lg, level=0, printit=False): +def plot_children(fig, lg=None, level=0): """Simple plotting to show where boxes are.""" - import matplotlib.pyplot as plt - import matplotlib.patches as mpatches - - fig.canvas.draw() - - colors = plt.rcParams["axes.prop_cycle"].by_key()["color"] + if lg is None: + _layoutgrids = fig.get_layout_engine().execute(fig) + lg = _layoutgrids[fig] + colors = mpl.rcParams["axes.prop_cycle"].by_key()["color"] col = colors[level] for i in range(lg.nrows): for j in range(lg.ncols): diff --git a/lib/matplotlib/_mathtext.py b/lib/matplotlib/_mathtext.py index 6f0e47b239e6..3a934c21fd50 100644 --- a/lib/matplotlib/_mathtext.py +++ b/lib/matplotlib/_mathtext.py @@ -2,28 +2,29 @@ Implementation details for :mod:`.mathtext`. """ +import copy from collections import namedtuple import enum import functools -from io import StringIO import logging import os +import re import types import unicodedata import numpy as np from pyparsing import ( - Combine, Empty, FollowedBy, Forward, Group, Literal, oneOf, OneOrMore, - Optional, ParseBaseException, ParseFatalException, ParserElement, - ParseResults, QuotedString, Regex, StringEnd, Suppress, ZeroOrMore) + Empty, Forward, Literal, NotAny, oneOf, OneOrMore, Optional, + ParseBaseException, ParseException, ParseExpression, ParseFatalException, + ParserElement, ParseResults, QuotedString, Regex, StringEnd, ZeroOrMore, + pyparsing_common) import matplotlib as mpl from . import _api, cbook from ._mathtext_data import ( - latex_to_bakoma, latex_to_standard, stix_virtual_fonts, tex2uni) -from .afm import AFM + latex_to_bakoma, stix_glyph_fixes, stix_virtual_fonts, tex2uni) from .font_manager import FontProperties, findfont, get_font -from .ft2font import KERNING_DEFAULT +from .ft2font import FT2Image, KERNING_DEFAULT ParserElement.enablePackrat() @@ -34,29 +35,28 @@ # FONTS -def get_unicode_index(symbol, math=True): +@_api.delete_parameter("3.6", "math") +def get_unicode_index(symbol, math=False): # Publicly exported. r""" Return the integer index (from the Unicode table) of *symbol*. Parameters ---------- symbol : str - A single unicode character, a TeX command (e.g. r'\pi') or a Type1 + A single (Unicode) character, a TeX command (e.g. r'\pi') or a Type1 symbol name (e.g. 'phi'). - math : bool, default: True - If False, always treat as a single unicode character. + math : bool, default: False + If True (deprecated), replace ASCII hyphen-minus by Unicode minus. """ - # for a non-math symbol, simply return its unicode index - if not math: - return ord(symbol) # From UTF #25: U+2212 minus sign is the preferred # representation of the unary and binary minus sign rather than # the ASCII-derived U+002D hyphen-minus, because minus sign is # unambiguous and because it is rendered with a more desirable # length, usually longer than a hyphen. - if symbol == '-': + # Remove this block when the 'math' parameter is deleted. + if math and symbol == '-': return 0x2212 - try: # This will succeed if symbol is a single unicode char + try: # This will succeed if symbol is a single Unicode char return ord(symbol) except TypeError: pass @@ -68,6 +68,85 @@ def get_unicode_index(symbol, math=True): .format(symbol)) from err +VectorParse = namedtuple("VectorParse", "width height depth glyphs rects", + module="matplotlib.mathtext") +VectorParse.__doc__ = r""" +The namedtuple type returned by ``MathTextParser("path").parse(...)``. + +This tuple contains the global metrics (*width*, *height*, *depth*), a list of +*glyphs* (including their positions) and of *rect*\angles. +""" + + +RasterParse = namedtuple("RasterParse", "ox oy width height depth image", + module="matplotlib.mathtext") +RasterParse.__doc__ = r""" +The namedtuple type returned by ``MathTextParser("agg").parse(...)``. + +This tuple contains the global metrics (*width*, *height*, *depth*), and a +raster *image*. The offsets *ox*, *oy* are always zero. +""" + + +class Output: + r""" + Result of `ship`\ping a box: lists of positioned glyphs and rectangles. + + This class is not exposed to end users, but converted to a `VectorParse` or + a `RasterParse` by `.MathTextParser.parse`. + """ + + def __init__(self, box): + self.box = box + self.glyphs = [] # (ox, oy, info) + self.rects = [] # (x1, y1, x2, y2) + + def to_vector(self): + w, h, d = map( + np.ceil, [self.box.width, self.box.height, self.box.depth]) + gs = [(info.font, info.fontsize, info.num, ox, h - oy + info.offset) + for ox, oy, info in self.glyphs] + rs = [(x1, h - y2, x2 - x1, y2 - y1) + for x1, y1, x2, y2 in self.rects] + return VectorParse(w, h + d, d, gs, rs) + + def to_raster(self): + # Metrics y's and mathtext y's are oriented in opposite directions, + # hence the switch between ymin and ymax. + xmin = min([*[ox + info.metrics.xmin for ox, oy, info in self.glyphs], + *[x1 for x1, y1, x2, y2 in self.rects], 0]) - 1 + ymin = min([*[oy - info.metrics.ymax for ox, oy, info in self.glyphs], + *[y1 for x1, y1, x2, y2 in self.rects], 0]) - 1 + xmax = max([*[ox + info.metrics.xmax for ox, oy, info in self.glyphs], + *[x2 for x1, y1, x2, y2 in self.rects], 0]) + 1 + ymax = max([*[oy - info.metrics.ymin for ox, oy, info in self.glyphs], + *[y2 for x1, y1, x2, y2 in self.rects], 0]) + 1 + w = xmax - xmin + h = ymax - ymin - self.box.depth + d = ymax - ymin - self.box.height + image = FT2Image(np.ceil(w), np.ceil(h + max(d, 0))) + + # Ideally, we could just use self.glyphs and self.rects here, shifting + # their coordinates by (-xmin, -ymin), but this yields slightly + # different results due to floating point slop; shipping twice is the + # old approach and keeps baseline images backcompat. + shifted = ship(self.box, (-xmin, -ymin)) + + for ox, oy, info in shifted.glyphs: + info.font.draw_glyph_to_bitmap( + image, ox, oy - info.metrics.iceberg, info.glyph, + antialiased=mpl.rcParams['text.antialiased']) + for x1, y1, x2, y2 in shifted.rects: + height = max(int(y2 - y1) - 1, 0) + if height == 0: + center = (y2 + y1) / 2 + y = int(center - (height + 1) / 2) + else: + y = int(y1) + image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height) + return RasterParse(0, 0, w, h + d, d, image) + + class Fonts: """ An abstract base class for a system of fonts to use for mathtext. @@ -77,27 +156,19 @@ class Fonts: to do the actual drawing. """ - def __init__(self, default_font_prop, mathtext_backend): + def __init__(self, default_font_prop, load_glyph_flags): """ Parameters ---------- default_font_prop : `~.font_manager.FontProperties` The default non-math font, or the base font for Unicode (generic) font rendering. - mathtext_backend : `MathtextBackend` subclass - Backend to which rendering is actually delegated. + load_glyph_flags : int + Flags passed to the glyph loader (e.g. ``FT_Load_Glyph`` and + ``FT_Load_Char`` for FreeType-based fonts). """ self.default_font_prop = default_font_prop - self.mathtext_backend = mathtext_backend - self.used_characters = {} - - @_api.deprecated("3.4") - def destroy(self): - """ - Fix any cyclical references before the object is about - to be destroyed. - """ - self.used_characters = None + self.load_glyph_flags = load_glyph_flags def get_kern(self, font1, fontclass1, sym1, fontsize1, font2, fontclass2, sym2, fontsize2, dpi): @@ -108,7 +179,7 @@ def get_kern(self, font1, fontclass1, sym1, fontsize1, """ return 0. - def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True): + def get_metrics(self, font, font_class, sym, fontsize, dpi): r""" Parameters ---------- @@ -127,8 +198,6 @@ def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True): Font size in points. dpi : float Rendering dots-per-inch. - math : bool - Whether we are currently in math mode or not. Returns ------- @@ -146,33 +215,23 @@ def get_metrics(self, font, font_class, sym, fontsize, dpi, math=True): - *slanted*: Whether the glyph should be considered as "slanted" (currently used for kerning sub/superscripts). """ - info = self._get_info(font, font_class, sym, fontsize, dpi, math) + info = self._get_info(font, font_class, sym, fontsize, dpi) return info.metrics - def set_canvas_size(self, w, h, d): - """ - Set the size of the buffer used to render the math expression. - Only really necessary for the bitmap backends. - """ - self.width, self.height, self.depth = np.ceil([w, h, d]) - self.mathtext_backend.set_canvas_size( - self.width, self.height, self.depth) - - @_api.rename_parameter("3.4", "facename", "font") - def render_glyph(self, ox, oy, font, font_class, sym, fontsize, dpi): + def render_glyph( + self, output, ox, oy, font, font_class, sym, fontsize, dpi): """ At position (*ox*, *oy*), draw the glyph specified by the remaining parameters (see `get_metrics` for their detailed description). """ info = self._get_info(font, font_class, sym, fontsize, dpi) - self.used_characters.setdefault(info.font.fname, set()).add(info.num) - self.mathtext_backend.render_glyph(ox, oy, info) + output.glyphs.append((ox, oy, info)) - def render_rect_filled(self, x1, y1, x2, y2): + def render_rect_filled(self, output, x1, y1, x2, y2): """ Draw a filled rectangle from (*x1*, *y1*) to (*x2*, *y2*). """ - self.mathtext_backend.render_rect_filled(x1, y1, x2, y2) + output.rects.append((x1, y1, x2, y2)) def get_xheight(self, font, fontsize, dpi): """ @@ -195,20 +254,6 @@ def get_used_characters(self): """ return self.used_characters - def get_results(self, box): - """ - Get the data needed by the backend to render the math - expression. The return value is backend-specific. - """ - result = self.mathtext_backend.get_results( - box, self.get_used_characters()) - if self.destroy != TruetypeFonts.destroy.__get__(self): - destroy = _api.deprecate_method_override( - __class__.destroy, self, since="3.4") - if destroy: - destroy() - return result - def get_sized_alternatives_for_symbol(self, fontname, sym): """ Override if your font provides multiple sizes of the same @@ -224,21 +269,18 @@ class TruetypeFonts(Fonts): A generic base class for all font setups that use Truetype fonts (through FT2Font). """ - def __init__(self, default_font_prop, mathtext_backend): - super().__init__(default_font_prop, mathtext_backend) - self.glyphd = {} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + # Per-instance cache. + self._get_info = functools.lru_cache(None)(self._get_info) self._fonts = {} - filename = findfont(default_font_prop) + filename = findfont(self.default_font_prop) default_font = get_font(filename) self._fonts['default'] = default_font self._fonts['regular'] = default_font - @_api.deprecated("3.4") - def destroy(self): - self.glyphd = None - super().destroy() - def _get_font(self, font): if font in self.fontmap: basename = self.fontmap[font] @@ -257,19 +299,11 @@ def _get_offset(self, font, glyph, fontsize, dpi): return (glyph.height / 64 / 2) + (fontsize/3 * dpi/72) return 0. - def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): - key = fontname, font_class, sym, fontsize, dpi - bunch = self.glyphd.get(key) - if bunch is not None: - return bunch - - font, num, symbol_name, fontsize, slanted = \ - self._get_glyph(fontname, font_class, sym, fontsize, math) - + # The return value of _get_info is cached per-instance. + def _get_info(self, fontname, font_class, sym, fontsize, dpi): + font, num, slanted = self._get_glyph(fontname, font_class, sym) font.set_size(fontsize, dpi) - glyph = font.load_char( - num, - flags=self.mathtext_backend.get_hinting_type()) + glyph = font.load_char(num, flags=self.load_glyph_flags) xmin, ymin, xmax, ymax = [val/64.0 for val in glyph.bbox] offset = self._get_offset(font, glyph, fontsize, dpi) @@ -286,17 +320,15 @@ def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): slanted = slanted ) - result = self.glyphd[key] = types.SimpleNamespace( + return types.SimpleNamespace( font = font, fontsize = fontsize, postscript_name = font.postscript_name, metrics = metrics, - symbol_name = symbol_name, num = num, glyph = glyph, offset = offset ) - return result def get_xheight(self, fontname, fontsize, dpi): font = self._get_font(fontname) @@ -356,8 +388,7 @@ def __init__(self, *args, **kwargs): _slanted_symbols = set(r"\int \oint".split()) - def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): - symbol_name = None + def _get_glyph(self, fontname, font_class, sym): font = None if fontname in self.fontmap and sym in latex_to_bakoma: basename, num = latex_to_bakoma[sym] @@ -368,17 +399,10 @@ def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): font = self._get_font(fontname) if font is not None: num = ord(sym) - - if font is not None: - gid = font.get_char_index(num) - if gid != 0: - symbol_name = font.get_glyph_name(gid) - - if symbol_name is None: - return self._stix_fallback._get_glyph( - fontname, font_class, sym, fontsize, math) - - return font, num, symbol_name, fontsize, slanted + if font is not None and font.get_char_index(num) != 0: + return font, num, slanted + else: + return self._stix_fallback._get_glyph(fontname, font_class, sym) # The Bakoma fonts contain many pre-sized alternatives for the # delimiters. The AutoSizedChar class will use these alternatives @@ -451,19 +475,23 @@ class UnicodeFonts(TruetypeFonts): This class will "fallback" on the Bakoma fonts when a required symbol can not be found in the font. """ - use_cmex = True # Unused; delete once mathtext becomes private. + + # Some glyphs are not present in the `cmr10` font, and must be brought in + # from `cmsy10`. Map the Unicode indices of those glyphs to the indices at + # which they are found in `cmsy10`. + _cmr10_substitutions = { + 0x00D7: 0x00A3, # Multiplication sign. + 0x2212: 0x00A1, # Minus sign. + } def __init__(self, *args, **kwargs): # This must come first so the backend's owner is set correctly fallback_rc = mpl.rcParams['mathtext.fallback'] - if mpl.rcParams['mathtext.fallback_to_cm'] is not None: - fallback_rc = ('cm' if mpl.rcParams['mathtext.fallback_to_cm'] - else None) font_cls = {'stix': StixFonts, 'stixsans': StixSansFonts, 'cm': BakomaFonts }.get(fallback_rc) - self.cm_fallback = font_cls(*args, **kwargs) if font_cls else None + self._fallback_font = font_cls(*args, **kwargs) if font_cls else None super().__init__(*args, **kwargs) self.fontmap = {} @@ -476,7 +504,7 @@ def __init__(self, *args, **kwargs): self.fontmap['ex'] = font # include STIX sized alternatives for glyphs if fallback is STIX - if isinstance(self.cm_fallback, StixFonts): + if isinstance(self._fallback_font, StixFonts): stixsizedaltfonts = { 0: 'STIXGeneral', 1: 'STIXSizeOneSym', @@ -495,14 +523,14 @@ def __init__(self, *args, **kwargs): def _map_virtual_font(self, fontname, font_class, uniindex): return fontname, uniindex - def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): + def _get_glyph(self, fontname, font_class, sym): try: - uniindex = get_unicode_index(sym, math) + uniindex = get_unicode_index(sym) found_symbol = True except ValueError: uniindex = ord('?') found_symbol = False - _log.warning("No TeX to unicode mapping for {!a}.".format(sym)) + _log.warning("No TeX to Unicode mapping for {!a}.".format(sym)) fontname, uniindex = self._map_virtual_font( fontname, font_class, uniindex) @@ -522,56 +550,56 @@ def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): found_symbol = False font = self._get_font(new_fontname) if font is not None: + if (uniindex in self._cmr10_substitutions + and font.family_name == "cmr10"): + font = get_font( + cbook._get_data_path("fonts/ttf/cmsy10.ttf")) + uniindex = self._cmr10_substitutions[uniindex] glyphindex = font.get_char_index(uniindex) if glyphindex != 0: found_symbol = True if not found_symbol: - if self.cm_fallback: + if self._fallback_font: if (fontname in ('it', 'regular') - and isinstance(self.cm_fallback, StixFonts)): + and isinstance(self._fallback_font, StixFonts)): fontname = 'rm' - g = self.cm_fallback._get_glyph(fontname, font_class, - sym, fontsize) - fname = g[0].family_name - if fname in list(BakomaFonts._fontmap.values()): - fname = "Computer Modern" - _log.info("Substituting symbol %s from %s", sym, fname) + g = self._fallback_font._get_glyph(fontname, font_class, sym) + family = g[0].family_name + if family in list(BakomaFonts._fontmap.values()): + family = "Computer Modern" + _log.info("Substituting symbol %s from %s", sym, family) return g else: if (fontname in ('it', 'regular') and isinstance(self, StixFonts)): - return self._get_glyph('rm', font_class, sym, fontsize) + return self._get_glyph('rm', font_class, sym) _log.warning("Font {!r} does not have a glyph for {!a} " "[U+{:x}], substituting with a dummy " "symbol.".format(new_fontname, sym, uniindex)) - fontname = 'rm' - font = self._get_font(fontname) + font = self._get_font('rm') uniindex = 0xA4 # currency char, for lack of anything better - glyphindex = font.get_char_index(uniindex) slanted = False - symbol_name = font.get_glyph_name(glyphindex) - return font, uniindex, symbol_name, fontsize, slanted + return font, uniindex, slanted def get_sized_alternatives_for_symbol(self, fontname, sym): - if self.cm_fallback: - return self.cm_fallback.get_sized_alternatives_for_symbol( + if self._fallback_font: + return self._fallback_font.get_sized_alternatives_for_symbol( fontname, sym) return [(fontname, sym)] class DejaVuFonts(UnicodeFonts): - use_cmex = False # Unused; delete once mathtext becomes private. def __init__(self, *args, **kwargs): # This must come first so the backend's owner is set correctly if isinstance(self, DejaVuSerifFonts): - self.cm_fallback = StixFonts(*args, **kwargs) + self._fallback_font = StixFonts(*args, **kwargs) else: - self.cm_fallback = StixSansFonts(*args, **kwargs) + self._fallback_font = StixSansFonts(*args, **kwargs) self.bakoma = BakomaFonts(*args, **kwargs) TruetypeFonts.__init__(self, *args, **kwargs) self.fontmap = {} @@ -588,11 +616,10 @@ def __init__(self, *args, **kwargs): self.fontmap[key] = fullpath self.fontmap[name] = fullpath - def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): + def _get_glyph(self, fontname, font_class, sym): # Override prime symbol to use Bakoma. if sym == r'\prime': - return self.bakoma._get_glyph( - fontname, font_class, sym, fontsize, math) + return self.bakoma._get_glyph(fontname, font_class, sym) else: # check whether the glyph is available in the display font uniindex = get_unicode_index(sym) @@ -600,11 +627,9 @@ def _get_glyph(self, fontname, font_class, sym, fontsize, math=True): if font is not None: glyphindex = font.get_char_index(uniindex) if glyphindex != 0: - return super()._get_glyph( - 'ex', font_class, sym, fontsize, math) + return super()._get_glyph('ex', font_class, sym) # otherwise return regular glyph - return super()._get_glyph( - fontname, font_class, sym, fontsize, math) + return super()._get_glyph(fontname, font_class, sym) class DejaVuSerifFonts(DejaVuFonts): @@ -667,8 +692,7 @@ class StixFonts(UnicodeFonts): 4: 'STIXSizeFourSym', 5: 'STIXSizeFiveSym', } - use_cmex = False # Unused; delete once mathtext becomes private. - cm_fallback = False + _fallback_font = False _sans = False def __init__(self, *args, **kwargs): @@ -718,6 +742,10 @@ def _map_virtual_font(self, fontname, font_class, uniindex): uniindex = 0x1 fontname = mpl.rcParams['mathtext.default'] + # Fix some incorrect glyphs. + if fontname in ('rm', 'it'): + uniindex = stix_glyph_fixes.get(uniindex, uniindex) + # Handle private use area glyphs if fontname in ('it', 'rm', 'bf') and 0xe000 <= uniindex <= 0xf8ff: fontname = 'nonuni' + fontname @@ -753,164 +781,6 @@ class StixSansFonts(StixFonts): _sans = True -class StandardPsFonts(Fonts): - """ - Use the standard postscript fonts for rendering to backend_ps - - Unlike the other font classes, BakomaFont and UnicodeFont, this - one requires the Ps backend. - """ - basepath = str(cbook._get_data_path('fonts/afm')) - - fontmap = { - 'cal': 'pzcmi8a', # Zapf Chancery - 'rm': 'pncr8a', # New Century Schoolbook - 'tt': 'pcrr8a', # Courier - 'it': 'pncri8a', # New Century Schoolbook Italic - 'sf': 'phvr8a', # Helvetica - 'bf': 'pncb8a', # New Century Schoolbook Bold - None: 'psyr', # Symbol - } - - def __init__(self, default_font_prop, mathtext_backend=None): - if mathtext_backend is None: - # Circular import, can be dropped after public access to - # StandardPsFonts is removed and mathtext_backend made a required - # parameter. - from . import mathtext - mathtext_backend = mathtext.MathtextBackendPath() - super().__init__(default_font_prop, mathtext_backend) - self.glyphd = {} - self.fonts = {} - - filename = findfont(default_font_prop, fontext='afm', - directory=self.basepath) - if filename is None: - filename = findfont('Helvetica', fontext='afm', - directory=self.basepath) - with open(filename, 'rb') as fd: - default_font = AFM(fd) - default_font.fname = filename - - self.fonts['default'] = default_font - self.fonts['regular'] = default_font - - pswriter = _api.deprecated("3.4")(property(lambda self: StringIO())) - - def _get_font(self, font): - if font in self.fontmap: - basename = self.fontmap[font] - else: - basename = font - - cached_font = self.fonts.get(basename) - if cached_font is None: - fname = os.path.join(self.basepath, basename + ".afm") - with open(fname, 'rb') as fd: - cached_font = AFM(fd) - cached_font.fname = fname - self.fonts[basename] = cached_font - self.fonts[cached_font.get_fontname()] = cached_font - return cached_font - - def _get_info(self, fontname, font_class, sym, fontsize, dpi, math=True): - """Load the cmfont, metrics and glyph with caching.""" - key = fontname, sym, fontsize, dpi - tup = self.glyphd.get(key) - - if tup is not None: - return tup - - # Only characters in the "Letter" class should really be italicized. - # This class includes greek letters, so we're ok - if (fontname == 'it' and - (len(sym) > 1 - or not unicodedata.category(sym).startswith("L"))): - fontname = 'rm' - - found_symbol = False - - if sym in latex_to_standard: - fontname, num = latex_to_standard[sym] - glyph = chr(num) - found_symbol = True - elif len(sym) == 1: - glyph = sym - num = ord(glyph) - found_symbol = True - else: - _log.warning( - "No TeX to built-in Postscript mapping for {!r}".format(sym)) - - slanted = (fontname == 'it') - font = self._get_font(fontname) - - if found_symbol: - try: - symbol_name = font.get_name_char(glyph) - except KeyError: - _log.warning( - "No glyph in standard Postscript font {!r} for {!r}" - .format(font.get_fontname(), sym)) - found_symbol = False - - if not found_symbol: - glyph = '?' - num = ord(glyph) - symbol_name = font.get_name_char(glyph) - - offset = 0 - - scale = 0.001 * fontsize - - xmin, ymin, xmax, ymax = [val * scale - for val in font.get_bbox_char(glyph)] - metrics = types.SimpleNamespace( - advance = font.get_width_char(glyph) * scale, - width = font.get_width_char(glyph) * scale, - height = font.get_height_char(glyph) * scale, - xmin = xmin, - xmax = xmax, - ymin = ymin+offset, - ymax = ymax+offset, - # iceberg is the equivalent of TeX's "height" - iceberg = ymax + offset, - slanted = slanted - ) - - self.glyphd[key] = types.SimpleNamespace( - font = font, - fontsize = fontsize, - postscript_name = font.get_fontname(), - metrics = metrics, - symbol_name = symbol_name, - num = num, - glyph = glyph, - offset = offset - ) - - return self.glyphd[key] - - def get_kern(self, font1, fontclass1, sym1, fontsize1, - font2, fontclass2, sym2, fontsize2, dpi): - if font1 == font2 and fontsize1 == fontsize2: - info1 = self._get_info(font1, fontclass1, sym1, fontsize1, dpi) - info2 = self._get_info(font2, fontclass2, sym2, fontsize2, dpi) - font = info1.font - return (font.get_kern_dist(info1.glyph, info2.glyph) - * 0.001 * fontsize1) - return super().get_kern(font1, fontclass1, sym1, fontsize1, - font2, fontclass2, sym2, fontsize2, dpi) - - def get_xheight(self, font, fontsize, dpi): - font = self._get_font(font) - return font.get_xheight() * 0.001 * fontsize - - def get_underline_thickness(self, font, fontsize, dpi): - font = self._get_font(font) - return font.get_underline_thickness() * 0.001 * fontsize - - ############################################################################## # TeX-LIKE BOX MODEL @@ -923,8 +793,8 @@ def get_underline_thickness(self, font, fontsize, dpi): # # The most relevant "chapters" are: # Data structures for boxes and their friends -# Shipping pages out (Ship class) -# Packaging (hpack and vpack) +# Shipping pages out (ship()) +# Packaging (hpack() and vpack()) # Data structures for math mode # Subroutines for math mode # Typesetting math formulas @@ -935,10 +805,8 @@ def get_underline_thickness(self, font, fontsize, dpi): # Note that (as TeX) y increases downward, unlike many other parts of # matplotlib. -# How much text shrinks when going to the next-smallest level. GROW_FACTOR -# must be the inverse of SHRINK_FACTOR. +# How much text shrinks when going to the next-smallest level. SHRINK_FACTOR = 0.7 -GROW_FACTOR = 1 / SHRINK_FACTOR # The number of different sizes of chars to use, beyond which they will not # get any smaller NUM_SIZE_LEVELS = 6 @@ -966,16 +834,16 @@ class FontConstantsBase: # superscript is present sub2 = 0.5 - # Percentage of x-height that sub/supercripts are offset relative to the + # Percentage of x-height that sub/superscripts are offset relative to the # nucleus edge for non-slanted nuclei delta = 0.025 # Additional percentage of last character height above 2/3 of the - # x-height that supercripts are offset relative to the subscript + # x-height that superscripts are offset relative to the subscript # for slanted nuclei delta_slanted = 0.2 - # Percentage of x-height that supercripts and subscripts are offset for + # Percentage of x-height that superscripts and subscripts are offset for # integrals delta_integral = 0.1 @@ -1042,12 +910,11 @@ class DejaVuSansFontConstants(FontConstantsBase): def _get_font_constant_set(state): constants = _font_constant_mapping.get( - state.font_output._get_font(state.font).family_name, - FontConstantsBase) + state.fontset._get_font(state.font).family_name, FontConstantsBase) # STIX sans isn't really its own fonts, just different code points # in the STIX fonts, so we have to detect this one separately. if (constants is STIXFontConstants and - isinstance(state.font_output, StixSansFonts)): + isinstance(state.fontset, StixSansFonts)): return STIXSansFontConstants return constants @@ -1059,7 +926,7 @@ def __init__(self): self.size = 0 def __repr__(self): - return self.__class__.__name__ + return type(self).__name__ def get_kerning(self, next): return 0.0 @@ -1071,15 +938,8 @@ def shrink(self): """ self.size += 1 - def grow(self): - """ - Grows one level larger. There is no limit to how big - something can get. - """ - self.size -= 1 - - def render(self, x, y): - pass + def render(self, output, x, y): + """Render this node.""" class Box(Node): @@ -1098,13 +958,7 @@ def shrink(self): self.height *= SHRINK_FACTOR self.depth *= SHRINK_FACTOR - def grow(self): - super().grow() - self.width *= GROW_FACTOR - self.height *= GROW_FACTOR - self.depth *= GROW_FACTOR - - def render(self, x1, y1, x2, y2): + def render(self, output, x1, y1, x2, y2): pass @@ -1135,15 +989,14 @@ class Char(Node): `Hlist`. """ - def __init__(self, c, state, math=True): + def __init__(self, c, state): super().__init__() self.c = c - self.font_output = state.font_output + self.fontset = state.fontset self.font = state.font self.font_class = state.font_class self.fontsize = state.fontsize self.dpi = state.dpi - self.math = math # The real width, height and depth will be set during the # pack phase, after we know the real fontsize self._update_metrics() @@ -1152,9 +1005,8 @@ def __repr__(self): return '`%s`' % self.c def _update_metrics(self): - metrics = self._metrics = self.font_output.get_metrics( - self.font, self.font_class, self.c, self.fontsize, self.dpi, - self.math) + metrics = self._metrics = self.fontset.get_metrics( + self.font, self.font_class, self.c, self.fontsize, self.dpi) if self.c == ' ': self.width = metrics.advance else: @@ -1175,18 +1027,15 @@ def get_kerning(self, next): advance = self._metrics.advance - self.width kern = 0. if isinstance(next, Char): - kern = self.font_output.get_kern( + kern = self.fontset.get_kern( self.font, self.font_class, self.c, self.fontsize, next.font, next.font_class, next.c, next.fontsize, self.dpi) return advance + kern - def render(self, x, y): - """ - Render the character to the canvas - """ - self.font_output.render_glyph( - x, y, + def render(self, output, x, y): + self.fontset.render_glyph( + output, x, y, self.font, self.font_class, self.c, self.fontsize, self.dpi) def shrink(self): @@ -1197,13 +1046,6 @@ def shrink(self): self.height *= SHRINK_FACTOR self.depth *= SHRINK_FACTOR - def grow(self): - super().grow() - self.fontsize *= GROW_FACTOR - self.width *= GROW_FACTOR - self.height *= GROW_FACTOR - self.depth *= GROW_FACTOR - class Accent(Char): """ @@ -1212,7 +1054,7 @@ class Accent(Char): TrueType fonts. """ def _update_metrics(self): - metrics = self._metrics = self.font_output.get_metrics( + metrics = self._metrics = self.fontset.get_metrics( self.font, self.font_class, self.c, self.fontsize, self.dpi) self.width = metrics.xmax - metrics.xmin self.height = metrics.ymax - metrics.ymin @@ -1222,16 +1064,9 @@ def shrink(self): super().shrink() self._update_metrics() - def grow(self): - super().grow() - self._update_metrics() - - def render(self, x, y): - """ - Render the character to the canvas. - """ - self.font_output.render_glyph( - x - self._metrics.xmin, y + self._metrics.ymin, + def render(self, output, x, y): + self.fontset.render_glyph( + output, x - self._metrics.xmin, y + self._metrics.ymin, self.font, self.font_class, self.c, self.fontsize, self.dpi) @@ -1248,27 +1083,16 @@ def __init__(self, elements): self.glue_order = 0 # The order of infinity (0 - 3) for the glue def __repr__(self): - return '[%s <%.02f %.02f %.02f %.02f> %s]' % ( + return '%s[%s]' % ( super().__repr__(), self.width, self.height, self.depth, self.shift_amount, - ' '.join([repr(x) for x in self.children])) - - @staticmethod - def _determine_order(totals): - """ - Determine the highest order of glue used by the members of this list. - - Helper function used by vpack and hpack. - """ - for i in range(len(totals))[::-1]: - if totals[i] != 0: - return i - return 0 + ', '.join([repr(x) for x in self.children])) def _set_glue(self, x, sign, totals, error_type): - o = self._determine_order(totals) - self.glue_order = o + self.glue_order = o = next( + # Highest order of glue used by the members of this list. + (i for i in range(len(totals))[::-1] if totals[i] != 0), 0) self.glue_sign = sign if totals[o] != 0.: self.glue_set = x / totals[o] @@ -1278,7 +1102,7 @@ def _set_glue(self, x, sign, totals, error_type): if o == 0: if len(self.children): _log.warning("%s %s: %r", - error_type, self.__class__.__name__, self) + error_type, type(self).__name__, self) def shrink(self): for child in self.children: @@ -1288,13 +1112,6 @@ def shrink(self): self.shift_amount *= SHRINK_FACTOR self.glue_set *= SHRINK_FACTOR - def grow(self): - for child in self.children: - child.grow() - super().grow() - self.shift_amount *= GROW_FACTOR - self.glue_set *= GROW_FACTOR - class Hlist(List): """A horizontal list of boxes.""" @@ -1303,7 +1120,7 @@ def __init__(self, elements, w=0., m='additional', do_kern=True): super().__init__(elements) if do_kern: self.kern() - self.hpack() + self.hpack(w=w, m=m) def kern(self): """ @@ -1404,9 +1221,9 @@ def hpack(self, w=0., m='additional'): self.glue_ratio = 0. return if x > 0.: - self._set_glue(x, 1, total_stretch, "Overfull") + self._set_glue(x, 1, total_stretch, "Overful") else: - self._set_glue(x, -1, total_shrink, "Underfull") + self._set_glue(x, -1, total_shrink, "Underful") class Vlist(List): @@ -1414,7 +1231,7 @@ class Vlist(List): def __init__(self, elements, h=0., m='additional'): super().__init__(elements) - self.vpack() + self.vpack(h=h, m=m) def vpack(self, h=0., m='additional', l=np.inf): """ @@ -1426,8 +1243,8 @@ def vpack(self, h=0., m='additional', l=np.inf): h : float, default: 0 A height. m : {'exactly', 'additional'}, default: 'additional' - Whether to produce a box whose height is 'exactly' *w*; or a box - with the natural height of the contents, plus *w* ('additional'). + Whether to produce a box whose height is 'exactly' *h*; or a box + with the natural height of the contents, plus *h* ('additional'). l : float, default: np.inf The maximum height. @@ -1483,9 +1300,9 @@ def vpack(self, h=0., m='additional', l=np.inf): return if x > 0.: - self._set_glue(x, 1, total_stretch, "Overfull") + self._set_glue(x, 1, total_stretch, "Overful") else: - self._set_glue(x, -1, total_shrink, "Underfull") + self._set_glue(x, -1, total_shrink, "Underful") class Rule(Box): @@ -1501,10 +1318,10 @@ class Rule(Box): def __init__(self, width, height, depth, state): super().__init__(width, height, depth) - self.font_output = state.font_output + self.fontset = state.fontset - def render(self, x, y, w, h): - self.font_output.render_rect_filled(x, y, x + w, y + h) + def render(self, output, x, y, w, h): + self.fontset.render_rect_filled(output, x, y, x + w, y + h) class Hrule(Rule): @@ -1512,8 +1329,7 @@ class Hrule(Rule): def __init__(self, state, thickness=None): if thickness is None: - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) + thickness = state.get_current_underline_thickness() height = depth = thickness * 0.5 super().__init__(np.inf, height, depth, state) @@ -1522,8 +1338,7 @@ class Vrule(Rule): """Convenience class to create a vertical rule.""" def __init__(self, state): - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) + thickness = state.get_current_underline_thickness() super().__init__(thickness, np.inf, np.inf, state) @@ -1549,10 +1364,7 @@ class Glue(Node): it's easier to stick to what TeX does.) """ - glue_subtype = _api.deprecated("3.3")(property(lambda self: "normal")) - - @_api.delete_parameter("3.3", "copy") - def __init__(self, glue_type, copy=False): + def __init__(self, glue_type): super().__init__() if isinstance(glue_type, str): glue_spec = _GlueSpec._named[glue_type] @@ -1568,56 +1380,6 @@ def shrink(self): g = self.glue_spec self.glue_spec = g._replace(width=g.width * SHRINK_FACTOR) - def grow(self): - super().grow() - g = self.glue_spec - self.glue_spec = g._replace(width=g.width * GROW_FACTOR) - - -# Some convenient ways to get common kinds of glue - - -@_api.deprecated("3.3", alternative="Glue('fil')") -class Fil(Glue): - def __init__(self): - super().__init__('fil') - - -@_api.deprecated("3.3", alternative="Glue('fill')") -class Fill(Glue): - def __init__(self): - super().__init__('fill') - - -@_api.deprecated("3.3", alternative="Glue('filll')") -class Filll(Glue): - def __init__(self): - super().__init__('filll') - - -@_api.deprecated("3.3", alternative="Glue('neg_fil')") -class NegFil(Glue): - def __init__(self): - super().__init__('neg_fil') - - -@_api.deprecated("3.3", alternative="Glue('neg_fill')") -class NegFill(Glue): - def __init__(self): - super().__init__('neg_fill') - - -@_api.deprecated("3.3", alternative="Glue('neg_filll')") -class NegFilll(Glue): - def __init__(self): - super().__init__('neg_filll') - - -@_api.deprecated("3.3", alternative="Glue('ss')") -class SsGlue(Glue): - def __init__(self): - super().__init__('ss') - class HCentered(Hlist): """ @@ -1665,25 +1427,6 @@ def shrink(self): if self.size < NUM_SIZE_LEVELS: self.width *= SHRINK_FACTOR - def grow(self): - super().grow() - self.width *= GROW_FACTOR - - -class SubSuperCluster(Hlist): - """ - A hack to get around that fact that this code does a two-pass parse like - TeX. This lets us store enough information in the hlist itself, namely the - nucleus, sub- and super-script, such that if another script follows that - needs to be attached, it can be reconfigured on the fly. - """ - - def __init__(self): - self.nucleus = None - self.sub = None - self.super = None - super().__init__([]) - class AutoHeightChar(Hlist): """ @@ -1695,10 +1438,10 @@ class AutoHeightChar(Hlist): """ def __init__(self, c, height, depth, state, always=False, factor=None): - alternatives = state.font_output.get_sized_alternatives_for_symbol( + alternatives = state.fontset.get_sized_alternatives_for_symbol( state.font, c) - xHeight = state.font_output.get_xheight( + xHeight = state.fontset.get_xheight( state.font, state.fontsize, state.dpi) state = state.copy() @@ -1712,7 +1455,7 @@ def __init__(self, c, height, depth, state, always=False, factor=None): break shift = 0 - if state.font != 0: + if state.font != 0 or len(alternatives) == 1: if factor is None: factor = target_total / (char.height + char.depth) state.fontsize *= factor @@ -1734,7 +1477,7 @@ class AutoWidthChar(Hlist): """ def __init__(self, c, width, state, always=False, char_class=Char): - alternatives = state.font_output.get_sized_alternatives_for_symbol( + alternatives = state.fontset.get_sized_alternatives_for_symbol( state.font, c) state = state.copy() @@ -1752,81 +1495,72 @@ def __init__(self, c, width, state, always=False, char_class=Char): self.width = char.width -class Ship: +def ship(box, xy=(0, 0)): """ - Ship boxes to output once they have been set up, this sends them to output. + Ship out *box* at offset *xy*, converting it to an `Output`. - Since boxes can be inside of boxes inside of boxes, the main work of `Ship` + Since boxes can be inside of boxes inside of boxes, the main work of `ship` is done by two mutually recursive routines, `hlist_out` and `vlist_out`, which traverse the `Hlist` nodes and `Vlist` nodes inside of horizontal and vertical boxes. The global variables used in TeX to store state as it - processes have become member variables here. + processes have become local variables here. """ + ox, oy = xy + cur_v = 0. + cur_h = 0. + off_h = ox + off_v = oy + box.height + output = Output(box) - def __call__(self, ox, oy, box): - self.max_push = 0 # Deepest nesting of push commands so far - self.cur_s = 0 - self.cur_v = 0. - self.cur_h = 0. - self.off_h = ox - self.off_v = oy + box.height - self.hlist_out(box) - - @staticmethod def clamp(value): - if value < -1000000000.: - return -1000000000. - if value > 1000000000.: - return 1000000000. - return value - - def hlist_out(self, box): - cur_g = 0 - cur_glue = 0. - glue_order = box.glue_order - glue_sign = box.glue_sign - base_line = self.cur_v - left_edge = self.cur_h - self.cur_s += 1 - self.max_push = max(self.cur_s, self.max_push) - clamp = self.clamp + return -1e9 if value < -1e9 else +1e9 if value > +1e9 else value + + def hlist_out(box): + nonlocal cur_v, cur_h, off_h, off_v + + cur_g = 0 + cur_glue = 0. + glue_order = box.glue_order + glue_sign = box.glue_sign + base_line = cur_v + left_edge = cur_h for p in box.children: if isinstance(p, Char): - p.render(self.cur_h + self.off_h, self.cur_v + self.off_v) - self.cur_h += p.width + p.render(output, cur_h + off_h, cur_v + off_v) + cur_h += p.width elif isinstance(p, Kern): - self.cur_h += p.width + cur_h += p.width elif isinstance(p, List): # node623 if len(p.children) == 0: - self.cur_h += p.width + cur_h += p.width else: - edge = self.cur_h - self.cur_v = base_line + p.shift_amount + edge = cur_h + cur_v = base_line + p.shift_amount if isinstance(p, Hlist): - self.hlist_out(p) + hlist_out(p) else: # p.vpack(box.height + box.depth, 'exactly') - self.vlist_out(p) - self.cur_h = edge + p.width - self.cur_v = base_line + vlist_out(p) + cur_h = edge + p.width + cur_v = base_line elif isinstance(p, Box): # node624 rule_height = p.height - rule_depth = p.depth - rule_width = p.width + rule_depth = p.depth + rule_width = p.width if np.isinf(rule_height): rule_height = box.height if np.isinf(rule_depth): rule_depth = box.depth if rule_height > 0 and rule_width > 0: - self.cur_v = base_line + rule_depth - p.render(self.cur_h + self.off_h, - self.cur_v + self.off_v, + cur_v = base_line + rule_depth + p.render(output, + cur_h + off_h, cur_v + off_v, rule_width, rule_height) - self.cur_v = base_line - self.cur_h += rule_width + cur_v = base_line + cur_h += rule_width elif isinstance(p, Glue): # node625 glue_spec = p.glue_spec @@ -1840,38 +1574,36 @@ def hlist_out(self, box): cur_glue += glue_spec.shrink cur_g = round(clamp(box.glue_set * cur_glue)) rule_width += cur_g - self.cur_h += rule_width - self.cur_s -= 1 - - def vlist_out(self, box): - cur_g = 0 - cur_glue = 0. - glue_order = box.glue_order - glue_sign = box.glue_sign - self.cur_s += 1 - self.max_push = max(self.max_push, self.cur_s) - left_edge = self.cur_h - self.cur_v -= box.height - top_edge = self.cur_v - clamp = self.clamp + cur_h += rule_width + + def vlist_out(box): + nonlocal cur_v, cur_h, off_h, off_v + + cur_g = 0 + cur_glue = 0. + glue_order = box.glue_order + glue_sign = box.glue_sign + left_edge = cur_h + cur_v -= box.height + top_edge = cur_v for p in box.children: if isinstance(p, Kern): - self.cur_v += p.width + cur_v += p.width elif isinstance(p, List): if len(p.children) == 0: - self.cur_v += p.height + p.depth + cur_v += p.height + p.depth else: - self.cur_v += p.height - self.cur_h = left_edge + p.shift_amount - save_v = self.cur_v + cur_v += p.height + cur_h = left_edge + p.shift_amount + save_v = cur_v p.width = box.width if isinstance(p, Hlist): - self.hlist_out(p) + hlist_out(p) else: - self.vlist_out(p) - self.cur_v = save_v + p.depth - self.cur_h = left_edge + vlist_out(p) + cur_v = save_v + p.depth + cur_h = left_edge elif isinstance(p, Box): rule_height = p.height rule_depth = p.depth @@ -1880,9 +1612,9 @@ def vlist_out(self, box): rule_width = box.width rule_height += rule_depth if rule_height > 0 and rule_depth > 0: - self.cur_v += rule_height - p.render(self.cur_h + self.off_h, - self.cur_v + self.off_v, + cur_v += rule_height + p.render(output, + cur_h + off_h, cur_v + off_v, rule_width, rule_height) elif isinstance(p, Glue): glue_spec = p.glue_spec @@ -1896,14 +1628,13 @@ def vlist_out(self, box): cur_glue += glue_spec.shrink cur_g = round(clamp(box.glue_set * cur_glue)) rule_height += cur_g - self.cur_v += rule_height + cur_v += rule_height elif isinstance(p, Char): raise RuntimeError( "Internal mathtext error: Char node found in vlist") - self.cur_s -= 1 - -ship = Ship() + hlist_out(box) + return output ############################################################################## @@ -1915,9 +1646,69 @@ def Error(msg): def raise_error(s, loc, toks): raise ParseFatalException(s, loc, msg) - empty = Empty() - empty.setParseAction(raise_error) - return empty + return Empty().setParseAction(raise_error) + + +class ParserState: + """ + Parser state. + + States are pushed and popped from a stack as necessary, and the "current" + state is always at the top of the stack. + + Upon entering and leaving a group { } or math/non-math, the stack is pushed + and popped accordingly. + """ + + def __init__(self, fontset, font, font_class, fontsize, dpi): + self.fontset = fontset + self._font = font + self.font_class = font_class + self.fontsize = fontsize + self.dpi = dpi + + def copy(self): + return copy.copy(self) + + @property + def font(self): + return self._font + + @font.setter + def font(self, name): + if name in ('rm', 'it', 'bf'): + self.font_class = name + self._font = name + + def get_current_underline_thickness(self): + """Return the underline thickness for this state.""" + return self.fontset.get_underline_thickness( + self.font, self.fontsize, self.dpi) + + +def cmd(expr, args): + r""" + Helper to define TeX commands. + + ``cmd("\cmd", args)`` is equivalent to + ``"\cmd" - (args | Error("Expected \cmd{arg}{...}"))`` where the names in + the error message are taken from element names in *args*. If *expr* + already includes arguments (e.g. "\cmd{arg}{...}"), then they are stripped + when constructing the parse element, but kept (and *expr* is used as is) in + the error message. + """ + + def names(elt): + if isinstance(elt, ParseExpression): + for expr in elt.exprs: + yield from names(expr) + elif elt.resultsName: + yield elt.resultsName + + csname = expr.split("{", 1)[0] + err = (csname + "".join("{%s}" % name for name in names(args)) + if expr == csname else expr) + return csname - (args | Error(f"Expected {err}")) class Parser: @@ -1930,51 +1721,53 @@ class Parser: """ class _MathStyle(enum.Enum): - DISPLAYSTYLE = enum.auto() - TEXTSTYLE = enum.auto() - SCRIPTSTYLE = enum.auto() - SCRIPTSCRIPTSTYLE = enum.auto() - - _binary_operators = set(''' - + * - - \\pm \\sqcap \\rhd - \\mp \\sqcup \\unlhd - \\times \\vee \\unrhd - \\div \\wedge \\oplus - \\ast \\setminus \\ominus - \\star \\wr \\otimes - \\circ \\diamond \\oslash - \\bullet \\bigtriangleup \\odot - \\cdot \\bigtriangledown \\bigcirc - \\cap \\triangleleft \\dagger - \\cup \\triangleright \\ddagger - \\uplus \\lhd \\amalg'''.split()) - - _relation_symbols = set(''' + DISPLAYSTYLE = 0 + TEXTSTYLE = 1 + SCRIPTSTYLE = 2 + SCRIPTSCRIPTSTYLE = 3 + + _binary_operators = set( + '+ * - \N{MINUS SIGN}' + r''' + \pm \sqcap \rhd + \mp \sqcup \unlhd + \times \vee \unrhd + \div \wedge \oplus + \ast \setminus \ominus + \star \wr \otimes + \circ \diamond \oslash + \bullet \bigtriangleup \odot + \cdot \bigtriangledown \bigcirc + \cap \triangleleft \dagger + \cup \triangleright \ddagger + \uplus \lhd \amalg + \dotplus \dotminus'''.split()) + + _relation_symbols = set(r''' = < > : - \\leq \\geq \\equiv \\models - \\prec \\succ \\sim \\perp - \\preceq \\succeq \\simeq \\mid - \\ll \\gg \\asymp \\parallel - \\subset \\supset \\approx \\bowtie - \\subseteq \\supseteq \\cong \\Join - \\sqsubset \\sqsupset \\neq \\smile - \\sqsubseteq \\sqsupseteq \\doteq \\frown - \\in \\ni \\propto \\vdash - \\dashv \\dots \\dotplus \\doteqdot'''.split()) - - _arrow_symbols = set(''' - \\leftarrow \\longleftarrow \\uparrow - \\Leftarrow \\Longleftarrow \\Uparrow - \\rightarrow \\longrightarrow \\downarrow - \\Rightarrow \\Longrightarrow \\Downarrow - \\leftrightarrow \\longleftrightarrow \\updownarrow - \\Leftrightarrow \\Longleftrightarrow \\Updownarrow - \\mapsto \\longmapsto \\nearrow - \\hookleftarrow \\hookrightarrow \\searrow - \\leftharpoonup \\rightharpoonup \\swarrow - \\leftharpoondown \\rightharpoondown \\nwarrow - \\rightleftharpoons \\leadsto'''.split()) + \leq \geq \equiv \models + \prec \succ \sim \perp + \preceq \succeq \simeq \mid + \ll \gg \asymp \parallel + \subset \supset \approx \bowtie + \subseteq \supseteq \cong \Join + \sqsubset \sqsupset \neq \smile + \sqsubseteq \sqsupseteq \doteq \frown + \in \ni \propto \vdash + \dashv \dots \doteqdot'''.split()) + + _arrow_symbols = set(r''' + \leftarrow \longleftarrow \uparrow + \Leftarrow \Longleftarrow \Uparrow + \rightarrow \longrightarrow \downarrow + \Rightarrow \Longrightarrow \Downarrow + \leftrightarrow \longleftrightarrow \updownarrow + \Leftrightarrow \Longleftrightarrow \Updownarrow + \mapsto \longmapsto \nearrow + \hookleftarrow \hookrightarrow \searrow + \leftharpoonup \rightharpoonup \swarrow + \leftharpoondown \rightharpoondown \nwarrow + \rightleftharpoons \leadsto'''.split()) _spaced_symbols = _binary_operators | _relation_symbols | _arrow_symbols @@ -1985,8 +1778,7 @@ class _MathStyle(enum.Enum): \bigwedge \bigodot \bigotimes \bigoplus \biguplus '''.split()) - _overunder_functions = set( - "lim liminf limsup sup max min".split()) + _overunder_functions = set("lim liminf limsup sup max min".split()) _dropsub_symbols = set(r'''\int \oint'''.split()) @@ -1997,208 +1789,142 @@ class _MathStyle(enum.Enum): liminf sin cos exp limsup sinh cosh gcd ln sup cot hom log tan coth inf max tanh""".split()) - _ambi_delim = set(""" - | \\| / \\backslash \\uparrow \\downarrow \\updownarrow \\Uparrow - \\Downarrow \\Updownarrow . \\vert \\Vert \\\\|""".split()) - - _left_delim = set(r"( [ \{ < \lfloor \langle \lceil".split()) - - _right_delim = set(r") ] \} > \rfloor \rangle \rceil".split()) + _ambi_delims = set(r""" + | \| / \backslash \uparrow \downarrow \updownarrow \Uparrow + \Downarrow \Updownarrow . \vert \Vert""".split()) + _left_delims = set(r"( [ \{ < \lfloor \langle \lceil".split()) + _right_delims = set(r") ] \} > \rfloor \rangle \rceil".split()) + _delims = _left_delims | _right_delims | _ambi_delims def __init__(self): p = types.SimpleNamespace() - # All forward declarations are here + + def set_names_and_parse_actions(): + for key, val in vars(p).items(): + if not key.startswith('_'): + # Set names on everything -- very useful for debugging + val.setName(key) + # Set actions + if hasattr(self, key): + val.setParseAction(getattr(self, key)) + + # Root definitions. + + # In TeX parlance, a csname is a control sequence name (a "\foo"). + def csnames(group, names): + ends_with_alpha = [] + ends_with_nonalpha = [] + for name in names: + if name[-1].isalpha(): + ends_with_alpha.append(name) + else: + ends_with_nonalpha.append(name) + return Regex(r"\\(?P<{}>(?:{})(?![A-Za-z]){})".format( + group, + "|".join(map(re.escape, ends_with_alpha)), + "".join(f"|{s}" for s in map(re.escape, ends_with_nonalpha)), + )) + + p.float_literal = Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)") + p.space = oneOf(self._space_widths)("space") + + p.style_literal = oneOf( + [str(e.value) for e in self._MathStyle])("style_literal") + + p.symbol = Regex( + r"[a-zA-Z0-9 +\-*/<>=:,.;!\?&'@()\[\]|\U00000080-\U0001ffff]" + r"|\\[%${}\[\]_|]" + + r"|\\(?:{})(?![A-Za-z])".format( + "|".join(map(re.escape, tex2uni))) + )("sym").leaveWhitespace() + p.unknown_symbol = Regex(r"\\[A-Za-z]*")("name") + + p.font = csnames("font", self._fontnames) + p.start_group = ( + Optional(r"\math" + oneOf(self._fontnames)("font")) + "{") + p.end_group = Literal("}") + + p.delim = oneOf(self._delims) + + set_names_and_parse_actions() # for root definitions. + + # Mutually recursive definitions. (Minimizing the number of Forward + # elements is important for speed.) p.accent = Forward() - p.ambi_delim = Forward() - p.apostrophe = Forward() p.auto_delim = Forward() p.binom = Forward() - p.bslash = Forward() - p.c_over_c = Forward() p.customspace = Forward() - p.end_group = Forward() - p.float_literal = Forward() - p.font = Forward() p.frac = Forward() p.dfrac = Forward() p.function = Forward() p.genfrac = Forward() p.group = Forward() - p.int_literal = Forward() - p.latexfont = Forward() - p.lbracket = Forward() - p.left_delim = Forward() - p.lbrace = Forward() - p.main = Forward() - p.math = Forward() - p.math_string = Forward() - p.non_math = Forward() p.operatorname = Forward() p.overline = Forward() p.overset = Forward() p.placeable = Forward() - p.rbrace = Forward() - p.rbracket = Forward() p.required_group = Forward() - p.right_delim = Forward() - p.right_delim_safe = Forward() p.simple = Forward() - p.simple_group = Forward() - p.single_symbol = Forward() - p.accentprefixed = Forward() - p.space = Forward() + p.optional_group = Forward() p.sqrt = Forward() - p.start_group = Forward() p.subsuper = Forward() - p.subsuperop = Forward() - p.symbol = Forward() - p.symbol_name = Forward() p.token = Forward() p.underset = Forward() - p.unknown_symbol = Forward() - - # Set names on everything -- very useful for debugging - for key, val in vars(p).items(): - if not key.startswith('_'): - val.setName(key) - - p.float_literal <<= Regex(r"[-+]?([0-9]+\.?[0-9]*|\.[0-9]+)") - p.int_literal <<= Regex("[-+]?[0-9]+") - - p.lbrace <<= Literal('{').suppress() - p.rbrace <<= Literal('}').suppress() - p.lbracket <<= Literal('[').suppress() - p.rbracket <<= Literal(']').suppress() - p.bslash <<= Literal('\\') - - p.space <<= oneOf(list(self._space_widths)) - p.customspace <<= ( - Suppress(Literal(r'\hspace')) - - ((p.lbrace + p.float_literal + p.rbrace) - | Error(r"Expected \hspace{n}")) - ) - - unicode_range = "\U00000080-\U0001ffff" - p.single_symbol <<= Regex( - r"([a-zA-Z0-9 +\-*/<>=:,.;!\?&'@()\[\]|%s])|(\\[%%${}\[\]_|])" % - unicode_range) - p.accentprefixed <<= Suppress(p.bslash) + oneOf(self._accentprefixed) - p.symbol_name <<= ( - Combine(p.bslash + oneOf(list(tex2uni))) - + FollowedBy(Regex("[^A-Za-z]").leaveWhitespace() | StringEnd()) - ) - p.symbol <<= (p.single_symbol | p.symbol_name).leaveWhitespace() - - p.apostrophe <<= Regex("'+") - - p.c_over_c <<= ( - Suppress(p.bslash) - + oneOf(list(self._char_over_chars)) - ) - - p.accent <<= Group( - Suppress(p.bslash) - + oneOf([*self._accent_map, *self._wide_accents]) - - p.placeable - ) - p.function <<= ( - Suppress(p.bslash) - + oneOf(list(self._function_names)) - ) + set_names_and_parse_actions() # for mutually recursive definitions. - p.start_group <<= Optional(p.latexfont) + p.lbrace - p.end_group <<= p.rbrace.copy() - p.simple_group <<= Group(p.lbrace + ZeroOrMore(p.token) + p.rbrace) - p.required_group <<= Group(p.lbrace + OneOrMore(p.token) + p.rbrace) - p.group <<= Group( - p.start_group + ZeroOrMore(p.token) + p.end_group - ) + p.customspace <<= cmd(r"\hspace", "{" + p.float_literal("space") + "}") - p.font <<= Suppress(p.bslash) + oneOf(list(self._fontnames)) - p.latexfont <<= ( - Suppress(p.bslash) - + oneOf(['math' + x for x in self._fontnames]) - ) + p.accent <<= ( + csnames("accent", [*self._accent_map, *self._wide_accents]) + - p.placeable("sym")) - p.frac <<= Group( - Suppress(Literal(r"\frac")) - - ((p.required_group + p.required_group) - | Error(r"Expected \frac{num}{den}")) - ) + p.function <<= csnames("name", self._function_names) + p.operatorname <<= cmd( + r"\operatorname", + "{" + ZeroOrMore(p.simple | p.unknown_symbol)("name") + "}") - p.dfrac <<= Group( - Suppress(Literal(r"\dfrac")) - - ((p.required_group + p.required_group) - | Error(r"Expected \dfrac{num}{den}")) - ) + p.group <<= p.start_group + ZeroOrMore(p.token)("group") + p.end_group - p.binom <<= Group( - Suppress(Literal(r"\binom")) - - ((p.required_group + p.required_group) - | Error(r"Expected \binom{num}{den}")) - ) + p.optional_group <<= "{" + ZeroOrMore(p.token)("group") + "}" + p.required_group <<= "{" + OneOrMore(p.token)("group") + "}" - p.ambi_delim <<= oneOf(list(self._ambi_delim)) - p.left_delim <<= oneOf(list(self._left_delim)) - p.right_delim <<= oneOf(list(self._right_delim)) - p.right_delim_safe <<= oneOf([*(self._right_delim - {'}'}), r'\}']) - - p.genfrac <<= Group( - Suppress(Literal(r"\genfrac")) - - (((p.lbrace - + Optional(p.ambi_delim | p.left_delim, default='') - + p.rbrace) - + (p.lbrace - + Optional(p.ambi_delim | p.right_delim_safe, default='') - + p.rbrace) - + (p.lbrace + p.float_literal + p.rbrace) - + p.simple_group + p.required_group + p.required_group) - | Error("Expected " - r"\genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}")) - ) + p.frac <<= cmd( + r"\frac", p.required_group("num") + p.required_group("den")) + p.dfrac <<= cmd( + r"\dfrac", p.required_group("num") + p.required_group("den")) + p.binom <<= cmd( + r"\binom", p.required_group("num") + p.required_group("den")) - p.sqrt <<= Group( - Suppress(Literal(r"\sqrt")) - - ((Group(Optional( - p.lbracket + OneOrMore(~p.rbracket + p.token) + p.rbracket)) - + p.required_group) - | Error("Expected \\sqrt{value}")) - ) + p.genfrac <<= cmd( + r"\genfrac", + "{" + Optional(p.delim)("ldelim") + "}" + + "{" + Optional(p.delim)("rdelim") + "}" + + "{" + p.float_literal("rulesize") + "}" + + "{" + Optional(p.style_literal)("style") + "}" + + p.required_group("num") + + p.required_group("den")) - p.overline <<= Group( - Suppress(Literal(r"\overline")) - - (p.required_group | Error("Expected \\overline{value}")) - ) - - p.overset <<= Group( - Suppress(Literal(r"\overset")) - - ((p.simple_group + p.simple_group) - | Error("Expected \\overset{body}{annotation}")) - ) - - p.underset <<= Group( - Suppress(Literal(r"\underset")) - - ((p.simple_group + p.simple_group) - | Error("Expected \\underset{body}{annotation}")) - ) + p.sqrt <<= cmd( + r"\sqrt{value}", + Optional("[" + OneOrMore(NotAny("]") + p.token)("root") + "]") + + p.required_group("value")) - p.unknown_symbol <<= Combine(p.bslash + Regex("[A-Za-z]*")) + p.overline <<= cmd(r"\overline", p.required_group("body")) - p.operatorname <<= Group( - Suppress(Literal(r"\operatorname")) - - ((p.lbrace + ZeroOrMore(p.simple | p.unknown_symbol) + p.rbrace) - | Error("Expected \\operatorname{value}")) - ) + p.overset <<= cmd( + r"\overset", + p.optional_group("annotation") + p.optional_group("body")) + p.underset <<= cmd( + r"\underset", + p.optional_group("annotation") + p.optional_group("body")) p.placeable <<= ( - p.accentprefixed # Must be before accent so named symbols that are - # prefixed with an accent name work - | p.accent # Must be before symbol as all accents are symbols - | p.symbol # Must be third to catch all named symbols and single + p.accent # Must be before symbol as all accents are symbols + | p.symbol # Must be second to catch all named symbols and single # chars not in a group - | p.c_over_c | p.function + | p.operatorname | p.group | p.frac | p.dfrac @@ -2208,7 +1934,6 @@ def __init__(self): | p.underset | p.sqrt | p.overline - | p.operatorname ) p.simple <<= ( @@ -2218,14 +1943,12 @@ def __init__(self): | p.subsuper ) - p.subsuperop <<= oneOf(["_", "^"]) - - p.subsuper <<= Group( - (Optional(p.placeable) - + OneOrMore(p.subsuperop - p.placeable) - + Optional(p.apostrophe)) - | (p.placeable + Optional(p.apostrophe)) - | p.apostrophe + p.subsuper <<= ( + (Optional(p.placeable)("nucleus") + + OneOrMore(oneOf(["_", "^"]) - p.placeable)("subsuper") + + Regex("'*")("apostrophes")) + | Regex("'+")("apostrophes") + | (p.placeable("nucleus") + Regex("'*")("apostrophes")) ) p.token <<= ( @@ -2235,34 +1958,26 @@ def __init__(self): ) p.auto_delim <<= ( - Suppress(Literal(r"\left")) - - ((p.left_delim | p.ambi_delim) - | Error("Expected a delimiter")) - + Group(ZeroOrMore(p.simple | p.auto_delim)) - + Suppress(Literal(r"\right")) - - ((p.right_delim | p.ambi_delim) - | Error("Expected a delimiter")) + r"\left" - (p.delim("left") | Error("Expected a delimiter")) + + ZeroOrMore(p.simple | p.auto_delim)("mid") + + r"\right" - (p.delim("right") | Error("Expected a delimiter")) ) - p.math <<= OneOrMore(p.token) - - p.math_string <<= QuotedString('$', '\\', unquoteResults=False) - - p.non_math <<= Regex(r"(?:(?:\\[$])|[^$])*").leaveWhitespace() - - p.main <<= ( + # Leaf definitions. + p.math = OneOrMore(p.token) + p.math_string = QuotedString('$', '\\', unquoteResults=False) + p.non_math = Regex(r"(?:(?:\\[$])|[^$])*").leaveWhitespace() + p.main = ( p.non_math + ZeroOrMore(p.math_string + p.non_math) + StringEnd() ) - - # Set actions - for key, val in vars(p).items(): - if not key.startswith('_'): - if hasattr(self, key): - val.setParseAction(getattr(self, key)) + set_names_and_parse_actions() # for leaf definitions. self._expression = p.main self._math_expression = p.math + # To add space to nucleus operators after sub/superscripts + self._in_subscript_or_superscript = False + def parse(self, s, fonts_object, fontsize, dpi): """ Parse expression *s* using the given *fonts_object* for @@ -2271,56 +1986,20 @@ def parse(self, s, fonts_object, fontsize, dpi): Returns the parse tree of `Node` instances. """ self._state_stack = [ - self.State(fonts_object, 'default', 'rm', fontsize, dpi)] + ParserState(fonts_object, 'default', 'rm', fontsize, dpi)] self._em_width_cache = {} try: result = self._expression.parseString(s) except ParseBaseException as err: - raise ValueError("\n".join(["", - err.line, - " " * (err.column - 1) + "^", - str(err)])) from err + # explain becomes a plain method on pyparsing 3 (err.explain(0)). + raise ValueError("\n" + ParseException.explain(err, 0)) from None self._state_stack = None + self._in_subscript_or_superscript = False + # prevent operator spacing from leaking into a new expression self._em_width_cache = {} self._expression.resetCache() return result[0] - # The state of the parser is maintained in a stack. Upon - # entering and leaving a group { } or math/non-math, the stack - # is pushed and popped accordingly. The current state always - # exists in the top element of the stack. - class State: - """ - Stores the state of the parser. - - States are pushed and popped from a stack as necessary, and - the "current" state is always at the top of the stack. - """ - def __init__(self, font_output, font, font_class, fontsize, dpi): - self.font_output = font_output - self._font = font - self.font_class = font_class - self.fontsize = fontsize - self.dpi = dpi - - def copy(self): - return Parser.State( - self.font_output, - self.font, - self.font_class, - self.fontsize, - self.dpi) - - @property - def font(self): - return self._font - - @font.setter - def font(self, name): - if name in ('rm', 'it', 'bf'): - self.font_class = name - self._font = name - def get_state(self): """Get the current `State` of the parser.""" return self._state_stack[-1] @@ -2346,21 +2025,27 @@ def math(self, s, loc, toks): def non_math(self, s, loc, toks): s = toks[0].replace(r'\$', '$') - symbols = [Char(c, self.get_state(), math=False) for c in s] + symbols = [Char(c, self.get_state()) for c in s] hlist = Hlist(symbols) # We're going into math now, so set font to 'it' self.push_state() self.get_state().font = mpl.rcParams['mathtext.default'] return [hlist] + float_literal = staticmethod(pyparsing_common.convertToFloat) + def _make_space(self, percentage): - # All spaces are relative to em width + # In TeX, an em (the unit usually used to measure horizontal lengths) + # is not the width of the character 'm'; it is the same in different + # font styles (e.g. roman or italic). Mathtext, however, uses 'm' in + # the italic style so that horizontal spaces don't depend on the + # current font style. state = self.get_state() key = (state.font, state.fontsize, state.dpi) width = self._em_width_cache.get(key) if width is None: - metrics = state.font_output.get_metrics( - state.font, mpl.rcParams['mathtext.default'], 'm', + metrics = state.fontset.get_metrics( + 'it', mpl.rcParams['mathtext.default'], 'm', state.fontsize, state.dpi) width = metrics.advance self._em_width_cache[key] = width @@ -2382,16 +2067,22 @@ def _make_space(self, percentage): } def space(self, s, loc, toks): - tok, = toks - num = self._space_widths[tok] + num = self._space_widths[toks["space"]] box = self._make_space(num) return [box] def customspace(self, s, loc, toks): - return [self._make_space(float(toks[0]))] + return [self._make_space(toks["space"])] def symbol(self, s, loc, toks): - c, = toks + c = toks["sym"] + if c == "-": + # "U+2212 minus sign is the preferred representation of the unary + # and binary minus sign rather than the ASCII-derived U+002D + # hyphen-minus, because minus sign is unambiguous and because it + # is rendered with a more desirable length, usually longer than a + # hyphen." (https://www.unicode.org/reports/tr25/) + c = "\N{MINUS SIGN}" try: char = Char(c, self.get_state()) except ValueError as err: @@ -2405,7 +2096,7 @@ def symbol(self, s, loc, toks): # Binary operators at start of string should not be spaced if (c in self._binary_operators and (len(s[:loc].split()) == 0 or prev_char == '{' or - prev_char in self._left_delim)): + prev_char in self._left_delims)): return [char] else: return [Hlist([self._make_space(0.2), @@ -2413,70 +2104,23 @@ def symbol(self, s, loc, toks): self._make_space(0.2)], do_kern=True)] elif c in self._punctuation_symbols: + prev_char = next((c for c in s[:loc][::-1] if c != ' '), '') + next_char = next((c for c in s[loc + 1:] if c != ' '), '') # Do not space commas between brackets if c == ',': - prev_char = next((c for c in s[:loc][::-1] if c != ' '), '') - next_char = next((c for c in s[loc + 1:] if c != ' '), '') if prev_char == '{' and next_char == '}': return [char] # Do not space dots as decimal separators - if c == '.' and s[loc - 1].isdigit() and s[loc + 1].isdigit(): + if c == '.' and prev_char.isdigit() and next_char.isdigit(): return [char] else: return [Hlist([char, self._make_space(0.2)], do_kern=True)] return [char] - accentprefixed = symbol - def unknown_symbol(self, s, loc, toks): - c, = toks - raise ParseFatalException(s, loc, "Unknown symbol: %s" % c) - - _char_over_chars = { - # The first 2 entries in the tuple are (font, char, sizescale) for - # the two symbols under and over. The third element is the space - # (in multiples of underline height) - r'AA': (('it', 'A', 1.0), (None, '\\circ', 0.5), 0.0), - } - - def c_over_c(self, s, loc, toks): - sym, = toks - state = self.get_state() - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) - - under_desc, over_desc, space = \ - self._char_over_chars.get(sym, (None, None, 0.0)) - if under_desc is None: - raise ParseFatalException("Error parsing symbol") - - over_state = state.copy() - if over_desc[0] is not None: - over_state.font = over_desc[0] - over_state.fontsize *= over_desc[2] - over = Accent(over_desc[1], over_state) - - under_state = state.copy() - if under_desc[0] is not None: - under_state.font = under_desc[0] - under_state.fontsize *= under_desc[2] - under = Char(under_desc[1], under_state) - - width = max(over.width, under.width) - - over_centered = HCentered([over]) - over_centered.hpack(width, 'exactly') - - under_centered = HCentered([under]) - under_centered.hpack(width, 'exactly') - - return Vlist([ - over_centered, - Vbox(0., thickness * space), - under_centered - ]) + raise ParseFatalException(s, loc, f"Unknown symbol: {toks['name']}") _accent_map = { r'hat': r'\circumflexaccent', @@ -2487,6 +2131,8 @@ def c_over_c(self, s, loc, toks): r'tilde': r'\combiningtilde', r'dot': r'\combiningdotabove', r'ddot': r'\combiningdiaeresis', + r'dddot': r'\combiningthreedotsabove', + r'ddddot': r'\combiningfourdotsabove', r'vec': r'\combiningrightarrowabove', r'"': r'\combiningdiaeresis', r"`": r'\combininggraveaccent', @@ -2501,17 +2147,11 @@ def c_over_c(self, s, loc, toks): _wide_accents = set(r"widehat widetilde widebar".split()) - # make a lambda and call it to get the namespace right - _accentprefixed = (lambda am: [ - p for p in tex2uni - if any(p.startswith(a) and a != p for a in am) - ])(set(_accent_map)) - def accent(self, s, loc, toks): state = self.get_state() - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) - (accent, sym), = toks + thickness = state.get_current_underline_thickness() + accent = toks["accent"] + sym = toks["sym"] if accent in self._wide_accents: accent_box = AutoWidthChar( '\\' + accent, sym.width, state, char_class=Accent) @@ -2530,7 +2170,7 @@ def accent(self, s, loc, toks): def function(self, s, loc, toks): hlist = self.operatorname(s, loc, toks) - hlist.function_name, = toks + hlist.function_name = toks["name"] return hlist def operatorname(self, s, loc, toks): @@ -2539,7 +2179,8 @@ def operatorname(self, s, loc, toks): state.font = 'rm' hlist_list = [] # Change the font of Chars, but leave Kerns alone - for c in toks[0]: + name = toks["name"] + for c in name: if isinstance(c, Char): c.font = 'rm' c._update_metrics() @@ -2548,38 +2189,47 @@ def operatorname(self, s, loc, toks): hlist_list.append(Char(c, state)) else: hlist_list.append(c) - next_char_loc = loc + len(toks[0]) + 1 - if isinstance(toks[0], ParseResults): + next_char_loc = loc + len(name) + 1 + if isinstance(name, ParseResults): next_char_loc += len('operatorname{}') next_char = next((c for c in s[next_char_loc:] if c != ' '), '') - delimiters = self._left_delim | self._ambi_delim | self._right_delim - delimiters |= {'^', '_'} + delimiters = self._delims | {'^', '_'} if (next_char not in delimiters and - toks[0] not in self._overunder_functions): + name not in self._overunder_functions): # Add thin space except when followed by parenthesis, bracket, etc. hlist_list += [self._make_space(self._space_widths[r'\,'])] self.pop_state() + # if followed by a super/subscript, set flag to true + # This flag tells subsuper to add space after this operator + if next_char in {'^', '_'}: + self._in_subscript_or_superscript = True + else: + self._in_subscript_or_superscript = False + return Hlist(hlist_list) def start_group(self, s, loc, toks): self.push_state() # Deal with LaTeX-style font tokens - if len(toks): - self.get_state().font = toks[0][4:] + if toks.get("font"): + self.get_state().font = toks.get("font") return [] def group(self, s, loc, toks): - grp = Hlist(toks[0]) + grp = Hlist(toks.get("group", [])) return [grp] - required_group = simple_group = group + + def required_group(self, s, loc, toks): + return Hlist(toks.get("group", [])) + + optional_group = required_group def end_group(self, s, loc, toks): self.pop_state() return [] def font(self, s, loc, toks): - name, = toks - self.get_state().font = name + self.get_state().font = toks["font"] return [] def is_overunder(self, nucleus): @@ -2603,72 +2253,36 @@ def is_between_brackets(self, s, loc): return False def subsuper(self, s, loc, toks): - assert len(toks) == 1 - - nucleus = None - sub = None - super = None - - # Pick all of the apostrophes out, including first apostrophes that - # have been parsed as characters - napostrophes = 0 - new_toks = [] - for tok in toks[0]: - if isinstance(tok, str) and tok not in ('^', '_'): - napostrophes += len(tok) - elif isinstance(tok, Char) and tok.c == "'": - napostrophes += 1 - else: - new_toks.append(tok) - toks = new_toks - - if len(toks) == 0: - assert napostrophes - nucleus = Hbox(0.0) - elif len(toks) == 1: - if not napostrophes: - return toks[0] # .asList() - else: - nucleus = toks[0] - elif len(toks) in (2, 3): - # single subscript or superscript - nucleus = toks[0] if len(toks) == 3 else Hbox(0.0) - op, next = toks[-2:] + nucleus = toks.get("nucleus", Hbox(0)) + subsuper = toks.get("subsuper", []) + napostrophes = len(toks.get("apostrophes", [])) + + if not subsuper and not napostrophes: + return nucleus + + sub = super = None + while subsuper: + op, arg, *subsuper = subsuper if op == '_': - sub = next - else: - super = next - elif len(toks) in (4, 5): - # subscript and superscript - nucleus = toks[0] if len(toks) == 5 else Hbox(0.0) - op1, next1, op2, next2 = toks[-4:] - if op1 == op2: - if op1 == '_': + if sub is not None: raise ParseFatalException("Double subscript") - else: - raise ParseFatalException("Double superscript") - if op1 == '_': - sub = next1 - super = next2 + sub = arg else: - super = next1 - sub = next2 - else: - raise ParseFatalException( - "Subscript/superscript sequence is too long. " - "Use braces { } to remove ambiguity.") + if super is not None: + raise ParseFatalException("Double superscript") + super = arg state = self.get_state() - rule_thickness = state.font_output.get_underline_thickness( + rule_thickness = state.fontset.get_underline_thickness( state.font, state.fontsize, state.dpi) - xHeight = state.font_output.get_xheight( + xHeight = state.fontset.get_xheight( state.font, state.fontsize, state.dpi) if napostrophes: if super is None: super = Hlist([]) for i in range(napostrophes): - super.children.extend(self.symbol(s, loc, ['\\prime'])) + super.children.extend(self.symbol(s, loc, {"sym": "\\prime"})) # kern() and hpack() needed to get the metrics right after # extending super.kern() @@ -2698,9 +2312,9 @@ def subsuper(self, s, loc, toks): hlist = HCentered([sub]) hlist.hpack(width, 'exactly') vlist.extend([Vbox(0, vgap), hlist]) - shift = hlist.height + vgap + shift = hlist.height + vgap + nucleus.depth vlist = Vlist(vlist) - vlist.shift_amount = shift + nucleus.depth + vlist.shift_amount = shift result = Hlist([vlist]) return [result] @@ -2789,18 +2403,22 @@ def subsuper(self, s, loc, toks): if not self.is_dropsub(last_char): x.width += constants.script_space * xHeight - result = Hlist([nucleus, x]) + # Do we need to add a space after the nucleus? + # To find out, check the flag set by operatorname + spaced_nucleus = [nucleus, x] + if self._in_subscript_or_superscript: + spaced_nucleus += [self._make_space(self._space_widths[r'\,'])] + self._in_subscript_or_superscript = False + + result = Hlist(spaced_nucleus) return [result] def _genfrac(self, ldelim, rdelim, rule, style, num, den): state = self.get_state() - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) - - rule = float(rule) + thickness = state.get_current_underline_thickness() - if style is not self._MathStyle.DISPLAYSTYLE: + for _ in range(style.value): num.shrink() den.shrink() cnum = HCentered([num]) @@ -2817,7 +2435,7 @@ def _genfrac(self, ldelim, rdelim, rule, style, num, den): # Shift so the fraction line sits in the middle of the # equals sign - metrics = state.font_output.get_metrics( + metrics = state.fontset.get_metrics( state.font, mpl.rcParams['mathtext.default'], '=', state.fontsize, state.dpi) shift = (cden.height - @@ -2834,37 +2452,36 @@ def _genfrac(self, ldelim, rdelim, rule, style, num, den): return self._auto_sized_delimiter(ldelim, result, rdelim) return result + def style_literal(self, s, loc, toks): + return self._MathStyle(int(toks["style_literal"])) + def genfrac(self, s, loc, toks): - args, = toks - return self._genfrac(*args) + return self._genfrac( + toks.get("ldelim", ""), toks.get("rdelim", ""), + toks["rulesize"], toks.get("style", self._MathStyle.TEXTSTYLE), + toks["num"], toks["den"]) def frac(self, s, loc, toks): - state = self.get_state() - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) - (num, den), = toks - return self._genfrac('', '', thickness, self._MathStyle.TEXTSTYLE, - num, den) + return self._genfrac( + "", "", self.get_state().get_current_underline_thickness(), + self._MathStyle.TEXTSTYLE, toks["num"], toks["den"]) def dfrac(self, s, loc, toks): - state = self.get_state() - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) - (num, den), = toks - return self._genfrac('', '', thickness, self._MathStyle.DISPLAYSTYLE, - num, den) + return self._genfrac( + "", "", self.get_state().get_current_underline_thickness(), + self._MathStyle.DISPLAYSTYLE, toks["num"], toks["den"]) def binom(self, s, loc, toks): - (num, den), = toks - return self._genfrac('(', ')', 0.0, self._MathStyle.TEXTSTYLE, - num, den) + return self._genfrac( + "(", ")", 0, + self._MathStyle.TEXTSTYLE, toks["num"], toks["den"]) - def _genset(self, state, annotation, body, overunder): - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) + def _genset(self, s, loc, toks): + annotation = toks["annotation"] + body = toks["body"] + thickness = self.get_state().get_current_underline_thickness() annotation.shrink() - cannotation = HCentered([annotation]) cbody = HCentered([body]) width = max(cannotation.width, cbody.width) @@ -2872,16 +2489,14 @@ def _genset(self, state, annotation, body, overunder): cbody.hpack(width, 'exactly') vgap = thickness * 3 - if overunder == "under": + if s[loc + 1] == "u": # \underset vlist = Vlist([cbody, # body Vbox(0, vgap), # space cannotation # annotation ]) # Shift so the body sits in the same vertical position - shift_amount = cbody.depth + cannotation.height + vgap - - vlist.shift_amount = shift_amount - else: + vlist.shift_amount = cbody.depth + cannotation.height + vgap + else: # \overset vlist = Vlist([cannotation, # annotation Vbox(0, vgap), # space cbody # body @@ -2891,11 +2506,13 @@ def _genset(self, state, annotation, body, overunder): # an Hlist and extend it with an Hbox(0, horizontal_gap) return vlist + overset = underset = _genset + def sqrt(self, s, loc, toks): - (root, body), = toks + root = toks.get("root") + body = toks["value"] state = self.get_state() - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) + thickness = state.get_current_underline_thickness() # Determine the height of the body, and add a little extra to # the height so it doesn't seem cramped @@ -2932,11 +2549,10 @@ def sqrt(self, s, loc, toks): return [hlist] def overline(self, s, loc, toks): - (body,), = toks + body = toks["body"] state = self.get_state() - thickness = state.font_output.get_underline_thickness( - state.font, state.fontsize, state.dpi) + thickness = state.get_current_underline_thickness() height = body.height - body.shift_amount + thickness * 3.0 depth = body.depth + body.shift_amount @@ -2951,24 +2567,6 @@ def overline(self, s, loc, toks): hlist = Hlist([rightside]) return [hlist] - def overset(self, s, loc, toks): - assert len(toks) == 1 - assert len(toks[0]) == 2 - - state = self.get_state() - annotation, body = toks[0] - - return self._genset(state, annotation, body, overunder="over") - - def underset(self, s, loc, toks): - assert len(toks) == 1 - assert len(toks[0]) == 2 - - state = self.get_state() - annotation, body = toks[0] - - return self._genset(state, annotation, body, overunder="under") - def _auto_sized_delimiter(self, front, middle, back): state = self.get_state() if len(middle): @@ -2992,6 +2590,8 @@ def _auto_sized_delimiter(self, front, middle, back): return hlist def auto_delim(self, s, loc, toks): - front, middle, back = toks - - return self._auto_sized_delimiter(front, middle.asList(), back) + return self._auto_sized_delimiter( + toks["left"], + # if "mid" in toks ... can be removed when requiring pyparsing 3. + toks["mid"].asList() if "mid" in toks else [], + toks["right"]) diff --git a/lib/matplotlib/_mathtext_data.py b/lib/matplotlib/_mathtext_data.py index e17f1e05903f..ef571b90712e 100644 --- a/lib/matplotlib/_mathtext_data.py +++ b/lib/matplotlib/_mathtext_data.py @@ -132,7 +132,7 @@ ']' : ('cmr10', 0x5d), '*' : ('cmsy10', 0xa4), - '-' : ('cmsy10', 0xa1), + '\N{MINUS SIGN}' : ('cmsy10', 0xa1), '\\Downarrow' : ('cmsy10', 0x2b), '\\Im' : ('cmsy10', 0x3d), '\\Leftarrow' : ('cmsy10', 0x28), @@ -236,173 +236,6 @@ '\\_' : ('cmtt10', 0x5f) } -latex_to_cmex = { # Unused; delete once mathtext becomes private. - r'\__sqrt__' : 112, - r'\bigcap' : 92, - r'\bigcup' : 91, - r'\bigodot' : 75, - r'\bigoplus' : 77, - r'\bigotimes' : 79, - r'\biguplus' : 93, - r'\bigvee' : 95, - r'\bigwedge' : 94, - r'\coprod' : 97, - r'\int' : 90, - r'\leftangle' : 173, - r'\leftbrace' : 169, - r'\oint' : 73, - r'\prod' : 89, - r'\rightangle' : 174, - r'\rightbrace' : 170, - r'\sum' : 88, - r'\widehat' : 98, - r'\widetilde' : 101, -} - -latex_to_standard = { - r'\cong' : ('psyr', 64), - r'\Delta' : ('psyr', 68), - r'\Phi' : ('psyr', 70), - r'\Gamma' : ('psyr', 89), - r'\alpha' : ('psyr', 97), - r'\beta' : ('psyr', 98), - r'\chi' : ('psyr', 99), - r'\delta' : ('psyr', 100), - r'\varepsilon' : ('psyr', 101), - r'\phi' : ('psyr', 102), - r'\gamma' : ('psyr', 103), - r'\eta' : ('psyr', 104), - r'\iota' : ('psyr', 105), - r'\varphi' : ('psyr', 106), - r'\kappa' : ('psyr', 108), - r'\nu' : ('psyr', 110), - r'\pi' : ('psyr', 112), - r'\theta' : ('psyr', 113), - r'\rho' : ('psyr', 114), - r'\sigma' : ('psyr', 115), - r'\tau' : ('psyr', 116), - r'\upsilon' : ('psyr', 117), - r'\varpi' : ('psyr', 118), - r'\omega' : ('psyr', 119), - r'\xi' : ('psyr', 120), - r'\psi' : ('psyr', 121), - r'\zeta' : ('psyr', 122), - r'\sim' : ('psyr', 126), - r'\leq' : ('psyr', 163), - r'\infty' : ('psyr', 165), - r'\clubsuit' : ('psyr', 167), - r'\diamondsuit' : ('psyr', 168), - r'\heartsuit' : ('psyr', 169), - r'\spadesuit' : ('psyr', 170), - r'\leftrightarrow' : ('psyr', 171), - r'\leftarrow' : ('psyr', 172), - r'\uparrow' : ('psyr', 173), - r'\rightarrow' : ('psyr', 174), - r'\downarrow' : ('psyr', 175), - r'\pm' : ('psyr', 176), - r'\geq' : ('psyr', 179), - r'\times' : ('psyr', 180), - r'\propto' : ('psyr', 181), - r'\partial' : ('psyr', 182), - r'\bullet' : ('psyr', 183), - r'\div' : ('psyr', 184), - r'\neq' : ('psyr', 185), - r'\equiv' : ('psyr', 186), - r'\approx' : ('psyr', 187), - r'\ldots' : ('psyr', 188), - r'\aleph' : ('psyr', 192), - r'\Im' : ('psyr', 193), - r'\Re' : ('psyr', 194), - r'\wp' : ('psyr', 195), - r'\otimes' : ('psyr', 196), - r'\oplus' : ('psyr', 197), - r'\oslash' : ('psyr', 198), - r'\cap' : ('psyr', 199), - r'\cup' : ('psyr', 200), - r'\supset' : ('psyr', 201), - r'\supseteq' : ('psyr', 202), - r'\subset' : ('psyr', 204), - r'\subseteq' : ('psyr', 205), - r'\in' : ('psyr', 206), - r'\notin' : ('psyr', 207), - r'\angle' : ('psyr', 208), - r'\nabla' : ('psyr', 209), - r'\textregistered' : ('psyr', 210), - r'\copyright' : ('psyr', 211), - r'\texttrademark' : ('psyr', 212), - r'\Pi' : ('psyr', 213), - r'\prod' : ('psyr', 213), - r'\surd' : ('psyr', 214), - r'\__sqrt__' : ('psyr', 214), - r'\cdot' : ('psyr', 215), - r'\urcorner' : ('psyr', 216), - r'\vee' : ('psyr', 217), - r'\wedge' : ('psyr', 218), - r'\Leftrightarrow' : ('psyr', 219), - r'\Leftarrow' : ('psyr', 220), - r'\Uparrow' : ('psyr', 221), - r'\Rightarrow' : ('psyr', 222), - r'\Downarrow' : ('psyr', 223), - r'\Diamond' : ('psyr', 224), - r'\Sigma' : ('psyr', 229), - r'\sum' : ('psyr', 229), - r'\forall' : ('psyr', 34), - r'\exists' : ('psyr', 36), - r'\lceil' : ('psyr', 233), - r'\lbrace' : ('psyr', 123), - r'\Psi' : ('psyr', 89), - r'\bot' : ('psyr', 0o136), - r'\Omega' : ('psyr', 0o127), - r'\leftbracket' : ('psyr', 0o133), - r'\rightbracket' : ('psyr', 0o135), - r'\leftbrace' : ('psyr', 123), - r'\leftparen' : ('psyr', 0o50), - r'\prime' : ('psyr', 0o242), - r'\sharp' : ('psyr', 0o43), - r'\slash' : ('psyr', 0o57), - r'\Lambda' : ('psyr', 0o114), - r'\neg' : ('psyr', 0o330), - r'\Upsilon' : ('psyr', 0o241), - r'\rightbrace' : ('psyr', 0o175), - r'\rfloor' : ('psyr', 0o373), - r'\lambda' : ('psyr', 0o154), - r'\to' : ('psyr', 0o256), - r'\Xi' : ('psyr', 0o130), - r'\emptyset' : ('psyr', 0o306), - r'\lfloor' : ('psyr', 0o353), - r'\rightparen' : ('psyr', 0o51), - r'\rceil' : ('psyr', 0o371), - r'\ni' : ('psyr', 0o47), - r'\epsilon' : ('psyr', 0o145), - r'\Theta' : ('psyr', 0o121), - r'\langle' : ('psyr', 0o341), - r'\leftangle' : ('psyr', 0o341), - r'\rangle' : ('psyr', 0o361), - r'\rightangle' : ('psyr', 0o361), - r'\rbrace' : ('psyr', 0o175), - r'\circ' : ('psyr', 0o260), - r'\diamond' : ('psyr', 0o340), - r'\mu' : ('psyr', 0o155), - r'\mid' : ('psyr', 0o352), - r'\imath' : ('pncri8a', 105), - r'\%' : ('pncr8a', 37), - r'\$' : ('pncr8a', 36), - r'\{' : ('pncr8a', 123), - r'\}' : ('pncr8a', 125), - r'\backslash' : ('pncr8a', 92), - r'\ast' : ('pncr8a', 42), - r'\#' : ('pncr8a', 35), - - r'\circumflexaccent' : ('pncri8a', 124), # for \hat - r'\combiningbreve' : ('pncri8a', 81), # for \breve - r'\combininggraveaccent' : ('pncri8a', 114), # for \grave - r'\combiningacuteaccent' : ('pncri8a', 63), # for \accute - r'\combiningdiaeresis' : ('pncri8a', 91), # for \ddot - r'\combiningtilde' : ('pncri8a', 75), # for \tilde - r'\combiningrightarrowabove' : ('pncri8a', 110), # for \vec - r'\combiningdotabove' : ('pncri8a', 26), # for \dot -} - # Automatically generated. type12uni = { @@ -693,7 +526,7 @@ 'succnsim' : 8937, 'gimel' : 8503, 'vert' : 124, - '|' : 124, + '|' : 8214, 'varrho' : 1009, 'P' : 182, 'approxident' : 8779, @@ -1116,6 +949,7 @@ 'cwopencirclearrow' : 8635, 'geqq' : 8807, 'rightleftarrows' : 8644, + 'aa' : 229, 'ac' : 8766, 'ae' : 230, 'int' : 8747, @@ -1166,6 +1000,8 @@ 'combiningtilde' : 771, 'combiningrightarrowabove' : 8407, 'combiningdotabove' : 775, + 'combiningthreedotsabove' : 8411, + 'combiningfourdotsabove' : 8412, 'to' : 8594, 'succeq' : 8829, 'emptyset' : 8709, @@ -1382,3 +1218,11 @@ (0x0061, 0x007a, 'rm', 0x1d68a) # a-z ], } + + +# Fix some incorrect glyphs. +stix_glyph_fixes = { + # Cap and Cup glyphs are swapped. + 0x22d2: 0x22d3, + 0x22d3: 0x22d2, +} diff --git a/lib/matplotlib/_pylab_helpers.py b/lib/matplotlib/_pylab_helpers.py index 27904dd84b6f..d32a69d4ff99 100644 --- a/lib/matplotlib/_pylab_helpers.py +++ b/lib/matplotlib/_pylab_helpers.py @@ -4,7 +4,6 @@ import atexit from collections import OrderedDict -import gc class Gcf: @@ -65,7 +64,7 @@ def destroy(cls, num): if hasattr(manager, "_cidgcf"): manager.canvas.mpl_disconnect(manager._cidgcf) manager.destroy() - gc.collect(1) + del manager, num @classmethod def destroy_fig(cls, fig): @@ -78,14 +77,10 @@ def destroy_fig(cls, fig): @classmethod def destroy_all(cls): """Destroy all figures.""" - # Reimport gc in case the module globals have already been removed - # during interpreter shutdown. - import gc for manager in list(cls.figs.values()): manager.canvas.mpl_disconnect(manager._cidgcf) manager.destroy() cls.figs.clear() - gc.collect(1) @classmethod def has_fignum(cls, num): diff --git a/lib/matplotlib/_text_helpers.py b/lib/matplotlib/_text_helpers.py new file mode 100644 index 000000000000..18bfb550c90b --- /dev/null +++ b/lib/matplotlib/_text_helpers.py @@ -0,0 +1,74 @@ +""" +Low-level text helper utilities. +""" + +import dataclasses + +from . import _api +from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING + + +LayoutItem = dataclasses.make_dataclass( + "LayoutItem", ["ft_object", "char", "glyph_idx", "x", "prev_kern"]) + + +def warn_on_missing_glyph(codepoint): + _api.warn_external( + "Glyph {} ({}) missing from current font.".format( + codepoint, + chr(codepoint).encode("ascii", "namereplace").decode("ascii"))) + block = ("Hebrew" if 0x0590 <= codepoint <= 0x05ff else + "Arabic" if 0x0600 <= codepoint <= 0x06ff else + "Devanagari" if 0x0900 <= codepoint <= 0x097f else + "Bengali" if 0x0980 <= codepoint <= 0x09ff else + "Gurmukhi" if 0x0a00 <= codepoint <= 0x0a7f else + "Gujarati" if 0x0a80 <= codepoint <= 0x0aff else + "Oriya" if 0x0b00 <= codepoint <= 0x0b7f else + "Tamil" if 0x0b80 <= codepoint <= 0x0bff else + "Telugu" if 0x0c00 <= codepoint <= 0x0c7f else + "Kannada" if 0x0c80 <= codepoint <= 0x0cff else + "Malayalam" if 0x0d00 <= codepoint <= 0x0d7f else + "Sinhala" if 0x0d80 <= codepoint <= 0x0dff else + None) + if block: + _api.warn_external( + f"Matplotlib currently does not support {block} natively.") + + +def layout(string, font, *, kern_mode=KERNING_DEFAULT): + """ + Render *string* with *font*. For each character in *string*, yield a + (glyph-index, x-position) pair. When such a pair is yielded, the font's + glyph is set to the corresponding character. + + Parameters + ---------- + string : str + The string to be rendered. + font : FT2Font + The font. + kern_mode : int + A FreeType kerning mode. + + Yields + ------ + glyph_index : int + x_position : float + """ + x = 0 + prev_glyph_idx = None + char_to_font = font._get_fontmap(string) + base_font = font + for char in string: + # This has done the fallback logic + font = char_to_font.get(char, base_font) + glyph_idx = font.get_char_index(ord(char)) + kern = ( + base_font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64 + if prev_glyph_idx is not None else 0. + ) + x += kern + glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING) + yield LayoutItem(font, char, glyph_idx, x, kern) + x += glyph.linearHoriAdvance / 65536 + prev_glyph_idx = glyph_idx diff --git a/lib/matplotlib/_text_layout.py b/lib/matplotlib/_text_layout.py deleted file mode 100644 index c7a87e848688..000000000000 --- a/lib/matplotlib/_text_layout.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Text layouting utilities. -""" - -import dataclasses - -from .ft2font import KERNING_DEFAULT, LOAD_NO_HINTING - - -LayoutItem = dataclasses.make_dataclass( - "LayoutItem", ["char", "glyph_idx", "x", "prev_kern"]) - - -def layout(string, font, *, kern_mode=KERNING_DEFAULT): - """ - Render *string* with *font*. For each character in *string*, yield a - (glyph-index, x-position) pair. When such a pair is yielded, the font's - glyph is set to the corresponding character. - - Parameters - ---------- - string : str - The string to be rendered. - font : FT2Font - The font. - kern_mode : int - A FreeType kerning mode. - - Yields - ------ - glyph_index : int - x_position : float - """ - x = 0 - prev_glyph_idx = None - for char in string: - glyph_idx = font.get_char_index(ord(char)) - kern = (font.get_kerning(prev_glyph_idx, glyph_idx, kern_mode) / 64 - if prev_glyph_idx is not None else 0.) - x += kern - glyph = font.load_glyph(glyph_idx, flags=LOAD_NO_HINTING) - yield LayoutItem(char, glyph_idx, x, kern) - x += glyph.linearHoriAdvance / 65536 - prev_glyph_idx = glyph_idx diff --git a/lib/matplotlib/_tight_bbox.py b/lib/matplotlib/_tight_bbox.py new file mode 100644 index 000000000000..e0fba389d120 --- /dev/null +++ b/lib/matplotlib/_tight_bbox.py @@ -0,0 +1,81 @@ +""" +Helper module for the *bbox_inches* parameter in `.Figure.savefig`. +""" + +from matplotlib.transforms import Bbox, TransformedBbox, Affine2D + + +def adjust_bbox(fig, bbox_inches, fixed_dpi=None): + """ + Temporarily adjust the figure so that only the specified area + (bbox_inches) is saved. + + It modifies fig.bbox, fig.bbox_inches, + fig.transFigure._boxout, and fig.patch. While the figure size + changes, the scale of the original figure is conserved. A + function which restores the original values are returned. + """ + origBbox = fig.bbox + origBboxInches = fig.bbox_inches + _boxout = fig.transFigure._boxout + + old_aspect = [] + locator_list = [] + sentinel = object() + for ax in fig.axes: + locator_list.append(ax.get_axes_locator()) + current_pos = ax.get_position(original=False).frozen() + ax.set_axes_locator(lambda a, r, _pos=current_pos: _pos) + # override the method that enforces the aspect ratio on the Axes + if 'apply_aspect' in ax.__dict__: + old_aspect.append(ax.apply_aspect) + else: + old_aspect.append(sentinel) + ax.apply_aspect = lambda pos=None: None + + def restore_bbox(): + for ax, loc, aspect in zip(fig.axes, locator_list, old_aspect): + ax.set_axes_locator(loc) + if aspect is sentinel: + # delete our no-op function which un-hides the original method + del ax.apply_aspect + else: + ax.apply_aspect = aspect + + fig.bbox = origBbox + fig.bbox_inches = origBboxInches + fig.transFigure._boxout = _boxout + fig.transFigure.invalidate() + fig.patch.set_bounds(0, 0, 1, 1) + + if fixed_dpi is None: + fixed_dpi = fig.dpi + tr = Affine2D().scale(fixed_dpi) + dpi_scale = fixed_dpi / fig.dpi + + fig.bbox_inches = Bbox.from_bounds(0, 0, *bbox_inches.size) + x0, y0 = tr.transform(bbox_inches.p0) + w1, h1 = fig.bbox.size * dpi_scale + fig.transFigure._boxout = Bbox.from_bounds(-x0, -y0, w1, h1) + fig.transFigure.invalidate() + + fig.bbox = TransformedBbox(fig.bbox_inches, tr) + + fig.patch.set_bounds(x0 / w1, y0 / h1, + fig.bbox.width / w1, fig.bbox.height / h1) + + return restore_bbox + + +def process_figure_for_rasterizing(fig, bbox_inches_restore, fixed_dpi=None): + """ + 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. + """ + + bbox_inches, restore_bbox = bbox_inches_restore + restore_bbox() + r = adjust_bbox(fig, bbox_inches, fixed_dpi) + + return bbox_inches, r diff --git a/lib/matplotlib/_tight_layout.py b/lib/matplotlib/_tight_layout.py new file mode 100644 index 000000000000..e99ba49bd284 --- /dev/null +++ b/lib/matplotlib/_tight_layout.py @@ -0,0 +1,301 @@ +""" +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, this module assumes that the margins (left margin, etc.) which are +differences between ``Axes.get_tightbbox`` and ``Axes.bbox`` are independent of +Axes position. This may fail if ``Axes.adjustable`` is ``datalim`` as well as +such cases as when left or right margin are affected by xlabel. +""" + +import numpy as np + +import matplotlib as mpl +from matplotlib import _api, artist as martist +from matplotlib.font_manager import FontProperties +from matplotlib.transforms import Bbox + + +def _auto_adjust_subplotpars( + fig, renderer, shape, span_pairs, subplot_list, + ax_bbox_list=None, pad=1.08, h_pad=None, w_pad=None, rect=None): + """ + Return a dict of subplot parameters to adjust spacing between subplots + or ``None`` if resulting axes would have zero height or width. + + Note that this function ignores geometry information of subplot itself, but + uses what is given by the *shape* and *subplot_list* parameters. Also, the + results could be incorrect if some subplots have ``adjustable=datalim``. + + Parameters + ---------- + shape : tuple[int, int] + Number of rows and columns of the grid. + span_pairs : list[tuple[slice, slice]] + List of rowspans and colspans occupied by each subplot. + subplot_list : list of subplots + List of subplots that will be used to calculate optimal subplot_params. + pad : float + Padding between the figure edge and the edges of subplots, as a + fraction of the font size. + h_pad, w_pad : float + Padding (height/width) between edges of adjacent subplots, as a + fraction of the font size. Defaults to *pad*. + rect : tuple + (left, bottom, right, top), default: None. + """ + rows, cols = shape + + font_size_inch = (FontProperties( + size=mpl.rcParams["font.size"]).get_size_in_points() / 72) + pad_inch = pad * font_size_inch + vpad_inch = h_pad * font_size_inch if h_pad is not None else pad_inch + hpad_inch = w_pad * font_size_inch if w_pad is not None else pad_inch + + if len(span_pairs) != len(subplot_list) or len(subplot_list) == 0: + raise ValueError + + if rect is None: + margin_left = margin_bottom = margin_right = margin_top = None + else: + margin_left, margin_bottom, _right, _top = rect + margin_right = 1 - _right if _right else None + margin_top = 1 - _top if _top else None + + vspaces = np.zeros((rows + 1, cols)) + hspaces = np.zeros((rows, cols + 1)) + + if ax_bbox_list is None: + ax_bbox_list = [ + Bbox.union([ax.get_position(original=True) for ax in subplots]) + for subplots in subplot_list] + + for subplots, ax_bbox, (rowspan, colspan) in zip( + subplot_list, ax_bbox_list, span_pairs): + if all(not ax.get_visible() for ax in subplots): + continue + + bb = [] + for ax in subplots: + if ax.get_visible(): + bb += [martist._get_tightbbox_for_layout_only(ax, renderer)] + + tight_bbox_raw = Bbox.union(bb) + tight_bbox = fig.transFigure.inverted().transform_bbox(tight_bbox_raw) + + hspaces[rowspan, colspan.start] += ax_bbox.xmin - tight_bbox.xmin # l + hspaces[rowspan, colspan.stop] += tight_bbox.xmax - ax_bbox.xmax # r + vspaces[rowspan.start, colspan] += tight_bbox.ymax - ax_bbox.ymax # t + vspaces[rowspan.stop, colspan] += ax_bbox.ymin - tight_bbox.ymin # b + + fig_width_inch, fig_height_inch = fig.get_size_inches() + + # margins can be negative for axes with aspect applied, so use max(, 0) to + # make them nonnegative. + if not margin_left: + margin_left = max(hspaces[:, 0].max(), 0) + pad_inch/fig_width_inch + suplabel = fig._supylabel + if suplabel and suplabel.get_in_layout(): + rel_width = fig.transFigure.inverted().transform_bbox( + suplabel.get_window_extent(renderer)).width + margin_left += rel_width + pad_inch/fig_width_inch + if not margin_right: + margin_right = max(hspaces[:, -1].max(), 0) + pad_inch/fig_width_inch + if not margin_top: + margin_top = max(vspaces[0, :].max(), 0) + pad_inch/fig_height_inch + if fig._suptitle and fig._suptitle.get_in_layout(): + rel_height = fig.transFigure.inverted().transform_bbox( + fig._suptitle.get_window_extent(renderer)).height + margin_top += rel_height + pad_inch/fig_height_inch + if not margin_bottom: + margin_bottom = max(vspaces[-1, :].max(), 0) + pad_inch/fig_height_inch + suplabel = fig._supxlabel + if suplabel and suplabel.get_in_layout(): + rel_height = fig.transFigure.inverted().transform_bbox( + suplabel.get_window_extent(renderer)).height + margin_bottom += rel_height + pad_inch/fig_height_inch + + if margin_left + margin_right >= 1: + _api.warn_external('Tight layout not applied. The left and right ' + 'margins cannot be made large enough to ' + 'accommodate all axes decorations.') + return None + if margin_bottom + margin_top >= 1: + _api.warn_external('Tight layout not applied. The bottom and top ' + 'margins cannot be made large enough to ' + 'accommodate all axes decorations.') + return None + + kwargs = dict(left=margin_left, + right=1 - margin_right, + bottom=margin_bottom, + top=1 - margin_top) + + if cols > 1: + hspace = hspaces[:, 1:-1].max() + hpad_inch / fig_width_inch + # axes widths: + h_axes = (1 - margin_right - margin_left - hspace * (cols - 1)) / cols + if h_axes < 0: + _api.warn_external('Tight layout not applied. tight_layout ' + 'cannot make axes width small enough to ' + 'accommodate all axes decorations') + return None + else: + kwargs["wspace"] = hspace / h_axes + if rows > 1: + vspace = vspaces[1:-1, :].max() + vpad_inch / fig_height_inch + v_axes = (1 - margin_top - margin_bottom - vspace * (rows - 1)) / rows + if v_axes < 0: + _api.warn_external('Tight layout not applied. tight_layout ' + 'cannot make axes height small enough to ' + 'accommodate all axes decorations.') + return None + else: + kwargs["hspace"] = vspace / v_axes + + return kwargs + + +def get_subplotspec_list(axes_list, grid_spec=None): + """ + 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. + + If grid_spec is given, None is inserted for those not from the given + grid_spec. + """ + subplotspec_list = [] + for ax in axes_list: + axes_or_locator = ax.get_axes_locator() + if axes_or_locator is None: + axes_or_locator = ax + + if hasattr(axes_or_locator, "get_subplotspec"): + subplotspec = axes_or_locator.get_subplotspec() + if subplotspec is not None: + subplotspec = subplotspec.get_topmost_subplotspec() + gs = subplotspec.get_gridspec() + if grid_spec is not None: + if gs != grid_spec: + subplotspec = None + elif gs.locally_modified_subplot_params(): + subplotspec = None + else: + subplotspec = None + + subplotspec_list.append(subplotspec) + + return subplotspec_list + + +def get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer, + pad=1.08, h_pad=None, w_pad=None, rect=None): + """ + Return subplot parameters for tight-layouted-figure with specified padding. + + Parameters + ---------- + fig : Figure + axes_list : list of Axes + subplotspec_list : list of `.SubplotSpec` + The subplotspecs of each axes. + renderer : renderer + pad : float + Padding between the figure edge and the edges of subplots, as a + fraction of the font size. + h_pad, w_pad : float + Padding (height/width) between edges of adjacent subplots. Defaults to + *pad*. + rect : tuple (left, bottom, right, top), default: None. + rectangle in normalized figure coordinates + that the whole subplots area (including labels) will fit into. + Defaults to using the entire figure. + + Returns + ------- + subplotspec or None + subplotspec kwargs to be passed to `.Figure.subplots_adjust` or + None if tight_layout could not be accomplished. + """ + + # Multiple axes can share same subplotspec (e.g., if using axes_grid1); + # we need to group them together. + ss_to_subplots = {ss: [] for ss in subplotspec_list} + for ax, ss in zip(axes_list, subplotspec_list): + ss_to_subplots[ss].append(ax) + if ss_to_subplots.pop(None, None): + _api.warn_external( + "This figure includes Axes that are not compatible with " + "tight_layout, so results might be incorrect.") + if not ss_to_subplots: + return {} + subplot_list = list(ss_to_subplots.values()) + ax_bbox_list = [ss.get_position(fig) for ss in ss_to_subplots] + + max_nrows = max(ss.get_gridspec().nrows for ss in ss_to_subplots) + max_ncols = max(ss.get_gridspec().ncols for ss in ss_to_subplots) + + span_pairs = [] + for ss in ss_to_subplots: + # The intent here is to support axes from different gridspecs where + # one's nrows (or ncols) is a multiple of the other (e.g. 2 and 4), + # but this doesn't actually work because the computed wspace, in + # relative-axes-height, corresponds to different physical spacings for + # the 2-row grid and the 4-row grid. Still, this code is left, mostly + # for backcompat. + rows, cols = ss.get_gridspec().get_geometry() + div_row, mod_row = divmod(max_nrows, rows) + div_col, mod_col = divmod(max_ncols, cols) + if mod_row != 0: + _api.warn_external('tight_layout not applied: number of rows ' + 'in subplot specifications must be ' + 'multiples of one another.') + return {} + if mod_col != 0: + _api.warn_external('tight_layout not applied: number of ' + 'columns in subplot specifications must be ' + 'multiples of one another.') + return {} + span_pairs.append(( + slice(ss.rowspan.start * div_row, ss.rowspan.stop * div_row), + slice(ss.colspan.start * div_col, ss.colspan.stop * div_col))) + + kwargs = _auto_adjust_subplotpars(fig, renderer, + shape=(max_nrows, max_ncols), + span_pairs=span_pairs, + subplot_list=subplot_list, + ax_bbox_list=ax_bbox_list, + pad=pad, h_pad=h_pad, w_pad=w_pad) + + # kwargs can be none if tight_layout fails... + if rect is not None and kwargs is not None: + # if rect is given, the whole subplots area (including + # labels) will fit into the rect instead of the + # figure. Note that the rect argument of + # *auto_adjust_subplotpars* specify the area that will be + # covered by the total area of axes.bbox. Thus we call + # auto_adjust_subplotpars twice, where the second run + # with adjusted rect parameters. + + left, bottom, right, top = rect + if left is not None: + left += kwargs["left"] + if bottom is not None: + bottom += kwargs["bottom"] + if right is not None: + right -= (1 - kwargs["right"]) + if top is not None: + top -= (1 - kwargs["top"]) + + kwargs = _auto_adjust_subplotpars(fig, renderer, + shape=(max_nrows, max_ncols), + span_pairs=span_pairs, + subplot_list=subplot_list, + ax_bbox_list=ax_bbox_list, + pad=pad, h_pad=h_pad, w_pad=w_pad, + rect=(left, bottom, right, top)) + + return kwargs diff --git a/lib/matplotlib/_type1font.py b/lib/matplotlib/_type1font.py new file mode 100644 index 000000000000..0413cb0016a0 --- /dev/null +++ b/lib/matplotlib/_type1font.py @@ -0,0 +1,877 @@ +""" +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. + +Usage:: + + font = Type1Font(filename) + clear_part, encrypted_part, finale = font.parts + slanted_font = font.transform({'slant': 0.167}) + extended_font = font.transform({'extend': 1.2}) + +Sources: + +* Adobe Technical Note #5040, Supporting Downloadable PostScript + Language Fonts. + +* Adobe Type 1 Font Format, Adobe Systems Incorporated, third printing, + v1.1, 1993. ISBN 0-201-57044-0. +""" + +import binascii +import functools +import logging +import re +import string +import struct + +import numpy as np + +from matplotlib.cbook import _format_approx +from . import _api + +_log = logging.getLogger(__name__) + + +class _Token: + """ + A token in a PostScript stream. + + Attributes + ---------- + pos : int + Position, i.e. offset from the beginning of the data. + raw : str + Raw text of the token. + kind : str + Description of the token (for debugging or testing). + """ + __slots__ = ('pos', 'raw') + kind = '?' + + def __init__(self, pos, raw): + _log.debug('type1font._Token %s at %d: %r', self.kind, pos, raw) + self.pos = pos + self.raw = raw + + def __str__(self): + return f"<{self.kind} {self.raw} @{self.pos}>" + + def endpos(self): + """Position one past the end of the token""" + return self.pos + len(self.raw) + + def is_keyword(self, *names): + """Is this a name token with one of the names?""" + return False + + def is_slash_name(self): + """Is this a name token that starts with a slash?""" + return False + + def is_delim(self): + """Is this a delimiter token?""" + return False + + def is_number(self): + """Is this a number token?""" + return False + + def value(self): + return self.raw + + +class _NameToken(_Token): + kind = 'name' + + def is_slash_name(self): + return self.raw.startswith('/') + + def value(self): + return self.raw[1:] + + +class _BooleanToken(_Token): + kind = 'boolean' + + def value(self): + return self.raw == 'true' + + +class _KeywordToken(_Token): + kind = 'keyword' + + def is_keyword(self, *names): + return self.raw in names + + +class _DelimiterToken(_Token): + kind = 'delimiter' + + def is_delim(self): + return True + + def opposite(self): + return {'[': ']', ']': '[', + '{': '}', '}': '{', + '<<': '>>', '>>': '<<' + }[self.raw] + + +class _WhitespaceToken(_Token): + kind = 'whitespace' + + +class _StringToken(_Token): + kind = 'string' + _escapes_re = re.compile(r'\\([\\()nrtbf]|[0-7]{1,3})') + _replacements = {'\\': '\\', '(': '(', ')': ')', 'n': '\n', + 'r': '\r', 't': '\t', 'b': '\b', 'f': '\f'} + _ws_re = re.compile('[\0\t\r\f\n ]') + + @classmethod + def _escape(cls, match): + group = match.group(1) + try: + return cls._replacements[group] + except KeyError: + return chr(int(group, 8)) + + @functools.lru_cache() + def value(self): + if self.raw[0] == '(': + return self._escapes_re.sub(self._escape, self.raw[1:-1]) + else: + data = self._ws_re.sub('', self.raw[1:-1]) + if len(data) % 2 == 1: + data += '0' + return binascii.unhexlify(data) + + +class _BinaryToken(_Token): + kind = 'binary' + + def value(self): + return self.raw[1:] + + +class _NumberToken(_Token): + kind = 'number' + + def is_number(self): + return True + + def value(self): + if '.' not in self.raw: + return int(self.raw) + else: + return float(self.raw) + + +def _tokenize(data: bytes, skip_ws: bool): + """ + A generator that produces _Token instances from Type-1 font code. + + The consumer of the generator may send an integer to the tokenizer to + indicate that the next token should be _BinaryToken of the given length. + + Parameters + ---------- + data : bytes + The data of the font to tokenize. + + skip_ws : bool + If true, the generator will drop any _WhitespaceTokens from the output. + """ + + text = data.decode('ascii', 'replace') + whitespace_or_comment_re = re.compile(r'[\0\t\r\f\n ]+|%[^\r\n]*') + token_re = re.compile(r'/{0,2}[^]\0\t\r\f\n ()<>{}/%[]+') + instring_re = re.compile(r'[()\\]') + hex_re = re.compile(r'^<[0-9a-fA-F\0\t\r\f\n ]*>$') + oct_re = re.compile(r'[0-7]{1,3}') + pos = 0 + next_binary = None + + while pos < len(text): + if next_binary is not None: + n = next_binary + next_binary = (yield _BinaryToken(pos, data[pos:pos+n])) + pos += n + continue + match = whitespace_or_comment_re.match(text, pos) + if match: + if not skip_ws: + next_binary = (yield _WhitespaceToken(pos, match.group())) + pos = match.end() + elif text[pos] == '(': + # PostScript string rules: + # - parentheses must be balanced + # - backslashes escape backslashes and parens + # - also codes \n\r\t\b\f and octal escapes are recognized + # - other backslashes do not escape anything + start = pos + pos += 1 + depth = 1 + while depth: + match = instring_re.search(text, pos) + if match is None: + raise ValueError( + f'Unterminated string starting at {start}') + pos = match.end() + if match.group() == '(': + depth += 1 + elif match.group() == ')': + depth -= 1 + else: # a backslash + char = text[pos] + if char in r'\()nrtbf': + pos += 1 + else: + octal = oct_re.match(text, pos) + if octal: + pos = octal.end() + else: + pass # non-escaping backslash + next_binary = (yield _StringToken(start, text[start:pos])) + elif text[pos:pos + 2] in ('<<', '>>'): + next_binary = (yield _DelimiterToken(pos, text[pos:pos + 2])) + pos += 2 + elif text[pos] == '<': + start = pos + try: + pos = text.index('>', pos) + 1 + except ValueError as e: + raise ValueError(f'Unterminated hex string starting at {start}' + ) from e + if not hex_re.match(text[start:pos]): + raise ValueError(f'Malformed hex string starting at {start}') + next_binary = (yield _StringToken(pos, text[start:pos])) + else: + match = token_re.match(text, pos) + if match: + raw = match.group() + if raw.startswith('/'): + next_binary = (yield _NameToken(pos, raw)) + elif match.group() in ('true', 'false'): + next_binary = (yield _BooleanToken(pos, raw)) + else: + try: + float(raw) + next_binary = (yield _NumberToken(pos, raw)) + except ValueError: + next_binary = (yield _KeywordToken(pos, raw)) + pos = match.end() + else: + next_binary = (yield _DelimiterToken(pos, text[pos])) + pos += 1 + + +class _BalancedExpression(_Token): + pass + + +def _expression(initial, tokens, data): + """ + Consume some number of tokens and return a balanced PostScript expression. + + Parameters + ---------- + initial : _Token + The token that triggered parsing a balanced expression. + tokens : iterator of _Token + Following tokens. + data : bytes + Underlying data that the token positions point to. + + Returns + ------- + _BalancedExpression + """ + delim_stack = [] + token = initial + while True: + if token.is_delim(): + if token.raw in ('[', '{'): + delim_stack.append(token) + elif token.raw in (']', '}'): + if not delim_stack: + raise RuntimeError(f"unmatched closing token {token}") + match = delim_stack.pop() + if match.raw != token.opposite(): + raise RuntimeError( + f"opening token {match} closed by {token}" + ) + if not delim_stack: + break + else: + raise RuntimeError(f'unknown delimiter {token}') + elif not delim_stack: + break + token = next(tokens) + return _BalancedExpression( + initial.pos, + data[initial.pos:token.endpos()].decode('ascii', 'replace') + ) + + +class Type1Font: + """ + A class representing a Type-1 font, for use by backends. + + Attributes + ---------- + parts : tuple + A 3-tuple of the cleartext part, the encrypted part, and the finale of + zeros. + + decrypted : bytes + The decrypted form of ``parts[1]``. + + prop : dict[str, Any] + A dictionary of font properties. Noteworthy keys include: + + - FontName: PostScript name of the font + - Encoding: dict from numeric codes to glyph names + - FontMatrix: bytes object encoding a matrix + - UniqueID: optional font identifier, dropped when modifying the font + - CharStrings: dict from glyph names to byte code + - Subrs: array of byte code subroutines + - OtherSubrs: bytes object encoding some PostScript code + """ + __slots__ = ('parts', 'decrypted', 'prop', '_pos', '_abbr') + # the _pos dict contains (begin, end) indices to parts[0] + decrypted + # so that they can be replaced when transforming the font; + # but since sometimes a definition appears in both parts[0] and decrypted, + # _pos[name] is an array of such pairs + # + # _abbr maps three standard abbreviations to their particular names in + # this font (e.g. 'RD' is named '-|' in some fonts) + + def __init__(self, input): + """ + Initialize a Type-1 font. + + Parameters + ---------- + input : str or 3-tuple + Either a pfb file name, or a 3-tuple of already-decoded Type-1 + font `~.Type1Font.parts`. + """ + if isinstance(input, tuple) and len(input) == 3: + self.parts = input + else: + with open(input, 'rb') as file: + data = self._read(file) + self.parts = self._split(data) + + self.decrypted = self._decrypt(self.parts[1], 'eexec') + self._abbr = {'RD': 'RD', 'ND': 'ND', 'NP': 'NP'} + self._parse() + + def _read(self, file): + """Read the font from a file, decoding into usable parts.""" + rawdata = file.read() + if not rawdata.startswith(b'\x80'): + return rawdata + + data = b'' + while rawdata: + if not rawdata.startswith(b'\x80'): + raise RuntimeError('Broken pfb file (expected byte 128, ' + 'got %d)' % rawdata[0]) + type = rawdata[1] + if type in (1, 2): + length, = struct.unpack('> 8)) + key = ((key+byte) * 52845 + 22719) & 0xffff + + return bytes(plaintext[ndiscard:]) + + @staticmethod + def _encrypt(plaintext, key, ndiscard=4): + """ + Encrypt plaintext using the Type-1 font algorithm. + + The algorithm is described in Adobe's "Adobe Type 1 Font Format". + The key argument can be an integer, or one of the strings + 'eexec' and 'charstring', which map to the key specified for the + corresponding part of Type-1 fonts. + + The ndiscard argument should be an integer, usually 4. That + number of bytes is prepended to the plaintext before encryption. + This function prepends NUL bytes for reproducibility, even though + the original algorithm uses random bytes, presumably to avoid + cryptanalysis. + """ + + key = _api.check_getitem({'eexec': 55665, 'charstring': 4330}, key=key) + ciphertext = [] + for byte in b'\0' * ndiscard + plaintext: + c = byte ^ (key >> 8) + ciphertext.append(c) + key = ((key + c) * 52845 + 22719) & 0xffff + + return bytes(ciphertext) + + def _parse(self): + """ + Find the values of various font properties. This limited kind + of parsing is described in Chapter 10 "Adobe Type Manager + Compatibility" of the Type-1 spec. + """ + # Start with reasonable defaults + prop = {'Weight': 'Regular', 'ItalicAngle': 0.0, 'isFixedPitch': False, + 'UnderlinePosition': -100, 'UnderlineThickness': 50} + pos = {} + data = self.parts[0] + self.decrypted + + source = _tokenize(data, True) + while True: + # See if there is a key to be assigned a value + # e.g. /FontName in /FontName /Helvetica def + try: + token = next(source) + except StopIteration: + break + if token.is_delim(): + # skip over this - we want top-level keys only + _expression(token, source, data) + if token.is_slash_name(): + key = token.value() + keypos = token.pos + else: + continue + + # Some values need special parsing + if key in ('Subrs', 'CharStrings', 'Encoding', 'OtherSubrs'): + prop[key], endpos = { + 'Subrs': self._parse_subrs, + 'CharStrings': self._parse_charstrings, + 'Encoding': self._parse_encoding, + 'OtherSubrs': self._parse_othersubrs + }[key](source, data) + pos.setdefault(key, []).append((keypos, endpos)) + continue + + try: + token = next(source) + except StopIteration: + break + + if isinstance(token, _KeywordToken): + # constructs like + # FontDirectory /Helvetica known {...} {...} ifelse + # mean the key was not really a key + continue + + if token.is_delim(): + value = _expression(token, source, data).raw + else: + value = token.value() + + # look for a 'def' possibly preceded by access modifiers + try: + kw = next( + kw for kw in source + if not kw.is_keyword('readonly', 'noaccess', 'executeonly') + ) + except StopIteration: + break + + # sometimes noaccess def and readonly def are abbreviated + if kw.is_keyword('def', self._abbr['ND'], self._abbr['NP']): + prop[key] = value + pos.setdefault(key, []).append((keypos, kw.endpos())) + + # detect the standard abbreviations + if value == '{noaccess def}': + self._abbr['ND'] = key + elif value == '{noaccess put}': + self._abbr['NP'] = key + elif value == '{string currentfile exch readstring pop}': + self._abbr['RD'] = key + + # Fill in the various *Name properties + if 'FontName' not in prop: + prop['FontName'] = (prop.get('FullName') or + prop.get('FamilyName') or + 'Unknown') + if 'FullName' not in prop: + prop['FullName'] = prop['FontName'] + if 'FamilyName' not in prop: + extras = ('(?i)([ -](regular|plain|italic|oblique|(semi)?bold|' + '(ultra)?light|extra|condensed))+$') + prop['FamilyName'] = re.sub(extras, '', prop['FullName']) + # Decrypt the encrypted parts + ndiscard = prop.get('lenIV', 4) + cs = prop['CharStrings'] + for key, value in cs.items(): + cs[key] = self._decrypt(value, 'charstring', ndiscard) + if 'Subrs' in prop: + prop['Subrs'] = [ + self._decrypt(value, 'charstring', ndiscard) + for value in prop['Subrs'] + ] + + self.prop = prop + self._pos = pos + + def _parse_subrs(self, tokens, _data): + count_token = next(tokens) + if not count_token.is_number(): + raise RuntimeError( + f"Token following /Subrs must be a number, was {count_token}" + ) + count = count_token.value() + array = [None] * count + next(t for t in tokens if t.is_keyword('array')) + for _ in range(count): + next(t for t in tokens if t.is_keyword('dup')) + index_token = next(tokens) + if not index_token.is_number(): + raise RuntimeError( + "Token following dup in Subrs definition must be a " + f"number, was {index_token}" + ) + nbytes_token = next(tokens) + if not nbytes_token.is_number(): + raise RuntimeError( + "Second token following dup in Subrs definition must " + f"be a number, was {nbytes_token}" + ) + token = next(tokens) + if not token.is_keyword(self._abbr['RD']): + raise RuntimeError( + f"Token preceding subr must be {self._abbr['RD']}, " + f"was {token}" + ) + binary_token = tokens.send(1+nbytes_token.value()) + array[index_token.value()] = binary_token.value() + + return array, next(tokens).endpos() + + @staticmethod + def _parse_charstrings(tokens, _data): + count_token = next(tokens) + if not count_token.is_number(): + raise RuntimeError( + "Token following /CharStrings must be a number, " + f"was {count_token}" + ) + count = count_token.value() + charstrings = {} + next(t for t in tokens if t.is_keyword('begin')) + while True: + token = next(t for t in tokens + if t.is_keyword('end') or t.is_slash_name()) + if token.raw == 'end': + return charstrings, token.endpos() + glyphname = token.value() + nbytes_token = next(tokens) + if not nbytes_token.is_number(): + raise RuntimeError( + f"Token following /{glyphname} in CharStrings definition " + f"must be a number, was {nbytes_token}" + ) + next(tokens) # usually RD or |- + binary_token = tokens.send(1+nbytes_token.value()) + charstrings[glyphname] = binary_token.value() + + @staticmethod + def _parse_encoding(tokens, _data): + # this only works for encodings that follow the Adobe manual + # but some old fonts include non-compliant data - we log a warning + # and return a possibly incomplete encoding + encoding = {} + while True: + token = next(t for t in tokens + if t.is_keyword('StandardEncoding', 'dup', 'def')) + if token.is_keyword('StandardEncoding'): + return _StandardEncoding, token.endpos() + if token.is_keyword('def'): + return encoding, token.endpos() + index_token = next(tokens) + if not index_token.is_number(): + _log.warning( + f"Parsing encoding: expected number, got {index_token}" + ) + continue + name_token = next(tokens) + if not name_token.is_slash_name(): + _log.warning( + f"Parsing encoding: expected slash-name, got {name_token}" + ) + continue + encoding[index_token.value()] = name_token.value() + + @staticmethod + def _parse_othersubrs(tokens, data): + init_pos = None + while True: + token = next(tokens) + if init_pos is None: + init_pos = token.pos + if token.is_delim(): + _expression(token, tokens, data) + elif token.is_keyword('def', 'ND', '|-'): + return data[init_pos:token.endpos()], token.endpos() + + def transform(self, effects): + """ + Return a new font that is slanted and/or extended. + + Parameters + ---------- + effects : dict + A dict with optional entries: + + - 'slant' : float, default: 0 + Tangent of the angle that the font is to be slanted to the + right. Negative values slant to the left. + - 'extend' : float, default: 1 + Scaling factor for the font width. Values less than 1 condense + the glyphs. + + Returns + ------- + `Type1Font` + """ + fontname = self.prop['FontName'] + italicangle = self.prop['ItalicAngle'] + + array = [ + float(x) for x in (self.prop['FontMatrix'] + .lstrip('[').rstrip(']').split()) + ] + oldmatrix = np.eye(3, 3) + oldmatrix[0:3, 0] = array[::2] + oldmatrix[0:3, 1] = array[1::2] + modifier = np.eye(3, 3) + + if 'slant' in effects: + slant = effects['slant'] + fontname += '_Slant_%d' % int(1000 * slant) + italicangle = round( + float(italicangle) - np.arctan(slant) / np.pi * 180, + 5 + ) + modifier[1, 0] = slant + + if 'extend' in effects: + extend = effects['extend'] + fontname += '_Extend_%d' % int(1000 * extend) + modifier[0, 0] = extend + + newmatrix = np.dot(modifier, oldmatrix) + array[::2] = newmatrix[0:3, 0] + array[1::2] = newmatrix[0:3, 1] + fontmatrix = ( + '[%s]' % ' '.join(_format_approx(x, 6) for x in array) + ) + replacements = ( + [(x, '/FontName/%s def' % fontname) + for x in self._pos['FontName']] + + [(x, '/ItalicAngle %a def' % italicangle) + for x in self._pos['ItalicAngle']] + + [(x, '/FontMatrix %s readonly def' % fontmatrix) + for x in self._pos['FontMatrix']] + + [(x, '') for x in self._pos.get('UniqueID', [])] + ) + + data = bytearray(self.parts[0]) + data.extend(self.decrypted) + len0 = len(self.parts[0]) + for (pos0, pos1), value in sorted(replacements, reverse=True): + data[pos0:pos1] = value.encode('ascii', 'replace') + if pos0 < len(self.parts[0]): + if pos1 >= len(self.parts[0]): + raise RuntimeError( + f"text to be replaced with {value} spans " + "the eexec boundary" + ) + len0 += len(value) - pos1 + pos0 + + data = bytes(data) + return Type1Font(( + data[:len0], + self._encrypt(data[len0:], 'eexec'), + self.parts[2] + )) + + +_StandardEncoding = { + **{ord(letter): letter for letter in string.ascii_letters}, + 0: '.notdef', + 32: 'space', + 33: 'exclam', + 34: 'quotedbl', + 35: 'numbersign', + 36: 'dollar', + 37: 'percent', + 38: 'ampersand', + 39: 'quoteright', + 40: 'parenleft', + 41: 'parenright', + 42: 'asterisk', + 43: 'plus', + 44: 'comma', + 45: 'hyphen', + 46: 'period', + 47: 'slash', + 48: 'zero', + 49: 'one', + 50: 'two', + 51: 'three', + 52: 'four', + 53: 'five', + 54: 'six', + 55: 'seven', + 56: 'eight', + 57: 'nine', + 58: 'colon', + 59: 'semicolon', + 60: 'less', + 61: 'equal', + 62: 'greater', + 63: 'question', + 64: 'at', + 91: 'bracketleft', + 92: 'backslash', + 93: 'bracketright', + 94: 'asciicircum', + 95: 'underscore', + 96: 'quoteleft', + 123: 'braceleft', + 124: 'bar', + 125: 'braceright', + 126: 'asciitilde', + 161: 'exclamdown', + 162: 'cent', + 163: 'sterling', + 164: 'fraction', + 165: 'yen', + 166: 'florin', + 167: 'section', + 168: 'currency', + 169: 'quotesingle', + 170: 'quotedblleft', + 171: 'guillemotleft', + 172: 'guilsinglleft', + 173: 'guilsinglright', + 174: 'fi', + 175: 'fl', + 177: 'endash', + 178: 'dagger', + 179: 'daggerdbl', + 180: 'periodcentered', + 182: 'paragraph', + 183: 'bullet', + 184: 'quotesinglbase', + 185: 'quotedblbase', + 186: 'quotedblright', + 187: 'guillemotright', + 188: 'ellipsis', + 189: 'perthousand', + 191: 'questiondown', + 193: 'grave', + 194: 'acute', + 195: 'circumflex', + 196: 'tilde', + 197: 'macron', + 198: 'breve', + 199: 'dotaccent', + 200: 'dieresis', + 202: 'ring', + 203: 'cedilla', + 205: 'hungarumlaut', + 206: 'ogonek', + 207: 'caron', + 208: 'emdash', + 225: 'AE', + 227: 'ordfeminine', + 232: 'Lslash', + 233: 'Oslash', + 234: 'OE', + 235: 'ordmasculine', + 241: 'ae', + 245: 'dotlessi', + 248: 'lslash', + 249: 'oslash', + 250: 'oe', + 251: 'germandbls', +} diff --git a/lib/matplotlib/_version.py b/lib/matplotlib/_version.py deleted file mode 100644 index 08dc9b6117f2..000000000000 --- a/lib/matplotlib/_version.py +++ /dev/null @@ -1,460 +0,0 @@ - -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.15 (https://github.com/warner/python-versioneer) - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "$Format:%d$" - git_full = "$Format:%H$" - keywords = {"refnames": git_refnames, "full": git_full} - return keywords - - -class VersioneerConfig: - pass - - -def get_config(): - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "pep440-post" - cfg.tag_prefix = "v" - cfg.parentdir_prefix = "matplotlib-" - cfg.versionfile_source = "lib/matplotlib/_version.py" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - pass - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - def decorate(f): - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - return None - return stdout - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - # Source tarballs conventionally unpack into a directory that includes - # both the project name and a version string. - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with " - "prefix '%s'" % (root, dirname, parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None} - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - if not keywords: - raise NotThisMethod("no keywords at all, weird") - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs-tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None - } - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags"} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - # this runs 'git' from the root of the source tree. This only gets called - # if the git-archive 'subst' keywords were *not* expanded, and - # _version.py hasn't already been rewritten with a short version string, - # meaning we're inside a checked out source tree. - - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %s" % root) - raise NotThisMethod("no .git directory") - - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - # if there is a tag, this yields TAG-NUM-gHEX[-dirty] - # if there are no tags, this yields HEX[-dirty] (no NUM) - describe_out = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long"], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - return pieces - - -def plus_or_dot(pieces): - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - # now build up version string, with post-release "local version - # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - # exceptions: - # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - # TAG[.post.devDISTANCE] . No -dirty - - # exceptions: - # 1: no tags. 0.post.devDISTANCE - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that - # .dev0 sorts backwards (a dirty tree will appear "older" than the - # corresponding clean one), but you shouldn't be releasing software with - # -dirty anyways. - - # exceptions: - # 1: no tags. 0.postDISTANCE[.dev0] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - - # exceptions: - # 1: no tags. 0.postDISTANCE[.dev0] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty - # --always' - - # exceptions: - # 1: no tags. HEX[-dirty] (note: no 'g' prefix) - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty - # --always -long'. The distance/hash is unconditional. - - # exceptions: - # 1: no tags. HEX[-dirty] (note: no 'g' prefix) - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"]} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None} - - -def get_versions(): - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for _ in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree"} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version"} diff --git a/lib/matplotlib/afm.py b/lib/matplotlib/afm.py index 0106664f5ccc..d95c88a0e2b4 100644 --- a/lib/matplotlib/afm.py +++ b/lib/matplotlib/afm.py @@ -1,532 +1,3 @@ -""" -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 has no external dependencies: - ->>> import matplotlib as mpl ->>> from pathlib import Path ->>> afm_path = Path(mpl.get_data_path(), 'fonts', 'afm', 'ptmr8a.afm') ->>> ->>> from matplotlib.afm import AFM ->>> with afm_path.open('rb') as fh: -... afm = AFM(fh) ->>> afm.string_width_height('What the heck?') -(6220.0, 694) ->>> afm.get_fontname() -'Times-Roman' ->>> afm.get_kern_dist('A', 'f') -0 ->>> afm.get_kern_dist('A', 'y') --92.0 ->>> afm.get_bbox_char('!') -[130, -9, 238, 676] - -As in the Adobe Font Metrics File Format Specification, all dimensions -are given in units of 1/1000 of the scale factor (point size) of the font -being used. -""" - -from collections import namedtuple -import logging -import re - -from ._mathtext_data import uni2type1 - - -_log = logging.getLogger(__name__) - - -def _to_int(x): - # Some AFM files have floats where we are expecting ints -- there is - # probably a better way to handle this (support floats, round rather than - # truncate). But I don't know what the best approach is now and this - # change to _to_int should at least prevent Matplotlib from crashing on - # these. JDH (2009-11-06) - return int(float(x)) - - -def _to_float(x): - # Some AFM files use "," instead of "." as decimal separator -- this - # shouldn't be ambiguous (unless someone is wicked enough to use "," as - # thousands separator...). - if isinstance(x, bytes): - # Encoding doesn't really matter -- if we have codepoints >127 the call - # to float() will error anyways. - x = x.decode('latin-1') - return float(x.replace(',', '.')) - - -def _to_str(x): - return x.decode('utf8') - - -def _to_list_of_ints(s): - s = s.replace(b',', b' ') - return [_to_int(val) for val in s.split()] - - -def _to_list_of_floats(s): - return [_to_float(val) for val in s.split()] - - -def _to_bool(s): - if s.lower().strip() in (b'false', b'0', b'no'): - return False - else: - return True - - -def _parse_header(fh): - """ - 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.: - - * 'False'->False - * '0'->0 - * '-168 -218 1000 898'-> [-168, -218, 1000, 898] - - Dictionary keys are - - StartFontMetrics, FontName, FullName, FamilyName, Weight, - ItalicAngle, IsFixedPitch, FontBBox, UnderlinePosition, - UnderlineThickness, Version, Notice, EncodingScheme, CapHeight, - XHeight, Ascender, Descender, StartCharMetrics - """ - header_converters = { - b'StartFontMetrics': _to_float, - b'FontName': _to_str, - b'FullName': _to_str, - b'FamilyName': _to_str, - b'Weight': _to_str, - b'ItalicAngle': _to_float, - b'IsFixedPitch': _to_bool, - b'FontBBox': _to_list_of_ints, - b'UnderlinePosition': _to_float, - b'UnderlineThickness': _to_float, - b'Version': _to_str, - # Some AFM files have non-ASCII characters (which are not allowed by - # the spec). Given that there is actually no public API to even access - # this field, just return it as straight bytes. - b'Notice': lambda x: x, - b'EncodingScheme': _to_str, - b'CapHeight': _to_float, # Is the second version a mistake, or - b'Capheight': _to_float, # do some AFM files contain 'Capheight'? -JKS - b'XHeight': _to_float, - b'Ascender': _to_float, - b'Descender': _to_float, - b'StdHW': _to_float, - b'StdVW': _to_float, - b'StartCharMetrics': _to_int, - b'CharacterSet': _to_str, - b'Characters': _to_int, - } - d = {} - first_line = True - for line in fh: - line = line.rstrip() - if line.startswith(b'Comment'): - continue - lst = line.split(b' ', 1) - key = lst[0] - if first_line: - # AFM spec, Section 4: The StartFontMetrics keyword - # [followed by a version number] must be the first line in - # the file, and the EndFontMetrics keyword must be the - # last non-empty line in the file. We just check the - # first header entry. - if key != b'StartFontMetrics': - raise RuntimeError('Not an AFM file') - first_line = False - if len(lst) == 2: - val = lst[1] - else: - val = b'' - try: - converter = header_converters[key] - except KeyError: - _log.error('Found an unknown keyword in AFM header (was %r)' % key) - continue - try: - d[key] = converter(val) - except ValueError: - _log.error('Value error parsing header in AFM: %s, %s', key, val) - continue - if key == b'StartCharMetrics': - break - else: - raise RuntimeError('Bad parse') - return d - - -CharMetrics = namedtuple('CharMetrics', 'width, name, bbox') -CharMetrics.__doc__ = """ - Represents the character metrics of a single character. - - Notes - ----- - The fields do currently only describe a subset of character metrics - information defined in the AFM standard. - """ -CharMetrics.width.__doc__ = """The character width (WX).""" -CharMetrics.name.__doc__ = """The character name (N).""" -CharMetrics.bbox.__doc__ = """ - The bbox of the character (B) as a tuple (*llx*, *lly*, *urx*, *ury*).""" - - -def _parse_char_metrics(fh): - """ - Parse the given filehandle for character metrics information and return - the information as dicts. - - It is assumed that the file cursor is on the line behind - 'StartCharMetrics'. - - Returns - ------- - ascii_d : dict - A mapping "ASCII num of the character" to `.CharMetrics`. - name_d : dict - A mapping "character name" to `.CharMetrics`. - - Notes - ----- - This function is incomplete per the standard, but thus far parses - all the sample afm files tried. - """ - required_keys = {'C', 'WX', 'N', 'B'} - - ascii_d = {} - name_d = {} - for line in fh: - # We are defensively letting values be utf8. The spec requires - # ascii, but there are non-compliant fonts in circulation - line = _to_str(line.rstrip()) # Convert from byte-literal - if line.startswith('EndCharMetrics'): - return ascii_d, name_d - # Split the metric line into a dictionary, keyed by metric identifiers - vals = dict(s.strip().split(' ', 1) for s in line.split(';') if s) - # There may be other metrics present, but only these are needed - if not required_keys.issubset(vals): - raise RuntimeError('Bad char metrics line: %s' % line) - num = _to_int(vals['C']) - wx = _to_float(vals['WX']) - name = vals['N'] - bbox = _to_list_of_floats(vals['B']) - bbox = list(map(int, bbox)) - metrics = CharMetrics(wx, name, bbox) - # Workaround: If the character name is 'Euro', give it the - # corresponding character code, according to WinAnsiEncoding (see PDF - # Reference). - if name == 'Euro': - num = 128 - elif name == 'minus': - num = ord("\N{MINUS SIGN}") # 0x2212 - if num != -1: - ascii_d[num] = metrics - name_d[name] = metrics - raise RuntimeError('Bad parse') - - -def _parse_kern_pairs(fh): - """ - Return a kern pairs dictionary; keys are (*char1*, *char2*) tuples and - values are the kern pair value. For example, a kern pairs line like - ``KPX A y -50`` - - will be represented as:: - - d[ ('A', 'y') ] = -50 - - """ - - line = next(fh) - if not line.startswith(b'StartKernPairs'): - raise RuntimeError('Bad start of kern pairs data: %s' % line) - - d = {} - for line in fh: - line = line.rstrip() - if not line: - continue - if line.startswith(b'EndKernPairs'): - next(fh) # EndKernData - return d - vals = line.split() - if len(vals) != 4 or vals[0] != b'KPX': - raise RuntimeError('Bad kern pairs line: %s' % line) - c1, c2, val = _to_str(vals[1]), _to_str(vals[2]), _to_float(vals[3]) - d[(c1, c2)] = val - raise RuntimeError('Bad kern pairs parse') - - -CompositePart = namedtuple('CompositePart', 'name, dx, dy') -CompositePart.__doc__ = """ - Represents the information on a composite element of a composite char.""" -CompositePart.name.__doc__ = """Name of the part, e.g. 'acute'.""" -CompositePart.dx.__doc__ = """x-displacement of the part from the origin.""" -CompositePart.dy.__doc__ = """y-displacement of the part from the origin.""" - - -def _parse_composites(fh): - """ - Parse the given filehandle for composites information return them as a - dict. - - It is assumed that the file cursor is on the line behind 'StartComposites'. - - Returns - ------- - dict - A dict mapping composite character names to a parts list. The parts - list is a list of `.CompositePart` entries describing the parts of - the composite. - - Examples - -------- - A composite definition line:: - - CC Aacute 2 ; PCC A 0 0 ; PCC acute 160 170 ; - - will be represented as:: - - composites['Aacute'] = [CompositePart(name='A', dx=0, dy=0), - CompositePart(name='acute', dx=160, dy=170)] - - """ - composites = {} - for line in fh: - line = line.rstrip() - if not line: - continue - if line.startswith(b'EndComposites'): - return composites - vals = line.split(b';') - cc = vals[0].split() - name, numParts = cc[1], _to_int(cc[2]) - pccParts = [] - for s in vals[1:-1]: - pcc = s.split() - part = CompositePart(pcc[1], _to_float(pcc[2]), _to_float(pcc[3])) - pccParts.append(part) - composites[name] = pccParts - - raise RuntimeError('Bad composites parse') - - -def _parse_optional(fh): - """ - Parse the optional fields for kern pair data and composites. - - Returns - ------- - kern_data : dict - A dict containing kerning information. May be empty. - See `._parse_kern_pairs`. - composites : dict - A dict containing composite information. May be empty. - See `._parse_composites`. - """ - optional = { - b'StartKernData': _parse_kern_pairs, - b'StartComposites': _parse_composites, - } - - d = {b'StartKernData': {}, - b'StartComposites': {}} - for line in fh: - line = line.rstrip() - if not line: - continue - key = line.split()[0] - - if key in optional: - d[key] = optional[key](fh) - - return d[b'StartKernData'], d[b'StartComposites'] - - -class AFM: - - def __init__(self, fh): - """Parse the AFM file in file object *fh*.""" - self._header = _parse_header(fh) - self._metrics, self._metrics_by_name = _parse_char_metrics(fh) - self._kern, self._composite = _parse_optional(fh) - - def get_bbox_char(self, c, isord=False): - if not isord: - c = ord(c) - return self._metrics[c].bbox - - def string_width_height(self, s): - """ - Return the string width (including kerning) and string height - as a (*w*, *h*) tuple. - """ - if not len(s): - return 0, 0 - total_width = 0 - namelast = None - miny = 1e9 - maxy = 0 - for c in s: - if c == '\n': - continue - wx, name, bbox = self._metrics[ord(c)] - - total_width += wx + self._kern.get((namelast, name), 0) - l, b, w, h = bbox - miny = min(miny, b) - maxy = max(maxy, b + h) - - namelast = name - - return total_width, maxy - miny - - def get_str_bbox_and_descent(self, s): - """Return the string bounding box and the maximal descent.""" - if not len(s): - return 0, 0, 0, 0, 0 - total_width = 0 - namelast = None - miny = 1e9 - maxy = 0 - left = 0 - if not isinstance(s, str): - s = _to_str(s) - for c in s: - if c == '\n': - continue - name = uni2type1.get(ord(c), f"uni{ord(c):04X}") - try: - wx, _, bbox = self._metrics_by_name[name] - except KeyError: - name = 'question' - wx, _, bbox = self._metrics_by_name[name] - total_width += wx + self._kern.get((namelast, name), 0) - l, b, w, h = bbox - left = min(left, l) - miny = min(miny, b) - maxy = max(maxy, b + h) - - namelast = name - - return left, miny, total_width, maxy - miny, -miny - - def get_str_bbox(self, s): - """Return the string bounding box.""" - return self.get_str_bbox_and_descent(s)[:4] - - def get_name_char(self, c, isord=False): - """Get the name of the character, i.e., ';' is 'semicolon'.""" - if not isord: - c = ord(c) - return self._metrics[c].name - - def get_width_char(self, c, isord=False): - """ - Get the width of the character from the character metric WX field. - """ - if not isord: - c = ord(c) - return self._metrics[c].width - - def get_width_from_char_name(self, name): - """Get the width of the character from a type1 character name.""" - return self._metrics_by_name[name].width - - def get_height_char(self, c, isord=False): - """Get the bounding box (ink) height of character *c* (space is 0).""" - if not isord: - c = ord(c) - return self._metrics[c].bbox[-1] - - def get_kern_dist(self, c1, c2): - """ - Return the kerning pair distance (possibly 0) for chars *c1* and *c2*. - """ - name1, name2 = self.get_name_char(c1), self.get_name_char(c2) - return self.get_kern_dist_from_name(name1, name2) - - def get_kern_dist_from_name(self, name1, name2): - """ - Return the kerning pair distance (possibly 0) for chars - *name1* and *name2*. - """ - return self._kern.get((name1, name2), 0) - - def get_fontname(self): - """Return the font name, e.g., 'Times-Roman'.""" - return self._header[b'FontName'] - - @property - def postscript_name(self): # For consistency with FT2Font. - return self.get_fontname() - - def get_fullname(self): - """Return the font full name, e.g., 'Times-Roman'.""" - name = self._header.get(b'FullName') - if name is None: # use FontName as a substitute - name = self._header[b'FontName'] - return name - - def get_familyname(self): - """Return the font family name, e.g., 'Times'.""" - name = self._header.get(b'FamilyName') - if name is not None: - return name - - # FamilyName not specified so we'll make a guess - name = self.get_fullname() - extras = (r'(?i)([ -](regular|plain|italic|oblique|bold|semibold|' - r'light|ultralight|extra|condensed))+$') - return re.sub(extras, '', name) - - @property - def family_name(self): - """The font family name, e.g., 'Times'.""" - return self.get_familyname() - - def get_weight(self): - """Return the font weight, e.g., 'Bold' or 'Roman'.""" - return self._header[b'Weight'] - - def get_angle(self): - """Return the fontangle as float.""" - return self._header[b'ItalicAngle'] - - def get_capheight(self): - """Return the cap height as float.""" - return self._header[b'CapHeight'] - - def get_xheight(self): - """Return the xheight as float.""" - return self._header[b'XHeight'] - - def get_underline_thickness(self): - """Return the underline thickness as float.""" - return self._header[b'UnderlineThickness'] - - def get_horizontal_stem_width(self): - """ - Return the standard horizontal stem width as float, or *None* if - not specified in AFM file. - """ - return self._header.get(b'StdHW', None) - - def get_vertical_stem_width(self): - """ - Return the standard vertical stem width as float, or *None* if - not specified in AFM file. - """ - return self._header.get(b'StdVW', None) +from matplotlib._afm import * # noqa: F401, F403 +from matplotlib import _api +_api.warn_deprecated("3.6", name=__name__, obj_type="module") diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index af9b9b82b3e0..2d8156a51599 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1,7 +1,8 @@ # TODO: # * Documentation -- this will need a new section of the User's Guide. # Both for Animations and just timers. -# - Also need to update http://www.scipy.org/Cookbook/Matplotlib/Animations +# - Also need to update +# https://scipy-cookbook.readthedocs.io/items/Matplotlib_Animations.html # * Blit # * Currently broken with Qt4 for widgets that don't start on screen # * Still a few edge cases that aren't working correctly @@ -16,6 +17,7 @@ # * Can blit be enabled for movies? # * Need to consider event sources to allow clicking through multiple figures + import abc import base64 import contextlib @@ -37,26 +39,21 @@ from matplotlib._animation_data import ( DISPLAY_TEMPLATE, INCLUDED_FRAMES, JS_INCLUDE, STYLE_INCLUDE) from matplotlib import _api, cbook - +import matplotlib.colors as mcolors _log = logging.getLogger(__name__) # Process creation flag for subprocess to prevent it raising a terminal -# window. See for example: -# https://stackoverflow.com/questions/24130623/using-python-subprocess-popen-cant-prevent-exe-stopped-working-prompt -if sys.platform == 'win32': - subprocess_creation_flags = CREATE_NO_WINDOW = 0x08000000 -else: - # Apparently None won't work here - subprocess_creation_flags = 0 +# window. See for example https://stackoverflow.com/q/24130623/ +subprocess_creation_flags = ( + subprocess.CREATE_NO_WINDOW if sys.platform == 'win32' else 0) # Other potential writing methods: # * http://pymedia.org/ # * libming (produces swf) python wrappers: https://github.com/libming/libming # * Wrap x264 API: -# (http://stackoverflow.com/questions/2940671/ -# how-to-encode-series-of-images-into-h264-using-x264-api-c-c ) +# (https://stackoverflow.com/q/2940671/) def adjusted_figsize(w, h, dpi, n): @@ -156,19 +153,17 @@ def __getitem__(self, name): class AbstractMovieWriter(abc.ABC): """ - Abstract base class for writing movies. Fundamentally, what a MovieWriter - does is provide is a way to grab frames by calling grab_frame(). + Abstract base class for writing movies, providing a way to grab frames by + calling `~AbstractMovieWriter.grab_frame`. - setup() is called to start the process and finish() is called afterwards. - - This class is set up to provide for writing movie frame data to a pipe. - saving() is provided as a context manager to facilitate this process as:: + `setup` is called to start the process and `finish` is called afterwards. + `saving` is provided as a context manager to facilitate this process as :: with moviewriter.saving(fig, outfile='myfile.mp4', dpi=100): # Iterate over frames moviewriter.grab_frame(**savefig_kwargs) - The use of the context manager ensures that setup() and finish() are + The use of the context manager ensures that `setup` and `finish` are performed as necessary. An instance of a concrete subclass of this class can be given as the @@ -198,6 +193,8 @@ def setup(self, fig, outfile, dpi=None): The DPI (or resolution) for the file. This controls the size in pixels of the resulting movie file. """ + # Check that path is valid + Path(outfile).parent.resolve(strict=True) self.outfile = outfile self.fig = fig if dpi is None: @@ -252,7 +249,7 @@ class MovieWriter(AbstractMovieWriter): The format used in writing frame data, defaults to 'rgba'. fig : `~matplotlib.figure.Figure` The figure to capture data from. - This must be provided by the sub-classes. + This must be provided by the subclasses. """ # Builtin writer subclasses additionally define the _exec_key and _args_key @@ -261,9 +258,6 @@ class MovieWriter(AbstractMovieWriter): # stored. Third-party writers cannot meaningfully set these as they cannot # extend rcParams with new keys. - exec_key = _api.deprecate_privatize_attribute("3.3") - args_key = _api.deprecate_privatize_attribute("3.3") - # Pipe-based writers only support RGBA, but file-based ones support more # formats. supported_formats = ["rgba"] @@ -339,29 +333,6 @@ def _run(self): def finish(self): """Finish any processing for writing the movie.""" - overridden_cleanup = _api.deprecate_method_override( - __class__.cleanup, self, since="3.4", alternative="finish()") - if overridden_cleanup is not None: - overridden_cleanup() - else: - self._cleanup() # Inline _cleanup() once cleanup() is removed. - - def grab_frame(self, **savefig_kwargs): - # docstring inherited - _log.debug('MovieWriter.grab_frame: Grabbing frame.') - # Readjust the figure size in case it has been changed by the user. - # All frames must have the same size to save the movie correctly. - self.fig.set_size_inches(self._w, self._h) - # Save the figure data to the sink, using the frame format and dpi. - self.fig.savefig(self._proc.stdin, format=self.frame_format, - dpi=self.dpi, **savefig_kwargs) - - def _args(self): - """Assemble list of encoder-specific command-line arguments.""" - return NotImplementedError("args needs to be implemented by subclass.") - - def _cleanup(self): # Inline to finish() once cleanup() is removed. - """Clean-up and collect the process used to write the movie file.""" out, err = self._proc.communicate() # Use the encoding/errors that universal_newlines would use. out = TextIOWrapper(BytesIO(out)).read() @@ -378,9 +349,19 @@ def _cleanup(self): # Inline to finish() once cleanup() is removed. raise subprocess.CalledProcessError( self._proc.returncode, self._proc.args, out, err) - @_api.deprecated("3.4") - def cleanup(self): - self._cleanup() + def grab_frame(self, **savefig_kwargs): + # docstring inherited + _log.debug('MovieWriter.grab_frame: Grabbing frame.') + # Readjust the figure size in case it has been changed by the user. + # All frames must have the same size to save the movie correctly. + self.fig.set_size_inches(self._w, self._h) + # Save the figure data to the sink, using the frame format and dpi. + self.fig.savefig(self._proc.stdin, format=self.frame_format, + dpi=self.dpi, **savefig_kwargs) + + def _args(self): + """Assemble list of encoder-specific command-line arguments.""" + return NotImplementedError("args needs to be implemented by subclass.") @classmethod def bin_path(cls): @@ -407,9 +388,7 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.frame_format = mpl.rcParams['animation.frame_format'] - @_api.delete_parameter("3.3", "clear_temp") - def setup(self, fig, outfile, dpi=None, frame_prefix=None, - clear_temp=True): + def setup(self, fig, outfile, dpi=None, frame_prefix=None): """ Setup for writing the movie file. @@ -423,14 +402,13 @@ def setup(self, fig, outfile, dpi=None, frame_prefix=None, The dpi of the output file. This, with the figure size, controls the size in pixels of the resulting movie file. frame_prefix : str, optional - The filename prefix to use for temporary files. If None (the + The filename prefix to use for temporary files. If *None* (the default), files are written to a temporary directory which is - deleted by `cleanup` (regardless of the value of *clear_temp*). - clear_temp : bool, optional - If the temporary files should be deleted after stitching - the final result. Setting this to ``False`` can be useful for - debugging. Defaults to ``True``. + deleted by `finish`; if not *None*, no temporary files are + deleted. """ + # Check that path is valid + Path(outfile).parent.resolve(strict=True) self.fig = fig self.outfile = outfile if dpi is None: @@ -444,24 +422,14 @@ def setup(self, fig, outfile, dpi=None, frame_prefix=None, else: self._tmpdir = None self.temp_prefix = frame_prefix - self._clear_temp = clear_temp self._frame_counter = 0 # used for generating sequential file names self._temp_paths = list() self.fname_format_str = '%s%%07d.%s' def __del__(self): - if self._tmpdir: + if hasattr(self, '_tmpdir') and self._tmpdir: self._tmpdir.cleanup() - @_api.deprecated("3.3") - @property - def clear_temp(self): - return self._clear_temp - - @clear_temp.setter - def clear_temp(self, value): - self._clear_temp = value - @property def frame_format(self): """ @@ -488,10 +456,9 @@ def _base_temp_name(self): def grab_frame(self, **savefig_kwargs): # docstring inherited - # Overloaded to explicitly close temp file. # Creates a filename for saving using basename and counter. path = Path(self._base_temp_name() % self._frame_counter) - self._temp_paths.append(path) # Record the filename for later cleanup. + self._temp_paths.append(path) # Record the filename for later use. self._frame_counter += 1 # Ensures each created name is unique. _log.debug('FileMovieWriter.grab_frame: Grabbing frame %d to path=%s', self._frame_counter, path) @@ -502,20 +469,15 @@ def grab_frame(self, **savefig_kwargs): def finish(self): # Call run here now that all frame grabbing is done. All temp files # are available to be assembled. - self._run() - super().finish() # Will call clean-up - - def _cleanup(self): # Inline to finish() once cleanup() is removed. - super()._cleanup() - if self._tmpdir: - _log.debug('MovieWriter: clearing temporary path=%s', self._tmpdir) - self._tmpdir.cleanup() - else: - if self._clear_temp: - _log.debug('MovieWriter: clearing temporary paths=%s', - self._temp_paths) - for path in self._temp_paths: - path.unlink() + try: + self._run() + super().finish() + finally: + if self._tmpdir: + _log.debug( + 'MovieWriter: clearing temporary path=%s', self._tmpdir + ) + self._tmpdir.cleanup() @writers.register('pillow') @@ -532,7 +494,6 @@ def grab_frame(self, **savefig_kwargs): buf = BytesIO() self.fig.savefig( buf, **{**savefig_kwargs, "format": "rgba", "dpi": self.dpi}) - renderer = self.fig.canvas.get_renderer() self._frames.append(Image.frombuffer( "RGBA", self.frame_size, buf.getbuffer(), "raw", "RGBA", 0, 1)) @@ -548,8 +509,8 @@ class FFMpegBase: """ Mixin class for FFMpeg output. - To be useful this must be multiply-inherited from with a - `MovieWriterBase` sub-class. + This is a base class for the concrete `FFMpegWriter` and `FFMpegFileWriter` + classes. """ _exec_key = 'animation.ffmpeg_path' @@ -583,17 +544,6 @@ def output_args(self): return args + ['-y', self.outfile] - @classmethod - def isAvailable(cls): - return ( - super().isAvailable() - # Ubuntu 12.04 ships a broken ffmpeg binary which we shouldn't use. - # NOTE: when removed, remove the same method in AVConvBase. - and b'LibAv' not in subprocess.run( - [cls.bin_path()], creationflags=subprocess_creation_flags, - stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, - stderr=subprocess.PIPE).stderr) - # Combine FFMpeg options with pipe-based writing @writers.register('ffmpeg') @@ -652,67 +602,47 @@ def _args(self): return [self.bin_path(), *args, *self.output_args] -# Base class of avconv information. AVConv has identical arguments to FFMpeg. -@_api.deprecated('3.3') -class AVConvBase(FFMpegBase): - """ - Mixin class for avconv output. - - To be useful this must be multiply-inherited from with a - `MovieWriterBase` sub-class. - """ - - _exec_key = 'animation.avconv_path' - _args_key = 'animation.avconv_args' - - # NOTE : should be removed when the same method is removed in FFMpegBase. - isAvailable = classmethod(MovieWriter.isAvailable.__func__) - - -# Combine AVConv options with pipe-based writing -@writers.register('avconv') -class AVConvWriter(AVConvBase, FFMpegWriter): - """ - Pipe-based avconv writer. - - Frames are streamed directly to avconv via a pipe and written in a single - pass. - """ - - -# Combine AVConv options with file-based writing -@writers.register('avconv_file') -class AVConvFileWriter(AVConvBase, FFMpegFileWriter): - """ - File-based avconv writer. - - Frames are written to temporary files on disk and then stitched - together at the end. - """ - - # Base class for animated GIFs with ImageMagick class ImageMagickBase: """ Mixin class for ImageMagick output. - To be useful this must be multiply-inherited from with a - `MovieWriterBase` sub-class. + This is a base class for the concrete `ImageMagickWriter` and + `ImageMagickFileWriter` classes, which define an ``input_names`` attribute + (or property) specifying the input names passed to ImageMagick. """ _exec_key = 'animation.convert_path' _args_key = 'animation.convert_args' + @_api.deprecated("3.6") @property def delay(self): return 100. / self.fps + @_api.deprecated("3.6") @property def output_args(self): extra_args = (self.extra_args if self.extra_args is not None else mpl.rcParams[self._args_key]) return [*extra_args, self.outfile] + def _args(self): + # ImageMagick does not recognize "raw". + fmt = "rgba" if self.frame_format == "raw" else self.frame_format + extra_args = (self.extra_args if self.extra_args is not None + else mpl.rcParams[self._args_key]) + return [ + self.bin_path(), + "-size", "%ix%i" % self.frame_size, + "-depth", "8", + "-delay", str(100 / self.fps), + "-loop", "0", + f"{fmt}:{self.input_names}", + *extra_args, + self.outfile, + ] + @classmethod def bin_path(cls): binpath = super().bin_path() @@ -734,18 +664,13 @@ def isAvailable(cls): @writers.register('imagemagick') class ImageMagickWriter(ImageMagickBase, MovieWriter): """ - Pipe-based animated gif. + Pipe-based animated gif writer. Frames are streamed directly to ImageMagick via a pipe and written in a single pass. - """ - def _args(self): - return ([self.bin_path(), - '-size', '%ix%i' % self.frame_size, '-depth', '8', - '-delay', str(self.delay), '-loop', '0', - '%s:-' % self.frame_format] - + self.output_args) + + input_names = "-" # stdin # Combine ImageMagick options with temp file-based writing @@ -759,23 +684,15 @@ class ImageMagickFileWriter(ImageMagickBase, FileMovieWriter): """ supported_formats = ['png', 'jpeg', 'tiff', 'raw', 'rgba'] - - def _args(self): - # Force format: ImageMagick does not recognize 'raw'. - fmt = 'rgba:' if self.frame_format == 'raw' else '' - return ([self.bin_path(), - '-size', '%ix%i' % self.frame_size, '-depth', '8', - '-delay', str(self.delay), '-loop', '0', - '%s%s*.%s' % (fmt, self.temp_prefix, self.frame_format)] - + self.output_args) + input_names = property( + lambda self: f'{self.temp_prefix}*.{self.frame_format}') # Taken directly from jakevdp's JSAnimation package at # http://github.com/jakevdp/JSAnimation -def _included_frames(paths, frame_format): - """paths should be a list of Paths""" - return INCLUDED_FRAMES.format(Nframes=len(paths), - frame_dir=paths[0].parent, +def _included_frames(frame_count, frame_format, frame_dir): + return INCLUDED_FRAMES.format(Nframes=frame_count, + frame_dir=frame_dir, frame_format=frame_format) @@ -795,8 +712,6 @@ class HTMLWriter(FileMovieWriter): """Writer for JavaScript-based HTML movies.""" supported_formats = ['png', 'jpeg', 'tiff', 'svg'] - args_key = _api.deprecated("3.3")(property( - lambda self: 'animation.html_args')) @classmethod def isAvailable(cls): @@ -824,7 +739,7 @@ def __init__(self, fps=30, codec=None, bitrate=None, extra_args=None, super().__init__(fps, codec, bitrate, extra_args, metadata) - def setup(self, fig, outfile, dpi, frame_dir=None): + def setup(self, fig, outfile, dpi=None, frame_dir=None): outfile = Path(outfile) _api.check_in_list(['.html', '.htm'], outfile_extension=outfile.suffix) @@ -871,11 +786,13 @@ def finish(self): if self.embed_frames: fill_frames = _embedded_frames(self._saved_frames, self.frame_format) - Nframes = len(self._saved_frames) + frame_count = len(self._saved_frames) else: # temp names is filled by FileMovieWriter - fill_frames = _included_frames(self._temp_paths, self.frame_format) - Nframes = len(self._temp_paths) + frame_count = len(self._temp_paths) + fill_frames = _included_frames( + frame_count, self.frame_format, + self._temp_paths[0].parent.relative_to(self.outfile.parent)) mode_dict = dict(once_checked='', loop_checked='', reflect_checked='') @@ -886,26 +803,19 @@ def finish(self): with open(self.outfile, 'w') as of: of.write(JS_INCLUDE + STYLE_INCLUDE) of.write(DISPLAY_TEMPLATE.format(id=uuid.uuid4().hex, - Nframes=Nframes, + Nframes=frame_count, fill_frames=fill_frames, interval=interval, **mode_dict)) - # duplicate the temporary file clean up logic from - # FileMovieWriter.cleanup. We can not call the inherited - # versions of finished or cleanup because both assume that - # there is a subprocess that we either need to call to merge - # many frames together or that there is a subprocess call that - # we need to clean up. + # Duplicate the temporary file clean up logic from + # FileMovieWriter.finish. We can not call the inherited version of + # finish because it assumes that there is a subprocess that we either + # need to call to merge many frames together or that there is a + # subprocess call that we need to clean up. if self._tmpdir: _log.debug('MovieWriter: clearing temporary path=%s', self._tmpdir) self._tmpdir.cleanup() - else: - if self._clear_temp: - _log.debug('MovieWriter: clearing temporary paths=%s', - self._temp_paths) - for path in self._temp_paths: - path.unlink() class Animation: @@ -934,7 +844,8 @@ class Animation: system notifications. blit : bool, default: False - Whether blitting is used to optimize drawing. + Whether blitting is used to optimize drawing. If the backend does not + support blitting, then this parameter has no effect. See Also -------- @@ -972,9 +883,11 @@ def __del__(self): if not getattr(self, '_draw_was_started', True): warnings.warn( 'Animation was deleted without rendering anything. This is ' - 'most likely unintended. To prevent deletion, assign the ' - 'Animation to a variable that exists for as long as you need ' - 'the Animation.') + 'most likely not intended. To prevent deletion, assign the ' + 'Animation to a variable, e.g. `anim`, that exists until you ' + 'output the Animation using `plt.show()` or ' + '`anim.save()`.' + ) def _start(self, *args): """ @@ -1048,7 +961,7 @@ class to use, such as 'ffmpeg'. extra_anim : list, default: [] Additional `Animation` objects that should be included in the saved movie file. These need to be from the same - `matplotlib.figure.Figure` instance. Also, animation frames will + `.Figure` instance. Also, animation frames will just be simply combined, so there should be a 1:1 correspondence between the frames from the different animations. @@ -1069,8 +982,7 @@ def func(current_frame: int, total_frames: int) -> Any Example code to write the progress to stdout:: - progress_callback =\ - lambda i, n: print(f'Saving frame {i} of {n}') + progress_callback = lambda i, n: print(f'Saving frame {i}/{n}') Notes ----- @@ -1080,6 +992,15 @@ def func(current_frame: int, total_frames: int) -> Any is a `.MovieWriter`, a `RuntimeError` will be raised. """ + all_anim = [self] + if extra_anim is not None: + all_anim.extend(anim for anim in extra_anim + if anim._fig is self._fig) + + # Disable "Animation was deleted without rendering" warning. + for anim in all_anim: + anim._draw_was_started = True + if writer is None: writer = mpl.rcParams['animation.writer'] elif (not isinstance(writer, str) and @@ -1094,6 +1015,9 @@ def func(current_frame: int, total_frames: int) -> Any if savefig_kwargs is None: savefig_kwargs = {} + else: + # we are going to mutate this below + savefig_kwargs = dict(savefig_kwargs) if fps is None and hasattr(self, '_interval'): # Convert interval in ms to frames per second @@ -1115,12 +1039,6 @@ def func(current_frame: int, total_frames: int) -> Any if metadata is not None: writer_kwargs['metadata'] = metadata - all_anim = [self] - if extra_anim is not None: - all_anim.extend(anim - for anim - in extra_anim if anim._fig is self._fig) - # If we have the name of a writer, instantiate an instance of the # registered class. if isinstance(writer, str): @@ -1149,6 +1067,18 @@ def func(current_frame: int, total_frames: int) -> Any _log.info("Disabling savefig.bbox = 'tight', as it may cause " "frame size to vary, which is inappropriate for " "animation.") + + facecolor = savefig_kwargs.get('facecolor', + mpl.rcParams['savefig.facecolor']) + if facecolor == 'auto': + facecolor = self._fig.get_facecolor() + + def _pre_composite_to_white(color): + r, g, b, a = mcolors.to_rgba(color) + return a * np.array([r, g, b]) + 1 - a + + savefig_kwargs['facecolor'] = _pre_composite_to_white(facecolor) + savefig_kwargs['transparent'] = False # just to be safe! # canvas._is_saving = True makes the draw_event animation-starting # callback a no-op; canvas.manager = None prevents resizing the GUI # widget (both are likewise done in savefig()). @@ -1161,7 +1091,7 @@ def func(current_frame: int, total_frames: int) -> Any frame_number = 0 # TODO: Currently only FuncAnimation has a save_count # attribute. Can we generalize this to all Animations? - save_count_list = [getattr(a, 'save_count', None) + save_count_list = [getattr(a, '_save_count', None) for a in all_anim] if None in save_count_list: total_frames = None @@ -1238,11 +1168,11 @@ def _blit_draw(self, artists): # Handles blitted drawing, which renders only the artists given instead # of the entire figure. updated_ax = {a.axes for a in artists} - # Enumerate artists to cache axes' backgrounds. We do not draw + # Enumerate artists to cache Axes backgrounds. We do not draw # artists yet to not cache foreground from plots with shared axes for ax in updated_ax: # If we haven't cached the background for the current view of this - # axes object, do so now. This might not always be reliable, but + # Axes object, do so now. This might not always be reliable, but # it's an attempt to automate the process. cur_view = ax._get_view() view, bg = self._blit_cache.get(ax, (object(), None)) @@ -1252,12 +1182,12 @@ def _blit_draw(self, artists): # Make a separate pass to draw foreground. for a in artists: a.axes.draw_artist(a) - # After rendering all the needed artists, blit each axes individually. + # After rendering all the needed artists, blit each Axes individually. for ax in updated_ax: ax.figure.canvas.blit(ax.bbox) def _blit_clear(self, artists): - # Get a list of the axes that need clearing from the artists that + # Get a list of the Axes that need clearing from the artists that # have been drawn. Grab the appropriate saved background from the # cache and restore. axes = {a.axes for a in artists} @@ -1272,13 +1202,19 @@ def _blit_clear(self, artists): self._blit_cache.pop(ax) def _setup_blit(self): - # Setting up the blit requires: a cache of the background for the - # axes + # Setting up the blit requires: a cache of the background for the Axes self._blit_cache = dict() self._drawn_artists = [] + # _post_draw needs to be called first to initialize the renderer + self._post_draw(None, self._blit) + # Then we need to clear the Frame for the initial draw + # This is typically handled in _on_resize because QT and Tk + # emit a resize event on launch, but the macosx backend does not, + # thus we force it here for everyone for consistency + self._init_draw() + # Connect to future resize events self._resize_id = self._fig.canvas.mpl_connect('resize_event', self._on_resize) - self._post_draw(None, self._blit) def _on_resize(self, event): # On resize, we need to disable the resize event handling so we don't @@ -1308,7 +1244,7 @@ def to_html5_video(self, embed_limit=None): This saves the animation as an h264 video, encoded in base64 directly into the HTML5 video tag. This respects :rc:`animation.writer` and :rc:`animation.bitrate`. This also makes use of the - ``interval`` to control the speed, and uses the ``repeat`` + *interval* to control the speed, and uses the *repeat* parameter to decide whether to loop. Parameters @@ -1371,7 +1307,7 @@ def to_html5_video(self, embed_limit=None): options = ['controls', 'autoplay'] # If we're set to repeat, make it loop - if hasattr(self, 'repeat') and self.repeat: + if getattr(self, '_repeat', False): options.append('loop') return VIDEO_TAG.format(video=self._base64_video, @@ -1381,15 +1317,29 @@ def to_html5_video(self, embed_limit=None): return 'Video too large to embed.' def to_jshtml(self, fps=None, embed_frames=True, default_mode=None): - """Generate HTML representation of the animation""" + """ + Generate HTML representation of the animation. + + Parameters + ---------- + fps : int, optional + Movie frame rate (per second). If not set, the frame rate from + the animation's frame interval. + embed_frames : bool, optional + default_mode : str, optional + What to do when the animation ends. Must be one of ``{'loop', + 'once', 'reflect'}``. Defaults to ``'loop'`` if the *repeat* + parameter is True, otherwise ``'once'``. + """ if fps is None and hasattr(self, '_interval'): # Convert interval in ms to frames per second fps = 1000 / self._interval # If we're not given a default mode, choose one base on the value of - # the repeat attribute + # the _repeat attribute if default_mode is None: - default_mode = 'loop' if self.repeat else 'once' + default_mode = 'loop' if getattr(self, '_repeat', + False) else 'once' if not hasattr(self, "_html_representation"): # Can't open a NamedTemporaryFile twice on Windows, so use a @@ -1453,13 +1403,12 @@ class TimedAnimation(Animation): blit : bool, default: False Whether blitting is used to optimize drawing. """ - def __init__(self, fig, interval=200, repeat_delay=0, repeat=True, event_source=None, *args, **kwargs): self._interval = interval # Undocumented support for repeat_delay = None as backcompat. self._repeat_delay = repeat_delay if repeat_delay is not None else 0 - self.repeat = repeat + self._repeat = repeat # If we're not given an event source, create a new timer. This permits # sharing timers between animation objects for syncing animations. if event_source is None: @@ -1475,19 +1424,34 @@ def _step(self, *args): # delay and set the callback to one which will then set the interval # back. still_going = super()._step(*args) - if not still_going and self.repeat: - self._init_draw() - self.frame_seq = self.new_frame_seq() - self.event_source.interval = self._repeat_delay - return True - else: - self.event_source.interval = self._interval - return still_going + if not still_going: + if self._repeat: + # Restart the draw loop + self._init_draw() + self.frame_seq = self.new_frame_seq() + self.event_source.interval = self._repeat_delay + return True + else: + # We are done with the animation. Call pause to remove + # animated flags from artists that were using blitting + self.pause() + if self._blit: + # Remove the resize callback if we were blitting + self._fig.canvas.mpl_disconnect(self._resize_id) + self._fig.canvas.mpl_disconnect(self._close_id) + self.event_source = None + return False + + self.event_source.interval = self._interval + return True + + repeat = _api.deprecate_privatize_attribute("3.7") class ArtistAnimation(TimedAnimation): """ - Animation using a fixed set of `.Artist` objects. + `TimedAnimation` subclass that creates an animation by using a fixed + set of `.Artist` objects. Before creating an instance, all plotting should have taken place and the relevant artists saved. @@ -1563,7 +1527,8 @@ def _draw_frame(self, artists): class FuncAnimation(TimedAnimation): """ - Makes an animation by repeatedly calling a function *func*. + `TimedAnimation` subclass that makes an animation by repeatedly calling + a function *func*. .. note:: @@ -1579,12 +1544,24 @@ class FuncAnimation(TimedAnimation): func : callable The function to call at each frame. The first argument will be the next value in *frames*. Any additional positional - arguments can be supplied via the *fargs* parameter. + arguments can be supplied using `functools.partial` or via the *fargs* + parameter. The required signature is:: def func(frame, *fargs) -> iterable_of_artists + It is often more convenient to provide the arguments using + `functools.partial`. In this way it is also possible to pass keyword + arguments. To pass a function with both positional and keyword + arguments, set all arguments as keyword arguments, just leaving the + *frame* argument unset:: + + def func(frame, art, *, y=None): + ... + + ani = FuncAnimation(fig, partial(func, art=ln, y='foo')) + If ``blit == True``, *func* must return an iterable of all artists that were modified or created. This information is used by the blitting algorithm to determine which parts of the figure have to be updated. @@ -1623,9 +1600,10 @@ def init_func() -> iterable_of_artists value is unused if ``blit == False`` and may be omitted in that case. fargs : tuple or None, optional - Additional arguments to pass to each call to *func*. + Additional arguments to pass to each call to *func*. Note: the use of + `functools.partial` is preferred over *fargs*. See *func* for details. - save_count : int, default: 100 + save_count : int, optional Fallback for the number of values from *frames* to cache. This is only used if the number of frames cannot be inferred from *frames*, i.e. when it's an iterator without length or a generator. @@ -1650,7 +1628,6 @@ def init_func() -> iterable_of_artists Whether frame data is cached. Disabling cache might be helpful when frames contain large objects. """ - def __init__(self, fig, func, frames=None, init_func=None, fargs=None, save_count=None, *, cache_frame_data=True, **kwargs): if fargs: @@ -1663,7 +1640,7 @@ def __init__(self, fig, func, frames=None, init_func=None, fargs=None, # Amount of framedata to keep around for saving movies. This is only # used if we don't know how many frames there will be: in the case # of no generator or in the case of a callable. - self.save_count = save_count + self._save_count = save_count # Set up a function that creates a new iterable when needed. If nothing # is passed in for frames, just use itertools.count, which will just # keep counting from 0. A callable passed in for frames is assumed to @@ -1683,19 +1660,31 @@ def iter_frames(frames=frames): else: self._iter_gen = lambda: iter(frames) if hasattr(frames, '__len__'): - self.save_count = len(frames) + self._save_count = len(frames) + if save_count is not None: + _api.warn_external( + f"You passed in an explicit {save_count=} " + "which is being ignored in favor of " + f"{len(frames)=}." + ) else: self._iter_gen = lambda: iter(range(frames)) - self.save_count = frames - - if self.save_count is None: - # If we're passed in and using the default, set save_count to 100. - self.save_count = 100 - else: - # itertools.islice returns an error when passed a numpy int instead - # of a native python int (http://bugs.python.org/issue30537). - # As a workaround, convert save_count to a native python int. - self.save_count = int(self.save_count) + self._save_count = frames + if save_count is not None: + _api.warn_external( + f"You passed in an explicit {save_count=} which is being " + f"ignored in favor of {frames=}." + ) + if self._save_count is None and cache_frame_data: + _api.warn_external( + f"{frames=!r} which we can infer the length of, " + "did not pass an explicit *save_count* " + f"and passed {cache_frame_data=}. To avoid a possibly " + "unbounded cache, frame data caching has been disabled. " + "To suppress this warning either pass " + "`cache_frame_data=False` or `save_count=MAX_FRAMES`." + ) + cache_frame_data = False self._cache_frame_data = cache_frame_data @@ -1722,26 +1711,18 @@ def new_saved_frame_seq(self): self._old_saved_seq = list(self._save_seq) return iter(self._old_saved_seq) else: - if self.save_count is not None: - return itertools.islice(self.new_frame_seq(), self.save_count) - - else: + if self._save_count is None: frame_seq = self.new_frame_seq() def gen(): try: - for _ in range(100): + while True: yield next(frame_seq) except StopIteration: pass - else: - _api.warn_deprecated( - "2.2", message="FuncAnimation.save has truncated " - "your animation to 100 frames. In the future, no " - "such truncation will occur; please pass " - "'save_count' accordingly.") - return gen() + else: + return itertools.islice(self.new_frame_seq(), self._save_count) def _init_draw(self): super()._init_draw() @@ -1750,8 +1731,21 @@ def _init_draw(self): # For blitting, the init_func should return a sequence of modified # artists. if self._init_func is None: - self._draw_frame(next(self.new_frame_seq())) - + try: + frame_data = next(self.new_frame_seq()) + except StopIteration: + # we can't start the iteration, it may have already been + # exhausted by a previous save or just be 0 length. + # warn and bail. + warnings.warn( + "Can not start iterating the frames for the initial draw. " + "This can be caused by passing in a 0 length sequence " + "for *frames*.\n\n" + "If you passed *frames* as a generator " + "it may be exhausted due to a previous display or save." + ) + return + self._draw_frame(frame_data) else: self._drawn_artists = self._init_func() if self._blit: @@ -1766,10 +1760,7 @@ def _draw_frame(self, framedata): if self._cache_frame_data: # Save the data for potential saving of movies. self._save_seq.append(framedata) - - # Make sure to respect save_count (keep only the last save_count - # around) - self._save_seq = self._save_seq[-self.save_count:] + self._save_seq = self._save_seq[-self._save_count:] # Call the func with framedata and args. If blitting is desired, # func needs to return a sequence of any artists that were modified. @@ -1785,7 +1776,7 @@ def _draw_frame(self, framedata): except TypeError: raise err from None - # check each item if is artist + # check each item if it's artist for i in self._drawn_artists: if not isinstance(i, mpl.artist.Artist): raise err @@ -1795,3 +1786,5 @@ def _draw_frame(self, framedata): for a in self._drawn_artists: a.set_animated(self._blit) + + save_count = _api.deprecate_privatize_attribute("3.7") diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index b6f13f898f50..44c128232235 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -1,6 +1,8 @@ -from collections import OrderedDict, namedtuple -from functools import wraps +from collections import namedtuple +import contextlib +from functools import lru_cache, wraps import inspect +from inspect import Signature, Parameter import logging from numbers import Number import re @@ -9,7 +11,9 @@ import numpy as np import matplotlib as mpl -from . import _api, cbook, docstring +from . import _api, cbook +from .colors import BoundaryNorm +from .cm import ScalarMappable from .path import Path from .transforms import (Bbox, IdentityTransform, Transform, TransformedBbox, TransformedPatchPath, TransformedPath) @@ -17,6 +21,27 @@ _log = logging.getLogger(__name__) +def _prevent_rasterization(draw): + # We assume that by default artists are not allowed to rasterize (unless + # its draw method is explicitly decorated). If it is being drawn after a + # rasterized artist and it has reached a raster_depth of 0, we stop + # rasterization so that it does not affect the behavior of normal artist + # (e.g., change in dpi). + + @wraps(draw) + def draw_wrapper(artist, renderer, *args, **kwargs): + if renderer._raster_depth == 0 and renderer._rasterizing: + # Only stop when we are not in a rasterized parent + # and something has been rasterized since last stop. + renderer.stop_rasterizing() + renderer._rasterizing = False + + return draw(artist, renderer, *args, **kwargs) + + draw_wrapper._supports_rasterization = False + return draw_wrapper + + def allow_rasterization(draw): """ Decorator for Artist.draw method. Provides routines @@ -26,12 +51,8 @@ def allow_rasterization(draw): renderer. """ - # Axes has a second (deprecated) argument inframe for its draw method. - # args and kwargs are deprecated, but we don't wrap this in - # _api.delete_parameter for performance; the relevant deprecation - # warning will be emitted by the inner draw() call. @wraps(draw) - def draw_wrapper(artist, renderer, *args, **kwargs): + def draw_wrapper(artist, renderer): try: if artist.get_rasterized(): if renderer._raster_depth == 0 and not renderer._rasterizing: @@ -48,7 +69,7 @@ def draw_wrapper(artist, renderer, *args, **kwargs): if artist.get_agg_filter() is not None: renderer.start_filter() - return draw(artist, renderer, *args, **kwargs) + return draw(artist, renderer) finally: if artist.get_agg_filter() is not None: renderer.stop_filter(artist.get_agg_filter()) @@ -87,6 +108,12 @@ def _stale_axes_callback(self, val): _XYPair = namedtuple("_XYPair", "x y") +class _Unset: + def __repr__(self): + return "" +_UNSET = _Unset() + + class Artist: """ Abstract base class for objects that render into a FigureCanvas. @@ -96,6 +123,60 @@ class Artist: zorder = 0 + def __init_subclass__(cls): + + # Decorate draw() method so that all artists are able to stop + # rastrization when necessary. If the artist's draw method is already + # decorated (has a `_supports_rasterization` attribute), it won't be + # decorated. + + if not hasattr(cls.draw, "_supports_rasterization"): + cls.draw = _prevent_rasterization(cls.draw) + + # Inject custom set() methods into the subclass with signature and + # docstring based on the subclasses' properties. + + if not hasattr(cls.set, '_autogenerated_signature'): + # Don't overwrite cls.set if the subclass or one of its parents + # has defined a set method set itself. + # If there was no explicit definition, cls.set is inherited from + # the hierarchy of auto-generated set methods, which hold the + # flag _autogenerated_signature. + return + + cls.set = lambda self, **kwargs: Artist.set(self, **kwargs) + cls.set.__name__ = "set" + cls.set.__qualname__ = f"{cls.__qualname__}.set" + cls._update_set_signature_and_docstring() + + _PROPERTIES_EXCLUDED_FROM_SET = [ + 'navigate_mode', # not a user-facing function + 'figure', # changing the figure is such a profound operation + # that we don't want this in set() + '3d_properties', # cannot be used as a keyword due to leading digit + ] + + @classmethod + def _update_set_signature_and_docstring(cls): + """ + Update the signature of the set function to list all properties + as keyword arguments. + + Property aliases are not listed in the signature for brevity, but + are still accepted as keyword arguments. + """ + cls.set.__signature__ = Signature( + [Parameter("self", Parameter.POSITIONAL_OR_KEYWORD), + *[Parameter(prop, Parameter.KEYWORD_ONLY, default=_UNSET) + for prop in ArtistInspector(cls).get_setters() + if prop not in Artist._PROPERTIES_EXCLUDED_FROM_SET]]) + cls.set._autogenerated_signature = True + + cls.set.__doc__ = ( + "Set multiple properties at once.\n\n" + "Supported properties are\n\n" + + kwdoc(cls)) + def __init__(self): self._stale = True self.stale_callback = None @@ -112,13 +193,12 @@ def __init__(self): self._clipon = True self._label = '' self._picker = None - self._contains = None self._rasterized = False self._agg_filter = None # Normally, artist classes need to be queried for mouseover info if and # only if they override get_cursor_data. self._mouseover = type(self).get_cursor_data != Artist.get_cursor_data - self._callbacks = cbook.CallbackRegistry() + self._callbacks = cbook.CallbackRegistry(signals=["pchanged"]) try: self.axes = None except AttributeError: @@ -136,7 +216,7 @@ def __init__(self): def __getstate__(self): d = self.__dict__.copy() # remove the unpicklable remove method, this will get re-added on load - # (by the axes) if the artist lives on an axes. + # (by the Axes) if the artist lives on an Axes. d['stale_callback'] = None return d @@ -154,7 +234,7 @@ def remove(self): Note: there is no support for removing the artist's legend entry. """ - # There is no method to set the callback. Instead the parent should + # There is no method to set the callback. Instead, the parent should # set the _remove_method attribute directly. This would be a # protected attribute if Python supported that sort of thing. The # callback has one parameter, which is the child to be removed. @@ -166,10 +246,8 @@ def remove(self): if hasattr(self, 'axes') and self.axes: # remove from the mouse hit list self.axes._mouseover_set.discard(self) - # mark the axes as stale self.axes.stale = True - # decouple the artist from the axes - self.axes = None + self.axes = None # decouple the artist from the Axes _ax_flag = True if self.figure: @@ -188,13 +266,13 @@ def remove(self): def have_units(self): """Return whether units are set on any axis.""" ax = self.axes - return ax and any(axis.have_units() for axis in ax._get_axis_list()) + return ax and any(axis.have_units() for axis in ax._axis_map.values()) def convert_xunits(self, x): """ Convert *x* using the unit type of the xaxis. - If the artist is not in contained in an Axes or if the xaxis does not + If the artist is not contained in an Axes or if the xaxis does not have units, *x* itself is returned. """ ax = getattr(self, 'axes', None) @@ -206,7 +284,7 @@ def convert_yunits(self, y): """ Convert *y* using the unit type of the yaxis. - If the artist is not in contained in an Axes or if the yaxis does not + If the artist is not contained in an Axes or if the yaxis does not have units, *y* itself is returned. """ ax = getattr(self, 'axes', None) @@ -251,9 +329,9 @@ def stale(self, val): if val and self.stale_callback is not None: self.stale_callback(self, val) - def get_window_extent(self, renderer): + def get_window_extent(self, renderer=None): """ - Get the axes bounding box in display space. + Get the artist's bounding box in display space. The bounding box' width and height are nonnegative. @@ -271,24 +349,7 @@ def get_window_extent(self, renderer): """ return Bbox([[0, 0], [0, 0]]) - def _get_clipping_extent_bbox(self): - """ - Return a bbox with the extents of the intersection of the clip_path - and clip_box for this artist, or None if both of these are - None, or ``get_clip_on`` is False. - """ - bbox = None - if self.get_clip_on(): - clip_box = self.get_clip_box() - if clip_box is not None: - bbox = clip_box - clip_path = self.get_clip_path() - if clip_path is not None and bbox is not None: - clip_path = clip_path.get_fully_transformed_path() - bbox = Bbox.intersection(bbox, clip_path.get_extents()) - return bbox - - def get_tightbbox(self, renderer): + def get_tightbbox(self, renderer=None): """ Like `.Artist.get_window_extent`, but includes any clipping. @@ -309,7 +370,7 @@ def get_tightbbox(self, renderer): if clip_box is not None: bbox = Bbox.intersection(bbox, clip_box) clip_path = self.get_clip_path() - if clip_path is not None and bbox is not None: + if clip_path is not None: clip_path = clip_path.get_fully_transformed_path() bbox = Bbox.intersection(bbox, clip_path.get_extents()) return bbox @@ -404,10 +465,9 @@ def _default_contains(self, mouseevent, figure=None): """ Base impl. for checking whether a mouseevent happened in an artist. - 1. If the artist defines a custom checker, use it (deprecated). - 2. If the artist figure is known and the event did not occur in that + 1. If the artist figure is known and the event did not occur in that figure (by checking its ``canvas`` attribute), reject it. - 3. Otherwise, return `None, {}`, indicating that the subclass' + 2. Otherwise, return `None, {}`, indicating that the subclass' implementation should be used. Subclasses should start their definition of `contains` as follows: @@ -420,8 +480,6 @@ def _default_contains(self, mouseevent, figure=None): The *figure* kwarg is provided for the implementation of `.Figure.contains`. """ - if callable(self._contains): - return self._contains(self, mouseevent) if figure is not None and mouseevent.canvas is not figure.canvas: return False, {} return None, {} @@ -449,45 +507,6 @@ def contains(self, mouseevent): _log.warning("%r needs 'contains' method", self.__class__.__name__) return False, {} - @_api.deprecated("3.3", alternative="set_picker") - def set_contains(self, picker): - """ - Define a custom contains test for the artist. - - The provided callable replaces the default `.contains` method - of the artist. - - Parameters - ---------- - picker : callable - A custom picker function to evaluate if an event is within the - artist. The function must have the signature:: - - def contains(artist: Artist, event: MouseEvent) -> bool, dict - - that returns: - - - a bool indicating if the event is within the artist - - a dict of additional information. The dict should at least - return the same information as the default ``contains()`` - implementation of the respective artist, but may provide - additional information. - """ - if not callable(picker): - raise TypeError("picker is not a callable") - self._contains = picker - - @_api.deprecated("3.3", alternative="get_picker") - def get_contains(self): - """ - Return the custom contains function of the artist if set, or *None*. - - See Also - -------- - set_contains - """ - return self._contains - def pickable(self): """ Return whether the artist is pickable. @@ -509,6 +528,7 @@ def pick(self, mouseevent): -------- set_picker, get_picker, pickable """ + from .backend_bases import PickEvent # Circular import. # Pick self if self.pickable(): picker = self.get_picker() @@ -517,20 +537,21 @@ def pick(self, mouseevent): else: inside, prop = self.contains(mouseevent) if inside: - self.figure.canvas.pick_event(mouseevent, self, **prop) + PickEvent("pick_event", self.figure.canvas, + mouseevent, self, **prop)._process() # Pick children for a in self.get_children(): - # make sure the event happened in the same axes + # make sure the event happened in the same Axes ax = getattr(a, 'axes', None) if (mouseevent.inaxes is None or ax is None or mouseevent.inaxes == ax): # we need to check if mouseevent.inaxes is None - # because some objects associated with an axes (e.g., a + # because some objects associated with an Axes (e.g., a # tick label) can be outside the bounding box of the - # axes and inaxes will be None + # Axes and inaxes will be None # also check that ax is None so that it traverse objects - # which do no have an axes property but children might + # which do not have an axes property but children might a.pick(mouseevent) def set_picker(self, picker): @@ -684,6 +705,9 @@ def set_sketch_params(self, scale=None, length=None, randomness=None): The scale factor by which the length is shrunken or expanded (default 16.0) + The PGF backend uses this argument as an RNG seed and not as + described above. Using the same seed yields the same random shape. + .. ACCEPTS: (scale: float, length: float, randomness: float) """ if scale is None: @@ -741,6 +765,11 @@ def set_clip_box(self, clipbox): Parameters ---------- clipbox : `.Bbox` + + Typically would be created from a `.TransformedBbox`. For + instance ``TransformedBbox(Bbox([[0, 0], [1, 1]]), ax.transAxes)`` + is the default clipping for an artist added to an Axes. + """ self.clipbox = clipbox self.pchanged() @@ -834,6 +863,30 @@ def get_in_layout(self): """ return self._in_layout + def _fully_clipped_to_axes(self): + """ + Return a boolean flag, ``True`` if the artist is clipped to the Axes + and can thus be skipped in layout calculations. Requires `get_clip_on` + is True, one of `clip_box` or `clip_path` is set, ``clip_box.extents`` + is equivalent to ``ax.bbox.extents`` (if set), and ``clip_path._patch`` + is equivalent to ``ax.patch`` (if set). + """ + # Note that ``clip_path.get_fully_transformed_path().get_extents()`` + # cannot be directly compared to ``axes.bbox.extents`` because the + # extents may be undefined (i.e. equivalent to ``Bbox.null()``) + # before the associated artist is drawn, and this method is meant + # to determine whether ``axes.get_tightbbox()`` may bypass drawing + clip_box = self.get_clip_box() + clip_path = self.get_clip_path() + return (self.axes is not None + and self.get_clip_on() + and (clip_box is not None or clip_path is not None) + and (clip_box is None + or np.all(clip_box.extents == self.axes.bbox.extents)) + and (clip_path is None + or isinstance(clip_path, TransformedPatchPath) + and clip_path._patch is self.axes.patch)) + def get_clip_on(self): """Return whether the artist uses clipping.""" return self._clipon @@ -860,7 +913,7 @@ def set_clip_on(self, b): """ Set whether the artist uses clipping. - When False artists will be visible outside of the axes which + When False, artists will be visible outside the Axes which can lead to unexpected results. Parameters @@ -903,7 +956,9 @@ def set_rasterized(self, rasterized): ---------- rasterized : bool """ - if rasterized and not hasattr(self.draw, "_supports_rasterization"): + supports_rasterization = getattr(self.draw, + "_supports_rasterization", False) + if rasterized and not supports_rasterization: _api.warn_external(f"Rasterization of '{self}' will be ignored") self._rasterized = rasterized @@ -919,18 +974,18 @@ def set_agg_filter(self, filter_func): Parameters ---------- filter_func : callable - A filter function, which takes a (m, n, 3) float array and a dpi - value, and returns a (m, n, 3) array. + A filter function, which takes a (m, n, depth) float array + and a dpi value, and returns a (m, n, depth) array and two + offsets from the bottom left corner of the image .. ACCEPTS: a filter function, which takes a (m, n, 3) float array - and a dpi value, and returns a (m, n, 3) array + and a dpi value, and returns a (m, n, 3) array and two offsets + from the bottom left corner of the image """ self._agg_filter = filter_func self.stale = True - @_api.delete_parameter("3.3", "args") - @_api.delete_parameter("3.3", "kwargs") - def draw(self, renderer, *args, **kwargs): + def draw(self, renderer): """ Draw the Artist (and its children) using the given renderer. @@ -1008,7 +1063,7 @@ def set_animated(self, b): If True, the artist is excluded from regular drawing of the figure. You have to call `.Figure.draw_artist` / `.Axes.draw_artist` - explicitly on the artist. This appoach is used to speed up animations + explicitly on the artist. This approach is used to speed up animations using blitting. See also `matplotlib.animation` and @@ -1035,38 +1090,6 @@ def set_in_layout(self, in_layout): """ self._in_layout = in_layout - def update(self, props): - """ - Update this artist's properties from the dict *props*. - - Parameters - ---------- - props : dict - """ - ret = [] - with cbook._setattr_cm(self, eventson=False): - for k, v in props.items(): - if k != k.lower(): - _api.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": - ret.append(setattr(self, k, v)) - else: - func = getattr(self, f"set_{k}", None) - if not callable(func): - raise AttributeError(f"{type(self).__name__!r} object " - f"has no property {k!r}") - ret.append(func(v)) - if ret: - self.pchanged() - self.stale = True - return ret - def get_label(self): """Return the label used for this artist in the legend.""" return self._label @@ -1117,6 +1140,11 @@ def sticky_edges(self): where one usually expects no margin on the bottom edge (0) of the histogram. + Moreover, margin expansion "bumps" against sticky edges and cannot + cross them. For example, if the upper data limit is 1.0, the upper + view limit computed by simple margin application is 1.2, but there is a + sticky edge at 1.1, then the actual upper view limit will be 1.1. + This attribute cannot be assigned to; however, the ``x`` and ``y`` lists can be modified in place as needed. @@ -1149,34 +1177,70 @@ def properties(self): """Return a dictionary of all the properties of the artist.""" return ArtistInspector(self).properties() + def _update_props(self, props, errfmt): + """ + Helper for `.Artist.set` and `.Artist.update`. + + *errfmt* is used to generate error messages for invalid property + names; it gets formatted with ``type(self)`` and the property name. + """ + ret = [] + with cbook._setattr_cm(self, eventson=False): + for k, v in props.items(): + # Allow attributes we want to be able to update through + # art.update, art.set, setp. + if k == "axes": + ret.append(setattr(self, k, v)) + else: + func = getattr(self, f"set_{k}", None) + if not callable(func): + raise AttributeError( + errfmt.format(cls=type(self), prop_name=k)) + ret.append(func(v)) + if ret: + self.pchanged() + self.stale = True + return ret + + def update(self, props): + """ + Update this artist's properties from the dict *props*. + + Parameters + ---------- + props : dict + """ + return self._update_props( + props, "{cls.__name__!r} object has no property {prop_name!r}") + + def _internal_update(self, kwargs): + """ + Update artist properties without prenormalizing them, but generating + errors as if calling `set`. + + The lack of prenormalization is to maintain backcompatibility. + """ + return self._update_props( + kwargs, "{cls.__name__}.set() got an unexpected keyword argument " + "{prop_name!r}") + def set(self, **kwargs): - """A property batch setter. Pass *kwargs* to set properties.""" - kwargs = cbook.normalize_kwargs(kwargs, self) - move_color_to_start = False - if "color" in kwargs: - keys = [*kwargs] - i_color = keys.index("color") - props = ["edgecolor", "facecolor"] - if any(tp.__module__ == "matplotlib.collections" - and tp.__name__ == "Collection" - for tp in type(self).__mro__): - props.append("alpha") - for other in props: - if other not in keys: - continue - i_other = keys.index(other) - if i_other < i_color: - move_color_to_start = True - _api.warn_deprecated( - "3.3", message=f"You have passed the {other!r} kwarg " - "before the 'color' kwarg. Artist.set() currently " - "reorders the properties to apply 'color' first, but " - "this is deprecated since %(since)s and will be " - "removed %(removal)s; please pass 'color' first " - "instead.") - if move_color_to_start: - kwargs = {"color": kwargs.pop("color"), **kwargs} - return self.update(kwargs) + # docstring and signature are auto-generated via + # Artist._update_set_signature_and_docstring() at the end of the + # module. + return self._internal_update(cbook.normalize_kwargs(kwargs, self)) + + @contextlib.contextmanager + def _cm_set(self, **kwargs): + """ + `.Artist.set` context-manager that restores original values at exit. + """ + orig_vals = {k: getattr(self, f"get_{k}")() for k in kwargs} + try: + self.set(**kwargs) + yield + finally: + self.set(**orig_vals) def findobj(self, match=None, include_self=True): """ @@ -1262,42 +1326,96 @@ def format_cursor_data(self, data): method yourself. The default implementation converts ints and floats and arrays of ints - and floats into a comma-separated string enclosed in square brackets. + and floats into a comma-separated string enclosed in square brackets, + unless the artist has an associated colorbar, in which case scalar + values are formatted using the colorbar's formatter. See Also -------- get_cursor_data """ - try: - data[0] - except (TypeError, IndexError): - data = [data] - data_str = ', '.join('{:0.3g}'.format(item) for item in data - if isinstance(item, Number)) - return "[" + data_str + "]" + if np.ndim(data) == 0 and isinstance(self, ScalarMappable): + # This block logically belongs to ScalarMappable, but can't be + # implemented in it because most ScalarMappable subclasses inherit + # from Artist first and from ScalarMappable second, so + # Artist.format_cursor_data would always have precedence over + # ScalarMappable.format_cursor_data. + n = self.cmap.N + if np.ma.getmask(data): + return "[]" + normed = self.norm(data) + if np.isfinite(normed): + if isinstance(self.norm, BoundaryNorm): + # not an invertible normalization mapping + cur_idx = np.argmin(np.abs(self.norm.boundaries - data)) + neigh_idx = max(0, cur_idx - 1) + # use max diff to prevent delta == 0 + delta = np.diff( + self.norm.boundaries[neigh_idx:cur_idx + 2] + ).max() - @property - def mouseover(self): - """ - If this property is set to *True*, the artist will be queried for - custom context information when the mouse cursor moves over it. + else: + # Midpoints of neighboring color intervals. + neighbors = self.norm.inverse( + (int(normed * n) + np.array([0, 1])) / n) + delta = abs(neighbors - data).max() + g_sig_digits = cbook._g_sig_digits(data, delta) + else: + g_sig_digits = 3 # Consistent with default below. + return "[{:-#.{}g}]".format(data, g_sig_digits) + else: + try: + data[0] + except (TypeError, IndexError): + data = [data] + data_str = ', '.join('{:0.3g}'.format(item) for item in data + if isinstance(item, Number)) + return "[" + data_str + "]" - See also :meth:`get_cursor_data`, :class:`.ToolCursorPosition` and - :class:`.NavigationToolbar2`. + def get_mouseover(self): + """ + Return whether this artist is queried for custom context information + when the mouse cursor moves over it. """ return self._mouseover - @mouseover.setter - def mouseover(self, val): - val = bool(val) - self._mouseover = val + def set_mouseover(self, mouseover): + """ + Set whether this artist is queried for custom context information when + the mouse cursor moves over it. + + Parameters + ---------- + mouseover : bool + + See Also + -------- + get_cursor_data + .ToolCursorPosition + .NavigationToolbar2 + """ + self._mouseover = bool(mouseover) ax = self.axes if ax: - if val: + if self._mouseover: ax._mouseover_set.add(self) else: ax._mouseover_set.discard(self) + mouseover = property(get_mouseover, set_mouseover) # backcompat. + + +def _get_tightbbox_for_layout_only(obj, *args, **kwargs): + """ + Matplotlib's `.Axes.get_tightbbox` and `.Axis.get_tightbbox` support a + *for_layout_only* kwarg; this helper tries to use the kwarg but skips it + when encountering third-party subclasses that do not support it. + """ + try: + return obj.get_tightbbox(*args, **{**kwargs, "for_layout_only": True}) + except TypeError: + return obj.get_tightbbox(*args, **kwargs) + class ArtistInspector: """ @@ -1382,7 +1500,7 @@ def get_valid_values(self, attr): # although barely relevant wrt. matplotlib's total import time. param_name = func.__code__.co_varnames[1] # We could set the presence * based on whether the parameter is a - # varargs (it can't be a varkwargs) but it's not really worth the it. + # varargs (it can't be a varkwargs) but it's not really worth it. match = re.search(r"(?m)^ *\*?{} : (.+)".format(param_name), docstring) if match: return match.group(1) @@ -1413,39 +1531,71 @@ def get_setters(self): continue func = getattr(self.o, name) if (not callable(func) - or len(inspect.signature(func).parameters) < 2 + or self.number_of_parameters(func) < 2 or self.is_alias(func)): continue setters.append(name[4:]) return setters - def is_alias(self, o): - """Return whether method object *o* is an alias for another method.""" - ds = inspect.getdoc(o) + @staticmethod + @lru_cache(maxsize=None) + def number_of_parameters(func): + """Return number of parameters of the callable *func*.""" + return len(inspect.signature(func).parameters) + + @staticmethod + @lru_cache(maxsize=None) + def is_alias(method): + """ + Return whether the object *method* is an alias for another method. + """ + + ds = inspect.getdoc(method) if ds is None: return False + return ds.startswith('Alias for ') def aliased_name(self, s): """ Return 'PROPNAME or alias' if *s* has an alias, else return 'PROPNAME'. - e.g., for the line markerfacecolor property, which has an + For example, for the line markerfacecolor property, which has an alias, return 'markerfacecolor or mfc' and for the transform property, which does not, return 'transform'. """ aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, []))) return s + aliases + _NOT_LINKABLE = { + # A set of property setter methods that are not available in our + # current docs. This is a workaround used to prevent trying to link + # these setters which would lead to "target reference not found" + # warnings during doc build. + 'matplotlib.image._ImageBase.set_alpha', + 'matplotlib.image._ImageBase.set_array', + 'matplotlib.image._ImageBase.set_data', + 'matplotlib.image._ImageBase.set_filternorm', + 'matplotlib.image._ImageBase.set_filterrad', + 'matplotlib.image._ImageBase.set_interpolation', + 'matplotlib.image._ImageBase.set_interpolation_stage', + 'matplotlib.image._ImageBase.set_resample', + 'matplotlib.text._AnnotationBase.set_annotation_clip', + } + def aliased_name_rest(self, s, target): """ Return 'PROPNAME or alias' if *s* has an alias, else return 'PROPNAME', formatted for reST. - e.g., for the line markerfacecolor property, which has an + For example, for the line markerfacecolor property, which has an alias, return 'markerfacecolor or mfc' and for the transform property, which does not, return 'transform'. """ + # workaround to prevent "reference target not found" + if target in self._NOT_LINKABLE: + return f'``{s}``' + aliases = ''.join(' or %s' % x for x in sorted(self.aliasd.get(s, []))) return ':meth:`%s <%s>`%s' % (s, target, aliases) @@ -1578,7 +1728,7 @@ def getp(obj, property=None): If *property* is 'somename', this function returns ``obj.get_somename()``. - If is is None (or unset), it *prints* all gettable properties from + If it's None (or unset), it *prints* all gettable properties from *obj*. Many properties have aliases for shorter typing, e.g. 'lw' is an alias for 'linewidth'. In the output, aliases and full property names will be listed as: @@ -1683,8 +1833,7 @@ def setp(obj, *args, file=None, **kwargs): if len(args) % 2: raise ValueError('The set args must be string, value pairs') - # put args into ordereddict to maintain order - funcvals = OrderedDict((k, v) for k, v in zip(args[::2], args[1::2])) + funcvals = dict(zip(args[::2], args[1::2])) ret = [o.update(funcvals) for o in objs] + [o.set(**kwargs) for o in objs] return list(cbook.flatten(ret)) @@ -1710,5 +1859,6 @@ def kwdoc(artist): if mpl.rcParams['docstring.hardcopy'] else 'Properties:\n' + '\n'.join(ai.pprint_setters(leadingspace=4))) - -docstring.interpd.update(Artist_kwdoc=kwdoc(Artist)) +# We defer this to the end of them module, because it needs ArtistInspector +# to be defined. +Artist._update_set_signature_and_docstring() diff --git a/lib/matplotlib/axes/__init__.py b/lib/matplotlib/axes/__init__.py index 4dd998c0d43d..f8c40889bce7 100644 --- a/lib/matplotlib/axes/__init__.py +++ b/lib/matplotlib/axes/__init__.py @@ -1,2 +1,18 @@ -from ._subplots import * +from . import _base from ._axes import * + +# Backcompat. +from ._axes import Axes as Subplot + + +class _SubplotBaseMeta(type): + def __instancecheck__(self, obj): + return (isinstance(obj, _base._AxesBase) + and obj.get_subplotspec() is not None) + + +class SubplotBase(metaclass=_SubplotBaseMeta): + pass + + +def subplot_class_factory(cls): return cls diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 9d59ed5f2eed..5593b58b9b6c 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -7,13 +7,13 @@ import numpy as np from numpy import ma -import matplotlib.category # Register category unit converter as side-effect. +import matplotlib as mpl +import matplotlib.category # Register category unit converter as side effect. import matplotlib.cbook as cbook import matplotlib.collections as mcoll import matplotlib.colors as mcolors import matplotlib.contour as mcontour -import matplotlib.dates # Register date unit converter as side-effect. -import matplotlib.docstring as docstring +import matplotlib.dates # noqa # Register date unit converter as side effect. import matplotlib.image as mimage import matplotlib.legend as mlegend import matplotlib.lines as mlines @@ -30,7 +30,7 @@ import matplotlib.transforms as mtransforms import matplotlib.tri as mtri import matplotlib.units as munits -from matplotlib import _api, _preprocess_data, rcParams +from matplotlib import _api, _docstring, _preprocess_data from matplotlib.axes._base import ( _AxesBase, _TransformedBoundsLocator, _process_plot_format) from matplotlib.axes._secondary_axes import SecondaryAxis @@ -43,17 +43,29 @@ # All the other methods should go in the _AxesBase class. +@_docstring.interpd class Axes(_AxesBase): """ - The `Axes` contains most of the figure elements: `~.axis.Axis`, + An Axes object encapsulates all the elements of an individual (sub-)plot in + a figure. + + It contains most of the (sub-)plot elements: `~.axis.Axis`, `~.axis.Tick`, `~.lines.Line2D`, `~.text.Text`, `~.patches.Polygon`, etc., and sets the coordinate system. + Like all visible elements in a figure, Axes is an `.Artist` subclass. + The `Axes` instance supports callbacks through a callbacks attribute which is a `~.cbook.CallbackRegistry` instance. The events you can connect to are 'xlim_changed' and 'ylim_changed' and the callback will be called with func(*ax*) where *ax* is the `Axes` instance. + .. note:: + + As a user, you do not instantiate Axes directly, but use Axes creation + methods instead; e.g. from `.pyplot` or `.Figure`: + `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`. + Attributes ---------- dataLim : `.Bbox` @@ -117,9 +129,9 @@ def set_title(self, label, fontdict=None, loc=None, pad=None, *, y=None, 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. + Vertical Axes location for the title (1.0 is the top). If + None (the default) and :rc:`axes.titley` is also None, 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. @@ -136,10 +148,10 @@ def set_title(self, label, fontdict=None, loc=None, pad=None, *, y=None, of valid text properties. """ if loc is None: - loc = rcParams['axes.titlelocation'] + loc = mpl.rcParams['axes.titlelocation'] if y is None: - y = rcParams['axes.titley'] + y = mpl.rcParams['axes.titley'] if y is None: y = 1.0 else: @@ -151,21 +163,21 @@ def set_title(self, label, fontdict=None, loc=None, pad=None, *, y=None, 'right': self._right_title} title = _api.check_getitem(titles, loc=loc.lower()) default = { - 'fontsize': rcParams['axes.titlesize'], - 'fontweight': rcParams['axes.titleweight'], + 'fontsize': mpl.rcParams['axes.titlesize'], + 'fontweight': mpl.rcParams['axes.titleweight'], 'verticalalignment': 'baseline', 'horizontalalignment': loc.lower()} - titlecolor = rcParams['axes.titlecolor'] + titlecolor = mpl.rcParams['axes.titlecolor'] if not cbook._str_lower_equal(titlecolor, 'auto'): default["color"] = titlecolor if pad is None: - pad = rcParams['axes.titlepad'] + pad = mpl.rcParams['axes.titlepad'] self._set_title_offset_trans(float(pad)) title.set_text(label) title.update(default) if fontdict is not None: title.update(fontdict) - title.update(kwargs) + title._internal_update(kwargs) return title def get_legend_handles_labels(self, legend_handler_map=None): @@ -182,7 +194,7 @@ def get_legend_handles_labels(self, legend_handler_map=None): [self], legend_handler_map) return handles, labels - @docstring.dedent_interpd + @_docstring.dedent_interpd def legend(self, *args, **kwargs): """ Place a legend on the Axes. @@ -190,10 +202,11 @@ def legend(self, *args, **kwargs): Call signatures:: legend() - legend(labels) legend(handles, labels) + legend(handles=handles) + legend(labels) - The call signatures correspond to these three different ways to use + The call signatures correspond to the following different ways to use this method: **1. Automatic detection of elements to be shown in the legend** @@ -214,34 +227,49 @@ def legend(self, *args, **kwargs): line.set_label('Label via method') ax.legend() - Specific lines can be excluded from the automatic legend element - selection by defining a label starting with an underscore. - This is default for all artists, so calling `.Axes.legend` without - any arguments and without setting the labels manually will result in - no legend being drawn. + .. note:: + Specific artists can be excluded from the automatic legend element + selection by using a label starting with an underscore, "_". + A string starting with an underscore is the default label for all + artists, so calling `.Axes.legend` without any arguments and + without setting the labels manually will result in no legend being + drawn. + + + **2. Explicitly listing the artists and labels in the legend** + For full control of which artists have a legend entry, it is possible + to pass an iterable of legend artists followed by an iterable of + legend labels respectively:: - **2. Labeling existing plot elements** + ax.legend([line1, line2, line3], ['label1', 'label2', 'label3']) - To make a legend for lines which already exist on the Axes - (via plot for instance), simply call this function with an iterable - of strings, one for each legend item. For example:: - ax.plot([1, 2, 3]) - ax.legend(['A simple line']) + **3. Explicitly listing the artists in the legend** - Note: This call signature is discouraged, because the relation between - plot elements and labels is only implicit by their order and can - easily be mixed up. + This is similar to 2, but the labels are taken from the artists' + label properties. Example:: + line1, = ax.plot([1, 2, 3], label='label1') + line2, = ax.plot([1, 2, 3], label='label2') + ax.legend(handles=[line1, line2]) - **3. Explicitly defining the elements in the legend** - For full control of which artists have a legend entry, it is possible - to pass an iterable of legend artists followed by an iterable of - legend labels respectively:: + **4. Labeling existing plot elements** + + .. admonition:: Discouraged + + This call signature is discouraged, because the relation between + plot elements and labels is only implicit by their order and can + easily be mixed up. + + To make a legend for all artists on an Axes, call this function with + an iterable of strings, one for each legend item. For example:: + + ax.plot([1, 2, 3]) + ax.plot([5, 6, 7]) + ax.legend(['First line', 'Second line']) - ax.legend([line1, line2, line3], ['label1', 'label2', 'label3']) Parameters ---------- @@ -266,7 +294,7 @@ def legend(self, *args, **kwargs): Other Parameters ---------------- - %(_legend_kw_doc)s + %(_legend_kw_axes)s See Also -------- @@ -311,13 +339,27 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): Defaults to `ax.transAxes`, i.e. the units of *rect* are in Axes-relative coordinates. + projection : {None, 'aitoff', 'hammer', 'lambert', 'mollweide', \ +'polar', 'rectilinear', str}, optional + The projection type of the inset `~.axes.Axes`. *str* is the name + of a custom projection, see `~matplotlib.projections`. The default + None results in a 'rectilinear' projection. + + polar : bool, default: False + If True, equivalent to projection='polar'. + + axes_class : subclass type of `~.axes.Axes`, optional + The `.axes.Axes` subclass that is instantiated. This parameter + is incompatible with *projection* and *polar*. See + :ref:`axisartist_users-guide-index` for examples. + zorder : number Defaults to 5 (same as `.Axes.legend`). Adjust higher or lower to change whether it is above or below data plotted on the parent Axes. **kwargs - Other keyword arguments are passed on to the child `.Axes`. + Other keyword arguments are passed on to the inset Axes class. Returns ------- @@ -343,7 +385,10 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): # This puts the rectangle into figure-relative coordinates. inset_locator = _TransformedBoundsLocator(bounds, transform) bounds = inset_locator(self, None).bounds - inset_ax = Axes(self.figure, bounds, zorder=zorder, **kwargs) + projection_class, pkw = self.figure._process_projection_requirements( + bounds, **kwargs) + inset_ax = projection_class(self.figure, bounds, zorder=zorder, **pkw) + # this locator lets the axes move if in data coordinates. # it gets called in `ax.apply_aspect() (of all places) inset_ax.set_axes_locator(inset_locator) @@ -352,7 +397,7 @@ def inset_axes(self, bounds, *, transform=None, zorder=5, **kwargs): return inset_ax - @docstring.dedent_interpd + @_docstring.dedent_interpd def indicate_inset(self, bounds, inset_ax=None, *, transform=None, facecolor='none', edgecolor='0.5', alpha=0.5, zorder=4.99, **kwargs): @@ -397,7 +442,7 @@ def indicate_inset(self, bounds, inset_ax=None, *, transform=None, **kwargs Other keyword arguments are passed on to the `.Rectangle` patch: - %(Rectangle_kwdoc)s + %(Rectangle:kwdoc)s Returns ------- @@ -504,10 +549,10 @@ def indicate_inset_zoom(self, inset_ax, **kwargs): rect = (xlim[0], ylim[0], xlim[1] - xlim[0], ylim[1] - ylim[0]) return self.indicate_inset(rect, inset_ax, **kwargs) - @docstring.dedent_interpd + @_docstring.dedent_interpd def secondary_xaxis(self, location, *, functions=None, **kwargs): """ - Add a second x-axis to this Axes. + Add a second x-axis to this `~.axes.Axes`. For example if we want to have a second scale for the data plotted on the xaxis. @@ -546,10 +591,10 @@ def invert(x): raise ValueError('secondary_xaxis location must be either ' 'a float or "top"/"bottom"') - @docstring.dedent_interpd + @_docstring.dedent_interpd def secondary_yaxis(self, location, *, functions=None, **kwargs): """ - Add a second y-axis to this Axes. + Add a second y-axis to this `~.axes.Axes`. For example if we want to have a second scale for the data plotted on the yaxis. @@ -578,7 +623,7 @@ def secondary_yaxis(self, location, *, functions=None, **kwargs): raise ValueError('secondary_yaxis location must be either ' 'a float or "left"/"right"') - @docstring.dedent_interpd + @_docstring.dedent_interpd def text(self, x, y, s, fontdict=None, **kwargs): """ Add text to the Axes. @@ -609,7 +654,7 @@ def text(self, x, y, s, fontdict=None, **kwargs): **kwargs : `~matplotlib.text.Text` properties. Other miscellaneous text parameters. - %(Text_kwdoc)s + %(Text:kwdoc)s Examples -------- @@ -646,10 +691,14 @@ def text(self, x, y, s, fontdict=None, **kwargs): self._add_text(t) return t - @_api.rename_parameter("3.3", "s", "text") - @docstring.dedent_interpd - def annotate(self, text, xy, *args, **kwargs): - a = mtext.Annotation(text, xy, *args, **kwargs) + @_docstring.dedent_interpd + def annotate(self, text, xy, xytext=None, xycoords='data', textcoords=None, + arrowprops=None, annotation_clip=None, **kwargs): + # Signature must match Annotation. This is verified in + # test_annotate_signature(). + a = mtext.Annotation(text, xy, xytext=xytext, xycoords=xycoords, + textcoords=textcoords, arrowprops=arrowprops, + annotation_clip=annotation_clip, **kwargs) a.set_transform(mtransforms.IdentityTransform()) if 'clip_on' in kwargs: a.set_clip_path(self.patch) @@ -658,10 +707,10 @@ def annotate(self, text, xy, *args, **kwargs): annotate.__doc__ = mtext.Annotation.__init__.__doc__ #### Lines and spans - @docstring.dedent_interpd + @_docstring.dedent_interpd def axhline(self, y=0, xmin=0, xmax=1, **kwargs): """ - Add a horizontal line across the axis. + Add a horizontal line across the Axes. Parameters ---------- @@ -683,10 +732,10 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs): Other Parameters ---------------- **kwargs - Valid keyword arguments are `.Line2D` properties, with the - exception of 'transform': + Valid keyword arguments are `.Line2D` properties, except for + 'transform': - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -722,10 +771,11 @@ def axhline(self, y=0, xmin=0, xmax=1, **kwargs): trans = self.get_yaxis_transform(which='grid') l = mlines.Line2D([xmin, xmax], [y, y], transform=trans, **kwargs) self.add_line(l) - self._request_autoscale_view(scalex=False, scaley=scaley) + if scaley: + self._request_autoscale_view("y") return l - @docstring.dedent_interpd + @_docstring.dedent_interpd def axvline(self, x=0, ymin=0, ymax=1, **kwargs): """ Add a vertical line across the Axes. @@ -750,10 +800,10 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs): Other Parameters ---------------- **kwargs - Valid keyword arguments are `.Line2D` properties, with the - exception of 'transform': + Valid keyword arguments are `.Line2D` properties, except for + 'transform': - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -789,7 +839,8 @@ def axvline(self, x=0, ymin=0, ymax=1, **kwargs): trans = self.get_xaxis_transform(which='grid') l = mlines.Line2D([x, x], [ymin, ymax], transform=trans, **kwargs) self.add_line(l) - self._request_autoscale_view(scalex=scalex, scaley=False) + if scalex: + self._request_autoscale_view("x") return l @staticmethod @@ -800,7 +851,7 @@ def _check_no_units(vals, names): raise ValueError(f"{name} must be a single scalar value, " f"but got {val}") - @docstring.dedent_interpd + @_docstring.dedent_interpd def axline(self, xy1, xy2=None, *, slope=None, **kwargs): """ Add an infinitely long straight line. @@ -837,7 +888,7 @@ def axline(self, xy1, xy2=None, *, slope=None, **kwargs): **kwargs Valid kwargs are `.Line2D` properties - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -866,15 +917,15 @@ def axline(self, xy1, xy2=None, *, slope=None, **kwargs): if line.get_clip_path() is None: line.set_clip_path(self.patch) if not line.get_label(): - line.set_label(f"_line{len(self.lines)}") - self.lines.append(line) - line._remove_method = self.lines.remove + line.set_label(f"_child{len(self._children)}") + self._children.append(line) + line._remove_method = self._children.remove self.update_datalim(datalim) self._request_autoscale_view() return line - @docstring.dedent_interpd + @_docstring.dedent_interpd def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): """ Add a horizontal span (rectangle) across the Axes. @@ -905,7 +956,7 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): ---------------- **kwargs : `~matplotlib.patches.Polygon` properties - %(Polygon_kwdoc)s + %(Polygon:kwdoc)s See Also -------- @@ -919,10 +970,10 @@ def axhspan(self, ymin, ymax, xmin=0, xmax=1, **kwargs): p = mpatches.Polygon(verts, **kwargs) p.set_transform(self.get_yaxis_transform(which="grid")) self.add_patch(p) - self._request_autoscale_view(scalex=False) + self._request_autoscale_view("y") return p - @docstring.dedent_interpd + @_docstring.dedent_interpd def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): """ Add a vertical span (rectangle) across the Axes. @@ -953,7 +1004,7 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): ---------------- **kwargs : `~matplotlib.patches.Polygon` properties - %(Polygon_kwdoc)s + %(Polygon:kwdoc)s See Also -------- @@ -974,8 +1025,9 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): verts = [(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)] p = mpatches.Polygon(verts, **kwargs) p.set_transform(self.get_xaxis_transform(which="grid")) + p.get_path()._interpolation_steps = 100 self.add_patch(p) - self._request_autoscale_view(scaley=False) + self._request_autoscale_view("x") return p @_preprocess_data(replace_names=["y", "xmin", "xmax", "colors"], @@ -992,7 +1044,7 @@ def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', xmin, xmax : float or array-like Respective beginning and end of each line. If scalars are - provided, all lines will have same length. + provided, all lines will have the same length. colors : list of colors, default: :rc:`lines.color` @@ -1006,6 +1058,8 @@ def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER **kwargs : `~matplotlib.collections.LineCollection` properties. See Also @@ -1040,16 +1094,17 @@ def hlines(self, y, xmin, xmax, colors=None, linestyles='solid', lines = mcoll.LineCollection(masked_verts, colors=colors, linestyles=linestyles, label=label) self.add_collection(lines, autolim=False) - lines.update(kwargs) + lines._internal_update(kwargs) if len(y) > 0: - minx = min(xmin.min(), xmax.min()) - maxx = max(xmin.max(), xmax.max()) - miny = y.min() - maxy = y.max() - + # Extreme values of xmin/xmax/y. Using masked_verts here handles + # the case of y being a masked *object* array (as can be generated + # e.g. by errorbar()), which would make nanmin/nanmax stumble. + minx = np.nanmin(masked_verts[..., 0]) + maxx = np.nanmax(masked_verts[..., 0]) + miny = np.nanmin(masked_verts[..., 1]) + maxy = np.nanmax(masked_verts[..., 1]) corners = (minx, miny), (maxx, maxy) - self.update_datalim(corners) self._request_autoscale_view() @@ -1069,7 +1124,7 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', ymin, ymax : float or array-like Respective beginning and end of each line. If scalars are - provided, all lines will have same length. + provided, all lines will have the same length. colors : list of colors, default: :rc:`lines.color` @@ -1083,6 +1138,8 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER **kwargs : `~matplotlib.collections.LineCollection` properties. See Also @@ -1117,14 +1174,16 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', lines = mcoll.LineCollection(masked_verts, colors=colors, linestyles=linestyles, label=label) self.add_collection(lines, autolim=False) - lines.update(kwargs) + lines._internal_update(kwargs) if len(x) > 0: - minx = x.min() - maxx = x.max() - miny = min(ymin.min(), ymax.min()) - maxy = max(ymin.max(), ymax.max()) - + # Extreme values of x/ymin/ymax. Using masked_verts here handles + # the case of x being a masked *object* array (as can be generated + # e.g. by errorbar()), which would make nanmin/nanmax stumble. + minx = np.nanmin(masked_verts[..., 0]) + maxx = np.nanmax(masked_verts[..., 0]) + miny = np.nanmin(masked_verts[..., 1]) + maxy = np.nanmax(masked_verts[..., 1]) corners = (minx, miny), (maxx, maxy) self.update_datalim(corners) self._request_autoscale_view() @@ -1134,9 +1193,9 @@ def vlines(self, x, ymin, ymax, colors=None, linestyles='solid', @_preprocess_data(replace_names=["positions", "lineoffsets", "linelengths", "linewidths", "colors", "linestyles"]) - @docstring.dedent_interpd + @_docstring.dedent_interpd def eventplot(self, positions, orientation='horizontal', lineoffsets=1, - linelengths=1, linewidths=None, colors=None, + linelengths=1, linewidths=None, colors=None, alpha=None, linestyles='solid', **kwargs): """ Plot identical parallel lines at the given positions. @@ -1198,6 +1257,13 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, If *positions* is 2D, this can be a sequence with length matching the length of *positions*. + alpha : float or array-like, default: 1 + The alpha blending value(s), between 0 (transparent) and 1 + (opaque). + + If *positions* is 2D, this can be a sequence with length matching + the length of *positions*. + linestyles : str or tuple or list of such values, default: 'solid' Default is 'solid'. Valid strings are ['solid', 'dashed', 'dashdot', 'dotted', '-', '--', '-.', ':']. Dash tuples @@ -1211,6 +1277,9 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, If *positions* is 2D, this can be a sequence with length matching the length of *positions*. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Other keyword arguments are line collection properties. See `.LineCollection` for a list of the valid properties. @@ -1222,8 +1291,8 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, Notes ----- - For *linelengths*, *linewidths*, *colors*, and *linestyles*, if only - a single value is given, that value is applied to all lines. If an + For *linelengths*, *linewidths*, *colors*, *alpha* and *linestyles*, if + only a single value is given, that value is applied to all lines. If an array-like is given, it must have the same length as *positions*, and each value will be applied to the corresponding row of the array. @@ -1231,10 +1300,11 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, -------- .. plot:: gallery/lines_bars_and_markers/eventplot_demo.py """ - # We do the conversion first since not all unitized data is uniform - positions, lineoffsets, linelengths = self._process_unit_info( - [("x", positions), ("y", lineoffsets), ("y", linelengths)], kwargs) + lineoffsets, linelengths = self._process_unit_info( + [("y", lineoffsets), ("y", linelengths)], kwargs) + + # fix positions, noting that it can be a list of lists: if not np.iterable(positions): positions = [positions] elif any(np.iterable(position) for position in positions): @@ -1245,6 +1315,11 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, if len(positions) == 0: return [] + poss = [] + for position in positions: + poss += self._process_unit_info([("x", position)], kwargs) + positions = poss + # prevent 'singular' keys from **kwargs dict from overriding the effect # of 'plural' keyword arguments (e.g. 'color' overriding 'colors') colors = cbook._local_over_kwdict(colors, kwargs, 'color') @@ -1259,6 +1334,8 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, linewidths = [linewidths] if not np.iterable(colors): colors = [colors] + if not np.iterable(alpha): + alpha = [alpha] if hasattr(linestyles, 'lower') or not np.iterable(linestyles): linestyles = [linestyles] @@ -1295,8 +1372,9 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, if len(linewidths) == 1: linewidths = np.tile(linewidths, len(positions)) if len(colors) == 1: - colors = list(colors) - colors = colors * len(positions) + colors = list(colors) * len(positions) + if len(alpha) == 1: + alpha = list(alpha) * len(positions) if len(linestyles) == 1: linestyles = [linestyles] * len(positions) @@ -1312,23 +1390,28 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, if len(colors) != len(positions): raise ValueError('colors and positions are unequal sized ' 'sequences') + if len(alpha) != len(positions): + raise ValueError('alpha and positions are unequal sized ' + 'sequences') if len(linestyles) != len(positions): raise ValueError('linestyles and positions are unequal sized ' 'sequences') colls = [] - for position, lineoffset, linelength, linewidth, color, linestyle in \ + for position, lineoffset, linelength, linewidth, color, alpha_, \ + linestyle in \ zip(positions, lineoffsets, linelengths, linewidths, - colors, linestyles): + colors, alpha, linestyles): coll = mcoll.EventCollection(position, orientation=orientation, lineoffset=lineoffset, linelength=linelength, linewidth=linewidth, color=color, + alpha=alpha_, linestyle=linestyle) self.add_collection(coll, autolim=False) - coll.update(kwargs) + coll._internal_update(kwargs) colls.append(coll) if len(positions) > 0: @@ -1344,10 +1427,9 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, minline = (lineoffsets - linelengths).min() maxline = (lineoffsets + linelengths).max() - if (orientation is not None and - orientation.lower() == "vertical"): + if orientation == "vertical": corners = (minline, minpos), (maxline, maxpos) - else: # "horizontal", None or "none" (see EventCollection) + else: # "horizontal" corners = (minpos, minline), (maxpos, maxline) self.update_datalim(corners) self._request_autoscale_view() @@ -1358,7 +1440,7 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, # Uses a custom implementation of data-kwarg handling in # _process_plot_var_args. - @docstring.dedent_interpd + @_docstring.dedent_interpd def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): """ Plot y versus x as lines and/or markers. @@ -1435,7 +1517,7 @@ def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): >>> plot(x1, y1, 'g^', x2, y2, 'g-') In this case, any additional keyword argument applies to all - datasets. Also this syntax cannot be combined with the *data* + datasets. Also, this syntax cannot be combined with the *data* parameter. By default, each line is assigned a different style specified by a @@ -1489,7 +1571,8 @@ def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): ---------------- scalex, scaley : bool, default: True These parameters determine if the view limits are adapted to the - data limits. The values are passed on to `autoscale_view`. + data limits. The values are passed on to + `~.axes.Axes.autoscale_view`. **kwargs : `.Line2D` properties, optional *kwargs* are used to specify properties like a line label (for @@ -1505,7 +1588,7 @@ def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): Here is a list of available `.Line2D` properties: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -1605,15 +1688,30 @@ def plot(self, *args, scalex=True, scaley=True, data=None, **kwargs): lines = [*self._get_lines(*args, data=data, **kwargs)] for line in lines: self.add_line(line) - self._request_autoscale_view(scalex=scalex, scaley=scaley) + if scalex: + self._request_autoscale_view("x") + if scaley: + self._request_autoscale_view("y") return lines @_preprocess_data(replace_names=["x", "y"], label_namer="y") - @docstring.dedent_interpd + @_docstring.dedent_interpd def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, **kwargs): """ - Plot co-ercing the axis to treat floats as dates. + [*Discouraged*] Plot coercing the axis to treat floats as dates. + + .. admonition:: Discouraged + + This method exists for historic reasons and will be deprecated in + the future. + + - ``datetime``-like data should directly be plotted using + `~.Axes.plot`. + - If you need to plot plain numeric data as :ref:`date-format` or + need to set a timezone, call ``ax.xaxis.axis_date`` / + ``ax.yaxis.axis_date`` before `~.Axes.plot`. See + `.Axis.axis_date`. Similar to `.plot`, this plots *y* vs. *x* as lines or markers. However, the axis labels are formatted as dates depending on *xdate* @@ -1642,15 +1740,17 @@ def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, Returns ------- - list of `~.Line2D` + list of `.Line2D` Objects representing the plotted data. Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER **kwargs Keyword arguments control the `.Line2D` properties: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -1676,10 +1776,10 @@ def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, return self.plot(x, y, fmt, **kwargs) # @_preprocess_data() # let 'plot' do the unpacking.. - @docstring.dedent_interpd + @_docstring.dedent_interpd def loglog(self, *args, **kwargs): """ - Make a plot with log scaling on both the x and y axis. + Make a plot with log scaling on both the x- and y-axis. Call signatures:: @@ -1687,7 +1787,7 @@ def loglog(self, *args, **kwargs): loglog([x], y, [fmt], [x2], y2, [fmt2], ..., **kwargs) This is just a thin wrapper around `.plot` which additionally changes - both the x-axis and the y-axis to log scaling. All of the concepts and + both the x-axis and the y-axis to log scaling. All the concepts and parameters of plot can be used here as well. The additional parameters *base*, *subs* and *nonpositive* control the @@ -1710,15 +1810,13 @@ def loglog(self, *args, **kwargs): Non-positive values can be masked as invalid, or clipped to a very small positive number. + **kwargs + All parameters supported by `.plot`. + Returns ------- - list of `~.Line2D` + list of `.Line2D` Objects representing the plotted data. - - Other Parameters - ---------------- - **kwargs - All parameters supported by `.plot`. """ dx = {k: v for k, v in kwargs.items() if k in ['base', 'subs', 'nonpositive', @@ -1732,10 +1830,10 @@ def loglog(self, *args, **kwargs): *args, **{k: v for k, v in kwargs.items() if k not in {*dx, *dy}}) # @_preprocess_data() # let 'plot' do the unpacking.. - @docstring.dedent_interpd + @_docstring.dedent_interpd def semilogx(self, *args, **kwargs): """ - Make a plot with log scaling on the x axis. + Make a plot with log scaling on the x-axis. Call signatures:: @@ -1743,8 +1841,8 @@ def semilogx(self, *args, **kwargs): semilogx([x], y, [fmt], [x2], y2, [fmt2], ..., **kwargs) This is just a thin wrapper around `.plot` which additionally changes - the x-axis to log scaling. All of the concepts and parameters of plot - can be used here as well. + the x-axis to log scaling. All the concepts and parameters of plot can + be used here as well. The additional parameters *base*, *subs*, and *nonpositive* control the x-axis properties. They are just forwarded to `.Axes.set_xscale`. @@ -1763,15 +1861,13 @@ def semilogx(self, *args, **kwargs): Non-positive values in x can be masked as invalid, or clipped to a very small positive number. + **kwargs + All parameters supported by `.plot`. + Returns ------- - list of `~.Line2D` + list of `.Line2D` Objects representing the plotted data. - - Other Parameters - ---------------- - **kwargs - All parameters supported by `.plot`. """ d = {k: v for k, v in kwargs.items() if k in ['base', 'subs', 'nonpositive', @@ -1781,10 +1877,10 @@ def semilogx(self, *args, **kwargs): *args, **{k: v for k, v in kwargs.items() if k not in d}) # @_preprocess_data() # let 'plot' do the unpacking.. - @docstring.dedent_interpd + @_docstring.dedent_interpd def semilogy(self, *args, **kwargs): """ - Make a plot with log scaling on the y axis. + Make a plot with log scaling on the y-axis. Call signatures:: @@ -1792,8 +1888,8 @@ def semilogy(self, *args, **kwargs): semilogy([x], y, [fmt], [x2], y2, [fmt2], ..., **kwargs) This is just a thin wrapper around `.plot` which additionally changes - the y-axis to log scaling. All of the concepts and parameters of plot - can be used here as well. + the y-axis to log scaling. All the concepts and parameters of plot can + be used here as well. The additional parameters *base*, *subs*, and *nonpositive* control the y-axis properties. They are just forwarded to `.Axes.set_yscale`. @@ -1812,15 +1908,13 @@ def semilogy(self, *args, **kwargs): Non-positive values in y can be masked as invalid, or clipped to a very small positive number. + **kwargs + All parameters supported by `.plot`. + Returns ------- - list of `~.Line2D` + list of `.Line2D` Objects representing the plotted data. - - Other Parameters - ---------------- - **kwargs - All parameters supported by `.plot`. """ d = {k: v for k, v in kwargs.items() if k in ['base', 'subs', 'nonpositive', @@ -1886,6 +1980,9 @@ def acorr(self, x, **kwargs): The marker for plotting the data points. Only used if *usevlines* is ``False``. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Additional parameters are passed to `.Axes.vlines` and `.Axes.axhline` if *usevlines* is ``True``; otherwise they are @@ -1960,6 +2057,9 @@ def xcorr(self, x, y, normed=True, detrend=mlab.detrend_none, The marker for plotting the data points. Only used if *usevlines* is ``False``. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Additional parameters are passed to `.Axes.vlines` and `.Axes.axhline` if *usevlines* is ``True``; otherwise they are @@ -2048,10 +2148,6 @@ def step(self, x, y, *args, where='pre', data=None, **kwargs): and plotted on the given positions, however, this is a rarely needed feature for step plots. - data : indexable object, optional - An object with labelled data. If given, provide the label names to - plot in *x* and *y*. - where : {'pre', 'post', 'mid'}, default: 'pre' Define where the steps should be placed: @@ -2063,19 +2159,17 @@ def step(self, x, y, *args, where='pre', data=None, **kwargs): value ``y[i]``. - 'mid': Steps occur half-way between the *x* positions. - Returns - ------- - list of `.Line2D` - Objects representing the plotted data. + data : indexable object, optional + An object with labelled data. If given, provide the label names to + plot in *x* and *y*. - Other Parameters - ---------------- **kwargs Additional parameters are the same as those for `.plot`. - Notes - ----- - .. [notes section required to get data note injection right] + Returns + ------- + list of `.Line2D` + Objects representing the plotted data. """ _api.check_in_list(('pre', 'post', 'mid'), where=where) kwargs['drawstyle'] = 'steps-' + where @@ -2111,14 +2205,22 @@ def _convert_dx(dx, x0, xconv, convert): # removes the units from unit packages like `pint` that # wrap numpy arrays. try: - x0 = cbook.safe_first_element(x0) + x0 = cbook._safe_first_finite(x0) except (TypeError, IndexError, KeyError): - x0 = x0 + pass + except StopIteration: + # this means we found no finite element, fall back to first + # element unconditionally + x0 = cbook.safe_first_element(x0) try: - x = cbook.safe_first_element(xconv) + x = cbook._safe_first_finite(xconv) except (TypeError, IndexError, KeyError): x = xconv + except StopIteration: + # this means we found no finite element, fall back to first + # element unconditionally + x = cbook.safe_first_element(xconv) delist = False if not np.iterable(dx): @@ -2134,7 +2236,7 @@ def _convert_dx(dx, x0, xconv, convert): return dx @_preprocess_data() - @docstring.dedent_interpd + @_docstring.dedent_interpd def bar(self, x, height, width=0.8, bottom=None, *, align="center", **kwargs): r""" @@ -2160,7 +2262,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", The width(s) of the bars. bottom : float or array-like, default: 0 - The y coordinate(s) of the bars bases. + The y coordinate(s) of the bottom side(s) of the bars. align : {'center', 'edge'}, default: 'center' Alignment of the bars to the *x* coordinates: @@ -2191,6 +2293,14 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", The tick labels of the bars. Default: None (Use default numeric labels.) + label : str or list of str, optional + A single label is attached to the resulting `.BarContainer` as a + label for the whole dataset. + If a list is provided, it must be the same length as *x* and + labels the individual bars. Repeated labels are not de-duplicated + and will cause repeated label entries, so this is best used when + bars also differ in style (e.g., by passing a list to *color*.) + xerr, yerr : float or array-like of shape(N,) or shape(2, N), optional If not *None*, add horizontal / vertical errorbars to the bar tips. The values are +/- sizes relative to the data: @@ -2202,8 +2312,8 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", errors. - *None*: No errorbar. (Default) - See :doc:`/gallery/statistics/errorbar_features` - for an example on the usage of ``xerr`` and ``yerr``. + See :doc:`/gallery/statistics/errorbar_features` for an example on + the usage of *xerr* and *yerr*. ecolor : color or list of color, default: 'black' The line color of the errorbars. @@ -2212,16 +2322,19 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", The length of the error bar caps in points. error_kw : dict, optional - Dictionary of kwargs to be passed to the `~.Axes.errorbar` - method. Values of *ecolor* or *capsize* defined here take - precedence over the independent kwargs. + Dictionary of keyword arguments to be passed to the + `~.Axes.errorbar` method. Values of *ecolor* or *capsize* defined + here take precedence over the independent keyword arguments. log : bool, default: False If *True*, set the y-axis to be log scale. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs : `.Rectangle` properties - %(Rectangle_kwdoc)s + %(Rectangle:kwdoc)s See Also -------- @@ -2254,7 +2367,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", ezorder += 0.01 error_kw.setdefault('zorder', ezorder) ecolor = kwargs.pop('ecolor', 'k') - capsize = kwargs.pop('capsize', rcParams["errorbar.capsize"]) + capsize = kwargs.pop('capsize', mpl.rcParams["errorbar.capsize"]) error_kw.setdefault('ecolor', ecolor) error_kw.setdefault('capsize', capsize) @@ -2271,7 +2384,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", if orientation == 'vertical': if y is None: y = 0 - elif orientation == 'horizontal': + else: # horizontal if x is None: x = 0 @@ -2280,7 +2393,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", [("x", x), ("y", height)], kwargs, convert=False) if log: self.set_yscale('log', nonpositive='clip') - elif orientation == 'horizontal': + else: # horizontal self._process_unit_info( [("x", width), ("y", y)], kwargs, convert=False) if log: @@ -2309,10 +2422,20 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", if orientation == 'vertical': tick_label_axis = self.xaxis tick_label_position = x - elif orientation == 'horizontal': + else: # horizontal tick_label_axis = self.yaxis tick_label_position = y + if not isinstance(label, str) and np.iterable(label): + bar_container_label = '_nolegend_' + patch_labels = label + else: + bar_container_label = label + patch_labels = ['_nolegend_'] * len(x) + if len(patch_labels) != len(x): + raise ValueError(f'number of labels ({len(patch_labels)}) ' + f'does not match number of bars ({len(x)}).') + linewidth = itertools.cycle(np.atleast_1d(linewidth)) hatch = itertools.cycle(np.atleast_1d(hatch)) color = itertools.chain(itertools.cycle(mcolors.to_rgba_array(color)), @@ -2338,7 +2461,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", f'and width ({width.dtype}) ' f'are incompatible') from e bottom = y - elif orientation == 'horizontal': + else: # horizontal try: bottom = y - height / 2 except TypeError as e: @@ -2346,27 +2469,27 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", f'and height ({height.dtype}) ' f'are incompatible') from e left = x - elif align == 'edge': + else: # edge left = x bottom = y patches = [] args = zip(left, bottom, width, height, color, edgecolor, linewidth, - hatch) - for l, b, w, h, c, e, lw, htch in args: + hatch, patch_labels) + for l, b, w, h, c, e, lw, htch, lbl in args: r = mpatches.Rectangle( xy=(l, b), width=w, height=h, facecolor=c, edgecolor=e, linewidth=lw, - label='_nolegend_', + label=lbl, hatch=htch, ) - r.update(kwargs) + r._internal_update(kwargs) r.get_path()._interpolation_steps = 100 if orientation == 'vertical': r.sticky_edges.y.append(b) - elif orientation == 'horizontal': + else: # horizontal r.sticky_edges.x.append(l) self.add_patch(r) patches.append(r) @@ -2377,7 +2500,7 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", ex = [l + 0.5 * w for l, w in zip(left, width)] ey = [b + h for b, h in zip(bottom, height)] - elif orientation == 'horizontal': + else: # horizontal # using list comps rather than arrays to preserve unit info ex = [l + w for l, w in zip(left, width)] ey = [b + 0.5 * h for b, h in zip(bottom, height)] @@ -2394,11 +2517,12 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", if orientation == 'vertical': datavalues = height - elif orientation == 'horizontal': + else: # horizontal datavalues = width bar_container = BarContainer(patches, errorbar, datavalues=datavalues, - orientation=orientation, label=label) + orientation=orientation, + label=bar_container_label) self.add_container(bar_container) if tick_labels is not None: @@ -2408,9 +2532,10 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", return bar_container - @docstring.dedent_interpd + # @_preprocess_data() # let 'bar' do the unpacking.. + @_docstring.dedent_interpd def barh(self, y, width, height=0.8, left=None, *, align="center", - **kwargs): + data=None, **kwargs): r""" Make a horizontal bar plot. @@ -2434,7 +2559,7 @@ def barh(self, y, width, height=0.8, left=None, *, align="center", The heights of the bars. left : float or array-like, default: 0 - The x coordinates of the left sides of the bars. + The x coordinates of the left side(s) of the bars. align : {'center', 'edge'}, default: 'center' Alignment of the base to the *y* coordinates*: @@ -2466,9 +2591,17 @@ def barh(self, y, width, height=0.8, left=None, *, align="center", The tick labels of the bars. Default: None (Use default numeric labels.) + label : str or list of str, optional + A single label is attached to the resulting `.BarContainer` as a + label for the whole dataset. + If a list is provided, it must be the same length as *y* and + labels the individual bars. Repeated labels are not de-duplicated + and will cause repeated label entries, so this is best used when + bars also differ in style (e.g., by passing a list to *color*.) + xerr, yerr : float or array-like of shape(N,) or shape(2, N), optional - If not ``None``, add horizontal / vertical errorbars to the - bar tips. The values are +/- sizes relative to the data: + If not *None*, add horizontal / vertical errorbars to the bar tips. + The values are +/- sizes relative to the data: - scalar: symmetric +/- values for all bars - shape(N,): symmetric +/- values for each bar @@ -2477,8 +2610,8 @@ def barh(self, y, width, height=0.8, left=None, *, align="center", errors. - *None*: No errorbar. (default) - See :doc:`/gallery/statistics/errorbar_features` - for an example on the usage of ``xerr`` and ``yerr``. + See :doc:`/gallery/statistics/errorbar_features` for an example on + the usage of *xerr* and *yerr*. ecolor : color or list of color, default: 'black' The line color of the errorbars. @@ -2487,16 +2620,20 @@ def barh(self, y, width, height=0.8, left=None, *, align="center", The length of the error bar caps in points. error_kw : dict, optional - Dictionary of kwargs to be passed to the `~.Axes.errorbar` - method. Values of *ecolor* or *capsize* defined here take - precedence over the independent kwargs. + Dictionary of keyword arguments to be passed to the + `~.Axes.errorbar` method. Values of *ecolor* or *capsize* defined + here take precedence over the independent keyword arguments. log : bool, default: False If ``True``, set the x-axis to be log scale. + data : indexable object, optional + If given, all parameters also accept a string ``s``, which is + interpreted as ``data[s]`` (unless this raises an exception). + **kwargs : `.Rectangle` properties - %(Rectangle_kwdoc)s + %(Rectangle:kwdoc)s See Also -------- @@ -2506,12 +2643,11 @@ def barh(self, y, width, height=0.8, left=None, *, align="center", ----- Stacked bars can be achieved by passing individual *left* values per bar. See - :doc:`/gallery/lines_bars_and_markers/horizontal_barchart_distribution` - . + :doc:`/gallery/lines_bars_and_markers/horizontal_barchart_distribution`. """ kwargs.setdefault('orientation', 'horizontal') patches = self.bar(x=left, height=height, width=width, bottom=y, - align=align, **kwargs) + align=align, data=data, **kwargs) return patches def bar_label(self, container, labels=None, *, fmt="%g", label_type="edge", @@ -2532,8 +2668,14 @@ def bar_label(self, container, labels=None, *, fmt="%g", label_type="edge", A list of label texts, that should be displayed. If not given, the label texts will be the data values formatted with *fmt*. - fmt : str, default: '%g' - A format string for the label. + fmt : str or callable, default: '%g' + An unnamed %-style or {}-style format string for the label or a + function to call with the value as the first argument. + When *fmt* is a string and can be interpreted in both formats, + %-style takes precedence over {}-style. + + .. versionadded:: 3.7 + Support for {}-style format string and callables. label_type : {'edge', 'center'}, default: 'edge' The label type. Possible values: @@ -2550,13 +2692,25 @@ def bar_label(self, container, labels=None, *, fmt="%g", label_type="edge", **kwargs Any remaining keyword arguments are passed through to - `.Axes.annotate`. + `.Axes.annotate`. The alignment parameters ( + *horizontalalignment* / *ha*, *verticalalignment* / *va*) are + not supported because the labels are automatically aligned to + the bars. Returns ------- list of `.Text` A list of `.Text` instances for the labels. """ + for key in ['horizontalalignment', 'ha', 'verticalalignment', 'va']: + if key in kwargs: + raise ValueError( + f"Passing {key!r} to bar_label() is not supported.") + + a, b = self.yaxis.get_view_interval() + y_inverted = a > b + c, d = self.xaxis.get_view_interval() + x_inverted = c > d # want to know whether to put label on positive or negative direction # cannot use np.sign here because it will return 0 if x == 0 @@ -2585,7 +2739,7 @@ def sign(x): annotations = [] for bar, err, dat, lbl in itertools.zip_longest( - bars, errs, datavalues, labels + bars, errs, datavalues, labels ): (x0, y0), (x1, y1) = bar.get_bbox().get_points() xc, yc = (x0 + x1) / 2, (y0 + y1) / 2 @@ -2593,48 +2747,70 @@ def sign(x): if orientation == "vertical": extrema = max(y0, y1) if dat >= 0 else min(y0, y1) length = abs(y0 - y1) - elif orientation == "horizontal": + else: # horizontal extrema = max(x0, x1) if dat >= 0 else min(x0, x1) length = abs(x0 - x1) - if err is None: + if err is None or np.size(err) == 0: endpt = extrema elif orientation == "vertical": endpt = err[:, 1].max() if dat >= 0 else err[:, 1].min() - elif orientation == "horizontal": + else: # horizontal endpt = err[:, 0].max() if dat >= 0 else err[:, 0].min() if label_type == "center": value = sign(dat) * length - elif label_type == "edge": + else: # edge value = extrema if label_type == "center": - xy = xc, yc - elif label_type == "edge" and orientation == "vertical": - xy = xc, endpt - elif label_type == "edge" and orientation == "horizontal": - xy = endpt, yc + xy = (0.5, 0.5) + kwargs["xycoords"] = ( + lambda r, b=bar: + mtransforms.Bbox.intersection( + b.get_window_extent(r), b.get_clip_box() + ) + ) + else: # edge + if orientation == "vertical": + xy = xc, endpt + else: # horizontal + xy = endpt, yc if orientation == "vertical": - xytext = 0, sign(dat) * padding - else: - xytext = sign(dat) * padding, 0 + y_direction = -1 if y_inverted else 1 + xytext = 0, y_direction * sign(dat) * padding + else: # horizontal + x_direction = -1 if x_inverted else 1 + xytext = x_direction * sign(dat) * padding, 0 if label_type == "center": ha, va = "center", "center" - elif label_type == "edge": + else: # edge if orientation == "vertical": ha = 'center' - va = 'top' if dat < 0 else 'bottom' # also handles NaN - elif orientation == "horizontal": - ha = 'right' if dat < 0 else 'left' # also handles NaN + if y_inverted: + va = 'top' if dat > 0 else 'bottom' # also handles NaN + else: + va = 'top' if dat < 0 else 'bottom' # also handles NaN + else: # horizontal + if x_inverted: + ha = 'right' if dat > 0 else 'left' # also handles NaN + else: + ha = 'right' if dat < 0 else 'left' # also handles NaN va = 'center' if np.isnan(dat): lbl = '' - annotation = self.annotate(fmt % value if lbl is None else lbl, + if lbl is None: + if isinstance(fmt, str): + lbl = cbook._auto_format_str(fmt, value) + elif callable(fmt): + lbl = fmt(value) + else: + raise TypeError("fmt must be a str or callable") + annotation = self.annotate(lbl, xy, xytext, textcoords="offset points", ha=ha, va=va, **kwargs) annotations.append(annotation) @@ -2642,7 +2818,7 @@ def sign(x): return annotations @_preprocess_data() - @docstring.dedent_interpd + @_docstring.dedent_interpd def broken_barh(self, xranges, yrange, **kwargs): """ Plot a horizontal sequence of rectangles. @@ -2650,10 +2826,6 @@ def broken_barh(self, xranges, yrange, **kwargs): A rectangle is drawn for each element of *xranges*. All rectangles have the same vertical position and size defined by *yrange*. - This is a convenience function for instantiating a - `.BrokenBarHCollection`, adding it to the Axes and autoscaling the - view. - Parameters ---------- xranges : sequence of tuples (*xmin*, *xwidth*) @@ -2665,11 +2837,13 @@ def broken_barh(self, xranges, yrange, **kwargs): Returns ------- - `~.collections.BrokenBarHCollection` + `~.collections.PolyCollection` Other Parameters ---------------- - **kwargs : `.BrokenBarHCollection` properties + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs : `.PolyCollection` properties Each *kwarg* can be either a single argument applying to all rectangles, e.g.:: @@ -2684,38 +2858,35 @@ def broken_barh(self, xranges, yrange, **kwargs): Supported keywords: - %(BrokenBarHCollection_kwdoc)s + %(PolyCollection:kwdoc)s """ # process the unit information - if len(xranges): - xdata = cbook.safe_first_element(xranges) - else: - xdata = None - if len(yrange): - ydata = cbook.safe_first_element(yrange) - else: - ydata = None + xdata = cbook._safe_first_finite(xranges) if len(xranges) else None + ydata = cbook._safe_first_finite(yrange) if len(yrange) else None self._process_unit_info( [("x", xdata), ("y", ydata)], kwargs, convert=False) - xranges_conv = [] - for xr in xranges: - if len(xr) != 2: - raise ValueError('each range in xrange must be a sequence ' - 'with two elements (i.e. an Nx2 array)') - # convert the absolute values, not the x and dx... - x_conv = np.asarray(self.convert_xunits(xr[0])) - x1 = self._convert_dx(xr[1], xr[0], x_conv, self.convert_xunits) - xranges_conv.append((x_conv, x1)) - - yrange_conv = self.convert_yunits(yrange) - - col = mcoll.BrokenBarHCollection(xranges_conv, yrange_conv, **kwargs) + + vertices = [] + y0, dy = yrange + y0, y1 = self.convert_yunits((y0, y0 + dy)) + for xr in xranges: # convert the absolute values, not the x and dx + try: + x0, dx = xr + except Exception: + raise ValueError( + "each range in xrange must be a sequence with two " + "elements (i.e. xrange must be an (N, 2) array)") from None + x0, x1 = self.convert_xunits((x0, x0 + dx)) + vertices.append([(x0, y0), (x0, y1), (x1, y1), (x1, y0)]) + + col = mcoll.PolyCollection(np.array(vertices), **kwargs) self.add_collection(col, autolim=True) self._request_autoscale_view() return col @_preprocess_data() + @_api.delete_parameter("3.6", "use_line_collection") def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, label=None, use_line_collection=True, orientation='vertical'): """ @@ -2731,8 +2902,9 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, stem([locs,] heads, linefmt=None, markerfmt=None, basefmt=None) - The *locs*-positions are optional. The formats may be provided either - as positional or as keyword-arguments. + The *locs*-positions are optional. *linefmt* may be provided as + positional, but all other formats must be provided as keyword + arguments. Parameters ---------- @@ -2765,15 +2937,15 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, markerfmt : str, optional A string defining the color and/or shape of the markers at the stem - heads. Default: 'C0o', i.e. filled circles with the first color of - the color cycle. + heads. If the marker is not given, use the marker 'o', i.e. filled + circles. If the color is not given, use the color from *linefmt*. basefmt : str, default: 'C3-' ('C2-' in classic mode) A format string defining the properties of the baseline. - orientation : str, default: 'vertical' + orientation : {'vertical', 'horizontal'}, default: 'vertical' If 'vertical', will produce a plot with stems oriented vertically, - otherwise the stems will be oriented horizontally. + If 'horizontal', the stems will be oriented horizontally. bottom : float, default: 0 The y/x-position of the baseline (depending on orientation). @@ -2782,11 +2954,15 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, The label to use for the stems in legends. use_line_collection : bool, default: True + *Deprecated since 3.6* + If ``True``, store and plot the stem lines as a `~.collections.LineCollection` instead of individual lines, which significantly increases performance. If ``False``, defaults to the - old behavior of using a list of `.Line2D` objects. This parameter - may be deprecated in the future. + old behavior of using a list of `.Line2D` objects. + + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER Returns ------- @@ -2801,8 +2977,8 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, `stem `_ which inspired this method. """ - if not 1 <= len(args) <= 5: - raise TypeError('stem expected between 1 and 5 positional ' + if not 1 <= len(args) <= 3: + raise TypeError('stem expected between 1 or 3 positional ' 'arguments, got {}'.format(args)) _api.check_in_list(['horizontal', 'vertical'], orientation=orientation) @@ -2810,65 +2986,47 @@ def stem(self, *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, heads, = args locs = np.arange(len(heads)) args = () + elif isinstance(args[1], str): + heads, *args = args + locs = np.arange(len(heads)) else: locs, heads, *args = args if orientation == 'vertical': locs, heads = self._process_unit_info([("x", locs), ("y", heads)]) - else: + else: # horizontal heads, locs = self._process_unit_info([("x", heads), ("y", locs)]) - # defaults for formats + # resolve line format if linefmt is None: - try: - # fallback to positional argument - linefmt = args[0] - except IndexError: - linecolor = 'C0' - linemarker = 'None' - linestyle = '-' - else: - linestyle, linemarker, linecolor = \ - _process_plot_format(linefmt) - else: - linestyle, linemarker, linecolor = _process_plot_format(linefmt) + linefmt = args[0] if len(args) > 0 else "C0-" + linestyle, linemarker, linecolor = _process_plot_format(linefmt) + # resolve marker format if markerfmt is None: - try: - # fallback to positional argument - markerfmt = args[1] - except IndexError: - markercolor = 'C0' - markermarker = 'o' - markerstyle = 'None' - else: - markerstyle, markermarker, markercolor = \ - _process_plot_format(markerfmt) - else: - markerstyle, markermarker, markercolor = \ - _process_plot_format(markerfmt) - + # if not given as kwarg, fall back to 'o' + markerfmt = "o" + if markerfmt == '': + markerfmt = ' ' # = empty line style; '' would resolve rcParams + markerstyle, markermarker, markercolor = \ + _process_plot_format(markerfmt) + if markermarker is None: + markermarker = 'o' + if markerstyle is None: + markerstyle = 'None' + if markercolor is None: + markercolor = linecolor + + # resolve baseline format if basefmt is None: - try: - # fallback to positional argument - basefmt = args[2] - except IndexError: - if rcParams['_internal.classic_mode']: - basecolor = 'C2' - else: - basecolor = 'C3' - basemarker = 'None' - basestyle = '-' - else: - basestyle, basemarker, basecolor = \ - _process_plot_format(basefmt) - else: - basestyle, basemarker, basecolor = _process_plot_format(basefmt) + basefmt = ("C2-" if mpl.rcParams["_internal.classic_mode"] else + "C3-") + basestyle, basemarker, basecolor = _process_plot_format(basefmt) # New behaviour in 3.1 is to use a LineCollection for the stemlines if use_line_collection: if linestyle is None: - linestyle = rcParams['lines.linestyle'] + linestyle = mpl.rcParams['lines.linestyle'] xlines = self.vlines if orientation == "vertical" else self.hlines stemlines = xlines( locs, bottom, heads, @@ -2917,14 +3075,12 @@ def pie(self, x, explode=None, labels=None, colors=None, autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, startangle=0, radius=1, counterclock=True, wedgeprops=None, textprops=None, center=(0, 0), - frame=False, rotatelabels=False, *, normalize=None): + frame=False, rotatelabels=False, *, normalize=True, hatch=None): """ Plot a pie chart. Make a pie chart of array *x*. The fractional area of each wedge is - given by ``x/sum(x)``. If ``sum(x) < 1``, then the values of *x* give - the fractional area directly and the array will not be normalized. The - resulting pie will have an empty wedge of size ``1 - sum(x)``. + given by ``x/sum(x)``. The wedges are plotted counterclockwise, by default starting from the x-axis. @@ -2945,37 +3101,34 @@ def pie(self, x, explode=None, labels=None, colors=None, A sequence of colors through which the pie chart will cycle. If *None*, will use the colors in the currently active cycle. + hatch : str or list, default: None + Hatching pattern applied to all pie wedges or sequence of patterns + through which the chart will cycle. For a list of valid patterns, + see :doc:`/gallery/shapes_and_collections/hatch_style_reference`. + + .. versionadded:: 3.7 + autopct : None or str or callable, default: None - If not *None*, is a string or function used to label the wedges - with their numeric value. The label will be placed inside the - wedge. If it is a format string, the label will be ``fmt % pct``. - If it is a function, it will be called. + If not *None*, *autopct* is a string or function used to label the + wedges with their numeric value. The label will be placed inside + the wedge. If *autopct* is a format string, the label will be + ``fmt % pct``. If *autopct* is a function, then it will be called. pctdistance : float, default: 0.6 - The ratio between the center of each pie slice and the start of - the text generated by *autopct*. Ignored if *autopct* is *None*. + The relative distance along the radius at which the the text + generated by *autopct* is drawn. To draw the text outside the pie, + set *pctdistance* > 1. This parameter is ignored if *autopct* is + ``None``. + + labeldistance : float or None, default: 1.1 + The relative distance along the radius at which the labels are + drawn. To draw the labels inside the pie, set *labeldistance* < 1. + If set to ``None``, labels are not drawn but are still stored for + use in `.legend`. shadow : bool, default: False Draw a shadow beneath the pie. - normalize : None or bool, default: None - When *True*, always make a full pie by normalizing x so that - ``sum(x) == 1``. *False* makes a partial pie if ``sum(x) <= 1`` - and raises a `ValueError` for ``sum(x) > 1``. - - When *None*, defaults to *True* if ``sum(x) >= 1`` and *False* if - ``sum(x) < 1``. - - Please note that the previous default value of *None* is now - deprecated, and the default will change to *True* in the next - release. Please pass ``normalize=False`` explicitly if you want to - draw a partial pie. - - labeldistance : float or None, default: 1.1 - The radial distance at which the pie labels are drawn. - If set to ``None``, label are not drawn, but are stored for use in - ``legend()`` - startangle : float, default: 0 degrees The angle by which the start of the pie is rotated, counterclockwise from the x-axis. @@ -2987,11 +3140,11 @@ def pie(self, x, explode=None, labels=None, colors=None, Specify fractions direction, clockwise or counterclockwise. wedgeprops : dict, default: None - Dict of arguments passed to the wedge objects making the pie. - For example, you can pass in ``wedgeprops = {'linewidth': 3}`` - to set the width of the wedge border lines equal to 3. - For more details, look at the doc/arguments of the wedge object. - By default ``clip_on=False``. + Dict of arguments passed to each `.patches.Wedge` of the pie. + For example, ``wedgeprops = {'linewidth': 3}`` sets the width of + the wedge border lines equal to 3. By default, ``clip_on=False``. + When there is a conflict between these properties and other + keywords, properties passed to *wedgeprops* take precedence. textprops : dict, default: None Dict of arguments to pass to the text objects. @@ -3005,6 +3158,14 @@ def pie(self, x, explode=None, labels=None, colors=None, rotatelabels : bool, default: False Rotate each label to the angle of the corresponding slice if true. + normalize : bool, default: True + When *True*, always make a full pie by normalizing x so that + ``sum(x) == 1``. *False* makes a partial pie if ``sum(x) <= 1`` + and raises a `ValueError` for ``sum(x) > 1``. + + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + Returns ------- patches : list @@ -3036,17 +3197,6 @@ def pie(self, x, explode=None, labels=None, colors=None, sx = x.sum() - if normalize is None: - if sx < 1: - _api.warn_deprecated( - "3.3", message="normalize=None does not normalize " - "if the sum is less than 1 but this behavior " - "is deprecated since %(since)s until %(removal)s. " - "After the deprecation " - "period the default value will be normalize=True. " - "To prevent normalization pass normalize=False ") - else: - normalize = True if normalize: x = x / sx elif sx > 1: @@ -3067,20 +3217,13 @@ def pie(self, x, explode=None, labels=None, colors=None, def get_next_color(): return next(color_cycle) - if radius is None: - _api.warn_deprecated( - "3.3", message="Support for passing a radius of None to mean " - "1 is deprecated since %(since)s and will be removed " - "%(removal)s.") - radius = 1 + hatch_cycle = itertools.cycle(np.atleast_1d(hatch)) + + _api.check_isinstance(Number, radius=radius, startangle=startangle) + if radius <= 0: + raise ValueError(f'radius must be a positive number, not {radius}') # Starting theta1 is the start fraction of the circle - if startangle is None: - _api.warn_deprecated( - "3.3", message="Support for passing a startangle of None to " - "mean 0 is deprecated since %(since)s and will be removed " - "%(removal)s.") - startangle = 0 theta1 = startangle / 360 if wedgeprops is None: @@ -3102,6 +3245,7 @@ def get_next_color(): w = mpatches.Wedge((x, y), radius, 360. * min(theta1, theta2), 360. * max(theta1, theta2), facecolor=get_next_color(), + hatch=next(hatch_cycle), clip_on=False, label=label) w.set(**wedgeprops) @@ -3129,7 +3273,7 @@ def get_next_color(): horizontalalignment=label_alignment_h, verticalalignment=label_alignment_v, rotation=label_rotation, - size=rcParams['xtick.labelsize']) + size=mpl.rcParams['xtick.labelsize']) t.set(**textprops) texts.append(t) @@ -3164,9 +3308,41 @@ def get_next_color(): else: return slices, texts, autotexts + @staticmethod + def _errorevery_to_mask(x, errorevery): + """ + Normalize `errorbar`'s *errorevery* to be a boolean mask for data *x*. + + This function is split out to be usable both by 2D and 3D errorbars. + """ + if isinstance(errorevery, Integral): + errorevery = (0, errorevery) + if isinstance(errorevery, tuple): + if (len(errorevery) == 2 and + isinstance(errorevery[0], Integral) and + isinstance(errorevery[1], Integral)): + errorevery = slice(errorevery[0], None, errorevery[1]) + else: + raise ValueError( + f'{errorevery=!r} is a not a tuple of two integers') + elif isinstance(errorevery, slice): + pass + elif not isinstance(errorevery, str) and np.iterable(errorevery): + try: + x[errorevery] # fancy indexing + except (ValueError, IndexError) as err: + raise ValueError( + f"{errorevery=!r} is iterable but not a valid NumPy fancy " + "index to match 'xerr'/'yerr'") from err + else: + raise ValueError(f"{errorevery=!r} is not a recognized value") + everymask = np.zeros(len(x), bool) + everymask[errorevery] = True + return everymask + @_preprocess_data(replace_names=["x", "y", "xerr", "yerr"], label_namer="y") - @docstring.dedent_interpd + @_docstring.dedent_interpd def errorbar(self, x, y, yerr=None, xerr=None, fmt='', ecolor=None, elinewidth=None, capsize=None, barsabove=False, lolims=False, uplims=False, @@ -3179,6 +3355,10 @@ def errorbar(self, x, y, yerr=None, xerr=None, sizes. By default, this draws the data markers/lines as well the errorbars. Use fmt='none' to draw errorbars without any data markers. + .. versionadded:: 3.7 + Caps and error lines are drawn in polar coordinates on polar plots. + + Parameters ---------- x, y : float or array-like @@ -3194,7 +3374,7 @@ def errorbar(self, x, y, yerr=None, xerr=None, errors. - *None*: No errorbar. - Note that all error arrays should have *positive* values. + All values must be >= 0. See :doc:`/gallery/statistics/errorbar_features` for an example on the usage of ``xerr`` and ``yerr``. @@ -3203,7 +3383,7 @@ def errorbar(self, x, y, yerr=None, xerr=None, The format for the data points / data lines. See `.plot` for details. - Use 'none' (case insensitive) to plot errorbars without any data + Use 'none' (case-insensitive) to plot errorbars without any data markers. ecolor : color, default: None @@ -3260,6 +3440,9 @@ def errorbar(self, x, y, yerr=None, xerr=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs All other keyword arguments are passed on to the `~.Axes.plot` call drawing the markers. For example, this code makes big red squares @@ -3273,63 +3456,87 @@ def errorbar(self, x, y, yerr=None, xerr=None, property names, *markerfacecolor*, *markeredgecolor*, *markersize* and *markeredgewidth*. - Valid kwargs for the marker properties are `.Line2D` properties: - - %(Line2D_kwdoc)s + Valid kwargs for the marker properties are: + + - *dashes* + - *dash_capstyle* + - *dash_joinstyle* + - *drawstyle* + - *fillstyle* + - *linestyle* + - *marker* + - *markeredgecolor* + - *markeredgewidth* + - *markerfacecolor* + - *markerfacecoloralt* + - *markersize* + - *markevery* + - *solid_capstyle* + - *solid_joinstyle* + + Refer to the corresponding `.Line2D` property for more details: + + %(Line2D:kwdoc)s """ kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D) - # anything that comes in as 'None', drop so the default thing - # happens down stream + # Drop anything that comes in as None to use the default instead. kwargs = {k: v for k, v in kwargs.items() if v is not None} kwargs.setdefault('zorder', 2) - self._process_unit_info([("x", x), ("y", y)], kwargs, convert=False) - - # Make sure all the args are iterable; use lists not arrays to preserve - # units. - if not np.iterable(x): - x = [x] - - if not np.iterable(y): - y = [y] + # Casting to object arrays preserves units. + if not isinstance(x, np.ndarray): + x = np.asarray(x, dtype=object) + if not isinstance(y, np.ndarray): + y = np.asarray(y, dtype=object) - if len(x) != len(y): - raise ValueError("'x' and 'y' must have the same size") + def _upcast_err(err): + """ + Safely handle tuple of containers that carry units. - if xerr is not None: - if not np.iterable(xerr): - xerr = [xerr] * len(x) + This function covers the case where the input to the xerr/yerr is a + length 2 tuple of equal length ndarray-subclasses that carry the + unit information in the container. - if yerr is not None: - if not np.iterable(yerr): - yerr = [yerr] * len(y) + If we have a tuple of nested numpy array (subclasses), we defer + coercing the units to be consistent to the underlying unit + library (and implicitly the broadcasting). - if isinstance(errorevery, Integral): - errorevery = (0, errorevery) - if isinstance(errorevery, tuple): - if (len(errorevery) == 2 and - isinstance(errorevery[0], Integral) and - isinstance(errorevery[1], Integral)): - errorevery = slice(errorevery[0], None, errorevery[1]) - else: - raise ValueError( - f'errorevery={errorevery!r} is a not a tuple of two ' - f'integers') + Otherwise, fallback to casting to an object array. + """ - elif isinstance(errorevery, slice): - pass + if ( + # make sure it is not a scalar + np.iterable(err) and + # and it is not empty + len(err) > 0 and + # and the first element is an array sub-class use + # safe_first_element because getitem is index-first not + # location first on pandas objects so err[0] almost always + # fails. + isinstance(cbook._safe_first_finite(err), np.ndarray) + ): + # Get the type of the first element + atype = type(cbook._safe_first_finite(err)) + # Promote the outer container to match the inner container + if atype is np.ndarray: + # Converts using np.asarray, because data cannot + # be directly passed to init of np.ndarray + return np.asarray(err, dtype=object) + # If atype is not np.ndarray, directly pass data to init. + # This works for types such as unyts and astropy units + return atype(err) + # Otherwise wrap it in an object array + return np.asarray(err, dtype=object) + + if xerr is not None and not isinstance(xerr, np.ndarray): + xerr = _upcast_err(xerr) + if yerr is not None and not isinstance(yerr, np.ndarray): + yerr = _upcast_err(yerr) + x, y = np.atleast_1d(x, y) # Make sure all the args are iterable. + if len(x) != len(y): + raise ValueError("'x' and 'y' must have the same size") - elif not isinstance(errorevery, str) and np.iterable(errorevery): - # fancy indexing - try: - x[errorevery] - except (ValueError, IndexError) as err: - raise ValueError( - f"errorevery={errorevery!r} is iterable but not a valid " - f"NumPy fancy index to match 'xerr'/'yerr'") from err - else: - raise ValueError( - f"errorevery={errorevery!r} is not a recognized value") + everymask = self._errorevery_to_mask(x, errorevery) label = kwargs.pop("label", None) kwargs['label'] = '_nolegend_' @@ -3367,9 +3574,11 @@ def errorbar(self, x, y, yerr=None, xerr=None, # Eject any line-specific information from format string, as it's not # needed for bars or caps. for key in ['marker', 'markersize', 'markerfacecolor', + 'markerfacecoloralt', 'markeredgewidth', 'markeredgecolor', 'markevery', 'linestyle', 'fillstyle', 'drawstyle', 'dash_capstyle', - 'dash_joinstyle', 'solid_capstyle', 'solid_joinstyle']: + 'dash_joinstyle', 'solid_capstyle', 'solid_joinstyle', + 'dashes']: base_style.pop(key, None) # Make the style dict for the line collections (the bars). @@ -3387,7 +3596,7 @@ def errorbar(self, x, y, yerr=None, xerr=None, # Make the style dict for caps (the "hats"). eb_cap_style = {**base_style, 'linestyle': 'none'} if capsize is None: - capsize = rcParams["errorbar.capsize"] + capsize = mpl.rcParams["errorbar.capsize"] if capsize > 0: eb_cap_style['markersize'] = 2. * capsize if capthick is not None: @@ -3402,134 +3611,96 @@ def errorbar(self, x, y, yerr=None, xerr=None, eb_cap_style['color'] = ecolor barcols = [] - caplines = [] - - # arrays fine here, they are booleans and hence not units - lolims = np.broadcast_to(lolims, len(x)).astype(bool) - uplims = np.broadcast_to(uplims, len(x)).astype(bool) - xlolims = np.broadcast_to(xlolims, len(x)).astype(bool) - xuplims = np.broadcast_to(xuplims, len(x)).astype(bool) - - everymask = np.zeros(len(x), bool) - everymask[errorevery] = True + caplines = {'x': [], 'y': []} + # Vectorized fancy-indexer. def apply_mask(arrays, mask): - # Return, for each array in *arrays*, the elements for which *mask* - # is True, without using fancy indexing. - return [[*itertools.compress(array, mask)] for array in arrays] - - def extract_err(name, err, data, lolims, uplims): - """ - Private function to compute error bars. - - Parameters - ---------- - name : {'x', 'y'} - Name used in the error message. - err : array-like - xerr or yerr from errorbar(). - data : array-like - x or y from errorbar(). - lolims : array-like - Error is only applied on **upper** side when this is True. See - the note in the main docstring about this parameter's name. - uplims : array-like - Error is only applied on **lower** side when this is True. See - the note in the main docstring about this parameter's name. - """ - try: # Asymmetric error: pair of 1D iterables. - a, b = err - iter(a) - iter(b) - except (TypeError, ValueError): - a = b = err # Symmetric error: 1D iterable. - if np.ndim(a) > 1 or np.ndim(b) > 1: + return [array[mask] for array in arrays] + + # dep: dependent dataset, indep: independent dataset + for (dep_axis, dep, err, lolims, uplims, indep, lines_func, + marker, lomarker, himarker) in [ + ("x", x, xerr, xlolims, xuplims, y, self.hlines, + "|", mlines.CARETRIGHTBASE, mlines.CARETLEFTBASE), + ("y", y, yerr, lolims, uplims, x, self.vlines, + "_", mlines.CARETUPBASE, mlines.CARETDOWNBASE), + ]: + if err is None: + continue + lolims = np.broadcast_to(lolims, len(dep)).astype(bool) + uplims = np.broadcast_to(uplims, len(dep)).astype(bool) + try: + np.broadcast_to(err, (2, len(dep))) + except ValueError: raise ValueError( - f"{name}err must be a scalar or a 1D or (2, n) array-like") - # Using list comprehensions rather than arrays to preserve units. - for e in [a, b]: - if len(data) != len(e): - raise ValueError( - f"The lengths of the data ({len(data)}) and the " - f"error {len(e)} do not match") - low = [v if lo else v - e for v, e, lo in zip(data, a, lolims)] - high = [v if up else v + e for v, e, up in zip(data, b, uplims)] - return low, high - - if xerr is not None: - left, right = extract_err('x', xerr, x, xlolims, xuplims) - barcols.append(self.hlines( - *apply_mask([y, left, right], everymask), **eb_lines_style)) - # select points without upper/lower limits in x and - # draw normal errorbars for these points - noxlims = ~(xlolims | xuplims) - if noxlims.any() and capsize > 0: - yo, lo, ro = apply_mask([y, left, right], noxlims & everymask) - caplines.extend([ - mlines.Line2D(lo, yo, marker='|', **eb_cap_style), - mlines.Line2D(ro, yo, marker='|', **eb_cap_style)]) - if xlolims.any(): - xo, yo, ro = apply_mask([x, y, right], xlolims & everymask) - if self.xaxis_inverted(): - marker = mlines.CARETLEFTBASE - else: - marker = mlines.CARETRIGHTBASE - caplines.append(mlines.Line2D( - ro, yo, ls='None', marker=marker, **eb_cap_style)) - if capsize > 0: - caplines.append(mlines.Line2D( - xo, yo, marker='|', **eb_cap_style)) - if xuplims.any(): - xo, yo, lo = apply_mask([x, y, left], xuplims & everymask) - if self.xaxis_inverted(): - marker = mlines.CARETRIGHTBASE - else: - marker = mlines.CARETLEFTBASE - caplines.append(mlines.Line2D( - lo, yo, ls='None', marker=marker, **eb_cap_style)) - if capsize > 0: - caplines.append(mlines.Line2D( - xo, yo, marker='|', **eb_cap_style)) - - if yerr is not None: - lower, upper = extract_err('y', yerr, y, lolims, uplims) - barcols.append(self.vlines( - *apply_mask([x, lower, upper], everymask), **eb_lines_style)) - # select points without upper/lower limits in y and - # draw normal errorbars for these points - noylims = ~(lolims | uplims) - if noylims.any() and capsize > 0: - xo, lo, uo = apply_mask([x, lower, upper], noylims & everymask) - caplines.extend([ - mlines.Line2D(xo, lo, marker='_', **eb_cap_style), - mlines.Line2D(xo, uo, marker='_', **eb_cap_style)]) - if lolims.any(): - xo, yo, uo = apply_mask([x, y, upper], lolims & everymask) - if self.yaxis_inverted(): - marker = mlines.CARETDOWNBASE - else: - marker = mlines.CARETUPBASE - caplines.append(mlines.Line2D( - xo, uo, ls='None', marker=marker, **eb_cap_style)) - if capsize > 0: - caplines.append(mlines.Line2D( - xo, yo, marker='_', **eb_cap_style)) - if uplims.any(): - xo, yo, lo = apply_mask([x, y, lower], uplims & everymask) - if self.yaxis_inverted(): - marker = mlines.CARETUPBASE - else: - marker = mlines.CARETDOWNBASE - caplines.append(mlines.Line2D( - xo, lo, ls='None', marker=marker, **eb_cap_style)) + f"'{dep_axis}err' (shape: {np.shape(err)}) must be a " + f"scalar or a 1D or (2, n) array-like whose shape matches " + f"'{dep_axis}' (shape: {np.shape(dep)})") from None + res = np.zeros(err.shape, dtype=bool) # Default in case of nan + if np.any(np.less(err, -err, out=res, where=(err == err))): + # like err<0, but also works for timedelta and nan. + raise ValueError( + f"'{dep_axis}err' must not contain negative values") + # This is like + # elow, ehigh = np.broadcast_to(...) + # return dep - elow * ~lolims, dep + ehigh * ~uplims + # except that broadcast_to would strip units. + low, high = dep + np.row_stack([-(1 - lolims), 1 - uplims]) * err + barcols.append(lines_func( + *apply_mask([indep, low, high], everymask), **eb_lines_style)) + if self.name == "polar" and dep_axis == "x": + for b in barcols: + for p in b.get_paths(): + p._interpolation_steps = 2 + # Normal errorbars for points without upper/lower limits. + nolims = ~(lolims | uplims) + if nolims.any() and capsize > 0: + indep_masked, lo_masked, hi_masked = apply_mask( + [indep, low, high], nolims & everymask) + for lh_masked in [lo_masked, hi_masked]: + # Since this has to work for x and y as dependent data, we + # first set both x and y to the independent variable and + # overwrite the respective dependent data in a second step. + line = mlines.Line2D(indep_masked, indep_masked, + marker=marker, **eb_cap_style) + line.set(**{f"{dep_axis}data": lh_masked}) + caplines[dep_axis].append(line) + for idx, (lims, hl) in enumerate([(lolims, high), (uplims, low)]): + if not lims.any(): + continue + hlmarker = ( + himarker + if getattr(self, f"{dep_axis}axis").get_inverted() ^ idx + else lomarker) + x_masked, y_masked, hl_masked = apply_mask( + [x, y, hl], lims & everymask) + # As above, we set the dependent data in a second step. + line = mlines.Line2D(x_masked, y_masked, + marker=hlmarker, **eb_cap_style) + line.set(**{f"{dep_axis}data": hl_masked}) + caplines[dep_axis].append(line) if capsize > 0: - caplines.append(mlines.Line2D( - xo, yo, marker='_', **eb_cap_style)) - - for l in caplines: - self.add_line(l) + caplines[dep_axis].append(mlines.Line2D( + x_masked, y_masked, marker=marker, **eb_cap_style)) + if self.name == 'polar': + for axis in caplines: + for l in caplines[axis]: + # Rotate caps to be perpendicular to the error bars + for theta, r in zip(l.get_xdata(), l.get_ydata()): + rotation = mtransforms.Affine2D().rotate(theta) + if axis == 'y': + rotation.rotate(-np.pi / 2) + ms = mmarkers.MarkerStyle(marker=marker, + transform=rotation) + self.add_line(mlines.Line2D([theta], [r], marker=ms, + **eb_cap_style)) + else: + for axis in caplines: + for l in caplines[axis]: + self.add_line(l) self._request_autoscale_view() + caplines = caplines['x'] + caplines['y'] errorbar_container = ErrorbarContainer( (data_line, tuple(caplines), tuple(barcols)), has_xerr=(xerr is not None), has_yerr=(yerr is not None), @@ -3546,28 +3717,41 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, showbox=None, showfliers=None, boxprops=None, labels=None, flierprops=None, medianprops=None, meanprops=None, capprops=None, whiskerprops=None, - manage_ticks=True, autorange=False, zorder=None): + manage_ticks=True, autorange=False, zorder=None, + capwidths=None): """ - Make a box and whisker plot. + Draw a box and whisker plot. + + The box extends from the first quartile (Q1) to the third + quartile (Q3) of the data, with a line at the median. The + whiskers extend from the box by 1.5x the inter-quartile range + (IQR). Flier points are those past the end of the whiskers. + See https://en.wikipedia.org/wiki/Box_plot for reference. + + .. code-block:: none + + Q1-1.5IQR Q1 median Q3 Q3+1.5IQR + |-----:-----| + o |--------| : |--------| o o + |-----:-----| + flier <-----------> fliers + IQR - Make a box and whisker plot for each column of *x* or each - vector in sequence *x*. The box extends from the lower to - upper quartile values of the data, with a line at the median. - The whiskers extend from the box to show the range of the - data. Flier points are those past the end of the whiskers. Parameters ---------- x : Array or a sequence of vectors. - The input data. + The input data. If a 2D array, a boxplot is drawn for each column + in *x*. If a sequence of 1D arrays, a boxplot is drawn for each + array in *x*. notch : bool, default: False - Whether to draw a notched box plot (`True`), or a rectangular box - plot (`False`). The notches represent the confidence interval (CI) - around the median. The documentation for *bootstrap* describes how - the locations of the notches are computed by default, but their - locations may also be overridden by setting the *conf_intervals* - parameter. + Whether to draw a notched boxplot (`True`), or a rectangular + boxplot (`False`). The notches represent the confidence interval + (CI) around the median. The documentation for *bootstrap* + describes how the locations of the notches are computed by + default, but their locations may also be overridden by setting the + *conf_intervals* parameter. .. note:: @@ -3642,7 +3826,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, patch_artist : bool, default: False If `False` produces boxes with the Line2D artist. Otherwise, - boxes and drawn with Patch artists. + boxes are drawn with Patch artists. labels : sequence, optional Labels for each dataset (one per dataset). @@ -3701,6 +3885,8 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, Show the arithmetic means. capprops : dict, default: None The style of the caps. + capwidths : float or array, default: None + The widths of the caps. boxprops : dict, default: None The style of the box. whiskerprops : dict, default: None @@ -3711,55 +3897,38 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, The style of the median. meanprops : dict, default: None The style of the mean. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER - Notes - ----- - Box plots provide insight into distribution properties of the data. - However, they can be challenging to interpret for the unfamiliar - reader. The figure below illustrates the different visual features of - a box plot. - - .. image:: /_static/boxplot_explanation.png - :alt: Illustration of box plot features - :scale: 50 % - - The whiskers mark the range of the non-outlier data. The most common - definition of non-outlier is ``[Q1 - 1.5xIQR, Q3 + 1.5xIQR]``, which - is also the default in this function. Other whisker meanings can be - applied via the *whis* parameter. - - See `Box plot `_ on Wikipedia - for further information. - - Violin plots (`~.Axes.violinplot`) add even more detail about the - statistical distribution by plotting the kernel density estimation - (KDE) as an estimation of the probability density function. + See Also + -------- + violinplot : Draw an estimate of the probability density function. """ # Missing arguments default to rcParams. if whis is None: - whis = rcParams['boxplot.whiskers'] + whis = mpl.rcParams['boxplot.whiskers'] if bootstrap is None: - bootstrap = rcParams['boxplot.bootstrap'] + bootstrap = mpl.rcParams['boxplot.bootstrap'] bxpstats = cbook.boxplot_stats(x, whis=whis, bootstrap=bootstrap, labels=labels, autorange=autorange) if notch is None: - notch = rcParams['boxplot.notch'] + notch = mpl.rcParams['boxplot.notch'] if vert is None: - vert = rcParams['boxplot.vertical'] + vert = mpl.rcParams['boxplot.vertical'] if patch_artist is None: - patch_artist = rcParams['boxplot.patchartist'] + patch_artist = mpl.rcParams['boxplot.patchartist'] if meanline is None: - meanline = rcParams['boxplot.meanline'] + meanline = mpl.rcParams['boxplot.meanline'] if showmeans is None: - showmeans = rcParams['boxplot.showmeans'] + showmeans = mpl.rcParams['boxplot.showmeans'] if showcaps is None: - showcaps = rcParams['boxplot.showcaps'] + showcaps = mpl.rcParams['boxplot.showcaps'] if showbox is None: - showbox = rcParams['boxplot.showbox'] + showbox = mpl.rcParams['boxplot.showbox'] if showfliers is None: - showfliers = rcParams['boxplot.showfliers'] + showfliers = mpl.rcParams['boxplot.showfliers'] if boxprops is None: boxprops = {} @@ -3781,7 +3950,7 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, # if non-default sym value, put it into the flier dictionary # the logic for providing the default symbol ('b+') now lives - # in bxp in the initial value of final_flierprops + # in bxp in the initial value of flierkw # handle all of the *sym* related logic here so we only have to pass # on the flierprops dict. if sym is not None: @@ -3846,7 +4015,8 @@ def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, medianprops=medianprops, meanprops=meanprops, meanline=meanline, showfliers=showfliers, capprops=capprops, whiskerprops=whiskerprops, - manage_ticks=manage_ticks, zorder=zorder) + manage_ticks=manage_ticks, zorder=zorder, + capwidths=capwidths) return artists def bxp(self, bxpstats, positions=None, widths=None, vert=True, @@ -3854,7 +4024,8 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True, showcaps=True, showbox=True, showfliers=True, boxprops=None, whiskerprops=None, flierprops=None, medianprops=None, capprops=None, meanprops=None, - meanline=False, manage_ticks=True, zorder=None): + meanline=False, manage_ticks=True, zorder=None, + capwidths=None): """ Drawing function for box and whisker plots. @@ -3866,89 +4037,53 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True, Parameters ---------- - bxpstats : list of dicts - A list of dictionaries containing stats for each boxplot. - Required keys are: - - - ``med``: The median (scalar float). - - - ``q1``: The first quartile (25th percentile) (scalar - float). - - - ``q3``: The third quartile (75th percentile) (scalar - float). - - - ``whislo``: Lower bound of the lower whisker (scalar - float). + bxpstats : list of dicts + A list of dictionaries containing stats for each boxplot. + Required keys are: - - ``whishi``: Upper bound of the upper whisker (scalar - float). + - ``med``: Median (scalar). + - ``q1``, ``q3``: First & third quartiles (scalars). + - ``whislo``, ``whishi``: Lower & upper whisker positions (scalars). Optional keys are: - - ``mean``: The mean (scalar float). Needed if - ``showmeans=True``. - - - ``fliers``: Data beyond the whiskers (sequence of floats). + - ``mean``: Mean (scalar). Needed if ``showmeans=True``. + - ``fliers``: Data beyond the whiskers (array-like). Needed if ``showfliers=True``. - - - ``cilo`` & ``cihi``: Lower and upper confidence intervals + - ``cilo``, ``cihi``: Lower & upper confidence intervals about the median. Needed if ``shownotches=True``. - - - ``label``: Name of the dataset (string). If available, + - ``label``: Name of the dataset (str). If available, this will be used a tick label for the boxplot positions : array-like, default: [1, 2, ..., n] The positions of the boxes. The ticks and limits are automatically set to match the positions. - widths : array-like, default: None - Either a scalar or a vector and sets the width of each - box. The default is ``0.15*(distance between extreme - positions)``, clipped to no less than 0.15 and no more than - 0.5. + widths : float or array-like, default: None + The widths of the boxes. The default is + ``clip(0.15*(distance between extreme positions), 0.15, 0.5)``. + + capwidths : float or array-like, default: None + Either a scalar or a vector and sets the width of each cap. + The default is ``0.5*(with of the box)``, see *widths*. vert : bool, default: True - If `True` (default), makes the boxes vertical. If `False`, - makes horizontal boxes. + If `True` (default), makes the boxes vertical. + If `False`, makes horizontal boxes. patch_artist : bool, default: False If `False` produces boxes with the `.Line2D` artist. If `True` produces boxes with the `~matplotlib.patches.Patch` artist. - shownotches : bool, default: False - If `False` (default), produces a rectangular box plot. - If `True`, will produce a notched box plot - - showmeans : bool, default: False - If `True`, will toggle on the rendering of the means - - showcaps : bool, default: True - If `True`, will toggle on the rendering of the caps - - showbox : bool, default: True - If `True`, will toggle on the rendering of the box - - showfliers : bool, default: True - If `True`, will toggle on the rendering of the fliers - - boxprops : dict or None (default) - If provided, will set the plotting style of the boxes - - whiskerprops : dict or None (default) - If provided, will set the plotting style of the whiskers - - capprops : dict or None (default) - If provided, will set the plotting style of the caps + shownotches, showmeans, showcaps, showbox, showfliers : bool + Whether to draw the CI notches, the mean value (both default to + False), the caps, the box, and the fliers (all three default to + True). - flierprops : dict or None (default) - If provided will set the plotting style of the fliers - - medianprops : dict or None (default) - If provided, will set the plotting style of the medians - - meanprops : dict or None (default) - If provided, will set the plotting style of the means + boxprops, whiskerprops, capprops, flierprops, medianprops, meanprops :\ + dict, optional + Artist properties for the boxes, whiskers, caps, fliers, medians, and + means. meanline : bool, default: False If `True` (and *showmeans* is `True`), will try to render the mean @@ -3970,28 +4105,19 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True, of the `.Line2D` instances created. That dictionary has the following keys (assuming vertical boxplots): - - ``boxes``: the main body of the boxplot showing the - quartiles and the median's confidence intervals if - enabled. - + - ``boxes``: main bodies of the boxplot showing the quartiles, and + the median's confidence intervals if enabled. - ``medians``: horizontal lines at the median of each box. - - - ``whiskers``: the vertical lines extending to the most - extreme, non-outlier data points. - - - ``caps``: the horizontal lines at the ends of the - whiskers. - - - ``fliers``: points representing data that extend beyond - the whiskers (fliers). - + - ``whiskers``: vertical lines up to the last non-outlier data. + - ``caps``: horizontal lines at the ends of the whiskers. + - ``fliers``: points representing data beyond the whiskers (fliers). - ``means``: points or lines representing the means. Examples -------- .. plot:: gallery/statistics/bxp.py - """ + # lists of artists to be output whiskers = [] caps = [] @@ -4009,72 +4135,46 @@ def bxp(self, bxpstats, positions=None, widths=None, vert=True, zdelta = 0.1 - def line_props_with_rcdefaults(subkey, explicit, zdelta=0, - use_marker=True): - d = {k.split('.')[-1]: v for k, v in rcParams.items() - if k.startswith(f'boxplot.{subkey}')} + def merge_kw_rc(subkey, explicit, zdelta=0, usemarker=True): + d = {k.split('.')[-1]: v for k, v in mpl.rcParams.items() + if k.startswith(f'boxplot.{subkey}props')} d['zorder'] = zorder + zdelta - if not use_marker: + if not usemarker: d['marker'] = '' d.update(cbook.normalize_kwargs(explicit, mlines.Line2D)) return d - # box properties - if patch_artist: - final_boxprops = { - 'linestyle': rcParams['boxplot.boxprops.linestyle'], - 'linewidth': rcParams['boxplot.boxprops.linewidth'], - 'edgecolor': rcParams['boxplot.boxprops.color'], - 'facecolor': ('white' if rcParams['_internal.classic_mode'] - else rcParams['patch.facecolor']), - 'zorder': zorder, - **cbook.normalize_kwargs(boxprops, mpatches.PathPatch) - } - else: - final_boxprops = line_props_with_rcdefaults('boxprops', boxprops, - use_marker=False) - final_whiskerprops = line_props_with_rcdefaults( - 'whiskerprops', whiskerprops, use_marker=False) - final_capprops = line_props_with_rcdefaults( - 'capprops', capprops, use_marker=False) - final_flierprops = line_props_with_rcdefaults( - 'flierprops', flierprops) - final_medianprops = line_props_with_rcdefaults( - 'medianprops', medianprops, zdelta, use_marker=False) - final_meanprops = line_props_with_rcdefaults( - 'meanprops', meanprops, zdelta) + box_kw = { + 'linestyle': mpl.rcParams['boxplot.boxprops.linestyle'], + 'linewidth': mpl.rcParams['boxplot.boxprops.linewidth'], + 'edgecolor': mpl.rcParams['boxplot.boxprops.color'], + 'facecolor': ('white' if mpl.rcParams['_internal.classic_mode'] + else mpl.rcParams['patch.facecolor']), + 'zorder': zorder, + **cbook.normalize_kwargs(boxprops, mpatches.PathPatch) + } if patch_artist else merge_kw_rc('box', boxprops, usemarker=False) + whisker_kw = merge_kw_rc('whisker', whiskerprops, usemarker=False) + cap_kw = merge_kw_rc('cap', capprops, usemarker=False) + flier_kw = merge_kw_rc('flier', flierprops) + median_kw = merge_kw_rc('median', medianprops, zdelta, usemarker=False) + mean_kw = merge_kw_rc('mean', meanprops, zdelta) removed_prop = 'marker' if meanline else 'linestyle' # Only remove the property if it's not set explicitly as a parameter. if meanprops is None or removed_prop not in meanprops: - final_meanprops[removed_prop] = '' - - def patch_list(xs, ys, **kwargs): - path = mpath.Path( - # Last vertex will have a CLOSEPOLY code and thus be ignored. - np.append(np.column_stack([xs, ys]), [(0, 0)], 0), - closed=True) - patch = mpatches.PathPatch(path, **kwargs) - self.add_artist(patch) - return [patch] + mean_kw[removed_prop] = '' # vertical or horizontal plot? - if vert: - def doplot(*args, **kwargs): - return self.plot(*args, **kwargs) + maybe_swap = slice(None) if vert else slice(None, None, -1) - def dopatch(xs, ys, **kwargs): - return patch_list(xs, ys, **kwargs) + def do_plot(xs, ys, **kwargs): + return self.plot(*[xs, ys][maybe_swap], **kwargs)[0] - else: - def doplot(*args, **kwargs): - shuffled = [] - for i in range(0, len(args), 2): - shuffled.extend([args[i + 1], args[i]]) - return self.plot(*shuffled, **kwargs) - - def dopatch(xs, ys, **kwargs): - xs, ys = ys, xs # flip X, Y - return patch_list(xs, ys, **kwargs) + def do_patch(xs, ys, **kwargs): + path = mpath.Path._create_closed( + np.column_stack([xs, ys][maybe_swap])) + patch = mpatches.PathPatch(path, **kwargs) + self.add_artist(patch) + return patch # input validation N = len(bxpstats) @@ -4098,38 +4198,45 @@ def dopatch(xs, ys, **kwargs): elif len(widths) != N: raise ValueError(datashape_message.format("widths")) - for pos, width, stats in zip(positions, widths, bxpstats): + # capwidth + if capwidths is None: + capwidths = 0.5 * np.array(widths) + elif np.isscalar(capwidths): + capwidths = [capwidths] * N + elif len(capwidths) != N: + raise ValueError(datashape_message.format("capwidths")) + + for pos, width, stats, capwidth in zip(positions, widths, bxpstats, + capwidths): # try to find a new label datalabels.append(stats.get('label', pos)) # whisker coords - whisker_x = np.ones(2) * pos - whiskerlo_y = np.array([stats['q1'], stats['whislo']]) - whiskerhi_y = np.array([stats['q3'], stats['whishi']]) - + whis_x = [pos, pos] + whislo_y = [stats['q1'], stats['whislo']] + whishi_y = [stats['q3'], stats['whishi']] # cap coords - cap_left = pos - width * 0.25 - cap_right = pos + width * 0.25 - cap_x = np.array([cap_left, cap_right]) - cap_lo = np.ones(2) * stats['whislo'] - cap_hi = np.ones(2) * stats['whishi'] - + cap_left = pos - capwidth * 0.5 + cap_right = pos + capwidth * 0.5 + cap_x = [cap_left, cap_right] + cap_lo = np.full(2, stats['whislo']) + cap_hi = np.full(2, stats['whishi']) # box and median coords box_left = pos - width * 0.5 box_right = pos + width * 0.5 med_y = [stats['med'], stats['med']] - # notched boxes if shownotches: - box_x = [box_left, box_right, box_right, cap_right, box_right, - box_right, box_left, box_left, cap_left, box_left, - box_left] + notch_left = pos - width * 0.25 + notch_right = pos + width * 0.25 + box_x = [box_left, box_right, box_right, notch_right, + box_right, box_right, box_left, box_left, notch_left, + box_left, box_left] box_y = [stats['q1'], stats['q1'], stats['cilo'], stats['med'], stats['cihi'], stats['q3'], stats['q3'], stats['cihi'], stats['med'], stats['cilo'], stats['q1']] - med_x = cap_x - + med_x = [notch_left, notch_right] # plain boxes else: box_x = [box_left, box_right, box_right, box_left, box_left] @@ -4137,50 +4244,33 @@ def dopatch(xs, ys, **kwargs): stats['q1']] med_x = [box_left, box_right] - # maybe draw the box: + # maybe draw the box if showbox: - if patch_artist: - boxes.extend(dopatch(box_x, box_y, **final_boxprops)) - else: - boxes.extend(doplot(box_x, box_y, **final_boxprops)) - + do_box = do_patch if patch_artist else do_plot + boxes.append(do_box(box_x, box_y, **box_kw)) # draw the whiskers - whiskers.extend(doplot( - whisker_x, whiskerlo_y, **final_whiskerprops - )) - whiskers.extend(doplot( - whisker_x, whiskerhi_y, **final_whiskerprops - )) - - # maybe draw the caps: + whiskers.append(do_plot(whis_x, whislo_y, **whisker_kw)) + whiskers.append(do_plot(whis_x, whishi_y, **whisker_kw)) + # maybe draw the caps if showcaps: - caps.extend(doplot(cap_x, cap_lo, **final_capprops)) - caps.extend(doplot(cap_x, cap_hi, **final_capprops)) - + caps.append(do_plot(cap_x, cap_lo, **cap_kw)) + caps.append(do_plot(cap_x, cap_hi, **cap_kw)) # draw the medians - medians.extend(doplot(med_x, med_y, **final_medianprops)) - + medians.append(do_plot(med_x, med_y, **median_kw)) # maybe draw the means if showmeans: if meanline: - means.extend(doplot( + means.append(do_plot( [box_left, box_right], [stats['mean'], stats['mean']], - **final_meanprops + **mean_kw )) else: - means.extend(doplot( - [pos], [stats['mean']], **final_meanprops - )) - + means.append(do_plot([pos], [stats['mean']], **mean_kw)) # maybe draw the fliers if showfliers: - # fliers coords flier_x = np.full(len(stats['fliers']), pos, dtype=np.float64) flier_y = stats['fliers'] - - fliers.extend(doplot( - flier_x, flier_y, **final_flierprops - )) + fliers.append(do_plot(flier_x, flier_y, **flier_kw)) if manage_ticks: axis_name = "x" if vert else "y" @@ -4212,8 +4302,7 @@ def dopatch(xs, ys, **kwargs): axis.set_major_formatter(formatter) formatter.seq = [*formatter.seq, *datalabels] - self._request_autoscale_view( - scalex=self._autoscaleXon, scaley=self._autoscaleYon) + self._request_autoscale_view() return dict(whiskers=whiskers, caps=caps, boxes=boxes, medians=medians, fliers=fliers, means=means) @@ -4289,7 +4378,7 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xsize, mcolors.to_rgba_array(kwcolor) except ValueError as err: raise ValueError( - "'color' kwarg must be an color or sequence of color " + "'color' kwarg must be a color or sequence of color " "specs. For a sequence of values to be color-mapped, use " "the 'c' argument instead.") from err if edgecolors is None: @@ -4297,18 +4386,18 @@ def _parse_scatter_color_args(c, edgecolors, kwargs, xsize, if facecolors is None: facecolors = kwcolor - if edgecolors is None and not rcParams['_internal.classic_mode']: - edgecolors = rcParams['scatter.edgecolors'] + if edgecolors is None and not mpl.rcParams['_internal.classic_mode']: + edgecolors = mpl.rcParams['scatter.edgecolors'] c_was_none = c is None if c is None: c = (facecolors if facecolors is not None - else "b" if rcParams['_internal.classic_mode'] + else "b" if mpl.rcParams['_internal.classic_mode'] else get_next_color_func()) c_is_string_or_strings = ( isinstance(c, str) or (np.iterable(c) and len(c) > 0 - and isinstance(cbook.safe_first_element(c), str))) + and isinstance(cbook._safe_first_finite(c), str))) def invalid_shape_exception(csize, xsize): return ValueError( @@ -4329,14 +4418,14 @@ def invalid_shape_exception(csize, xsize): c_is_mapped = False if c.size != xsize: valid_shape = False - # If c can be either mapped values or a RGB(A) color, prefer + # If c can be either mapped values or an RGB(A) color, prefer # the former if shapes match, the latter otherwise. elif c.size == xsize: c = c.ravel() c_is_mapped = True else: # Wrong size; it must not be intended for mapping. if c.shape in ((3,), (4,)): - _log.warning( + _api.warn_external( "*c* argument looks like a single numeric RGB or " "RGBA sequence, which should be avoided as value-" "mapping will have precedence in case its length " @@ -4358,7 +4447,7 @@ def invalid_shape_exception(csize, xsize): # severe failure => one may appreciate a verbose feedback. raise ValueError( f"'c' argument must be a color, a sequence of colors, " - f"or a sequence of numbers, not {c}") from err + f"or a sequence of numbers, not {c!r}") from err else: if len(colors) not in (0, 1, xsize): # NB: remember that a single color is also acceptable. @@ -4372,6 +4461,7 @@ def invalid_shape_exception(csize, xsize): "edgecolors", "c", "facecolor", "facecolors", "color"], label_namer="y") + @_docstring.interpd def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, *, edgecolors=None, plotnonfinite=False, **kwargs): @@ -4384,7 +4474,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, The data positions. s : float or array-like, shape (n, ), optional - The marker size in points**2. + The marker size in points**2 (typographic points are 1/72 in.). Default is ``rcParams['lines.markersize'] ** 2``. c : array-like or list of colors or color, optional @@ -4399,9 +4489,9 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, Note that *c* should not be a single numeric RGB or RGBA sequence because that is indistinguishable from an array of values to be colormapped. If you want to specify the same RGB or RGBA value for - all points, use a 2D array with a single row. Otherwise, value- - matching will have precedence in case of a size matching with *x* - and *y*. + all points, use a 2D array with a single row. Otherwise, + value-matching will have precedence in case of a size matching with + *x* and *y*. If you wish to specify a single color for all points prefer the *color* keyword argument. @@ -4418,21 +4508,17 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, See :mod:`matplotlib.markers` for more information about marker styles. - cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` - A `.Colormap` instance or registered colormap name. *cmap* is only - used if *c* is an array of floats. + %(cmap_doc)s + + This parameter is ignored if *c* is RGB(A). + + %(norm_doc)s - norm : `~matplotlib.colors.Normalize`, default: None - If *c* is an array of floats, *norm* is used to scale the color - data, *c*, in the range 0 to 1, in order to map into the colormap - *cmap*. - If *None*, use the default `.colors.Normalize`. + This parameter is ignored if *c* is RGB(A). - vmin, vmax : float, default: None - *vmin* and *vmax* are used in conjunction with the default norm to - map the color array *c* to the colormap *cmap*. If None, the - respective min and max of the color array is used. - It is deprecated to use *vmin*/*vmax* when *norm* is given. + %(vmin_vmax_doc)s + + This parameter is ignored if *c* is RGB(A). alpha : float, default: None The alpha blending value, between 0 (transparent) and 1 (opaque). @@ -4464,6 +4550,8 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER **kwargs : `~matplotlib.collections.Collection` properties See Also @@ -4487,9 +4575,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, """ # Process **kwargs to handle aliases, conflicts with explicit kwargs: - x, y = self._process_unit_info([("x", x), ("y", y)], kwargs) - # np.ma.ravel yields an ndarray, not a masked array, # unless its argument is a masked array. x = np.ma.ravel(x) @@ -4498,8 +4584,8 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, raise ValueError("x and y must be the same size") if s is None: - s = (20 if rcParams['_internal.classic_mode'] else - rcParams['lines.markersize'] ** 2.0) + s = (20 if mpl.rcParams['_internal.classic_mode'] else + mpl.rcParams['lines.markersize'] ** 2.0) s = np.ma.ravel(s) if (len(s) not in (1, x.size) or (not np.issubdtype(s.dtype, np.floating) and @@ -4535,7 +4621,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, # load default marker from rcParams if marker is None: - marker = rcParams['scatter.marker'] + marker = mpl.rcParams['scatter.marker'] if isinstance(marker, mmarkers.MarkerStyle): marker_obj = marker @@ -4567,8 +4653,8 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, # promote the facecolor to be the edgecolor edgecolors = colors # set the facecolor to 'none' (at the last chance) because - # we can not not fill a path if the facecolor is non-null. - # (which is defendable at the renderer level) + # we can not fill a path if the facecolor is non-null + # (which is defendable at the renderer level). colors = 'none' else: # if we are not nulling the face color we can do this @@ -4576,38 +4662,47 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, edgecolors = 'face' if linewidths is None: - linewidths = rcParams['lines.linewidth'] + linewidths = mpl.rcParams['lines.linewidth'] elif np.iterable(linewidths): linewidths = [ - lw if lw is not None else rcParams['lines.linewidth'] + lw if lw is not None else mpl.rcParams['lines.linewidth'] for lw in linewidths] offsets = np.ma.column_stack([x, y]) collection = mcoll.PathCollection( - (path,), scales, - facecolors=colors, - edgecolors=edgecolors, - linewidths=linewidths, - offsets=offsets, - transOffset=kwargs.pop('transform', self.transData), - alpha=alpha - ) + (path,), scales, + facecolors=colors, + edgecolors=edgecolors, + linewidths=linewidths, + offsets=offsets, + offset_transform=kwargs.pop('transform', self.transData), + alpha=alpha, + ) collection.set_transform(mtransforms.IdentityTransform()) - collection.update(kwargs) - if colors is None: collection.set_array(c) collection.set_cmap(cmap) collection.set_norm(norm) collection._scale_norm(norm, vmin, vmax) + else: + extra_kwargs = { + 'cmap': cmap, 'norm': norm, 'vmin': vmin, 'vmax': vmax + } + extra_keys = [k for k, v in extra_kwargs.items() if v is not None] + if any(extra_keys): + keys_str = ", ".join(f"'{k}'" for k in extra_keys) + _api.warn_external( + "No data for colormapping provided via 'c'. " + f"Parameters {keys_str} will be ignored") + collection._internal_update(kwargs) # Classic mode only: # ensure there are margins to allow for the # finite size of the symbols. In v2.x, margins # are present by default, so we disable this # scatter-specific override. - if rcParams['_internal.classic_mode']: + if mpl.rcParams['_internal.classic_mode']: if self._xmargin < 0.05 and x.size > 0: self.set_xmargin(0.05) if self._ymargin < 0.05 and x.size > 0: @@ -4619,7 +4714,7 @@ def scatter(self, x, y, s=None, c=None, marker=None, cmap=None, norm=None, return collection @_preprocess_data(replace_names=["x", "y", "C"], label_namer="y") - @docstring.dedent_interpd + @_docstring.dedent_interpd def hexbin(self, x, y, C=None, gridsize=100, bins=None, xscale='linear', yscale='linear', extent=None, cmap=None, norm=None, vmin=None, vmax=None, @@ -4650,7 +4745,31 @@ def hexbin(self, x, y, C=None, gridsize=100, bins=None, the hexagons are approximately regular. Alternatively, if a tuple (*nx*, *ny*), the number of hexagons - in the *x*-direction and the *y*-direction. + in the *x*-direction and the *y*-direction. In the + *y*-direction, counting is done along vertically aligned + hexagons, not along the zig-zag chains of hexagons; see the + following illustration. + + .. plot:: + + import numpy + import matplotlib.pyplot as plt + + np.random.seed(19680801) + n= 300 + x = np.random.standard_normal(n) + y = np.random.standard_normal(n) + + fig, ax = plt.subplots(figsize=(4, 4)) + h = ax.hexbin(x, y, gridsize=(5, 3)) + hx, hy = h.get_offsets().T + ax.plot(hx[24::3], hy[24::3], 'ro-') + ax.plot(hx[-3:], hy[-3:], 'ro-') + ax.set_title('gridsize=(5, 3)') + ax.axis('off') + + To get approximately regular hexagons, choose + :math:`n_x = \\sqrt{3}\\,n_y`. bins : 'log' or int or sequence, default: None Discretization of the hexagon values. @@ -4680,17 +4799,16 @@ def hexbin(self, x, y, C=None, gridsize=100, bins=None, colormapped rectangles along the bottom of the x-axis and left of the y-axis. - extent : float, default: *None* - The limits of the bins. The default assigns the limits - based on *gridsize*, *x*, *y*, *xscale* and *yscale*. + extent : 4-tuple of float, default: *None* + The limits of the bins (xmin, xmax, ymin, ymax). + The default assigns the limits based on + *gridsize*, *x*, *y*, *xscale* and *yscale*. If *xscale* or *yscale* is set to 'log', the limits are expected to be the exponent for a power of 10. E.g. for x-limits of 1 and 50 in 'linear' scale and y-limits of 10 and 1000 in 'log' scale, enter (1, 50, 1, 3). - Order of scalars is (left, right, bottom, top). - Returns ------- `~matplotlib.collections.PolyCollection` @@ -4707,21 +4825,11 @@ def hexbin(self, x, y, C=None, gridsize=100, bins=None, Other Parameters ---------------- - cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` - The Colormap instance or registered colormap name used to map - the bin values to colors. - - norm : `~matplotlib.colors.Normalize`, optional - The Normalize instance scales the bin values to the canonical - colormap range [0, 1] for mapping to colors. By default, the data - range is mapped to the colorbar range using linear scaling. - - vmin, vmax : float, default: None - The colorbar range. If *None*, suitable min/max values are - automatically chosen by the `~.Normalize` instance (defaults to - the respective min/max values of the bins in case of the default - linear scaling). - It is deprecated to use *vmin*/*vmax* when *norm* is given. + %(cmap_doc)s + + %(norm_doc)s + + %(vmin_vmax_doc)s alpha : float between 0 and 1, optional The alpha blending value, between 0 (transparent) and 1 (opaque). @@ -4750,11 +4858,17 @@ def reduce_C_function(C: array) -> float - `numpy.sum`: integral of the point values - `numpy.amax`: value taken from the largest point + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs : `~matplotlib.collections.PolyCollection` properties All other keyword arguments are passed on to `.PolyCollection`: - %(PolyCollection_kwdoc)s + %(PolyCollection:kwdoc)s + See Also + -------- + hist2d : 2D histogram rectangular bins """ self._process_unit_info([("x", x), ("y", y)], kwargs, convert=False) @@ -4767,28 +4881,39 @@ def reduce_C_function(C: array) -> float nx = gridsize ny = int(nx / math.sqrt(3)) # Count the number of data in each hexagon - x = np.array(x, float) - y = np.array(y, float) + x = np.asarray(x, float) + y = np.asarray(y, float) + + # Will be log()'d if necessary, and then rescaled. + tx = x + ty = y + if xscale == 'log': if np.any(x <= 0.0): - raise ValueError("x contains non-positive values, so can not" - " be log-scaled") - x = np.log10(x) + raise ValueError("x contains non-positive values, so can not " + "be log-scaled") + tx = np.log10(tx) if yscale == 'log': if np.any(y <= 0.0): - raise ValueError("y contains non-positive values, so can not" - " be log-scaled") - y = np.log10(y) + raise ValueError("y contains non-positive values, so can not " + "be log-scaled") + ty = np.log10(ty) if extent is not None: xmin, xmax, ymin, ymax = extent else: - xmin, xmax = (np.min(x), np.max(x)) if len(x) else (0, 1) - ymin, ymax = (np.min(y), np.max(y)) if len(y) else (0, 1) + xmin, xmax = (tx.min(), tx.max()) if len(x) else (0, 1) + ymin, ymax = (ty.min(), ty.max()) if len(y) else (0, 1) # to avoid issues with singular data, expand the min/max pairs xmin, xmax = mtransforms.nonsingular(xmin, xmax, expander=0.1) ymin, ymax = mtransforms.nonsingular(ymin, ymax, expander=0.1) + nx1 = nx + 1 + ny1 = ny + 1 + nx2 = nx + ny2 = ny + n = nx1 * ny1 + nx2 * ny2 + # In the x-direction, the hexagons exactly cover the region from # xmin to xmax. Need some padding to avoid roundoff errors. padding = 1.e-9 * (xmax - xmin) @@ -4796,80 +4921,48 @@ def reduce_C_function(C: array) -> float xmax += padding sx = (xmax - xmin) / nx sy = (ymax - ymin) / ny - - if marginals: - xorig = x.copy() - yorig = y.copy() - - x = (x - xmin) / sx - y = (y - ymin) / sy - ix1 = np.round(x).astype(int) - iy1 = np.round(y).astype(int) - ix2 = np.floor(x).astype(int) - iy2 = np.floor(y).astype(int) - - nx1 = nx + 1 - ny1 = ny + 1 - nx2 = nx - ny2 = ny - n = nx1 * ny1 + nx2 * ny2 - - d1 = (x - ix1) ** 2 + 3.0 * (y - iy1) ** 2 - d2 = (x - ix2 - 0.5) ** 2 + 3.0 * (y - iy2 - 0.5) ** 2 + # Positions in hexagon index coordinates. + ix = (tx - xmin) / sx + iy = (ty - ymin) / sy + ix1 = np.round(ix).astype(int) + iy1 = np.round(iy).astype(int) + ix2 = np.floor(ix).astype(int) + iy2 = np.floor(iy).astype(int) + # flat indices, plus one so that out-of-range points go to position 0. + i1 = np.where((0 <= ix1) & (ix1 < nx1) & (0 <= iy1) & (iy1 < ny1), + ix1 * ny1 + iy1 + 1, 0) + i2 = np.where((0 <= ix2) & (ix2 < nx2) & (0 <= iy2) & (iy2 < ny2), + ix2 * ny2 + iy2 + 1, 0) + + d1 = (ix - ix1) ** 2 + 3.0 * (iy - iy1) ** 2 + d2 = (ix - ix2 - 0.5) ** 2 + 3.0 * (iy - iy2 - 0.5) ** 2 bdist = (d1 < d2) - if C is None: - lattice1 = np.zeros((nx1, ny1)) - lattice2 = np.zeros((nx2, ny2)) - c1 = (0 <= ix1) & (ix1 < nx1) & (0 <= iy1) & (iy1 < ny1) & bdist - c2 = (0 <= ix2) & (ix2 < nx2) & (0 <= iy2) & (iy2 < ny2) & ~bdist - np.add.at(lattice1, (ix1[c1], iy1[c1]), 1) - np.add.at(lattice2, (ix2[c2], iy2[c2]), 1) - if mincnt is not None: - lattice1[lattice1 < mincnt] = np.nan - lattice2[lattice2 < mincnt] = np.nan - accum = np.concatenate([lattice1.ravel(), lattice2.ravel()]) - good_idxs = ~np.isnan(accum) + if C is None: # [1:] drops out-of-range points. + counts1 = np.bincount(i1[bdist], minlength=1 + nx1 * ny1)[1:] + counts2 = np.bincount(i2[~bdist], minlength=1 + nx2 * ny2)[1:] + accum = np.concatenate([counts1, counts2]).astype(float) + if mincnt is not None: + accum[accum < mincnt] = np.nan + C = np.ones(len(x)) else: - if mincnt is None: - mincnt = 0 - - # create accumulation arrays - lattice1 = np.empty((nx1, ny1), dtype=object) - for i in range(nx1): - for j in range(ny1): - lattice1[i, j] = [] - lattice2 = np.empty((nx2, ny2), dtype=object) - for i in range(nx2): - for j in range(ny2): - lattice2[i, j] = [] - + # store the C values in a list per hexagon index + Cs_at_i1 = [[] for _ in range(1 + nx1 * ny1)] + Cs_at_i2 = [[] for _ in range(1 + nx2 * ny2)] for i in range(len(x)): if bdist[i]: - if 0 <= ix1[i] < nx1 and 0 <= iy1[i] < ny1: - lattice1[ix1[i], iy1[i]].append(C[i]) + Cs_at_i1[i1[i]].append(C[i]) else: - if 0 <= ix2[i] < nx2 and 0 <= iy2[i] < ny2: - lattice2[ix2[i], iy2[i]].append(C[i]) - - for i in range(nx1): - for j in range(ny1): - vals = lattice1[i, j] - if len(vals) > mincnt: - lattice1[i, j] = reduce_C_function(vals) - else: - lattice1[i, j] = np.nan - for i in range(nx2): - for j in range(ny2): - vals = lattice2[i, j] - if len(vals) > mincnt: - lattice2[i, j] = reduce_C_function(vals) - else: - lattice2[i, j] = np.nan + Cs_at_i2[i2[i]].append(C[i]) + if mincnt is None: + mincnt = 0 + accum = np.array( + [reduce_C_function(acc) if len(acc) > mincnt else np.nan + for Cs_at_i in [Cs_at_i1, Cs_at_i2] + for acc in Cs_at_i[1:]], # [1:] drops out-of-range points. + float) - accum = np.concatenate([lattice1.astype(float).ravel(), - lattice2.astype(float).ravel()]) - good_idxs = ~np.isnan(accum) + good_idxs = ~np.isnan(accum) offsets = np.zeros((n, 2), float) offsets[:nx1 * ny1, 0] = np.repeat(np.arange(nx1), ny1) @@ -4913,8 +5006,9 @@ def reduce_C_function(C: array) -> float edgecolors=edgecolors, linewidths=linewidths, offsets=offsets, - transOffset=mtransforms.AffineDeltaTransform(self.transData), - ) + offset_transform=mtransforms.AffineDeltaTransform( + self.transData), + ) # Set normalizer if bins is 'log' if bins == 'log': @@ -4922,16 +5016,11 @@ def reduce_C_function(C: array) -> float _api.warn_external("Only one of 'bins' and 'norm' arguments " f"can be supplied, ignoring bins={bins}") else: - norm = mcolors.LogNorm() + norm = mcolors.LogNorm(vmin=vmin, vmax=vmax) + vmin = vmax = None bins = None - if isinstance(norm, mcolors.LogNorm): - if (accum == 0).any(): - # make sure we have no zeros - accum += 1 - - # autoscale the norm with current accum values if it hasn't - # been set + # autoscale the norm with current accum values if it hasn't been set if norm is not None: if norm.vmin is None and norm.vmax is None: norm.autoscale(accum) @@ -4948,7 +5037,7 @@ def reduce_C_function(C: array) -> float collection.set_cmap(cmap) collection.set_norm(norm) collection.set_alpha(alpha) - collection.update(kwargs) + collection._internal_update(kwargs) collection._scale_norm(norm, vmin, vmax) corners = ((xmin, ymin), (xmax, ymax)) @@ -4960,96 +5049,62 @@ def reduce_C_function(C: array) -> float if not marginals: return collection - if C is None: - C = np.ones(len(x)) - - def coarse_bin(x, y, coarse): - ind = coarse.searchsorted(x).clip(0, len(coarse) - 1) - mus = np.zeros(len(coarse)) - for i in range(len(coarse)): - yi = y[ind == i] - if len(yi) > 0: - mu = reduce_C_function(yi) - else: - mu = np.nan - mus[i] = mu - return mus - - coarse = np.linspace(xmin, xmax, gridsize) - - xcoarse = coarse_bin(xorig, C, coarse) - valid = ~np.isnan(xcoarse) - verts, values = [], [] - for i, val in enumerate(xcoarse): - thismin = coarse[i] - if i < len(coarse) - 1: - thismax = coarse[i + 1] - else: - thismax = thismin + np.diff(coarse)[-1] - - if not valid[i]: - continue - - verts.append([(thismin, 0), - (thismin, 0.05), - (thismax, 0.05), - (thismax, 0)]) - values.append(val) - - values = np.array(values) - trans = self.get_xaxis_transform(which='grid') + # Process marginals + bars = [] + for zname, z, zmin, zmax, zscale, nbins in [ + ("x", x, xmin, xmax, xscale, nx), + ("y", y, ymin, ymax, yscale, 2 * ny), + ]: - hbar = mcoll.PolyCollection(verts, transform=trans, edgecolors='face') - - hbar.set_array(values) - hbar.set_cmap(cmap) - hbar.set_norm(norm) - hbar.set_alpha(alpha) - hbar.update(kwargs) - self.add_collection(hbar, autolim=False) - - coarse = np.linspace(ymin, ymax, gridsize) - ycoarse = coarse_bin(yorig, C, coarse) - valid = ~np.isnan(ycoarse) - verts, values = [], [] - for i, val in enumerate(ycoarse): - thismin = coarse[i] - if i < len(coarse) - 1: - thismax = coarse[i + 1] + if zscale == "log": + bin_edges = np.geomspace(zmin, zmax, nbins + 1) else: - thismax = thismin + np.diff(coarse)[-1] - if not valid[i]: - continue - verts.append([(0, thismin), (0.0, thismax), - (0.05, thismax), (0.05, thismin)]) - values.append(val) - - values = np.array(values) - - trans = self.get_yaxis_transform(which='grid') - - vbar = mcoll.PolyCollection(verts, transform=trans, edgecolors='face') - vbar.set_array(values) - vbar.set_cmap(cmap) - vbar.set_norm(norm) - vbar.set_alpha(alpha) - vbar.update(kwargs) - self.add_collection(vbar, autolim=False) - - collection.hbar = hbar - collection.vbar = vbar + bin_edges = np.linspace(zmin, zmax, nbins + 1) + + verts = np.empty((nbins, 4, 2)) + verts[:, 0, 0] = verts[:, 1, 0] = bin_edges[:-1] + verts[:, 2, 0] = verts[:, 3, 0] = bin_edges[1:] + verts[:, 0, 1] = verts[:, 3, 1] = .00 + verts[:, 1, 1] = verts[:, 2, 1] = .05 + if zname == "y": + verts = verts[:, :, ::-1] # Swap x and y. + + # Sort z-values into bins defined by bin_edges. + bin_idxs = np.searchsorted(bin_edges, z) - 1 + values = np.empty(nbins) + for i in range(nbins): + # Get C-values for each bin, and compute bin value with + # reduce_C_function. + ci = C[bin_idxs == i] + values[i] = reduce_C_function(ci) if len(ci) > 0 else np.nan + + mask = ~np.isnan(values) + verts = verts[mask] + values = values[mask] + + trans = getattr(self, f"get_{zname}axis_transform")(which="grid") + bar = mcoll.PolyCollection( + verts, transform=trans, edgecolors="face") + bar.set_array(values) + bar.set_cmap(cmap) + bar.set_norm(norm) + bar.set_alpha(alpha) + bar._internal_update(kwargs) + bars.append(self.add_collection(bar, autolim=False)) + + collection.hbar, collection.vbar = bars def on_changed(collection): - hbar.set_cmap(collection.get_cmap()) - hbar.set_clim(collection.get_clim()) - vbar.set_cmap(collection.get_cmap()) - vbar.set_clim(collection.get_clim()) + collection.hbar.set_cmap(collection.get_cmap()) + collection.hbar.set_cmap(collection.get_cmap()) + collection.vbar.set_clim(collection.get_clim()) + collection.vbar.set_clim(collection.get_clim()) - collection.callbacksSM.connect('changed', on_changed) + collection.callbacks.connect('changed', on_changed) return collection - @docstring.dedent_interpd + @_docstring.dedent_interpd def arrow(self, x, y, dx, dy, **kwargs): """ Add an arrow to the Axes. @@ -5058,12 +5113,6 @@ def arrow(self, x, y, dx, dy, **kwargs): Parameters ---------- - x, y : float - The x and y coordinates of the arrow base. - - dx, dy : float - The length of the arrow along x and y direction. - %(FancyArrow)s Returns @@ -5094,44 +5143,40 @@ def arrow(self, x, y, dx, dy, **kwargs): self._request_autoscale_view() return a - @docstring.copy(mquiver.QuiverKey.__init__) - def quiverkey(self, Q, X, Y, U, label, **kw): - qk = mquiver.QuiverKey(Q, X, Y, U, label, **kw) + @_docstring.copy(mquiver.QuiverKey.__init__) + def quiverkey(self, Q, X, Y, U, label, **kwargs): + qk = mquiver.QuiverKey(Q, X, Y, U, label, **kwargs) self.add_artist(qk) return qk # Handle units for x and y, if they've been passed - def _quiver_units(self, args, kw): + def _quiver_units(self, args, kwargs): if len(args) > 3: x, y = args[0:2] - x, y = self._process_unit_info([("x", x), ("y", y)], kw) + x, y = self._process_unit_info([("x", x), ("y", y)], kwargs) return (x, y) + args[2:] return args - # args can by a combination if X, Y, U, V, C and all should be replaced + # args can be a combination of X, Y, U, V, C and all should be replaced @_preprocess_data() - def quiver(self, *args, **kw): + @_docstring.dedent_interpd + def quiver(self, *args, **kwargs): + """%(quiver_doc)s""" # Make sure units are handled for x and y values - args = self._quiver_units(args, kw) - - q = mquiver.Quiver(self, *args, **kw) - + args = self._quiver_units(args, kwargs) + q = mquiver.Quiver(self, *args, **kwargs) self.add_collection(q, autolim=True) self._request_autoscale_view() return q - quiver.__doc__ = mquiver.Quiver.quiver_doc # args can be some combination of X, Y, U, V, C and all should be replaced @_preprocess_data() - @docstring.dedent_interpd - def barbs(self, *args, **kw): - """ - %(barbs_doc)s - """ + @_docstring.dedent_interpd + def barbs(self, *args, **kwargs): + """%(barbs_doc)s""" # Make sure units are handled for x and y values - args = self._quiver_units(args, kw) - - b = mquiver.Barbs(self, *args, **kw) + args = self._quiver_units(args, kwargs) + b = mquiver.Barbs(self, *args, **kwargs) self.add_collection(b, autolim=True) self._request_autoscale_view() return b @@ -5264,25 +5309,24 @@ def _fill_between_x_or_y( Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs All other keyword arguments are passed on to `.PolyCollection`. They control the `.Polygon` properties: - %(PolyCollection_kwdoc)s + %(PolyCollection:kwdoc)s See Also -------- fill_between : Fill between two sets of y-values. fill_betweenx : Fill between two sets of x-values. - - Notes - ----- - .. [notes section required to get data note injection right] """ dep_dir = {"x": "y", "y": "x"}[ind_dir] - if not rcParams["_internal.classic_mode"]: + if not mpl.rcParams["_internal.classic_mode"]: kwargs = cbook.normalize_kwargs(kwargs, mcoll.Collection) if not any(c in kwargs for c in ("color", "facecolor")): kwargs["facecolor"] = \ @@ -5306,9 +5350,10 @@ def _fill_between_x_or_y( raise ValueError(f"where size ({where.size}) does not match " f"{ind_dir} size ({ind.size})") where = where & ~functools.reduce( - np.logical_or, map(np.ma.getmask, [ind, dep1, dep2])) + np.logical_or, map(np.ma.getmaskarray, [ind, dep1, dep2])) - ind, dep1, dep2 = np.broadcast_arrays(np.atleast_1d(ind), dep1, dep2) + ind, dep1, dep2 = np.broadcast_arrays( + np.atleast_1d(ind), dep1, dep2, subok=True) polys = [] for idx0, idx1 in cbook.contiguous_regions(where): @@ -5392,7 +5437,7 @@ def fill_between(self, x, y1, y2=0, where=None, interpolate=False, dir="horizontal", ind="x", dep="y" ) fill_between = _preprocess_data( - docstring.dedent_interpd(fill_between), + _docstring.dedent_interpd(fill_between), replace_names=["x", "y1", "y2", "where"]) def fill_betweenx(self, y, x1, x2=0, where=None, @@ -5406,14 +5451,17 @@ def fill_betweenx(self, y, x1, x2=0, where=None, dir="vertical", ind="y", dep="x" ) fill_betweenx = _preprocess_data( - docstring.dedent_interpd(fill_betweenx), + _docstring.dedent_interpd(fill_betweenx), replace_names=["y", "x1", "x2", "where"]) #### plotting z(x, y): imshow, pcolor and relatives, contour + @_preprocess_data() - def imshow(self, X, cmap=None, norm=None, aspect=None, - interpolation=None, alpha=None, vmin=None, vmax=None, - origin=None, extent=None, *, filternorm=True, filterrad=4.0, + @_docstring.interpd + def imshow(self, X, cmap=None, norm=None, *, aspect=None, + interpolation=None, alpha=None, + vmin=None, vmax=None, origin=None, extent=None, + interpolation_stage=None, filternorm=True, filterrad=4.0, resample=None, url=None, **kwargs): """ Display data as an image, i.e., on a 2D regular raster. @@ -5448,15 +5496,17 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, Out-of-range RGB(A) values are clipped. - cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` - The Colormap instance or registered colormap name used to map - scalar data to colors. This parameter is ignored for RGB(A) data. + %(cmap_doc)s + + This parameter is ignored if *X* is RGB(A). - norm : `~matplotlib.colors.Normalize`, optional - The `.Normalize` instance used to scale scalar data to the [0, 1] - range before mapping to colors using *cmap*. By default, a linear - scaling mapping the lowest value to 0 and the highest to 1 is used. - This parameter is ignored for RGB(A) data. + %(norm_doc)s + + This parameter is ignored if *X* is RGB(A). + + %(vmin_vmax_doc)s + + This parameter is ignored if *X* is RGB(A). aspect : {'equal', 'auto'} or float, default: :rc:`image.aspect` The aspect ratio of the Axes. This parameter is particularly @@ -5505,18 +5555,17 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, which can be set by *filterrad*. Additionally, the antigrain image resize filter is controlled by the parameter *filternorm*. + interpolation_stage : {'data', 'rgba'}, default: 'data' + If 'data', interpolation + is carried out on the data provided by the user. If 'rgba', the + interpolation is carried out after the colormapping has been + applied (visual interpolation). + alpha : float or array-like, optional The alpha blending value, between 0 (transparent) and 1 (opaque). If *alpha* is an array, the alpha blending values are applied pixel by pixel, and *alpha* must have the same shape as *X*. - vmin, vmax : float, optional - When using scalar data and no explicit *norm*, *vmin* and *vmax* - define the data range that the colormap covers. By default, - the colormap covers the complete value range of the supplied - data. It is deprecated to use *vmin*/*vmax* when *norm* is given. - When using RGB(A) data, parameters *vmin*/*vmax* are ignored. - origin : {'upper', 'lower'}, default: :rc:`image.origin` Place the [0, 0] index of the array in the upper left or lower left corner of the Axes. The convention (the default) 'upper' is @@ -5530,6 +5579,7 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, extent : floats (left, right, bottom, top), optional The bounding box in data coordinates that the image will fill. + These values may be unitful and match the units of the Axes. The image is stretched individually along x and y to fill the box. The default extent is determined by the following conditions. @@ -5574,6 +5624,9 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs : `~matplotlib.artist.Artist` properties These parameters are passed on to the constructor of the `.AxesImage` artist. @@ -5600,11 +5653,14 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, (unassociated) alpha representation. """ if aspect is None: - aspect = rcParams['image.aspect'] + aspect = mpl.rcParams['image.aspect'] self.set_aspect(aspect) - im = mimage.AxesImage(self, cmap, norm, interpolation, origin, extent, - filternorm=filternorm, filterrad=filterrad, - resample=resample, **kwargs) + im = mimage.AxesImage(self, cmap=cmap, norm=norm, + interpolation=interpolation, origin=origin, + extent=extent, filternorm=filternorm, + filterrad=filterrad, resample=resample, + interpolation_stage=interpolation_stage, + **kwargs) im.set_data(X) im.set_alpha(alpha) @@ -5621,7 +5677,7 @@ def imshow(self, X, cmap=None, norm=None, aspect=None, self.add_image(im) return im - def _pcolorargs(self, funcname, *args, shading='flat', **kwargs): + def _pcolorargs(self, funcname, *args, shading='auto', **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; @@ -5631,7 +5687,7 @@ def _pcolorargs(self, funcname, *args, shading='flat', **kwargs): _valid_shading = ['gouraud', 'nearest', 'flat', 'auto'] try: _api.check_in_list(_valid_shading, shading=shading) - except ValueError as err: + except ValueError: _api.warn_external(f"shading value '{shading}' not in list of " f"valid values {_valid_shading}. Setting " "shading='auto'.") @@ -5639,7 +5695,7 @@ def _pcolorargs(self, funcname, *args, shading='flat', **kwargs): if len(args) == 1: C = np.asanyarray(args[0]) - nrows, ncols = C.shape + nrows, ncols = C.shape[:2] if shading in ['gouraud', 'nearest']: X, Y = np.meshgrid(np.arange(ncols), np.arange(nrows)) else: @@ -5668,10 +5724,9 @@ def _pcolorargs(self, funcname, *args, shading='flat', **kwargs): X = X.data # strip mask as downstream doesn't like it... if isinstance(Y, np.ma.core.MaskedArray): Y = Y.data - nrows, ncols = C.shape + nrows, ncols = C.shape[:2] else: - raise TypeError(f'{funcname}() takes 1 or 3 positional arguments ' - f'but {len(args)} were given') + raise _api.nargs_error(funcname, takes="1 or 3", given=len(args)) Nx = X.shape[-1] Ny = Y.shape[0] @@ -5682,9 +5737,8 @@ def _pcolorargs(self, funcname, *args, shading='flat', **kwargs): y = Y.reshape(Ny, 1) Y = y.repeat(Nx, axis=1) if X.shape != Y.shape: - raise TypeError( - 'Incompatible X, Y inputs to %s; see help(%s)' % ( - funcname, funcname)) + raise TypeError(f'Incompatible X, Y inputs to {funcname}; ' + f'see help({funcname})') if shading == 'auto': if ncols == Nx and nrows == Ny: @@ -5693,25 +5747,17 @@ def _pcolorargs(self, funcname, *args, shading='flat', **kwargs): shading = 'flat' if shading == 'flat': - if not (ncols in (Nx, Nx - 1) and nrows in (Ny, Ny - 1)): - raise TypeError('Dimensions of C %s are incompatible with' - ' X (%d) and/or Y (%d); see help(%s)' % ( - C.shape, Nx, Ny, funcname)) - if (ncols == Nx or nrows == Ny): - _api.warn_deprecated( - "3.3", message="shading='flat' when X and Y have the same " - "dimensions as C is deprecated since %(since)s. Either " - "specify the corners of the quadrilaterals with X and Y, " - "or pass shading='auto', 'nearest' or 'gouraud', or set " - "rcParams['pcolor.shading']. This will become an error " - "%(removal)s.") - C = C[:Ny - 1, :Nx - 1] + if (Nx, Ny) != (ncols + 1, nrows + 1): + raise TypeError(f"Dimensions of C {C.shape} should" + f" be one smaller than X({Nx}) and Y({Ny})" + f" while using shading='flat'" + f" see help({funcname})") else: # ['nearest', 'gouraud']: if (Nx, Ny) != (ncols, nrows): raise TypeError('Dimensions of C %s are incompatible with' ' X (%d) and/or Y (%d); see help(%s)' % ( C.shape, Nx, Ny, funcname)) - if shading in ['nearest', 'auto']: + if shading == 'nearest': # grid is specified at the center, so define corners # at the midpoints between the grid centers and then use the # flat algorithm. @@ -5748,7 +5794,7 @@ def _interp_grid(X): return X, Y, C, shading @_preprocess_data() - @docstring.dedent_interpd + @_docstring.dedent_interpd def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, **kwargs): r""" @@ -5772,15 +5818,16 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, Parameters ---------- C : 2D array-like - The color-mapped values. + The color-mapped values. Color-mapping is controlled by *cmap*, + *norm*, *vmin*, and *vmax*. X, Y : array-like, optional The coordinates of the corners of quadrilaterals of a pcolormesh:: (X[i+1, j], Y[i+1, j]) (X[i+1, j+1], Y[i+1, j+1]) - +-----+ - | | - +-----+ + â—╶───╴◠+ │ │ + â—╶───╴◠(X[i, j], Y[i, j]) (X[i, j+1], Y[i, j+1]) Note that the column index corresponds to the x-coordinate, and @@ -5801,9 +5848,8 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, expanded as needed into the appropriate 2D arrays, making a rectangular grid. - shading : {'flat', 'nearest', 'auto'}, optional - The fill style for the quadrilateral; defaults to 'flat' or - :rc:`pcolor.shading`. Possible values: + shading : {'flat', 'nearest', 'auto'}, default: :rc:`pcolor.shading` + The fill style for the quadrilateral. Possible values: - 'flat': A solid color is used for each quad. The color of the quad (i, j), (i+1, j), (i, j+1), (i+1, j+1) is given by @@ -5820,21 +5866,11 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, See :doc:`/gallery/images_contours_and_fields/pcolormesh_grids` for more description. - cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` - A Colormap instance or registered colormap name. The colormap - maps the *C* values to colors. + %(cmap_doc)s - norm : `~matplotlib.colors.Normalize`, optional - The Normalize instance scales the data values to the canonical - colormap range [0, 1] for mapping to colors. By default, the data - range is mapped to the colorbar range using linear scaling. + %(norm_doc)s - vmin, vmax : float, default: None - The colorbar range. If *None*, suitable min/max values are - automatically chosen by the `~.Normalize` instance (defaults to - the respective min/max values of *C* in case of the default linear - scaling). - It is deprecated to use *vmin*/*vmax* when *norm* is given. + %(vmin_vmax_doc)s edgecolors : {'none', None, 'face', color, color sequence}, optional The color of the edges. Defaults to 'none'. Possible values: @@ -5870,11 +5906,14 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, Stroking the edges may be preferred if *alpha* is 1, but will cause artifacts otherwise. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Additionally, the following arguments are allowed. They are passed along to the `~matplotlib.collections.PolyCollection` constructor: - %(PolyCollection_kwdoc)s + %(PolyCollection:kwdoc)s See Also -------- @@ -5902,7 +5941,7 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, """ if shading is None: - shading = rcParams['pcolor.shading'] + shading = mpl.rcParams['pcolor.shading'] shading = shading.lower() X, Y, C, shading = self._pcolorargs('pcolor', *args, shading=shading, kwargs=kwargs) @@ -5955,14 +5994,9 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, kwargs.setdefault('snap', False) - collection = mcoll.PolyCollection(verts, **kwargs) - - collection.set_alpha(alpha) - collection.set_array(C) - collection.set_cmap(cmap) - collection.set_norm(norm) + collection = mcoll.PolyCollection( + verts, array=C, cmap=cmap, norm=norm, alpha=alpha, **kwargs) collection._scale_norm(norm, vmin, vmax) - self.grid(False) x = X.compressed() y = Y.compressed() @@ -5994,7 +6028,7 @@ def pcolor(self, *args, shading=None, alpha=None, norm=None, cmap=None, return collection @_preprocess_data() - @docstring.dedent_interpd + @_docstring.dedent_interpd def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, shading=None, antialiased=False, **kwargs): """ @@ -6015,16 +6049,26 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, Parameters ---------- - C : 2D array-like - The color-mapped values. + C : array-like + The mesh data. Supported array shapes are: + + - (M, N) or M*N: a mesh with scalar data. The values are mapped to + colors using normalization and a colormap. See parameters *norm*, + *cmap*, *vmin*, *vmax*. + - (M, N, 3): an image with RGB values (0-1 float or 0-255 int). + - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int), + i.e. including transparency. + + The first two dimensions (M, N) define the rows and columns of + the mesh data. X, Y : array-like, optional The coordinates of the corners of quadrilaterals of a pcolormesh:: (X[i+1, j], Y[i+1, j]) (X[i+1, j+1], Y[i+1, j+1]) - +-----+ - | | - +-----+ + â—╶───╴◠+ │ │ + â—╶───╴◠(X[i, j], Y[i, j]) (X[i, j+1], Y[i, j+1]) Note that the column index corresponds to the x-coordinate, and @@ -6047,21 +6091,11 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, expanded as needed into the appropriate 2D arrays, making a rectangular grid. - cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` - A Colormap instance or registered colormap name. The colormap - maps the *C* values to colors. + %(cmap_doc)s - norm : `~matplotlib.colors.Normalize`, optional - The Normalize instance scales the data values to the canonical - colormap range [0, 1] for mapping to colors. By default, the data - range is mapped to the colorbar range using linear scaling. + %(norm_doc)s - vmin, vmax : float, default: None - The colorbar range. If *None*, suitable min/max values are - automatically chosen by the `~.Normalize` instance (defaults to - the respective min/max values of *C* in case of the default linear - scaling). - It is deprecated to use *vmin*/*vmax* when *norm* is given. + %(vmin_vmax_doc)s edgecolors : {'none', None, 'face', color, color sequence}, optional The color of the edges. Defaults to 'none'. Possible values: @@ -6079,7 +6113,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, shading : {'flat', 'nearest', 'gouraud', 'auto'}, optional The fill style for the quadrilateral; defaults to - 'flat' or :rc:`pcolor.shading`. Possible values: + :rc:`pcolor.shading`. Possible values: - 'flat': A solid color is used for each quad. The color of the quad (i, j), (i+1, j), (i, j+1), (i+1, j+1) is given by @@ -6104,7 +6138,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, snap : bool, default: False Whether to snap the mesh to pixel boundaries. - rasterized: bool, optional + rasterized : bool, optional Rasterize the pcolormesh when drawing vector graphics. This can speed up rendering and produce smaller files for large data sets. See also :doc:`/gallery/misc/rasterization_demo`. @@ -6115,11 +6149,14 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Additionally, the following arguments are allowed. They are passed along to the `~matplotlib.collections.QuadMesh` constructor: - %(QuadMesh_kwdoc)s + %(QuadMesh:kwdoc)s See Also -------- @@ -6176,31 +6213,25 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, """ if shading is None: - shading = rcParams['pcolor.shading'] + shading = mpl.rcParams['pcolor.shading'] shading = shading.lower() kwargs.setdefault('edgecolors', 'none') X, Y, C, shading = self._pcolorargs('pcolormesh', *args, shading=shading, kwargs=kwargs) - Ny, Nx = X.shape - X = X.ravel() - Y = Y.ravel() - - # convert to one dimensional arrays - C = C.ravel() - coords = np.column_stack((X, Y)).astype(float, copy=False) - collection = mcoll.QuadMesh(Nx - 1, Ny - 1, coords, - antialiased=antialiased, shading=shading, - **kwargs) - snap = kwargs.get('snap', rcParams['pcolormesh.snap']) - collection.set_snap(snap) - collection.set_alpha(alpha) - collection.set_array(C) - collection.set_cmap(cmap) - collection.set_norm(norm) + coords = np.stack([X, Y], axis=-1) + # convert to one dimensional array, except for 3D RGB(A) arrays + if C.ndim != 3: + C = C.ravel() + + kwargs.setdefault('snap', mpl.rcParams['pcolormesh.snap']) + + collection = mcoll.QuadMesh( + coords, antialiased=antialiased, shading=shading, + array=C, cmap=cmap, norm=norm, alpha=alpha, **kwargs) collection._scale_norm(norm, vmin, vmax) - self.grid(False) + coords = coords.reshape(-1, 2) # flatten the grid structure; keep x, y # Transform from native to data coordinates? t = collection._transform @@ -6224,7 +6255,7 @@ def pcolormesh(self, *args, alpha=None, norm=None, cmap=None, vmin=None, return collection @_preprocess_data() - @docstring.dedent_interpd + @_docstring.dedent_interpd def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, **kwargs): """ @@ -6247,15 +6278,15 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, - It supports only flat shading (no outlines) - It lacks support for log scaling of the axes. - - It does not have a have a pyplot wrapper. + - It does not have a pyplot wrapper. Parameters ---------- C : array-like The image data. Supported array shapes are: - - (M, N): an image with scalar data. The data is visualized - using a colormap. + - (M, N): an image with scalar data. Color-mapping is controlled + by *cmap*, *norm*, *vmin*, and *vmax*. - (M, N, 3): an image with RGB values (0-1 float or 0-255 int). - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int), i.e. including transparency. @@ -6298,21 +6329,17 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, These arguments can only be passed positionally. - cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` - A Colormap instance or registered colormap name. The colormap - maps the *C* values to colors. + %(cmap_doc)s + + This parameter is ignored if *C* is RGB(A). + + %(norm_doc)s - norm : `~matplotlib.colors.Normalize`, optional - The Normalize instance scales the data values to the canonical - colormap range [0, 1] for mapping to colors. By default, the data - range is mapped to the colorbar range using linear scaling. + This parameter is ignored if *C* is RGB(A). - vmin, vmax : float, default: None - The colorbar range. If *None*, suitable min/max values are - automatically chosen by the `~.Normalize` instance (defaults to - the respective min/max values of *C* in case of the default linear - scaling). - It is deprecated to use *vmin*/*vmax* when *norm* is given. + %(vmin_vmax_doc)s + + This parameter is ignored if *C* is RGB(A). alpha : float, default: None The alpha blending value, between 0 (transparent) and 1 (opaque). @@ -6331,13 +6358,12 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Supported additional parameters depend on the type of grid. See return types of *image* for further description. - - Notes - ----- - .. [notes section required to get data note injection right] """ C = args[-1] @@ -6371,14 +6397,10 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, if style == "quadmesh": # data point in each cell is value at lower left corner coords = np.stack([x, y], axis=-1) - if np.ndim(C) == 2: - qm_kwargs = {"array": np.ma.ravel(C)} - elif np.ndim(C) == 3: - qm_kwargs = {"color": np.ma.reshape(C, (-1, C.shape[-1]))} - else: + if np.ndim(C) not in {2, 3}: raise ValueError("C must be 2D or 3D") collection = mcoll.QuadMesh( - nc, nr, coords, **qm_kwargs, + coords, array=C, alpha=alpha, cmap=cmap, norm=norm, antialiased=False, edgecolors="none") self.add_collection(collection, autolim=False) @@ -6389,7 +6411,7 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, extent = xl, xr, yb, yt = x[0], x[-1], y[0], y[-1] if style == "image": im = mimage.AxesImage( - self, cmap, norm, + self, cmap=cmap, norm=norm, data=C, alpha=alpha, extent=extent, interpolation='nearest', origin='lower', **kwargs) @@ -6415,32 +6437,36 @@ def pcolorfast(self, *args, alpha=None, norm=None, cmap=None, vmin=None, return ret @_preprocess_data() + @_docstring.dedent_interpd def contour(self, *args, **kwargs): - kwargs['filled'] = False - contours = mcontour.QuadContourSet(self, *args, **kwargs) - self._request_autoscale_view() - return contours - contour.__doc__ = """ + """ Plot contour lines. Call signature:: contour([X, Y,] Z, [levels], **kwargs) - """ + mcontour.QuadContourSet._contour_doc - - @_preprocess_data() - def contourf(self, *args, **kwargs): - kwargs['filled'] = True + %(contour_doc)s + """ + kwargs['filled'] = False contours = mcontour.QuadContourSet(self, *args, **kwargs) self._request_autoscale_view() return contours - contourf.__doc__ = """ + + @_preprocess_data() + @_docstring.dedent_interpd + def contourf(self, *args, **kwargs): + """ Plot filled contours. Call signature:: contourf([X, Y,] Z, [levels], **kwargs) - """ + mcontour.QuadContourSet._contour_doc + %(contour_doc)s + """ + kwargs['filled'] = True + contours = mcontour.QuadContourSet(self, *args, **kwargs) + self._request_autoscale_view() + return contours def clabel(self, CS, levels=None, **kwargs): """ @@ -6450,7 +6476,7 @@ def clabel(self, CS, levels=None, **kwargs): Parameters ---------- - CS : `~.ContourSet` instance + CS : `.ContourSet` instance Line contours to label. levels : array-like, optional @@ -6470,23 +6496,33 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, orientation='vertical', rwidth=None, log=False, color=None, label=None, stacked=False, **kwargs): """ - Plot a histogram. + Compute and plot a histogram. - Compute and draw the histogram of *x*. The return value is a tuple - (*n*, *bins*, *patches*) or ([*n0*, *n1*, ...], *bins*, [*patches0*, - *patches1*, ...]) if the input contains multiple data. See the - documentation of the *weights* parameter to draw a histogram of - already-binned data. + This method uses `numpy.histogram` to bin the data in *x* and count the + number of values in each bin, then draws the distribution either as a + `.BarContainer` or `.Polygon`. The *bins*, *range*, *density*, and + *weights* parameters are forwarded to `numpy.histogram`. - Multiple data can be provided via *x* as a list of datasets - of potentially different length ([*x0*, *x1*, ...]), or as - a 2D ndarray in which each column is a dataset. Note that - the ndarray form is transposed relative to the list form. + If the data has already been binned and counted, use `~.bar` or + `~.stairs` to plot the distribution:: - Masked arrays are not supported. + counts, bins = np.histogram(x) + plt.stairs(counts, bins) - The *bins*, *range*, *weights*, and *density* parameters behave as in - `numpy.histogram`. + Alternatively, plot pre-computed bins and counts using ``hist()`` by + treating each bin as a single point with a weight equal to its count:: + + plt.hist(bins[:-1], bins, weights=counts) + + The data input *x* can be a singular array, a list of datasets of + potentially different lengths ([*x0*, *x1*, ...]), or a 2D ndarray in + which each column is a dataset. Note that the ndarray form is + transposed relative to the list form. If the input is an array, then + the return value is a tuple (*n*, *bins*, *patches*); if the input is a + sequence of arrays, then the return value is a tuple + ([*n0*, *n1*, ...], *bins*, [*patches0*, *patches1*, ...]). + + Masked arrays are not supported. Parameters ---------- @@ -6540,15 +6576,6 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, normalized, so that the integral of the density over the range remains 1. - This parameter can be used to draw a histogram of data that has - already been binned, e.g. using `numpy.histogram` (by treating each - bin as a single point with a weight equal to its count) :: - - counts, bins = np.histogram(data) - plt.hist(bins[:-1], bins, weights=counts) - - (or you may alternatively use `~.bar()`). - cumulative : bool or -1, default: False If ``True``, then a histogram is computed where each bin gives the counts in that bin plus all bins for smaller values. The last bin @@ -6563,7 +6590,7 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, equals 1. bottom : array-like, scalar, or None, default: None - Location of the bottom of each bin, ie. bins are drawn from + Location of the bottom of each bin, i.e. bins are drawn from ``bottom`` to ``bottom + hist(x, bins)`` If a scalar, the bottom of each bin is shifted by the same amount. If an array, each bin is shifted independently and the length of bottom must match the @@ -6636,18 +6663,22 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs `~matplotlib.patches.Patch` properties See Also -------- - hist2d : 2D histograms + hist2d : 2D histogram with rectangular bins + hexbin : 2D histogram with hexagonal bins Notes ----- - For large numbers of bins (>1000), 'step' and 'stepfilled' can be - significantly faster than 'bar' and 'barstacked'. - + For large numbers of bins (>1000), plotting can be significantly faster + if *histtype* is set to 'step' or 'stepfilled' rather than 'bar' or + 'barstacked'. """ # Avoid shadowing the builtin. bin_range = range @@ -6657,7 +6688,7 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, x = [x] if bins is None: - bins = rcParams['hist.bins'] + bins = mpl.rcParams['hist.bins'] # Validate string inputs here to avoid cluttering subsequent code. _api.check_in_list(['bar', 'barstacked', 'step', 'stepfilled'], @@ -6708,13 +6739,13 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, input_empty = False if color is None: - color = [self._get_lines.get_next_color() for i in range(nx)] + colors = [self._get_lines.get_next_color() for i in range(nx)] else: - color = mcolors.to_rgba_array(color) - if len(color) != nx: + colors = mcolors.to_rgba_array(color) + if len(colors) != nx: raise ValueError(f"The 'color' keyword argument must have one " f"color per dataset, but {nx} datasets and " - f"{len(color)} colors were provided") + f"{len(colors)} colors were provided") hist_kwargs = dict() @@ -6759,6 +6790,7 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, m, bins = np.histogram(x[i], bins, weights=w[i], **hist_kwargs) tops.append(m) tops = np.array(tops, float) # causes problems later if it's an int + bins = np.array(bins, float) # causes problems if float16 if stacked: tops = tops.cumsum(axis=0) # If a stacked density plot, normalize so the area of all the @@ -6783,7 +6815,7 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, if rwidth is not None: dr = np.clip(rwidth, 0, 1) elif (len(tops) > 1 and - ((not stacked) or rcParams['_internal.classic_mode'])): + ((not stacked) or mpl.rcParams['_internal.classic_mode'])): dr = 0.8 else: dr = 1.0 @@ -6808,19 +6840,19 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, _barfunc = self.bar bottom_kwarg = 'bottom' - for m, c in zip(tops, color): + for top, color in zip(tops, colors): if bottom is None: - bottom = np.zeros(len(m)) + bottom = np.zeros(len(top)) if stacked: - height = m - bottom + height = top - bottom else: - height = m + height = top bars = _barfunc(bins[:-1]+boffset, height, width, align='center', log=log, - color=c, **{bottom_kwarg: bottom}) + color=color, **{bottom_kwarg: bottom}) patches.append(bars) if stacked: - bottom = m + bottom = top boffset += dw # Remove stickies from all bars but the lowest ones, as otherwise # margin expansion would be unable to cross the stickies in the @@ -6859,12 +6891,12 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, fill = (histtype == 'stepfilled') xvals, yvals = [], [] - for m in tops: + for top in tops: if stacked: # top of the previous polygon becomes the bottom y[2*len(bins)-1:] = y[1:2*len(bins)-1][::-1] # set the top of this polygon - y[1:2*len(bins)-1:2] = y[2:2*len(bins):2] = m + bottom + y[1:2*len(bins)-1:2] = y[2:2*len(bins):2] = top + bottom # The starting point of the polygon has not yet been # updated. So far only the endpoint was adjusted. This @@ -6884,12 +6916,12 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, # add patches in reverse order so that when stacking, # items lower in the stack are plotted on top of # items higher in the stack - for x, y, c in reversed(list(zip(xvals, yvals, color))): + for x, y, color in reversed(list(zip(xvals, yvals, colors))): patches.append(self.fill( x[:split], y[:split], closed=True if fill else None, - facecolor=c, - edgecolor=None if fill else c, + facecolor=color, + edgecolor=None if fill else color, fill=fill if fill else None, zorder=None if fill else mlines.Line2D.zorder)) for patch_list in patches: @@ -6908,11 +6940,11 @@ def hist(self, x, bins=None, range=None, density=False, weights=None, for patch, lbl in itertools.zip_longest(patches, labels): if patch: p = patch[0] - p.update(kwargs) + p._internal_update(kwargs) if lbl is not None: p.set_label(lbl) for p in patch[1:]: - p.update(kwargs) + p._internal_update(kwargs) p.set_label('_nolegend_') if nx == 1: @@ -6957,6 +6989,9 @@ def stairs(self, values, edges=None, *, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs `~matplotlib.patches.StepPatch` properties @@ -6967,7 +7002,7 @@ def stairs(self, values, edges=None, *, else: _color = self._get_lines.get_next_color() if fill: - kwargs.setdefault('edgecolor', 'none') + kwargs.setdefault('linewidth', 0) kwargs.setdefault('facecolor', _color) else: kwargs.setdefault('edgecolor', _color) @@ -6997,7 +7032,7 @@ def stairs(self, values, edges=None, *, return patch @_preprocess_data(replace_names=["x", "y", "weights"]) - @docstring.dedent_interpd + @_docstring.dedent_interpd def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, cmin=None, cmax=None, **kwargs): """ @@ -7049,27 +7084,25 @@ def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, histogrammed along the first dimension and values in y are histogrammed along the second dimension. xedges : 1D array - The bin edges along the x axis. + The bin edges along the x-axis. yedges : 1D array - The bin edges along the y axis. + The bin edges along the y-axis. image : `~.matplotlib.collections.QuadMesh` Other Parameters ---------------- - cmap : Colormap or str, optional - A `.colors.Colormap` instance. If not set, use rc settings. + %(cmap_doc)s - norm : Normalize, optional - A `.colors.Normalize` instance is used to - scale luminance data to ``[0, 1]``. If not set, defaults to - `.colors.Normalize()`. + %(norm_doc)s - vmin/vmax : None or scalar, optional - Arguments passed to the `~.colors.Normalize` instance. + %(vmin_vmax_doc)s alpha : ``0 <= scalar <= 1`` or ``None``, optional The alpha blending value. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Additional parameters are passed along to the `~.Axes.pcolormesh` method and `~matplotlib.collections.QuadMesh` @@ -7078,6 +7111,7 @@ def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, See Also -------- hist : 1D histogram plotting + hexbin : 2D histogram with hexagonal bins Notes ----- @@ -7105,7 +7139,7 @@ def hist2d(self, x, y, bins=10, range=None, density=False, weights=None, return h, xedges, yedges, pc @_preprocess_data(replace_names=["x"]) - @docstring.dedent_interpd + @_docstring.dedent_interpd def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, return_line=None, **kwargs): @@ -7157,10 +7191,13 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Keyword arguments control the `.Line2D` properties: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -7201,12 +7238,9 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, self.set_xlabel('Frequency') self.set_ylabel('Power Spectral Density (%s)' % psd_units) self.grid(True) - vmin, vmax = self.viewLim.intervaly - intv = vmax - vmin - logi = int(np.log10(intv)) - if logi == 0: - logi = .1 - step = 10 * logi + + vmin, vmax = self.get_ybound() + step = max(10 * int(np.log10(vmax - vmin)), 1) ticks = np.arange(math.floor(vmin), math.ceil(vmax) + 1, step) self.set_yticks(ticks) @@ -7216,7 +7250,7 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, return pxx, freqs, line @_preprocess_data(replace_names=["x", "y"], label_namer="y") - @docstring.dedent_interpd + @_docstring.dedent_interpd def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, return_line=None, **kwargs): @@ -7270,10 +7304,13 @@ def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Keyword arguments control the `.Line2D` properties: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -7303,11 +7340,9 @@ def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, self.set_xlabel('Frequency') self.set_ylabel('Cross Spectrum Magnitude (dB)') self.grid(True) - vmin, vmax = self.viewLim.intervaly - - intv = vmax - vmin - step = 10 * int(np.log10(intv)) + vmin, vmax = self.get_ybound() + step = max(10 * int(np.log10(vmax - vmin)), 1) ticks = np.arange(math.floor(vmin), math.ceil(vmax) + 1, step) self.set_yticks(ticks) @@ -7317,7 +7352,7 @@ def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, return pxy, freqs, line @_preprocess_data(replace_names=["x"]) - @docstring.dedent_interpd + @_docstring.dedent_interpd def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, scale=None, **kwargs): @@ -7360,10 +7395,13 @@ def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Keyword arguments control the `.Line2D` properties: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -7400,7 +7438,7 @@ def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, line @_preprocess_data(replace_names=["x"]) - @docstring.dedent_interpd + @_docstring.dedent_interpd def angle_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): """ @@ -7437,10 +7475,13 @@ def angle_spectrum(self, x, Fs=None, Fc=None, window=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Keyword arguments control the `.Line2D` properties: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -7466,7 +7507,7 @@ def angle_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] @_preprocess_data(replace_names=["x"]) - @docstring.dedent_interpd + @_docstring.dedent_interpd def phase_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): """ @@ -7503,10 +7544,13 @@ def phase_spectrum(self, x, Fs=None, Fc=None, window=None, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Keyword arguments control the `.Line2D` properties: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See Also -------- @@ -7532,15 +7576,14 @@ def phase_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] @_preprocess_data(replace_names=["x", "y"]) - @docstring.dedent_interpd + @_docstring.dedent_interpd def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, window=mlab.window_hanning, noverlap=0, pad_to=None, sides='default', scale_by_freq=None, **kwargs): r""" Plot the coherence between *x* and *y*. - Plot the coherence between *x* and *y*. Coherence is the - normalized cross spectral density: + Coherence is the normalized cross spectral density: .. math:: @@ -7570,10 +7613,13 @@ def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Keyword arguments control the `.Line2D` properties: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s References ---------- @@ -7582,7 +7628,8 @@ def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, """ cxy, freqs = mlab.cohere(x=x, y=y, NFFT=NFFT, Fs=Fs, detrend=detrend, window=window, noverlap=noverlap, - scale_by_freq=scale_by_freq) + scale_by_freq=scale_by_freq, sides=sides, + pad_to=pad_to) freqs += Fc self.plot(freqs, cxy, **kwargs) @@ -7593,7 +7640,7 @@ def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, return cxy, freqs @_preprocess_data(replace_names=["x"]) - @docstring.dedent_interpd + @_docstring.dedent_interpd def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, cmap=None, xextent=None, pad_to=None, sides=None, @@ -7630,7 +7677,7 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, scale : {'default', 'linear', 'dB'} The scaling of the values in the *spec*. 'linear' is no scaling. 'dB' returns the values in dB scale. When *mode* is 'psd', - this is dB power (10 * log10). Otherwise this is dB amplitude + this is dB power (10 * log10). Otherwise, this is dB amplitude (20 * log10). 'default' is 'dB' if *mode* is 'psd' or 'magnitude' and 'linear' otherwise. This must be 'linear' if *mode* is 'angle' or 'phase'. @@ -7648,6 +7695,9 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, right border of the last bin. Note that for *noverlap>0* the width of the bins is smaller than those of the segments. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Additional keyword arguments are passed on to `~.axes.Axes.imshow` which makes the specgram image. The origin keyword argument @@ -7724,7 +7774,7 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, else: Z = 20. * np.log10(spec) else: - raise ValueError('Unknown scale %s', scale) + raise ValueError(f'Unknown scale {scale!r}') Z = np.flipud(Z) @@ -7737,8 +7787,7 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, extent = xmin, xmax, freqs[0], freqs[-1] if 'origin' in kwargs: - raise TypeError("specgram() got an unexpected keyword argument " - "'origin'") + raise _api.kwarg_error("specgram", "origin") im = self.imshow(Z, cmap, extent=extent, vmin=vmin, vmax=vmax, origin='upper', **kwargs) @@ -7746,7 +7795,7 @@ def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, return spec, freqs, t, im - @docstring.dedent_interpd + @_docstring.dedent_interpd def spy(self, Z, precision=0, marker=None, markersize=None, aspect='equal', origin="upper", **kwargs): """ @@ -7823,7 +7872,7 @@ def spy(self, Z, precision=0, marker=None, markersize=None, For the marker style, you can pass any `.Line2D` property except for *linestyle*: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s """ if marker is None and markersize is None and hasattr(Z, 'tocoo'): marker = 's' @@ -7836,8 +7885,7 @@ def spy(self, Z, precision=0, marker=None, markersize=None, kwargs['cmap'] = mcolors.ListedColormap(['w', 'k'], name='binary') if 'interpolation' in kwargs: - raise TypeError( - "spy() got an unexpected keyword argument 'interpolation'") + raise _api.kwarg_error("spy", "interpolation") if 'norm' not in kwargs: kwargs['norm'] = mcolors.NoNorm() ret = self.imshow(mask, interpolation='nearest', @@ -7862,8 +7910,7 @@ def spy(self, Z, precision=0, marker=None, markersize=None, if markersize is None: markersize = 10 if 'linestyle' in kwargs: - raise TypeError( - "spy() got an unexpected keyword argument 'linestyle'") + raise _api.kwarg_error("spy", "linestyle") ret = mlines.Line2D( x, y, linestyle='None', marker=marker, markersize=markersize, **kwargs) @@ -7878,7 +7925,7 @@ def spy(self, Z, precision=0, marker=None, markersize=None, self.title.set_y(1.05) if origin == "upper": self.xaxis.tick_top() - else: + else: # lower self.xaxis.tick_bottom() self.xaxis.set_ticks_position('both') self.xaxis.set_major_locator( @@ -7990,8 +8037,12 @@ def violinplot(self, dataset, positions=None, vert=True, widths=0.5, The method used to calculate the estimator bandwidth. This can be 'scott', 'silverman', a scalar constant or a callable. If a scalar, this will be used directly as `kde.factor`. If a - callable, it should take a `GaussianKDE` instance as its only - parameter and return a scalar. If None (default), 'scott' is used. + callable, it should take a `matplotlib.mlab.GaussianKDE` instance as + its only parameter and return a scalar. If None (default), 'scott' + is used. + + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER Returns ------- @@ -8025,8 +8076,8 @@ def violinplot(self, dataset, positions=None, vert=True, widths=0.5, """ def _kde_method(X, coords): - if hasattr(X, 'values'): # support pandas.Series - X = X.values + # Unpack in case of e.g. Pandas or xarray object + X = cbook._unpack_to_numpy(X) # fallback gracefully if the vector contains only one value if np.all(X[0] == X): return (X[0] == coords).astype(float) @@ -8124,7 +8175,6 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5, - ``cquantiles``: A `~.collections.LineCollection` instance created to identify the quantiles values of each of the violin's distribution. - """ # Statistical quantities to be plotted on the violins @@ -8132,10 +8182,11 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5, mins = [] maxes = [] medians = [] - quantiles = np.asarray([]) + quantiles = [] - # Collections to be returned - artists = {} + qlens = [] # Number of quantiles in each dataset. + + artists = {} # Collections to be returned N = len(vpstats) datashape_message = ("List of violinplot statistics and `{0}` " @@ -8153,84 +8204,56 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5, elif len(widths) != N: raise ValueError(datashape_message.format("widths")) - # Calculate ranges for statistics lines - pmins = -0.25 * np.array(widths) + positions - pmaxes = 0.25 * np.array(widths) + positions + # Calculate ranges for statistics lines (shape (2, N)). + line_ends = [[-0.25], [0.25]] * np.array(widths) + positions + + # Colors. + if mpl.rcParams['_internal.classic_mode']: + fillcolor = 'y' + linecolor = 'r' + else: + fillcolor = linecolor = self._get_lines.get_next_color() # Check whether we are rendering vertically or horizontally if vert: fill = self.fill_betweenx - perp_lines = self.hlines - par_lines = self.vlines + perp_lines = functools.partial(self.hlines, colors=linecolor) + par_lines = functools.partial(self.vlines, colors=linecolor) else: fill = self.fill_between - perp_lines = self.vlines - par_lines = self.hlines - - if rcParams['_internal.classic_mode']: - fillcolor = 'y' - edgecolor = 'r' - else: - fillcolor = edgecolor = self._get_lines.get_next_color() + perp_lines = functools.partial(self.vlines, colors=linecolor) + par_lines = functools.partial(self.hlines, colors=linecolor) # Render violins bodies = [] for stats, pos, width in zip(vpstats, positions, widths): - # The 0.5 factor reflects the fact that we plot from v-p to - # v+p + # The 0.5 factor reflects the fact that we plot from v-p to v+p. vals = np.array(stats['vals']) vals = 0.5 * width * vals / vals.max() - bodies += [fill(stats['coords'], - -vals + pos, - vals + pos, - facecolor=fillcolor, - alpha=0.3)] + bodies += [fill(stats['coords'], -vals + pos, vals + pos, + facecolor=fillcolor, alpha=0.3)] means.append(stats['mean']) mins.append(stats['min']) maxes.append(stats['max']) medians.append(stats['median']) - q = stats.get('quantiles') - if q is not None: - # If exist key quantiles, assume it's a list of floats - quantiles = np.concatenate((quantiles, q)) + q = stats.get('quantiles') # a list of floats, or None + if q is None: + q = [] + quantiles.extend(q) + qlens.append(len(q)) artists['bodies'] = bodies - # Render means - if showmeans: - artists['cmeans'] = perp_lines(means, pmins, pmaxes, - colors=edgecolor) - - # Render extrema - if showextrema: - artists['cmaxes'] = perp_lines(maxes, pmins, pmaxes, - colors=edgecolor) - artists['cmins'] = perp_lines(mins, pmins, pmaxes, - colors=edgecolor) - artists['cbars'] = par_lines(positions, mins, maxes, - colors=edgecolor) - - # Render medians - if showmedians: - artists['cmedians'] = perp_lines(medians, - pmins, - pmaxes, - colors=edgecolor) - - # Render quantile values - if quantiles.size > 0: - # Recalculate ranges for statistics lines for quantiles. - # ppmins are the left end of quantiles lines - ppmins = np.asarray([]) - # pmaxes are the right end of quantiles lines - ppmaxs = np.asarray([]) - for stats, cmin, cmax in zip(vpstats, pmins, pmaxes): - q = stats.get('quantiles') - if q is not None: - ppmins = np.concatenate((ppmins, [cmin] * np.size(q))) - ppmaxs = np.concatenate((ppmaxs, [cmax] * np.size(q))) - # Start rendering - artists['cquantiles'] = perp_lines(quantiles, ppmins, ppmaxs, - colors=edgecolor) + if showmeans: # Render means + artists['cmeans'] = perp_lines(means, *line_ends) + if showextrema: # Render extrema + artists['cmaxes'] = perp_lines(maxes, *line_ends) + artists['cmins'] = perp_lines(mins, *line_ends) + artists['cbars'] = par_lines(positions, mins, maxes) + if showmedians: # Render medians + artists['cmedians'] = perp_lines(medians, *line_ends) + if quantiles: # Render quantiles: each width is repeated qlen times. + artists['cquantiles'] = perp_lines( + quantiles, *np.repeat(line_ends, qlens, axis=1)) return artists @@ -8238,7 +8261,7 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5, table = mtable.table - # args can by either Y or y1, y2, ... and all should be replaced + # args can be either Y or y1, y2, ... and all should be replaced stackplot = _preprocess_data()(mstack.stackplot) streamplot = _preprocess_data( @@ -8248,3 +8271,13 @@ def violin(self, vpstats, positions=None, vert=True, widths=0.5, tricontourf = mtri.tricontourf tripcolor = mtri.tripcolor triplot = mtri.triplot + + def _get_aspect_ratio(self): + """ + Convenience method to calculate the aspect ratio of the axes in + the display coordinate system. + """ + figure_size = self.get_figure().get_size_inches() + ll, ur = self.get_position() * figure_size + width, height = ur - ll + return height / (width * self.get_data_ratio()) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 0ded09888d56..fe1fac98eeeb 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -1,4 +1,4 @@ -from collections import OrderedDict +from collections.abc import Iterable, Sequence from contextlib import ExitStack import functools import inspect @@ -11,22 +11,23 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, cbook +from matplotlib import _api, cbook, _docstring, offsetbox +import matplotlib.artist as martist +import matplotlib.axis as maxis from matplotlib.cbook import _OrderedSet, _check_1d, index_of -from matplotlib import docstring +import matplotlib.collections as mcoll import matplotlib.colors as mcolors +import matplotlib.font_manager as font_manager +from matplotlib.gridspec import SubplotSpec +import matplotlib.image as mimage import matplotlib.lines as mlines import matplotlib.patches as mpatches -import matplotlib.artist as martist -import matplotlib.transforms as mtransforms -import matplotlib.ticker as mticker -import matplotlib.axis as maxis +from matplotlib.rcsetup import cycler, validate_axisbelow import matplotlib.spines as mspines -import matplotlib.font_manager as font_manager +import matplotlib.table as mtable import matplotlib.text as mtext -import matplotlib.image as mimage -import matplotlib.path as mpath -from matplotlib.rcsetup import cycler, validate_axisbelow +import matplotlib.ticker as mticker +import matplotlib.transforms as mtransforms _log = logging.getLogger(__name__) @@ -45,7 +46,7 @@ class _axis_method_wrapper: The docstring of ``get_foo`` is built by replacing "this Axis" by "the {attr_name}" (i.e., "the xaxis", "the yaxis") in the wrapped method's - dedented docstring; additional replacements can by given in *doc_sub*. + dedented docstring; additional replacements can be given in *doc_sub*. """ def __init__(self, attr_name, method_name, *, doc_sub=None): @@ -94,16 +95,16 @@ def wrapper(self, *args, **kwargs): class _TransformedBoundsLocator: """ - Axes locator for `.Axes.inset_axes` and similarly positioned axes. + Axes locator for `.Axes.inset_axes` and similarly positioned Axes. The locator is a callable object used in `.Axes.set_aspect` to compute the - axes location depending on the renderer. + Axes location depending on the renderer. """ def __init__(self, bounds, transform): """ *bounds* (a ``[l, b, w, h]`` rectangle) and *transform* together - specify the position of the inset axes. + specify the position of the inset Axes. """ self._bounds = bounds self._transform = transform @@ -117,7 +118,7 @@ def __call__(self, ax, renderer): self._transform - ax.figure.transSubfigure) -def _process_plot_format(fmt): +def _process_plot_format(fmt, *, ambiguous_fmt_datakey=False): """ Convert a MATLAB style color/line style format string to a (*linestyle*, *marker*, *color*) tuple. @@ -162,31 +163,31 @@ def _process_plot_format(fmt): except ValueError: pass # No, not just a color. + errfmt = ("{!r} is neither a data key nor a valid format string ({})" + if ambiguous_fmt_datakey else + "{!r} is not a valid format string ({})") + i = 0 while i < len(fmt): c = fmt[i] if fmt[i:i+2] in mlines.lineStyles: # First, the two-char styles. if linestyle is not None: - raise ValueError( - 'Illegal format string "%s"; two linestyle symbols' % fmt) + raise ValueError(errfmt.format(fmt, "two linestyle symbols")) linestyle = fmt[i:i+2] i += 2 elif c in mlines.lineStyles: if linestyle is not None: - raise ValueError( - 'Illegal format string "%s"; two linestyle symbols' % fmt) + raise ValueError(errfmt.format(fmt, "two linestyle symbols")) linestyle = c i += 1 elif c in mlines.lineMarkers: if marker is not None: - raise ValueError( - 'Illegal format string "%s"; two marker symbols' % fmt) + raise ValueError(errfmt.format(fmt, "two marker symbols")) marker = c i += 1 elif c in mcolors.get_named_colors_mapping(): if color is not None: - raise ValueError( - 'Illegal format string "%s"; two color symbols' % fmt) + raise ValueError(errfmt.format(fmt, "two color symbols")) color = c i += 1 elif c == 'C' and i < len(fmt) - 1: @@ -195,7 +196,7 @@ def _process_plot_format(fmt): i += 2 else: raise ValueError( - 'Unrecognized character %c in format string' % c) + errfmt.format(fmt, f"unrecognized character {c!r}")) if linestyle is None and marker is None: linestyle = mpl.rcParams['lines.linestyle'] @@ -221,7 +222,7 @@ class _process_plot_var_args: def __init__(self, axes, command='plot'): self.axes = axes self.command = command - self.set_prop_cycle() + self.set_prop_cycle(None) def __getstate__(self): # note: it is not possible to pickle a generator (and thus a cycler). @@ -229,26 +230,20 @@ def __getstate__(self): def __setstate__(self, state): self.__dict__ = state.copy() - self.set_prop_cycle() - - def set_prop_cycle(self, *args, **kwargs): - # Can't do `args == (None,)` as that crashes cycler. - if not (args or kwargs) or (len(args) == 1 and args[0] is None): - prop_cycler = mpl.rcParams['axes.prop_cycle'] - else: - prop_cycler = cycler(*args, **kwargs) + self.set_prop_cycle(None) - self.prop_cycler = itertools.cycle(prop_cycler) - # This should make a copy - self._prop_keys = prop_cycler.keys + def set_prop_cycle(self, cycler): + if cycler is None: + cycler = mpl.rcParams['axes.prop_cycle'] + self.prop_cycler = itertools.cycle(cycler) + self._prop_keys = cycler.keys # This should make a copy def __call__(self, *args, data=None, **kwargs): self.axes._process_unit_info(kwargs=kwargs) for pos_only in "xy": if pos_only in kwargs: - raise TypeError("{} got an unexpected keyword argument {!r}" - .format(self.command, pos_only)) + raise _api.kwarg_error(self.command, pos_only) if not args: return @@ -297,6 +292,7 @@ def __call__(self, *args, data=None, **kwargs): kwargs["label"] = mpl._label_from_arg( replaced[label_namer_idx], args[label_namer_idx]) args = replaced + ambiguous_fmt_datakey = data is not None and len(args) == 2 if len(args) >= 4 and not cbook.is_scalar_or_string( kwargs.get("label")): @@ -312,7 +308,8 @@ def __call__(self, *args, data=None, **kwargs): if args and isinstance(args[0], str): this += args[0], args = args[1:] - yield from self._plot_args(this, kwargs) + yield from self._plot_args( + this, kwargs, ambiguous_fmt_datakey=ambiguous_fmt_datakey) def get_next_color(self): """Return the next color in the cycle.""" @@ -406,7 +403,8 @@ def _makefill(self, x, y, kw, kwargs): seg.set(**kwargs) return seg, kwargs - def _plot_args(self, tup, kwargs, return_kwargs=False): + def _plot_args(self, tup, kwargs, *, + return_kwargs=False, ambiguous_fmt_datakey=False): """ Process the arguments of ``plot([x], y, [fmt], **kwargs)`` calls. @@ -433,9 +431,13 @@ def _plot_args(self, tup, kwargs, return_kwargs=False): The keyword arguments passed to ``plot()``. return_kwargs : bool - If true, return the effective keyword arguments after label + Whether to also return the effective keyword arguments after label unpacking as well. + ambiguous_fmt_datakey : bool + Whether the format string in *tup* could also have been a + misspelled data key. + Returns ------- result @@ -449,7 +451,8 @@ def _plot_args(self, tup, kwargs, return_kwargs=False): if len(tup) > 1 and isinstance(tup[-1], str): # xy is tup with fmt stripped (could still be (y,) only) *xy, fmt = tup - linestyle, marker, color = _process_plot_format(fmt) + linestyle, marker, color = _process_plot_format( + fmt, ambiguous_fmt_datakey=ambiguous_fmt_datakey) elif len(tup) == 3: raise ValueError('third arg must be a format string') else: @@ -517,6 +520,8 @@ def _plot_args(self, tup, kwargs, return_kwargs=False): ncx, ncy = x.shape[1], y.shape[1] if ncx > 1 and ncy > 1 and ncx != ncy: raise ValueError(f"x has {ncx} columns but y has {ncy} columns") + if ncx == 0 or ncy == 0: + return [] label = kwargs.get('label') n_datasets = max(ncx, ncy) @@ -539,20 +544,35 @@ def _plot_args(self, tup, kwargs, return_kwargs=False): return [l[0] for l in result] -@cbook._define_aliases({"facecolor": ["fc"]}) +@_api.define_aliases({"facecolor": ["fc"]}) class _AxesBase(martist.Artist): name = "rectilinear" - _shared_x_axes = cbook.Grouper() - _shared_y_axes = cbook.Grouper() + # axis names are the prefixes for the attributes that contain the + # respective axis; e.g. 'x' <-> self.xaxis, containing an XAxis. + # Note that PolarAxes uses these attributes as well, so that we have + # 'x' <-> self.xaxis, containing a ThetaAxis. In particular we do not + # have 'theta' in _axis_names. + # In practice, this is ('x', 'y') for all 2D Axes and ('x', 'y', 'z') + # for Axes3D. + _axis_names = ("x", "y") + _shared_axes = {name: cbook.Grouper() for name in _axis_names} _twinned_axes = cbook.Grouper() + _subclass_uses_cla = False + + @property + def _axis_map(self): + """A mapping of axis names, e.g. 'x', to `Axis` instances.""" + return {name: getattr(self, f"{name}axis") + for name in self._axis_names} + def __str__(self): return "{0}({1[0]:g},{1[1]:g};{1[2]:g}x{1[3]:g})".format( type(self).__name__, self._position.bounds) - @_api.make_keyword_only("3.4", "facecolor") - def __init__(self, fig, rect, + def __init__(self, fig, + *args, facecolor=None, # defaults to rc axes.facecolor frameon=True, sharex=None, # use Axes instance's xaxis info @@ -564,32 +584,41 @@ def __init__(self, fig, rect, **kwargs ): """ - Build an axes in a figure. + Build an Axes in a figure. Parameters ---------- fig : `~matplotlib.figure.Figure` - The axes is build in the `.Figure` *fig*. + The Axes is built in the `.Figure` *fig*. + + *args + ``*args`` can be a single ``(left, bottom, width, height)`` + rectangle or a single `.Bbox`. This specifies the rectangle (in + figure coordinates) where the Axes is positioned. - rect : [left, bottom, width, height] - The axes is build in the rectangle *rect*. *rect* is in - `.Figure` coordinates. + ``*args`` can also consist of three numbers or a single three-digit + number; in the latter case, the digits are considered as + independent numbers. The numbers are interpreted as ``(nrows, + ncols, index)``: ``(nrows, ncols)`` specifies the size of an array + of subplots, and ``index`` is the 1-based index of the subplot + being created. Finally, ``*args`` can also directly be a + `.SubplotSpec` instance. sharex, sharey : `~.axes.Axes`, optional - The x or y `~.matplotlib.axis` is shared with the x or - y axis in the input `~.axes.Axes`. + The x- or y-`~.matplotlib.axis` is shared with the x- or y-axis in + the input `~.axes.Axes`. frameon : bool, default: True - Whether the axes frame is visible. + Whether the Axes frame is visible. box_aspect : float, optional - Set a fixed aspect for the axes box, i.e. the ratio of height to + Set a fixed aspect for the Axes box, i.e. the ratio of height to width. See `~.axes.Axes.set_box_aspect` for details. **kwargs Other optional keyword arguments: - %(Axes_kwdoc)s + %(Axes:kwdoc)s Returns ------- @@ -598,10 +627,21 @@ def __init__(self, fig, rect, """ super().__init__() - if isinstance(rect, mtransforms.Bbox): - self._position = rect + if "rect" in kwargs: + if args: + raise TypeError( + "'rect' cannot be used together with positional arguments") + rect = kwargs.pop("rect") + _api.check_isinstance((mtransforms.Bbox, Iterable), rect=rect) + args = (rect,) + subplotspec = None + if len(args) == 1 and isinstance(args[0], mtransforms.Bbox): + self._position = args[0] + elif len(args) == 1 and np.iterable(args[0]): + self._position = mtransforms.Bbox.from_bounds(*args[0]) else: - self._position = mtransforms.Bbox.from_bounds(*rect) + self._position = self._originalPosition = mtransforms.Bbox.unit() + subplotspec = SubplotSpec._from_subplot_args(fig, args) if self._position.width < 0 or self._position.height < 0: raise ValueError('Width and height specified must be non-negative') self._originalPosition = self._position.frozen() @@ -609,15 +649,24 @@ def __init__(self, fig, rect, self._aspect = 'auto' self._adjustable = 'box' self._anchor = 'C' - self._stale_viewlim_x = False - self._stale_viewlim_y = False + self._stale_viewlims = {name: False for name in self._axis_names} self._sharex = sharex self._sharey = sharey self.set_label(label) self.set_figure(fig) + # The subplotspec needs to be set after the figure (so that + # figure-level subplotpars are taken into account), but the figure + # needs to be set after self._position is initialized. + if subplotspec: + self.set_subplotspec(subplotspec) + else: + self._subplotspec = None self.set_box_aspect(box_aspect) self._axes_locator = None # Optionally set via update(kwargs). - # placeholder for any colorbars added that use this axes. + + self._children = [] + + # placeholder for any colorbars added that use this Axes. # (see colorbar.py): self._colorbars = [] self.spines = mspines.Spines.from_dict(self._gen_axes_spines()) @@ -631,7 +680,7 @@ def __init__(self, fig, rect, self.set_axisbelow(mpl.rcParams['axes.axisbelow']) self._rasterization_zorder = None - self.cla() + self.clear() # funcs used to format x and y - fall back on major formatters self.fmt_xdata = None @@ -645,12 +694,11 @@ def __init__(self, fig, rect, if yscale: self.set_yscale(yscale) - self.update(kwargs) + self._internal_update(kwargs) - for name, axis in self._get_axis_map().items(): - axis.callbacks._pickled_cids.add( - axis.callbacks.connect( - 'units finalize', self._unit_change_handler(name))) + for name, axis in self._axis_map.items(): + axis.callbacks._connect_picklable( + 'units', self._unit_change_handler(name)) rcParams = mpl.rcParams self.tick_params( @@ -683,25 +731,38 @@ def __init__(self, fig, rect, rcParams['ytick.major.right']), which='major') + def __init_subclass__(cls, **kwargs): + parent_uses_cla = super(cls, cls)._subclass_uses_cla + if 'cla' in cls.__dict__: + _api.warn_deprecated( + '3.6', + pending=True, + message=f'Overriding `Axes.cla` in {cls.__qualname__} is ' + 'pending deprecation in %(since)s and will be fully ' + 'deprecated in favor of `Axes.clear` in the future. ' + 'Please report ' + f'this to the {cls.__module__!r} author.') + cls._subclass_uses_cla = 'cla' in cls.__dict__ or parent_uses_cla + super().__init_subclass__(**kwargs) + def __getstate__(self): - # The renderer should be re-created by the figure, and then cached at - # that point. state = super().__getstate__() # Prune the sharing & twinning info to only contain the current group. - for grouper_name in [ - '_shared_x_axes', '_shared_y_axes', '_twinned_axes']: - grouper = getattr(self, grouper_name) - state[grouper_name] = (grouper.get_siblings(self) - if self in grouper else None) + state["_shared_axes"] = { + name: self._shared_axes[name].get_siblings(self) + for name in self._axis_names if self in self._shared_axes[name]} + state["_twinned_axes"] = (self._twinned_axes.get_siblings(self) + if self in self._twinned_axes else None) return state def __setstate__(self, state): # Merge the grouping info back into the global groupers. - for grouper_name in [ - '_shared_x_axes', '_shared_y_axes', '_twinned_axes']: - siblings = state.pop(grouper_name) - if siblings: - getattr(self, grouper_name).join(*siblings) + shared_axes = state.pop("_shared_axes") + for name, shared_siblings in shared_axes.items(): + self._shared_axes[name].join(*shared_siblings) + twinned_siblings = state.pop("_twinned_axes") + if twinned_siblings: + self._twinned_axes.join(*twinned_siblings) self.__dict__ = state self._stale = True @@ -709,25 +770,40 @@ def __repr__(self): fields = [] if self.get_label(): fields += [f"label={self.get_label()!r}"] - titles = [] - for k in ["left", "center", "right"]: - title = self.get_title(loc=k) - if title: - titles.append(f"{k!r}:{title!r}") - if titles: - fields += ["title={" + ",".join(titles) + "}"] - if self.get_xlabel(): - fields += [f"xlabel={self.get_xlabel()!r}"] - if self.get_ylabel(): - fields += [f"ylabel={self.get_ylabel()!r}"] - return f"<{self.__class__.__name__}:" + ", ".join(fields) + ">" - - def get_window_extent(self, *args, **kwargs): - """ - Return the axes bounding box in display space; *args* and *kwargs* + if hasattr(self, "get_title"): + titles = {} + for k in ["left", "center", "right"]: + title = self.get_title(loc=k) + if title: + titles[k] = title + if titles: + fields += [f"title={titles}"] + for name, axis in self._axis_map.items(): + if axis.get_label() and axis.get_label().get_text(): + fields += [f"{name}label={axis.get_label().get_text()!r}"] + return f"<{self.__class__.__name__}: " + ", ".join(fields) + ">" + + def get_subplotspec(self): + """Return the `.SubplotSpec` associated with the subplot, or None.""" + return self._subplotspec + + def set_subplotspec(self, subplotspec): + """Set the `.SubplotSpec`. associated with the subplot.""" + self._subplotspec = subplotspec + self._set_position(subplotspec.get_position(self.figure)) + + def get_gridspec(self): + """Return the `.GridSpec` associated with the subplot, or None.""" + return self._subplotspec.get_gridspec() if self._subplotspec else None + + @_api.delete_parameter("3.6", "args") + @_api.delete_parameter("3.6", "kwargs") + def get_window_extent(self, renderer=None, *args, **kwargs): + """ + Return the Axes bounding box in display space; *args* and *kwargs* are empty. - This bounding box does not include the spines, ticks, ticklables, + This bounding box does not include the spines, ticks, ticklabels, or other labels. For a bounding box including these elements use `~matplotlib.axes.Axes.get_tightbbox`. @@ -747,7 +823,6 @@ def _init_axis(self): self.yaxis = maxis.YAxis(self) self.spines.left.register_axis(self.yaxis) self.spines.right.register_axis(self.yaxis) - self._update_transScale() def set_figure(self, fig): # docstring inherited @@ -766,31 +841,42 @@ def set_figure(self, fig): def _unstale_viewLim(self): # We should arrange to store this information once per share-group # instead of on every axis. - scalex = any(ax._stale_viewlim_x - for ax in self._shared_x_axes.get_siblings(self)) - scaley = any(ax._stale_viewlim_y - for ax in self._shared_y_axes.get_siblings(self)) - if scalex or scaley: - for ax in self._shared_x_axes.get_siblings(self): - ax._stale_viewlim_x = False - for ax in self._shared_y_axes.get_siblings(self): - ax._stale_viewlim_y = False - self.autoscale_view(scalex=scalex, scaley=scaley) + need_scale = { + name: any(ax._stale_viewlims[name] + for ax in self._shared_axes[name].get_siblings(self)) + for name in self._axis_names} + if any(need_scale.values()): + for name in need_scale: + for ax in self._shared_axes[name].get_siblings(self): + ax._stale_viewlims[name] = False + self.autoscale_view(**{f"scale{name}": scale + for name, scale in need_scale.items()}) @property def viewLim(self): self._unstale_viewLim() return self._viewLim - # API could be better, right now this is just to match the old calls to - # autoscale_view() after each plotting method. - def _request_autoscale_view(self, tight=None, scalex=True, scaley=True): + def _request_autoscale_view(self, axis="all", tight=None): + """ + Mark a single axis, or all of them, as stale wrt. autoscaling. + + No computation is performed until the next autoscaling; thus, separate + calls to control individual axises incur negligible performance cost. + + Parameters + ---------- + axis : str, default: "all" + Either an element of ``self._axis_names``, or "all". + tight : bool or None, default: None + """ + axis_names = _api.check_getitem( + {**{k: [k] for k in self._axis_names}, "all": self._axis_names}, + axis=axis) + for name in axis_names: + self._stale_viewlims[name] = True if tight is not None: self._tight = tight - if scalex: - self._stale_viewlim_x = True # Else keep old state. - if scaley: - self._stale_viewlim_y = True def _set_lim_and_transforms(self): """ @@ -801,7 +887,7 @@ def _set_lim_and_transforms(self): This method is primarily used by rectilinear projections of the `~matplotlib.axes.Axes` class, and is meant to be overridden by - new kinds of projection axes that need different transformations + new kinds of projection Axes that need different transformations and limits. (See `~matplotlib.projections.polar.PolarAxes` for an example.) """ @@ -840,6 +926,10 @@ def get_xaxis_transform(self, which='grid'): `~matplotlib.axis.Axis` class, and is meant to be overridden by new kinds of projections that may need to place axis elements in different locations. + + Parameters + ---------- + which : {'grid', 'tick1', 'tick2'} """ if which == 'grid': return self._xaxis_transform @@ -850,7 +940,7 @@ def get_xaxis_transform(self, which='grid'): # for cartesian projection, this is top spine return self.spines.top.get_spine_transform() else: - raise ValueError('unknown value for which') + raise ValueError(f'unknown value for which: {which!r}') def get_xaxis_text1_transform(self, pad_points): """ @@ -858,7 +948,7 @@ def get_xaxis_text1_transform(self, pad_points): ------- transform : Transform The transform used for drawing x-axis labels, which will add - *pad_points* of padding (in points) between the axes and the label. + *pad_points* of padding (in points) between the axis and the label. The x-direction is in data coordinates and the y-direction is in axis coordinates valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'} @@ -884,7 +974,7 @@ def get_xaxis_text2_transform(self, pad_points): ------- transform : Transform The transform used for drawing secondary x-axis labels, which will - add *pad_points* of padding (in points) between the axes and the + add *pad_points* of padding (in points) between the axis and the label. The x-direction is in data coordinates and the y-direction is in axis coordinates valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'} @@ -916,6 +1006,10 @@ def get_yaxis_transform(self, which='grid'): `~matplotlib.axis.Axis` class, and is meant to be overridden by new kinds of projections that may need to place axis elements in different locations. + + Parameters + ---------- + which : {'grid', 'tick1', 'tick2'} """ if which == 'grid': return self._yaxis_transform @@ -926,7 +1020,7 @@ def get_yaxis_transform(self, which='grid'): # for cartesian projection, this is top spine return self.spines.right.get_spine_transform() else: - raise ValueError('unknown value for which') + raise ValueError(f'unknown value for which: {which!r}') def get_yaxis_text1_transform(self, pad_points): """ @@ -934,7 +1028,7 @@ def get_yaxis_text1_transform(self, pad_points): ------- transform : Transform The transform used for drawing y-axis labels, which will add - *pad_points* of padding (in points) between the axes and the label. + *pad_points* of padding (in points) between the axis and the label. The x-direction is in axis coordinates and the y-direction is in data coordinates valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'} @@ -960,7 +1054,7 @@ def get_yaxis_text2_transform(self, pad_points): ------- transform : Transform The transform used for drawing secondart y-axis labels, which will - add *pad_points* of padding (in points) between the axes and the + add *pad_points* of padding (in points) between the axis and the label. The x-direction is in axis coordinates and the y-direction is in data coordinates valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'} @@ -984,20 +1078,15 @@ def _update_transScale(self): self.transScale.set( mtransforms.blended_transform_factory( self.xaxis.get_transform(), self.yaxis.get_transform())) - for line in getattr(self, "lines", []): # Not set during init. - try: - line._transformed_path.invalidate() - except AttributeError: - pass def get_position(self, original=False): """ - Get a copy of the axes rectangle as a `.Bbox`. + Return the position of the Axes within the figure as a `.Bbox`. Parameters ---------- original : bool - If ``True``, return the original position. Otherwise return the + If ``True``, return the original position. Otherwise, return the active position. For an explanation of the positions see `.set_position`. @@ -1016,7 +1105,7 @@ def get_position(self, original=False): def set_position(self, pos, which='both'): """ - Set the axes position. + Set the Axes position. Axes have two position attributes. The 'original' position is the position allocated for the Axes. The 'active' position is the @@ -1027,7 +1116,7 @@ def set_position(self, pos, which='both'): Parameters ---------- pos : [left, bottom, width, height] or `~matplotlib.transforms.Bbox` - The new position of the in `.Figure` coordinates. + The new position of the Axes in `.Figure` coordinates. which : {'both', 'active', 'original'}, default: 'both' Determines which position variables to change. @@ -1046,7 +1135,7 @@ def _set_position(self, pos, which='both'): """ Private version of set_position. - Call this internally to get the same functionality of `get_position`, + Call this internally to get the same functionality of `set_position`, but not to take the axis out of the constrained_layout hierarchy. """ if not isinstance(pos, mtransforms.BboxBase): @@ -1062,8 +1151,9 @@ def reset_position(self): """ Reset the active position to the original position. - This resets the a possible position change due to aspect constraints. - For an explanation of the positions see `.set_position`. + This undoes changes to the active position (as defined in + `.set_position`) which may have been performed to satisfy fixed-aspect + constraints. """ for ax in self._twinned_axes.get_siblings(self): pos = ax.get_position(original=True) @@ -1071,7 +1161,7 @@ def reset_position(self): def set_axes_locator(self, locator): """ - Set the axes locator. + Set the Axes locator. Parameters ---------- @@ -1087,13 +1177,13 @@ def get_axes_locator(self): return self._axes_locator def _set_artist_props(self, a): - """Set the boilerplate props for artists added to axes.""" + """Set the boilerplate props for artists added to Axes.""" a.set_figure(self.figure) if not a.is_transform_set(): a.set_transform(self.transData) a.axes = self - if a.mouseover: + if a.get_mouseover(): self._mouseover_set.add(a) def _gen_axes_patch(self): @@ -1101,10 +1191,10 @@ def _gen_axes_patch(self): Returns ------- Patch - The patch used to draw the background of the axes. It is also used - as the clipping path for any data elements on the axes. + The patch used to draw the background of the Axes. It is also used + as the clipping path for any data elements on the Axes. - In the standard axes, this is a rectangle, but in other projections + In the standard Axes, this is a rectangle, but in other projections it may not be. Notes @@ -1119,30 +1209,30 @@ def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'): ------- dict Mapping of spine names to `.Line2D` or `.Patch` instances that are - used to draw axes spines. + used to draw Axes spines. - In the standard axes, spines are single line segments, but in other + In the standard Axes, spines are single line segments, but in other projections they may not be. Notes ----- Intended to be overridden by new projection types. """ - return OrderedDict((side, mspines.Spine.linear_spine(self, side)) - for side in ['left', 'right', 'bottom', 'top']) + return {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. + Axes, and cannot be used if the x-axis is already being shared with + another Axes. """ _api.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._shared_axes["x"].join(self, other) self._sharex = other self.xaxis.major = other.xaxis.major # Ticker instances holding self.xaxis.minor = other.xaxis.minor # locator and formatter. @@ -1155,13 +1245,13 @@ 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. + Axes, and cannot be used if the y-axis is already being shared with + another Axes. """ _api.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._shared_axes["y"].join(self, other) self._sharey = other self.yaxis.major = other.yaxis.major # Ticker instances holding self.yaxis.minor = other.yaxis.minor # locator and formatter. @@ -1169,9 +1259,12 @@ def sharey(self, other): self.set_ylim(y0, y1, emit=False, auto=other.get_autoscaley_on()) self.yaxis._scale = other.yaxis._scale - def cla(self): - """Clear the axes.""" - # Note: this is called by Axes.__init__() + def __clear(self): + """Clear the Axes.""" + # The actual implementation of clear() as long as clear() has to be + # an adapter delegating to the correct implementation. + # The implementation can move back into clear() when the + # deprecation on cla() subclassing expires. # stash the current visibility state if hasattr(self, 'patch'): @@ -1182,31 +1275,14 @@ def cla(self): xaxis_visible = self.xaxis.get_visible() yaxis_visible = self.yaxis.get_visible() - self.xaxis.clear() - self.yaxis.clear() - - for name, spine in self.spines.items(): + for axis in self._axis_map.values(): + axis.clear() # Also resets the scale to linear. + for spine in self.spines.values(): spine.clear() self.ignore_existing_data_limits = True - self.callbacks = cbook.CallbackRegistry() - - if self._sharex is not None: - 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.sharey(self._sharey) - else: - self.yaxis._set_scale('linear') - try: - self.set_ylim(0, 1) - except TypeError: - pass + self.callbacks = cbook.CallbackRegistry( + signals=["xlim_changed", "ylim_changed", "zlim_changed"]) # update the minor locator for x and y axis based on rcParams if mpl.rcParams['xtick.minor.visible']: @@ -1214,32 +1290,23 @@ def cla(self): if mpl.rcParams['ytick.minor.visible']: self.yaxis.set_minor_locator(mticker.AutoMinorLocator()) - if self._sharex is None: - self._autoscaleXon = True - if self._sharey is None: - self._autoscaleYon = True self._xmargin = mpl.rcParams['axes.xmargin'] self._ymargin = mpl.rcParams['axes.ymargin'] self._tight = None self._use_sticky_edges = True - self._update_transScale() # needed? self._get_lines = _process_plot_var_args(self) self._get_patches_for_fill = _process_plot_var_args(self, 'fill') self._gridOn = mpl.rcParams['axes.grid'] - self.lines = [] - self.patches = [] - self.texts = [] - self.tables = [] - self.artists = [] - self.images = [] + old_children, self._children = self._children, [] + for chld in old_children: + chld.axes = chld.figure = None self._mouseover_set = _OrderedSet() self.child_axes = [] self._current_image = None # strictly for pyplot via _sci, _gci self._projection_init = None # strictly for pyplot.subplot self.legend_ = None - self.collections = [] # collection.Collection instances self.containers = [] self.grid(False) # Disable grid on init to use rcParameter @@ -1281,13 +1348,13 @@ def cla(self): for _title in (self.title, self._left_title, self._right_title): self._set_artist_props(_title) - # The patch draws the background of the axes. We want this to be below + # The patch draws the background of the Axes. We want this to be below # the other artists. We use the frame to draw the edges so we are # setting the edgecolor to None. self.patch = self._gen_axes_patch() self.patch.set_figure(self.figure) self.patch.set_facecolor(self._facecolor) - self.patch.set_edgecolor('None') + self.patch.set_edgecolor('none') self.patch.set_linewidth(0) self.patch.set_transform(self.transAxes) @@ -1296,8 +1363,8 @@ def cla(self): self.xaxis.set_clip_path(self.patch) self.yaxis.set_clip_path(self.patch) - self._shared_x_axes.clean() - self._shared_y_axes.clean() + self._shared_axes["x"].clean() + self._shared_axes["y"].clean() if self._sharex is not None: self.xaxis.set_visible(xaxis_visible) self.patch.set_visible(patch_visible) @@ -1305,11 +1372,136 @@ def cla(self): self.yaxis.set_visible(yaxis_visible) self.patch.set_visible(patch_visible) + # This comes last, as the call to _set_lim may trigger an autoscale (in + # case of shared axes), requiring children to be already set up. + for name, axis in self._axis_map.items(): + share = getattr(self, f"_share{name}") + if share is not None: + getattr(self, f"share{name}")(share) + else: + axis._set_scale("linear") + axis._set_lim(0, 1, auto=True) + self._update_transScale() + self.stale = True def clear(self): - """Clear the axes.""" - self.cla() + """Clear the Axes.""" + # Act as an alias, or as the superclass implementation depending on the + # subclass implementation. + if self._subclass_uses_cla: + self.cla() + else: + self.__clear() + + def cla(self): + """Clear the Axes.""" + # Act as an alias, or as the superclass implementation depending on the + # subclass implementation. + if self._subclass_uses_cla: + self.__clear() + else: + self.clear() + + class ArtistList(Sequence): + """ + A sublist of Axes children based on their type. + + The type-specific children sublists were made immutable in Matplotlib + 3.7. In the future these artist lists may be replaced by tuples. Use + as if this is a tuple already. + """ + def __init__(self, axes, prop_name, + valid_types=None, invalid_types=None): + """ + Parameters + ---------- + axes : `~matplotlib.axes.Axes` + The Axes from which this sublist will pull the children + Artists. + prop_name : str + The property name used to access this sublist from the Axes; + used to generate deprecation warnings. + valid_types : list of type, optional + A list of types that determine which children will be returned + by this sublist. If specified, then the Artists in the sublist + must be instances of any of these types. If unspecified, then + any type of Artist is valid (unless limited by + *invalid_types*.) + invalid_types : tuple, optional + A list of types that determine which children will *not* be + returned by this sublist. If specified, then Artists in the + sublist will never be an instance of these types. Otherwise, no + types will be excluded. + """ + self._axes = axes + self._prop_name = prop_name + self._type_check = lambda artist: ( + (not valid_types or isinstance(artist, valid_types)) and + (not invalid_types or not isinstance(artist, invalid_types)) + ) + + def __repr__(self): + return f'' + + def __len__(self): + return sum(self._type_check(artist) + for artist in self._axes._children) + + def __iter__(self): + for artist in list(self._axes._children): + if self._type_check(artist): + yield artist + + def __getitem__(self, key): + return [artist + for artist in self._axes._children + if self._type_check(artist)][key] + + def __add__(self, other): + if isinstance(other, (list, _AxesBase.ArtistList)): + return [*self, *other] + if isinstance(other, (tuple, _AxesBase.ArtistList)): + return (*self, *other) + return NotImplemented + + def __radd__(self, other): + if isinstance(other, list): + return other + list(self) + if isinstance(other, tuple): + return other + tuple(self) + return NotImplemented + + @property + def artists(self): + return self.ArtistList(self, 'artists', invalid_types=( + mcoll.Collection, mimage.AxesImage, mlines.Line2D, mpatches.Patch, + mtable.Table, mtext.Text)) + + @property + def collections(self): + return self.ArtistList(self, 'collections', + valid_types=mcoll.Collection) + + @property + def images(self): + return self.ArtistList(self, 'images', valid_types=mimage.AxesImage) + + @property + def lines(self): + return self.ArtistList(self, 'lines', valid_types=mlines.Line2D) + + @property + def patches(self): + return self.ArtistList(self, 'patches', valid_types=mpatches.Patch) + + @property + def tables(self): + return self.ArtistList(self, 'tables', valid_types=mtable.Table) + + @property + def texts(self): + return self.ArtistList(self, 'texts', valid_types=mtext.Text) def get_facecolor(self): """Get the facecolor of the Axes.""" @@ -1357,13 +1549,13 @@ def set_prop_cycle(self, *args, **kwargs): Form 2 creates a `~cycler.Cycler` which cycles over one or more properties simultaneously and set it as the property cycle of the - axes. If multiple properties are given, their value lists must have + Axes. If multiple properties are given, their value lists must have the same length. This is just a shortcut for explicitly creating a cycler and passing it to the function, i.e. it's short for ``set_prop_cycle(cycler(label=values label2=values2, ...))``. Form 3 creates a `~cycler.Cycler` for a single property and set it - as the property cycle of the axes. This form exists for compatibility + as the property cycle of the Axes. This form exists for compatibility with the original `cycler.cycler` interface. Its use is discouraged in favor of the kwarg form, i.e. ``set_prop_cycle(label=values)``. @@ -1414,11 +1606,16 @@ def set_prop_cycle(self, *args, **kwargs): self._get_patches_for_fill.set_prop_cycle(prop_cycle) def get_aspect(self): + """ + Return the aspect ratio of the axes scaling. + + This is either "auto" or a float giving the ratio of y/x-scale. + """ return self._aspect def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): """ - Set the aspect of the axis scaling, i.e. the ratio of y-unit to x-unit. + Set the aspect ratio of the axes scaling, i.e. y/x-scale. Parameters ---------- @@ -1427,8 +1624,10 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): - 'auto': fill the position rectangle with data. - 'equal': same as ``aspect=1``, i.e. same scaling for x and y. - - *float*: A circle will be stretched such that the height - is *float* times the width. + - *float*: The displayed size of 1 unit in y-data coordinates will + be *aspect* times the displayed size of 1 unit in x-data + coordinates; e.g. for ``aspect=2`` a square in data coordinates + will be rendered with a height of twice its width. adjustable : None or {'box', 'datalim'}, optional If not ``None``, this defines which parameter will be adjusted to @@ -1437,7 +1636,7 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): anchor : None or str or (float, float), optional If not ``None``, this defines where the Axes will be drawn if there - is extra space due to aspect constraints. The most common way to + is extra space due to aspect constraints. The most common way to specify the anchor are abbreviations of cardinal directions: ===== ===================== @@ -1466,10 +1665,12 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): aspect = 1 if not cbook._str_equal(aspect, 'auto'): aspect = float(aspect) # raise ValueError if necessary + if aspect <= 0 or not np.isfinite(aspect): + raise ValueError("aspect must be finite and positive ") if share: - axes = {*self._shared_x_axes.get_siblings(self), - *self._shared_y_axes.get_siblings(self)} + axes = {sibling for name in self._axis_names + for sibling in self._shared_axes[name].get_siblings(self)} else: axes = [self] @@ -1530,8 +1731,8 @@ def set_adjustable(self, adjustable, share=False): """ _api.check_in_list(["box", "datalim"], adjustable=adjustable) if share: - axs = {*self._shared_x_axes.get_siblings(self), - *self._shared_y_axes.get_siblings(self)} + axs = {sibling for name in self._axis_names + for sibling in self._shared_axes[name].get_siblings(self)} else: axs = [self] if (adjustable == "datalim" @@ -1540,7 +1741,7 @@ def set_adjustable(self, adjustable, share=False): for ax in axs)): # Limits adjustment by apply_aspect assumes that the axes' aspect # ratio can be computed from the data limits and scales. - raise ValueError("Cannot set axes adjustable to 'datalim' for " + raise ValueError("Cannot set Axes adjustable to 'datalim' for " "Axes which override 'get_data_ratio'") for ax in axs: ax._adjustable = adjustable @@ -1548,7 +1749,7 @@ def set_adjustable(self, adjustable, share=False): def get_box_aspect(self): """ - Return the axes box aspect, i.e. the ratio of height to width. + Return the Axes box aspect, i.e. the ratio of height to width. The box aspect is ``None`` (i.e. chosen depending on the available figure space) unless explicitly specified. @@ -1564,21 +1765,21 @@ def get_box_aspect(self): def set_box_aspect(self, aspect=None): """ - Set the axes box aspect, i.e. the ratio of height to width. + Set the Axes box aspect, i.e. the ratio of height to width. - This defines the aspect of the axes in figure space and is not to be + This defines the aspect of the Axes in figure space and is not to be confused with the data aspect (see `~.Axes.set_aspect`). Parameters ---------- aspect : float or None Changes the physical dimensions of the Axes, such that the ratio - of the axes height to the axes width in physical units is equal to + of the Axes height to the Axes width in physical units is equal to *aspect*. Defining a box aspect will change the *adjustable* property to 'datalim' (see `~.Axes.set_adjustable`). *None* will disable a fixed box aspect so that height and width - of the axes are chosen independently. + of the Axes are chosen independently. See Also -------- @@ -1623,28 +1824,19 @@ def set_anchor(self, anchor, share=False): Parameters ---------- - anchor : 2-tuple of floats or {'C', 'SW', 'S', 'SE', ...} - The anchor position may be either: - - - a sequence (*cx*, *cy*). *cx* and *cy* may range from 0 - to 1, where 0 is left or bottom and 1 is right or top. - - - a string using cardinal directions as abbreviation: - - - 'C' for centered - - 'S' (south) for bottom-center - - 'SW' (south west) for bottom-left - - etc. - - Here is an overview of the possible positions: - - +------+------+------+ - | 'NW' | 'N' | 'NE' | - +------+------+------+ - | 'W' | 'C' | 'E' | - +------+------+------+ - | 'SW' | 'S' | 'SE' | - +------+------+------+ + anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', ...} + Either an (*x*, *y*) pair of relative coordinates (0 is left or + bottom, 1 is right or top), 'C' (center), or a cardinal direction + ('SW', southwest, is bottom left, etc.). str inputs are shorthands + for (*x*, *y*) coordinates, as shown in the following diagram:: + + ┌─────────────────┬─────────────────┬─────────────────┠+ │ 'NW' (0.0, 1.0) │ 'N' (0.5, 1.0) │ 'NE' (1.0, 1.0) │ + ├─────────────────┼─────────────────┼─────────────────┤ + │ 'W' (0.0, 0.5) │ 'C' (0.5, 0.5) │ 'E' (1.0, 0.5) │ + ├─────────────────┼─────────────────┼─────────────────┤ + │ 'SW' (0.0, 0.0) │ 'S' (0.5, 0.0) │ 'SE' (1.0, 0.0) │ + └─────────────────┴─────────────────┴─────────────────┘ share : bool, default: False If ``True``, apply the settings to all shared Axes. @@ -1658,8 +1850,8 @@ def set_anchor(self, anchor, share=False): raise ValueError('argument must be among %s' % ', '.join(mtransforms.Bbox.coefs)) if share: - axes = {*self._shared_x_axes.get_siblings(self), - *self._shared_y_axes.get_siblings(self)} + axes = {sibling for name in self._axis_names + for sibling in self._shared_axes[name].get_siblings(self)} else: axes = [self] for ax in axes: @@ -1689,6 +1881,13 @@ def apply_aspect(self, position=None): Axes box (position) or the view limits. In the former case, `~matplotlib.axes.Axes.get_anchor` will affect the position. + Parameters + ---------- + position : None or .Bbox + If not ``None``, this defines the position of the + Axes within the figure as a Bbox. See `~.Axes.get_position` + for further details. + Notes ----- This is called automatically when each Axes is drawn. You may need @@ -1714,7 +1913,7 @@ def apply_aspect(self, position=None): return trans = self.get_figure().transSubfigure - bb = mtransforms.Bbox.from_bounds(0, 0, 1, 1).transformed(trans) + bb = mtransforms.Bbox.unit().transformed(trans) # this is the physical aspect of the panel (or figure): fig_aspect = bb.height / bb.width @@ -1774,12 +1973,14 @@ def apply_aspect(self, position=None): xm = 0 ym = 0 - shared_x = self in self._shared_x_axes - shared_y = self in self._shared_y_axes - # Not sure whether we need this check: + shared_x = self in self._shared_axes["x"] + shared_y = self in self._shared_axes["y"] + if shared_x and shared_y: - raise RuntimeError("adjustable='datalim' is not allowed when both " - "axes are shared") + raise RuntimeError("set_aspect(..., adjustable='datalim') or " + "axis('equal') are not allowed when both axes " + "are shared. Try set_aspect(..., " + "adjustable='box').") # If y is shared, then we are only allowed to change x, etc. if shared_y: @@ -1803,7 +2004,7 @@ def apply_aspect(self, position=None): x1 = xc + Xsize / 2.0 self.set_xbound(x_trf.inverted().transform([x0, x1])) - def axis(self, *args, emit=True, **kwargs): + def axis(self, arg=None, /, *, emit=True, **kwargs): """ Convenience method to get or set some axis properties. @@ -1861,38 +2062,34 @@ def axis(self, *args, emit=True, **kwargs): matplotlib.axes.Axes.set_xlim matplotlib.axes.Axes.set_ylim """ - if len(args) > 1: - raise TypeError("axis() takes 0 or 1 positional arguments but " - f"{len(args)} were given") - elif len(args) == 1 and isinstance(args[0], (str, bool)): - s = args[0] - if s is True: - s = 'on' - if s is False: - s = 'off' - s = s.lower() - if s == 'on': + if isinstance(arg, (str, bool)): + if arg is True: + arg = 'on' + if arg is False: + arg = 'off' + arg = arg.lower() + if arg == 'on': self.set_axis_on() - elif s == 'off': + elif arg == 'off': self.set_axis_off() - elif s in ('equal', 'tight', 'scaled', 'auto', 'image', 'square'): + elif arg in [ + 'equal', 'tight', 'scaled', 'auto', 'image', 'square']: self.set_autoscale_on(True) self.set_aspect('auto') self.autoscale_view(tight=False) - # self.apply_aspect() - if s == 'equal': + if arg == 'equal': self.set_aspect('equal', adjustable='datalim') - elif s == 'scaled': + elif arg == 'scaled': self.set_aspect('equal', adjustable='box', anchor='C') self.set_autoscale_on(False) # Req. by Mark Bakker - elif s == 'tight': + elif arg == 'tight': self.autoscale_view(tight=True) self.set_autoscale_on(False) - elif s == 'image': + elif arg == 'image': self.autoscale_view(tight=True) self.set_autoscale_on(False) self.set_aspect('equal', adjustable='box', anchor='C') - elif s == 'square': + elif arg == 'square': self.set_aspect('equal', adjustable='box', anchor='C') self.set_autoscale_on(False) xlim = self.get_xlim() @@ -1903,13 +2100,12 @@ def axis(self, *args, emit=True, **kwargs): self.set_ylim([ylim[0], ylim[0] + edge_size], emit=emit, auto=False) else: - raise ValueError('Unrecognized string %s to axis; ' - 'try on or off' % s) + raise ValueError(f"Unrecognized string {arg!r} to axis; " + "try 'on' or 'off'") else: - if len(args) == 1: - limits = args[0] + if arg is not None: try: - xmin, xmax, ymin, ymax = limits + xmin, xmax, ymin, ymax = arg except (TypeError, ValueError) as err: raise TypeError('the first argument to axis() must be an ' 'iterable of the form ' @@ -1928,8 +2124,7 @@ def axis(self, *args, emit=True, **kwargs): self.set_xlim(xmin, xmax, emit=emit, auto=xauto) self.set_ylim(ymin, ymax, emit=emit, auto=yauto) if kwargs: - raise TypeError(f"axis() got an unexpected keyword argument " - f"'{next(iter(kwargs))}'") + raise _api.kwarg_error("axis", kwargs) return (*self.get_xlim(), *self.get_ylim()) def get_legend(self): @@ -1946,19 +2141,23 @@ def get_lines(self): def get_xaxis(self): """ - Return the XAxis instance. + [*Discouraged*] Return the XAxis instance. + + .. admonition:: Discouraged - The use of this function is discouraged. You should instead directly - access the attribute ``ax.xaxis``. + The use of this function is discouraged. You should instead + directly access the attribute ``ax.xaxis``. """ return self.xaxis def get_yaxis(self): """ - Return the YAxis instance. + [*Discouraged*] Return the YAxis instance. - The use of this function is discouraged. You should instead directly - access the attribute ``ax.yaxis``. + .. admonition:: Discouraged + + The use of this function is discouraged. You should instead + directly access the attribute ``ax.yaxis``. """ return self.yaxis @@ -1974,13 +2173,16 @@ def _sci(self, im): Set the current image. This image will be the target of colormap functions like - `~.pyplot.viridis`, and other functions such as `~.pyplot.clim`. The - current image is an attribute of the current axes. + ``pyplot.viridis``, and other functions such as `~.pyplot.clim`. The + current image is an attribute of the current Axes. """ + _api.check_isinstance( + (mpl.contour.ContourSet, mcoll.Collection, mimage.AxesImage), + im=im) if isinstance(im, mpl.contour.ContourSet): - if im.collections[0] not in self.collections: + if im.collections[0] not in self._children: raise ValueError("ContourSet must be in current Axes") - elif im not in self.images and im not in self.collections: + elif im not in self._children: raise ValueError("Argument must be an image, collection, or " "ContourSet in this Axes") self._current_image = im @@ -1991,21 +2193,19 @@ def _gci(self): def has_data(self): """ - Return whether any artists have been added to the axes. + Return whether any artists have been added to the Axes. This should not be used to determine whether the *dataLim* need to be updated, and may not actually be useful for anything. """ - return ( - len(self.collections) + - len(self.images) + - len(self.lines) + - len(self.patches)) > 0 + return any(isinstance(a, (mcoll.Collection, mimage.AxesImage, + mlines.Line2D, mpatches.Patch)) + for a in self._children) def add_artist(self, a): """ - Add an `~.Artist` to the axes, and return the artist. + Add an `.Artist` to the Axes; return the artist. Use `add_artist` only for artists for which there is no dedicated "add" method; and if necessary, use a method such as `update_datalim` @@ -2017,8 +2217,8 @@ def add_artist(self, a): ``ax.transData``. """ a.axes = self - self.artists.append(a) - a._remove_method = self.artists.remove + self._children.append(a) + a._remove_method = self._children.remove self._set_artist_props(a) a.set_clip_path(self.patch) self.stale = True @@ -2026,12 +2226,12 @@ def add_artist(self, a): def add_child_axes(self, ax): """ - Add an `~.AxesBase` to the axes' children; return the child axes. + Add an `.AxesBase` to the Axes' children; return the child Axes. This is the lowlevel version. See `.axes.Axes.inset_axes`. """ - # normally axes have themselves as the axes, but these need to have + # normally Axes have themselves as the Axes, but these need to have # their parent... # Need to bypass the getter... ax._axes = self @@ -2044,13 +2244,14 @@ def add_child_axes(self, ax): def add_collection(self, collection, autolim=True): """ - Add a `~.Collection` to the axes' collections; return the collection. + Add a `.Collection` to the Axes; return the collection. """ + _api.check_isinstance(mcoll.Collection, collection=collection) label = collection.get_label() if not label: - collection.set_label('_collection%d' % len(self.collections)) - self.collections.append(collection) - collection._remove_method = self.collections.remove + collection.set_label(f'_child{len(self._children)}') + self._children.append(collection) + collection._remove_method = self._children.remove self._set_artist_props(collection) if collection.get_clip_path() is None: @@ -2076,13 +2277,14 @@ def add_collection(self, collection, autolim=True): def add_image(self, image): """ - Add an `~.AxesImage` to the axes' images; return the image. + Add an `.AxesImage` to the Axes; return the image. """ + _api.check_isinstance(mimage.AxesImage, image=image) self._set_artist_props(image) if not image.get_label(): - image.set_label('_image%d' % len(self.images)) - self.images.append(image) - image._remove_method = self.images.remove + image.set_label(f'_child{len(self._children)}') + self._children.append(image) + image._remove_method = self._children.remove self.stale = True return image @@ -2092,27 +2294,29 @@ def _update_image_limits(self, image): def add_line(self, line): """ - Add a `.Line2D` to the axes' lines; return the line. + Add a `.Line2D` to the Axes; return the line. """ + _api.check_isinstance(mlines.Line2D, line=line) self._set_artist_props(line) if line.get_clip_path() is None: line.set_clip_path(self.patch) self._update_line_limits(line) if not line.get_label(): - line.set_label('_line%d' % len(self.lines)) - self.lines.append(line) - line._remove_method = self.lines.remove + line.set_label(f'_child{len(self._children)}') + self._children.append(line) + line._remove_method = self._children.remove self.stale = True return line def _add_text(self, txt): """ - Add a `~.Text` to the axes' texts; return the text. + Add a `.Text` to the Axes; return the text. """ + _api.check_isinstance(mtext.Text, txt=txt) self._set_artist_props(txt) - self.texts.append(txt) - txt._remove_method = self.texts.remove + self._children.append(txt) + txt._remove_method = self._children.remove self.stale = True return txt @@ -2124,52 +2328,57 @@ def _update_line_limits(self, line): if path.vertices.size == 0: return - line_trans = line.get_transform() + line_trf = line.get_transform() - if line_trans == self.transData: + if line_trf == self.transData: data_path = path - - elif any(line_trans.contains_branch_seperately(self.transData)): - # identify the transform to go from line's coordinates - # to data coordinates - trans_to_data = line_trans - self.transData - - # if transData is affine we can use the cached non-affine component - # of line's path. (since the non-affine part of line_trans is - # entirely encapsulated in trans_to_data). + elif any(line_trf.contains_branch_seperately(self.transData)): + # Compute the transform from line coordinates to data coordinates. + trf_to_data = line_trf - self.transData + # If transData is affine we can use the cached non-affine component + # of line's path (since the non-affine part of line_trf is + # entirely encapsulated in trf_to_data). if self.transData.is_affine: line_trans_path = line._get_transformed_path() na_path, _ = line_trans_path.get_transformed_path_and_affine() - data_path = trans_to_data.transform_path_affine(na_path) + data_path = trf_to_data.transform_path_affine(na_path) else: - data_path = trans_to_data.transform_path(path) + data_path = trf_to_data.transform_path(path) else: - # for backwards compatibility we update the dataLim with the + # For backwards compatibility we update the dataLim with the # coordinate range of the given path, even though the coordinate # systems are completely different. This may occur in situations # such as when ax.transAxes is passed through for absolute # positioning. data_path = path - if data_path.vertices.size > 0: - updatex, updatey = line_trans.contains_branch_seperately( - self.transData) - self.dataLim.update_from_path(data_path, - self.ignore_existing_data_limits, - updatex=updatex, - updatey=updatey) - self.ignore_existing_data_limits = False + if not data_path.vertices.size: + return + + updatex, updatey = line_trf.contains_branch_seperately(self.transData) + if self.name != "rectilinear": + # This block is mostly intended to handle axvline in polar plots, + # for which updatey would otherwise be True. + if updatex and line_trf == self.get_yaxis_transform(): + updatex = False + if updatey and line_trf == self.get_xaxis_transform(): + updatey = False + self.dataLim.update_from_path(data_path, + self.ignore_existing_data_limits, + updatex=updatex, updatey=updatey) + self.ignore_existing_data_limits = False def add_patch(self, p): """ - Add a `~.Patch` to the axes' patches; return the patch. + Add a `.Patch` to the Axes; return the patch. """ + _api.check_isinstance(mpatches.Patch, p=p) self._set_artist_props(p) if p.get_clip_path() is None: p.set_clip_path(self.patch) self._update_patch_limits(p) - self.patches.append(p) - p._remove_method = self.patches.remove + self._children.append(p) + p._remove_method = self._children.remove return p def _update_patch_limits(self, patch): @@ -2186,33 +2395,46 @@ def _update_patch_limits(self, patch): ((not patch.get_width()) and (not patch.get_height()))): return p = patch.get_path() - vertices = p.vertices if p.codes is None else p.vertices[np.isin( - p.codes, (mpath.Path.CLOSEPOLY, mpath.Path.STOP), invert=True)] - if vertices.size > 0: - xys = patch.get_patch_transform().transform(vertices) - if patch.get_data_transform() != self.transData: - patch_to_data = (patch.get_data_transform() - - self.transData) - xys = patch_to_data.transform(xys) - - updatex, updatey = patch.get_transform().\ - contains_branch_seperately(self.transData) - self.update_datalim(xys, updatex=updatex, - updatey=updatey) + # Get all vertices on the path + # Loop through each segment to get extrema for Bezier curve sections + vertices = [] + for curve, code in p.iter_bezier(simplify=False): + # Get distance along the curve of any extrema + _, dzeros = curve.axis_aligned_extrema() + # Calculate vertices of start, end and any extrema in between + vertices.append(curve([0, *dzeros, 1])) + + if len(vertices): + vertices = np.row_stack(vertices) + + patch_trf = patch.get_transform() + updatex, updatey = patch_trf.contains_branch_seperately(self.transData) + if not (updatex or updatey): + return + if self.name != "rectilinear": + # As in _update_line_limits, but for axvspan. + if updatex and patch_trf == self.get_yaxis_transform(): + updatex = False + if updatey and patch_trf == self.get_xaxis_transform(): + updatey = False + trf_to_data = patch_trf - self.transData + xys = trf_to_data.transform(vertices) + self.update_datalim(xys, updatex=updatex, updatey=updatey) def add_table(self, tab): """ - Add a `~.Table` to the axes' tables; return the table. + Add a `.Table` to the Axes; return the table. """ + _api.check_isinstance(mtable.Table, tab=tab) self._set_artist_props(tab) - self.tables.append(tab) + self._children.append(tab) tab.set_clip_path(self.patch) - tab._remove_method = self.tables.remove + tab._remove_method = self._children.remove return tab def add_container(self, container): """ - Add a `~.Container` to the axes' containers; return the container. + Add a `.Container` to the Axes' containers; return the container. """ label = container.get_label() if not label: @@ -2228,16 +2450,17 @@ def _unit_change_handler(self, axis_name, event=None): if event is None: # Allow connecting `self._unit_change_handler(name)` return functools.partial( self._unit_change_handler, axis_name, event=object()) - _api.check_in_list(self._get_axis_map(), axis_name=axis_name) + _api.check_in_list(self._axis_map, axis_name=axis_name) + for line in self.lines: + line.recache_always() self.relim() - self._request_autoscale_view(scalex=(axis_name == "x"), - scaley=(axis_name == "y")) + self._request_autoscale_view(axis_name) def relim(self, visible_only=False): """ Recompute the data limits based on current artists. - At present, `~.Collection` instances are not supported. + At present, `.Collection` instances are not supported. Parameters ---------- @@ -2250,17 +2473,14 @@ def relim(self, visible_only=False): self.dataLim.set_points(mtransforms.Bbox.null().get_points()) self.ignore_existing_data_limits = True - for line in self.lines: - if not visible_only or line.get_visible(): - self._update_line_limits(line) - - for p in self.patches: - if not visible_only or p.get_visible(): - self._update_patch_limits(p) - - for image in self.images: - if not visible_only or image.get_visible(): - self._update_image_limits(image) + for artist in self._children: + if not visible_only or artist.get_visible(): + if isinstance(artist, mlines.Line2D): + self._update_line_limits(artist) + elif isinstance(artist, mpatches.Patch): + self._update_patch_limits(artist) + elif isinstance(artist, mimage.AxesImage): + self._update_image_limits(artist) def update_datalim(self, xys, updatex=True, updatey=True): """ @@ -2287,19 +2507,6 @@ def update_datalim(self, xys, updatex=True, updatey=True): updatex=updatex, updatey=updatey) self.ignore_existing_data_limits = False - @_api.deprecated( - "3.3", alternative="ax.dataLim.set(Bbox.union([ax.dataLim, bounds]))") - def update_datalim_bounds(self, bounds): - """ - Extend the `~.Axes.datalim` Bbox to include the given - `~matplotlib.transforms.Bbox`. - - Parameters - ---------- - bounds : `~matplotlib.transforms.Bbox` - """ - self.dataLim.set(mtransforms.Bbox.union([self.dataLim, bounds])) - def _process_unit_info(self, datasets=None, kwargs=None, *, convert=True): """ Set axis units based on *datasets* and *kwargs*, and optionally apply @@ -2309,11 +2516,12 @@ def _process_unit_info(self, datasets=None, kwargs=None, *, convert=True): ---------- datasets : list List of (axis_name, dataset) pairs (where the axis name is defined - as in `._get_axis_map`. + as in `._axis_map`). Individual datasets can also be None + (which gets passed through). kwargs : dict Other parameters from which unit info (i.e., the *xunits*, - *yunits*, *zunits* (for 3D axes), *runits* and *thetaunits* (for - polar axes) entries) is popped, if present. Note that this dict is + *yunits*, *zunits* (for 3D Axes), *runits* and *thetaunits* (for + polar) entries) is popped, if present. Note that this dict is mutated in-place! convert : bool, default: True Whether to return the original datasets or the converted ones. @@ -2330,7 +2538,7 @@ def _process_unit_info(self, datasets=None, kwargs=None, *, convert=True): # (e.g. if some are scalars, etc.). datasets = datasets or [] kwargs = kwargs or {} - axis_map = self._get_axis_map() + axis_map = self._axis_map for axis_name, data in datasets: try: axis = axis_map[axis_name] @@ -2356,7 +2564,8 @@ def _process_unit_info(self, datasets=None, kwargs=None, *, convert=True): for dataset_axis_name, data in datasets: if dataset_axis_name == axis_name and data is not None: axis.update_units(data) - return [axis_map[axis_name].convert_units(data) if convert else data + return [axis_map[axis_name].convert_units(data) + if convert and data is not None else data for axis_name, data in datasets] def in_axes(self, mouseevent): @@ -2365,57 +2574,27 @@ def in_axes(self, mouseevent): """ return self.patch.contains(mouseevent)[0] - def get_autoscale_on(self): - """ - Get whether autoscaling is applied for both axes on plot commands - """ - return self._autoscaleXon and self._autoscaleYon + get_autoscalex_on = _axis_method_wrapper("xaxis", "_get_autoscale_on") + get_autoscaley_on = _axis_method_wrapper("yaxis", "_get_autoscale_on") + set_autoscalex_on = _axis_method_wrapper("xaxis", "_set_autoscale_on") + set_autoscaley_on = _axis_method_wrapper("yaxis", "_set_autoscale_on") - def get_autoscalex_on(self): - """ - Get whether autoscaling for the x-axis is applied on plot commands - """ - return self._autoscaleXon - - def get_autoscaley_on(self): - """ - Get whether autoscaling for the y-axis is applied on plot commands - """ - return self._autoscaleYon + def get_autoscale_on(self): + """Return True if each axis is autoscaled, False otherwise.""" + return all(axis._get_autoscale_on() + for axis in self._axis_map.values()) def set_autoscale_on(self, b): """ - Set whether autoscaling is applied to axes on the next draw or call to - `.Axes.autoscale_view`. - - Parameters - ---------- - b : bool - """ - self._autoscaleXon = b - self._autoscaleYon = b - - def set_autoscalex_on(self, b): - """ - Set whether autoscaling for the x-axis is applied to axes on the next - draw or call to `.Axes.autoscale_view`. - - Parameters - ---------- - b : bool - """ - self._autoscaleXon = b - - def set_autoscaley_on(self, b): - """ - Set whether autoscaling for the y-axis is applied to axes on the next - draw or call to `.Axes.autoscale_view`. + Set whether autoscaling is applied to each axis on the next draw or + call to `.Axes.autoscale_view`. Parameters ---------- b : bool """ - self._autoscaleYon = b + for axis in self._axis_map.values(): + axis._set_autoscale_on(b) @property def use_sticky_edges(self): @@ -2437,20 +2616,19 @@ def use_sticky_edges(self): @use_sticky_edges.setter def use_sticky_edges(self, b): self._use_sticky_edges = bool(b) - # No effect until next autoscaling, which will mark the axes as stale. + # No effect until next autoscaling, which will mark the Axes as stale. def set_xmargin(self, m): """ Set padding of X data limits prior to autoscaling. - *m* times the data interval will be added to each - end of that interval before it is used in autoscaling. - For example, if your data is in the range [0, 2], a factor of - ``m = 0.1`` will result in a range [-0.2, 2.2]. + *m* times the data interval will be added to each end of that interval + before it is used in autoscaling. If *m* is negative, this will clip + the data range instead of expanding it. - Negative values -0.5 < m < 0 will result in clipping of the data range. - I.e. for a data range [0, 2], a factor of ``m = -0.1`` will result in - a range [0.2, 1.8]. + For example, if your data is in the range [0, 2], a margin of 0.1 will + result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range + of [0.2, 1.8]. Parameters ---------- @@ -2459,21 +2637,20 @@ def set_xmargin(self, m): if m <= -0.5: raise ValueError("margin must be greater than -0.5") self._xmargin = m - self._request_autoscale_view(scalex=True, scaley=False) + self._request_autoscale_view("x") self.stale = True def set_ymargin(self, m): """ Set padding of Y data limits prior to autoscaling. - *m* times the data interval will be added to each - end of that interval before it is used in autoscaling. - For example, if your data is in the range [0, 2], a factor of - ``m = 0.1`` will result in a range [-0.2, 2.2]. + *m* times the data interval will be added to each end of that interval + before it is used in autoscaling. If *m* is negative, this will clip + the data range instead of expanding it. - Negative values -0.5 < m < 0 will result in clipping of the data range. - I.e. for a data range [0, 2], a factor of ``m = -0.1`` will result in - a range [0.2, 1.8]. + For example, if your data is in the range [0, 2], a margin of 0.1 will + result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range + of [0.2, 1.8]. Parameters ---------- @@ -2482,14 +2659,14 @@ def set_ymargin(self, m): if m <= -0.5: raise ValueError("margin must be greater than -0.5") self._ymargin = m - self._request_autoscale_view(scalex=False, scaley=True) + self._request_autoscale_view("y") self.stale = True def margins(self, *margins, x=None, y=None, tight=True): """ Set or retrieve autoscaling margins. - The padding added to each limit of the axes is the *margin* + The padding added to each limit of the Axes is the *margin* times the data interval. All input parameters must be floats within the range [0, 1]. Passing both positional and keyword arguments is invalid and will raise a TypeError. If no @@ -2517,11 +2694,11 @@ def margins(self, *margins, x=None, y=None, tight=True): only the y-axis. tight : bool or None, default: True - The *tight* parameter is passed to :meth:`autoscale_view`, + The *tight* parameter is passed to `~.axes.Axes.autoscale_view`, which is executed after a margin is changed; the default here is *True*, on the assumption that when margins are specified, no additional padding to match tick marks is - usually desired. Set *tight* to *None* will preserve + usually desired. Setting *tight* to *None* preserves the previous setting. Returns @@ -2537,7 +2714,7 @@ def margins(self, *margins, x=None, y=None, tight=True): before calling :meth:`margins`. """ - if margins and x is not None and y is not None: + if margins and (x is not None or y is not None): raise TypeError('Cannot pass both positional and keyword ' 'arguments for x and/or y.') elif len(margins) == 1: @@ -2591,7 +2768,7 @@ def autoscale(self, enable=True, axis='both', tight=None): Convenience method for simple axis view autoscaling. It turns autoscaling on or off, and then, if autoscaling for either axis is on, it performs - the autoscaling on the specified axis or axes. + the autoscaling on the specified axis or Axes. Parameters ---------- @@ -2599,29 +2776,35 @@ def autoscale(self, enable=True, axis='both', tight=None): True turns autoscaling on, False turns it off. None leaves the autoscaling state unchanged. axis : {'both', 'x', 'y'}, default: 'both' - Which axis to operate on. + The axis on which to operate. (For 3D Axes, *axis* can also be set + to 'z', and 'both' refers to all three axes.) tight : bool or None, default: None If True, first set the margins to zero. Then, this argument is - forwarded to `autoscale_view` (regardless of its value); see the - description of its behavior there. + forwarded to `~.axes.Axes.autoscale_view` (regardless of + its value); see the description of its behavior there. """ if enable is None: scalex = True scaley = True else: - scalex = False - scaley = False if axis in ['x', 'both']: - self._autoscaleXon = bool(enable) - scalex = self._autoscaleXon + self.set_autoscalex_on(bool(enable)) + scalex = self.get_autoscalex_on() + else: + scalex = False if axis in ['y', 'both']: - self._autoscaleYon = bool(enable) - scaley = self._autoscaleYon + self.set_autoscaley_on(bool(enable)) + scaley = self.get_autoscaley_on() + else: + scaley = False if tight and scalex: self._xmargin = 0 if tight and scaley: self._ymargin = 0 - self._request_autoscale_view(tight=tight, scalex=scalex, scaley=scaley) + if scalex: + self._request_autoscale_view("x", tight=tight) + if scaley: + self._request_autoscale_view("y", tight=tight) def autoscale_view(self, tight=None, scalex=True, scaley=True): """ @@ -2644,10 +2827,10 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): behaves like True). scalex : bool, default: True - Whether to autoscale the x axis. + Whether to autoscale the x-axis. scaley : bool, default: True - Whether to autoscale the y axis. + Whether to autoscale the y-axis. Notes ----- @@ -2658,7 +2841,7 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): case, use :meth:`matplotlib.axes.Axes.relim` prior to calling autoscale_view. - If the views of the axes are fixed, e.g. via `set_xlim`, they will + If the views of the Axes are fixed, e.g. via `set_xlim`, they will not be changed by autoscale_view(). See :meth:`matplotlib.axes.Axes.autoscale` for an alternative. """ @@ -2667,53 +2850,47 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True): x_stickies = y_stickies = np.array([]) if self.use_sticky_edges: - # Only iterate over axes and artists if needed. The check for - # ``hasattr(ax, "lines")`` is necessary because this can be called - # very early in the axes init process (e.g., for twin axes) when - # these attributes don't even exist yet, in which case - # `get_children` would raise an AttributeError. - if self._xmargin and scalex and self._autoscaleXon: + if self._xmargin and scalex and self.get_autoscalex_on(): x_stickies = np.sort(np.concatenate([ artist.sticky_edges.x - for ax in self._shared_x_axes.get_siblings(self) - if hasattr(ax, "lines") + for ax in self._shared_axes["x"].get_siblings(self) for artist in ax.get_children()])) - if self._ymargin and scaley and self._autoscaleYon: + if self._ymargin and scaley and self.get_autoscaley_on(): y_stickies = np.sort(np.concatenate([ artist.sticky_edges.y - for ax in self._shared_y_axes.get_siblings(self) - if hasattr(ax, "lines") + for ax in self._shared_axes["y"].get_siblings(self) for artist in ax.get_children()])) if self.get_xscale() == 'log': x_stickies = x_stickies[x_stickies > 0] if self.get_yscale() == 'log': y_stickies = y_stickies[y_stickies > 0] - def handle_single_axis(scale, autoscaleon, shared_axes, interval, - minpos, axis, margin, stickies, set_bound): + def handle_single_axis( + scale, shared_axes, name, axis, margin, stickies, set_bound): - if not (scale and autoscaleon): + if not (scale and axis._get_autoscale_on()): return # nothing to do... shared = shared_axes.get_siblings(self) # Base autoscaling on finite data limits when there is at least one # finite data limit among all the shared_axes and intervals. - # Also, find the minimum minpos for use in the margin calculation. - x_values = [] - minimum_minpos = np.inf - for ax in shared: - x_values.extend(getattr(ax.dataLim, interval)) - minimum_minpos = min(minimum_minpos, - getattr(ax.dataLim, minpos)) - x_values = np.extract(np.isfinite(x_values), x_values) - if x_values.size >= 1: - x0, x1 = (x_values.min(), x_values.max()) + values = [val for ax in shared + for val in getattr(ax.dataLim, f"interval{name}") + if np.isfinite(val)] + if values: + x0, x1 = (min(values), max(values)) + elif getattr(self._viewLim, f"mutated{name}")(): + # No data, but explicit viewLims already set: + # in mutatedx or mutatedy. + return else: x0, x1 = (-np.inf, np.inf) - # If x0 and x1 are non finite, use the locator to figure out - # default limits. + # If x0 and x1 are nonfinite, get default limits from the locator. locator = axis.get_major_locator() x0, x1 = locator.nonsingular(x0, x1) + # Find the minimum minpos for use in the margin calculation. + minimum_minpos = min( + getattr(ax.dataLim, f"minpos{name}") for ax in shared) # Prevent margin addition from crossing a sticky value. A small # tolerance must be added due to floating point issues with @@ -2751,32 +2928,11 @@ def handle_single_axis(scale, autoscaleon, shared_axes, interval, # End of definition of internal function 'handle_single_axis'. handle_single_axis( - scalex, self._autoscaleXon, self._shared_x_axes, 'intervalx', - 'minposx', self.xaxis, self._xmargin, x_stickies, self.set_xbound) + scalex, self._shared_axes["x"], 'x', self.xaxis, self._xmargin, + x_stickies, self.set_xbound) handle_single_axis( - scaley, self._autoscaleYon, self._shared_y_axes, 'intervaly', - 'minposy', self.yaxis, self._ymargin, y_stickies, self.set_ybound) - - def _get_axis_list(self): - return self.xaxis, self.yaxis - - def _get_axis_map(self): - """ - Return a mapping of `Axis` "names" to `Axis` instances. - - The `Axis` name is derived from the attribute under which the instance - is stored, so e.g. for polar axes, the theta-axis is still named "x" - and the r-axis is still named "y" (for back-compatibility). - - In practice, this means that the entries are typically "x" and "y", and - additionally "z" for 3D axes. - """ - d = {} - axis_list = self._get_axis_list() - for k, v in vars(self).items(): - if k.endswith("axis") and v in axis_list: - d[k[:-len("axis")]] = v - return d + scaley, self._shared_axes["y"], 'y', self.yaxis, self._ymargin, + y_stickies, self.set_ybound) def _update_title_position(self, renderer): """ @@ -2789,35 +2945,39 @@ def _update_title_position(self, renderer): titles = (self.title, self._left_title, self._right_title) + # Need to check all our twins too, and all the children as well. + axs = self._twinned_axes.get_siblings(self) + self.child_axes + for ax in self.child_axes: # Child positions must be updated first. + locator = ax.get_axes_locator() + ax.apply_aspect(locator(self, renderer) if locator else None) + for title in titles: x, _ = title.get_position() # need to start again in case of window resizing title.set_position((x, 1.0)) - # need to check all our twins too... - axs = self._twinned_axes.get_siblings(self) - # and all the children - for ax in self.child_axes: - if ax is not None: - locator = ax.get_axes_locator() - if locator: - pos = locator(self, renderer) - ax.apply_aspect(pos) - else: - ax.apply_aspect() - axs = axs + [ax] - top = -np.Inf + top = -np.inf for ax in axs: + bb = None if (ax.xaxis.get_ticks_position() in ['top', 'unknown'] or ax.xaxis.get_label_position() == 'top'): bb = ax.xaxis.get_tightbbox(renderer) - else: - bb = ax.get_window_extent(renderer) - if bb is not None: - top = max(top, bb.ymax) + if bb is None: + if 'outline' in ax.spines: + # Special case for colorbars: + bb = ax.spines['outline'].get_window_extent() + else: + bb = ax.get_window_extent(renderer) + top = max(top, bb.ymax) + if title.get_text(): + ax.yaxis.get_tightbbox(renderer) # update offsetText + if ax.yaxis.offsetText.get_text(): + bb = ax.yaxis.offsetText.get_tightbbox(renderer) + if bb.intersection(title.get_tightbbox(renderer), bb): + top = bb.ymax if top < 0: - # the top of axes is not even on the figure, so don't try and + # the top of Axes is not even on the figure, so don't try and # automatically place it. - _log.debug('top of axes not in the figure, so title not moved') + _log.debug('top of Axes not in the figure, so title not moved') return if title.get_window_extent(renderer).ymin < top: _, y = self.transAxes.inverted().transform((0, top)) @@ -2837,17 +2997,8 @@ def _update_title_position(self, renderer): # Drawing @martist.allow_rasterization - @_api.delete_parameter( - "3.3", "inframe", alternative="Axes.redraw_in_frame()") - def draw(self, renderer=None, inframe=False): + def draw(self, renderer): # docstring inherited - if renderer is None: - _api.warn_deprecated( - "3.3", message="Support for not passing the 'renderer' " - "parameter to Axes.draw() is deprecated since %(since)s and " - "will be removed %(removal)s. Use axes.draw_artist(axes) " - "instead.") - renderer = self.figure._cachedRenderer if renderer is None: raise RuntimeError('No renderer defined') if not self.get_visible(): @@ -2859,18 +3010,14 @@ def draw(self, renderer=None, inframe=False): # prevent triggering call backs during the draw process self._stale = True - # loop over self and child axes... + # loop over self and child Axes... locator = self.get_axes_locator() - if locator: - pos = locator(self, renderer) - self.apply_aspect(pos) - else: - self.apply_aspect() + self.apply_aspect(locator(self, renderer) if locator else None) artists = self.get_children() artists.remove(self.patch) - # the frame draws the edges around the axes patch -- we + # the frame draws the edges around the Axes patch -- we # decouple these so the patch can be in the background and the # frame in the foreground. Do this before drawing the axis # objects so that the spine has the opportunity to update them. @@ -2880,18 +3027,14 @@ def draw(self, renderer=None, inframe=False): self._update_title_position(renderer) - if not self.axison or inframe: - for _axis in self._get_axis_list(): + if not self.axison: + for _axis in self._axis_map.values(): artists.remove(_axis) - if inframe: - artists.remove(self.title) - artists.remove(self._left_title) - artists.remove(self._right_title) - if not self.figure.canvas.is_saving(): - artists = [a for a in artists - if not a.get_animated() or a in self.images] + artists = [ + a for a in artists + if not a.get_animated() or isinstance(a, mimage.AxesImage)] artists = sorted(artists, key=attrgetter('zorder')) # rasterize artists with negative zorder @@ -2900,25 +3043,26 @@ def draw(self, renderer=None, inframe=False): if (rasterization_zorder is not None and artists and artists[0].zorder < rasterization_zorder): - renderer.start_rasterizing() - artists_rasterized = [a for a in artists - if a.zorder < rasterization_zorder] - artists = [a for a in artists - if a.zorder >= rasterization_zorder] + split_index = np.searchsorted( + [art.zorder for art in artists], + rasterization_zorder, side='right' + ) + artists_rasterized = artists[:split_index] + artists = artists[split_index:] else: artists_rasterized = [] - # the patch draws the background rectangle -- the frame below - # will draw the edges if self.axison and self._frameon: - self.patch.draw(renderer) + if artists_rasterized: + artists_rasterized = [self.patch] + artists_rasterized + else: + artists = [self.patch] + artists if artists_rasterized: - for a in artists_rasterized: - a.draw(renderer) - renderer.stop_rasterizing() + _draw_rasterized(self.figure, artists_rasterized, renderer) - mimage._draw_list_compositing_images(renderer, self, artists) + mimage._draw_list_compositing_images( + renderer, self, artists, self.figure.suppressComposite) renderer.close_group('axes') self.stale = False @@ -2926,44 +3070,32 @@ def draw(self, renderer=None, inframe=False): def draw_artist(self, a): """ Efficiently redraw a single artist. - - This method can only be used after an initial draw of the figure, - because that creates and caches the renderer needed here. """ - if self.figure._cachedRenderer is None: - raise AttributeError("draw_artist can only be used after an " - "initial draw which caches the renderer") - a.draw(self.figure._cachedRenderer) + a.draw(self.figure.canvas.get_renderer()) def redraw_in_frame(self): """ 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 " - "initial draw which caches the renderer") with ExitStack() as stack: - for artist in [*self._get_axis_list(), + for artist in [*self._axis_map.values(), self.title, self._left_title, self._right_title]: - stack.callback(artist.set_visible, artist.get_visible()) - artist.set_visible(False) - self.draw(self.figure._cachedRenderer) + stack.enter_context(artist._cm_set(visible=False)) + self.draw(self.figure.canvas.get_renderer()) + @_api.deprecated("3.6", alternative="Axes.figure.canvas.get_renderer()") def get_renderer_cache(self): - return self.figure._cachedRenderer + return self.figure.canvas.get_renderer() # Axes rectangle characteristics def get_frame_on(self): - """Get whether the axes rectangle patch is drawn.""" + """Get whether the Axes rectangle patch is drawn.""" return self._frameon def set_frame_on(self, b): """ - Set whether the axes rectangle patch is drawn. + Set whether the Axes rectangle patch is drawn. Parameters ---------- @@ -3009,31 +3141,29 @@ def set_axisbelow(self, b): -------- get_axisbelow """ + # Check that b is True, False or 'line' self._axisbelow = axisbelow = validate_axisbelow(b) - if axisbelow is True: - zorder = 0.5 - elif axisbelow is False: - zorder = 2.5 - elif axisbelow == "line": - zorder = 1.5 - else: - raise ValueError("Unexpected axisbelow value") - for axis in self._get_axis_list(): + zorder = { + True: 0.5, + 'line': 1.5, + False: 2.5, + }[axisbelow] + for axis in self._axis_map.values(): axis.set_zorder(zorder) self.stale = True - @docstring.dedent_interpd - def grid(self, b=None, which='major', axis='both', **kwargs): + @_docstring.dedent_interpd + def grid(self, visible=None, which='major', axis='both', **kwargs): """ Configure the grid lines. Parameters ---------- - b : bool or None, optional - Whether to show the grid lines. If any *kwargs* are supplied, - it is assumed you want the grid on and *b* will be set to True. + visible : bool or None, optional + Whether to show the grid lines. If any *kwargs* are supplied, it + is assumed you want the grid on and *visible* will be set to True. - If *b* is *None* and there are no *kwargs*, this toggles the + If *visible* is *None* and there are no *kwargs*, this toggles the visibility of the lines. which : {'major', 'minor', 'both'}, optional @@ -3049,7 +3179,7 @@ def grid(self, b=None, which='major', axis='both', **kwargs): Valid keyword arguments are: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s Notes ----- @@ -3061,14 +3191,14 @@ def grid(self, b=None, which='major', axis='both', **kwargs): """ _api.check_in_list(['x', 'y', 'both'], axis=axis) if axis in ['x', 'both']: - self.xaxis.grid(b, which=which, **kwargs) + self.xaxis.grid(visible, which=which, **kwargs) if axis in ['y', 'both']: - self.yaxis.grid(b, which=which, **kwargs) + self.yaxis.grid(visible, which=which, **kwargs) def ticklabel_format(self, *, axis='both', style='', scilimits=None, useOffset=None, useLocale=None, useMathText=None): r""" - Configure the `.ScalarFormatter` used by default for linear axes. + Configure the `.ScalarFormatter` used by default for linear Axes. If a parameter is not set, the corresponding property of the formatter is left unchanged. @@ -3076,7 +3206,7 @@ def ticklabel_format(self, *, axis='both', style='', scilimits=None, Parameters ---------- axis : {'x', 'y', 'both'}, default: 'both' - The axes to configure. Only major ticks are affected. + The axis to configure. Only major ticks are affected. style : {'sci', 'scientific', 'plain'} Whether to use scientific notation. @@ -3121,8 +3251,8 @@ def ticklabel_format(self, *, axis='both', style='', scilimits=None, ) from err STYLES = {'sci': True, 'scientific': True, 'plain': False, '': None} is_sci_style = _api.check_getitem(STYLES, style=style) - axis_map = {**{k: [v] for k, v in self._get_axis_map().items()}, - 'both': self._get_axis_list()} + axis_map = {**{k: [v] for k, v in self._axis_map.items()}, + 'both': list(self._axis_map.values())} axises = _api.check_getitem(axis_map, axis=axis) try: for axis in axises: @@ -3150,8 +3280,8 @@ def locator_params(self, axis='both', tight=None, **kwargs): Parameters ---------- axis : {'both', 'x', 'y'}, default: 'both' - The axis on which to operate. - + The axis on which to operate. (For 3D Axes, *axis* can also be + set to 'z', and 'both' refers to all three axes.) tight : bool or None, optional Parameter passed to `~.Axes.autoscale_view`. Default is None, for no change. @@ -3163,7 +3293,7 @@ def locator_params(self, axis='both', tight=None, **kwargs): ``set_params()`` method of the locator. Supported keywords depend on the type of the locator. See for example `~.ticker.MaxNLocator.set_params` for the `.ticker.MaxNLocator` - used by default for linear axes. + used by default for linear. Examples -------- @@ -3173,15 +3303,12 @@ def locator_params(self, axis='both', tight=None, **kwargs): ax.locator_params(tight=True, nbins=4) """ - _api.check_in_list(['x', 'y', 'both'], axis=axis) - update_x = axis in ['x', 'both'] - update_y = axis in ['y', 'both'] - if update_x: - self.xaxis.get_major_locator().set_params(**kwargs) - if update_y: - self.yaxis.get_major_locator().set_params(**kwargs) - self._request_autoscale_view(tight=tight, - scalex=update_x, scaley=update_y) + _api.check_in_list([*self._axis_names, "both"], axis=axis) + for name in self._axis_names: + if axis in [name, "both"]: + loc = self._axis_map[name].get_major_locator() + loc.set_params(**kwargs) + self._request_autoscale_view(name, tight=tight) self.stale = True def tick_params(self, axis='both', **kwargs): @@ -3189,7 +3316,8 @@ def tick_params(self, axis='both', **kwargs): 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. + arguments remain unchanged unless *reset* is True. For the current + style settings, see `.Axis.get_tick_params`. Parameters ---------- @@ -3203,7 +3331,7 @@ def tick_params(self, axis='both', **kwargs): Other Parameters ---------------- direction : {'in', 'out', 'inout'} - Puts ticks inside the axes, outside the axes, or both. + Puts ticks inside the Axes, outside the Axes, or both. length : float Tick length in points. width : float @@ -3301,7 +3429,7 @@ def set_xlabel(self, xlabel, fontdict=None, labelpad=None, *, The label text. labelpad : float, default: :rc:`axes.labelpad` - Spacing in points from the axes bounding box including ticks + Spacing in points from the Axes bounding box including ticks and tick labels. If None, the previous value is left as is. loc : {'left', 'center', 'right'}, default: :rc:`xaxis.labellocation` @@ -3326,15 +3454,19 @@ def set_xlabel(self, xlabel, fontdict=None, labelpad=None, *, f"its corresponding low level keyword " f"arguments ({protected_kw}) are also " f"supplied") - loc = 'center' + else: loc = (loc if loc is not None else mpl.rcParams['xaxis.labellocation']) - _api.check_in_list(('left', 'center', 'right'), loc=loc) - if loc == 'left': - kwargs.update(x=0, horizontalalignment='left') - elif loc == 'right': - kwargs.update(x=1, horizontalalignment='right') + _api.check_in_list(('left', 'center', 'right'), loc=loc) + + x = { + 'left': 0, + 'center': 0.5, + 'right': 1, + }[loc] + kwargs.update(x=x, horizontalalignment=loc) + return self.xaxis.set_label_text(xlabel, fontdict, **kwargs) def invert_xaxis(self): @@ -3371,7 +3503,7 @@ def set_xbound(self, lower=None, upper=None): """ Set the lower and upper numerical bounds of the x-axis. - This method will honor axes inversion regardless of parameter order. + This method will honor axis inversion regardless of parameter order. It will not change the autoscaling setting (`.get_autoscalex_on()`). Parameters @@ -3410,7 +3542,7 @@ def get_xlim(self): See Also -------- - set_xlim + .Axes.set_xlim set_xbound, get_xbound invert_xaxis, xaxis_inverted @@ -3418,7 +3550,6 @@ def get_xlim(self): ----- The x-axis may be inverted, in which case the *left* value will be greater than the *right* value. - """ return tuple(self.viewLim.intervalx) @@ -3439,6 +3570,7 @@ def _validate_converted_limits(self, limit, convert): raise ValueError("Axis limits cannot be NaN or Inf") return converted_limit + @_api.make_keyword_only("3.6", "emit") def set_xlim(self, left=None, right=None, emit=True, auto=False, *, xmin=None, xmax=None): """ @@ -3468,9 +3600,8 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, False turns off, None leaves unchanged. xmin, xmax : float, optional - They are equivalent to left and right respectively, - and it is an error to pass both *xmin* and *left* or - *xmax* and *right*. + They are equivalent to left and right respectively, and it is an + error to pass both *xmin* and *left* or *xmax* and *right*. Returns ------- @@ -3505,126 +3636,28 @@ def set_xlim(self, left=None, right=None, emit=True, auto=False, present is on the right. >>> set_xlim(5000, 0) - """ if right is None and np.iterable(left): left, right = left if xmin is not None: if left is not None: - raise TypeError('Cannot pass both `xmin` and `left`') + raise TypeError("Cannot pass both 'left' and 'xmin'") left = xmin if xmax is not None: if right is not None: - raise TypeError('Cannot pass both `xmax` and `right`') + raise TypeError("Cannot pass both 'right' and 'xmax'") right = xmax - - self._process_unit_info([("x", (left, right))], convert=False) - left = self._validate_converted_limits(left, self.convert_xunits) - right = self._validate_converted_limits(right, self.convert_xunits) - - if left is None or right is None: - # Axes init calls set_xlim(0, 1) before get_xlim() can be called, - # so only grab the limits if we really need them. - old_left, old_right = self.get_xlim() - if left is None: - left = old_left - if right is None: - right = old_right - - if self.get_xscale() == 'log' and (left <= 0 or right <= 0): - # Axes init calls set_xlim(0, 1) before get_xlim() can be called, - # so only grab the limits if we really need them. - old_left, old_right = self.get_xlim() - if left <= 0: - _api.warn_external( - 'Attempted to set non-positive left xlim on a ' - 'log-scaled axis.\n' - 'Invalid limit will be ignored.') - left = old_left - if right <= 0: - _api.warn_external( - 'Attempted to set non-positive right xlim on a ' - 'log-scaled axis.\n' - 'Invalid limit will be ignored.') - right = old_right - if left == right: - _api.warn_external( - f"Attempting to set identical left == right == {left} results " - f"in singular transformations; automatically expanding.") - reverse = left > right - left, right = self.xaxis.get_major_locator().nonsingular(left, right) - left, right = self.xaxis.limit_range_for_scale(left, right) - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - left, right = sorted([left, right], reverse=bool(reverse)) - - self._viewLim.intervalx = (left, right) - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_x_axes.get_siblings(self): - ax._stale_viewlim_x = False - if auto is not None: - self._autoscaleXon = bool(auto) - - if emit: - self.callbacks.process('xlim_changed', self) - # Call all of the other x-axes that are shared with this one - for other in self._shared_x_axes.get_siblings(self): - if other is not self: - other.set_xlim(self.viewLim.intervalx, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return left, right + return self.xaxis._set_lim(left, right, emit=emit, auto=auto) get_xscale = _axis_method_wrapper("xaxis", "get_scale") - - def set_xscale(self, value, **kwargs): - """ - Set the x-axis scale. - - Parameters - ---------- - value : {"linear", "log", "symlog", "logit", ...} or `.ScaleBase` - The axis scale type to apply. - - **kwargs - Different keyword arguments are accepted, depending on the scale. - See the respective class keyword arguments: - - - `matplotlib.scale.LinearScale` - - `matplotlib.scale.LogScale` - - `matplotlib.scale.SymmetricalLogScale` - - `matplotlib.scale.LogitScale` - - `matplotlib.scale.FuncScale` - - Notes - ----- - By default, Matplotlib supports the above mentioned scales. - Additionally, custom scales may be registered using - `matplotlib.scale.register_scale`. These scales can then also - be used here. - """ - old_default_lims = (self.xaxis.get_major_locator() - .nonsingular(-np.inf, np.inf)) - g = self.get_shared_x_axes() - for ax in g.get_siblings(self): - ax.xaxis._set_scale(value, **kwargs) - ax._update_transScale() - ax.stale = True - new_default_lims = (self.xaxis.get_major_locator() - .nonsingular(-np.inf, np.inf)) - if old_default_lims != new_default_lims: - # Force autoscaling now, to take advantage of the scale locator's - # nonsingular() before it possibly gets swapped out by the user. - self.autoscale_view(scaley=False) - + set_xscale = _axis_method_wrapper("xaxis", "_set_axes_scale") get_xticks = _axis_method_wrapper("xaxis", "get_ticklocs") set_xticks = _axis_method_wrapper("xaxis", "set_ticks") get_xmajorticklabels = _axis_method_wrapper("xaxis", "get_majorticklabels") get_xminorticklabels = _axis_method_wrapper("xaxis", "get_minorticklabels") get_xticklabels = _axis_method_wrapper("xaxis", "get_ticklabels") set_xticklabels = _axis_method_wrapper( - "xaxis", "_set_ticklabels", + "xaxis", "set_ticklabels", doc_sub={"Axis.set_ticks": "Axes.set_xticks"}) def get_ylabel(self): @@ -3645,7 +3678,7 @@ def set_ylabel(self, ylabel, fontdict=None, labelpad=None, *, The label text. labelpad : float, default: :rc:`axes.labelpad` - Spacing in points from the axes bounding box including ticks + Spacing in points from the Axes bounding box including ticks and tick labels. If None, the previous value is left as is. loc : {'bottom', 'center', 'top'}, default: :rc:`yaxis.labellocation` @@ -3670,15 +3703,19 @@ def set_ylabel(self, ylabel, fontdict=None, labelpad=None, *, f"its corresponding low level keyword " f"arguments ({protected_kw}) are also " f"supplied") - loc = 'center' + else: loc = (loc if loc is not None else mpl.rcParams['yaxis.labellocation']) - _api.check_in_list(('bottom', 'center', 'top'), loc=loc) - if loc == 'bottom': - kwargs.update(y=0, horizontalalignment='left') - elif loc == 'top': - kwargs.update(y=1, horizontalalignment='right') + _api.check_in_list(('bottom', 'center', 'top'), loc=loc) + + y, ha = { + 'bottom': (0, 'left'), + 'center': (0.5, 'center'), + 'top': (1, 'right') + }[loc] + kwargs.update(y=y, horizontalalignment=ha) + return self.yaxis.set_label_text(ylabel, fontdict, **kwargs) def invert_yaxis(self): @@ -3715,7 +3752,7 @@ def set_ybound(self, lower=None, upper=None): """ Set the lower and upper numerical bounds of the y-axis. - This method will honor axes inversion regardless of parameter order. + This method will honor axis inversion regardless of parameter order. It will not change the autoscaling setting (`.get_autoscaley_on()`). Parameters @@ -3754,7 +3791,7 @@ def get_ylim(self): See Also -------- - set_ylim + .Axes.set_ylim set_ybound, get_ybound invert_yaxis, yaxis_inverted @@ -3762,10 +3799,10 @@ def get_ylim(self): ----- The y-axis may be inverted, in which case the *bottom* value will be greater than the *top* value. - """ return tuple(self.viewLim.intervaly) + @_api.make_keyword_only("3.6", "emit") def set_ylim(self, bottom=None, top=None, emit=True, auto=False, *, ymin=None, ymax=None): """ @@ -3795,9 +3832,8 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, *False* turns off, *None* leaves unchanged. ymin, ymax : float, optional - They are equivalent to bottom and top respectively, - and it is an error to pass both *ymin* and *bottom* or - *ymax* and *top*. + They are equivalent to bottom and top respectively, and it is an + error to pass both *ymin* and *bottom* or *ymax* and *top*. Returns ------- @@ -3837,121 +3873,23 @@ def set_ylim(self, bottom=None, top=None, emit=True, auto=False, bottom, top = bottom if ymin is not None: if bottom is not None: - raise TypeError('Cannot pass both `ymin` and `bottom`') + raise TypeError("Cannot pass both 'bottom' and 'ymin'") bottom = ymin if ymax is not None: if top is not None: - raise TypeError('Cannot pass both `ymax` and `top`') + raise TypeError("Cannot pass both 'top' and 'ymax'") top = ymax - - self._process_unit_info([("y", (bottom, top))], convert=False) - bottom = self._validate_converted_limits(bottom, self.convert_yunits) - top = self._validate_converted_limits(top, self.convert_yunits) - - if bottom is None or top is None: - # Axes init calls set_ylim(0, 1) before get_ylim() can be called, - # so only grab the limits if we really need them. - old_bottom, old_top = self.get_ylim() - if bottom is None: - bottom = old_bottom - if top is None: - top = old_top - - if self.get_yscale() == 'log' and (bottom <= 0 or top <= 0): - # Axes init calls set_xlim(0, 1) before get_xlim() can be called, - # so only grab the limits if we really need them. - old_bottom, old_top = self.get_ylim() - if bottom <= 0: - _api.warn_external( - 'Attempted to set non-positive bottom ylim on a ' - 'log-scaled axis.\n' - 'Invalid limit will be ignored.') - bottom = old_bottom - if top <= 0: - _api.warn_external( - 'Attempted to set non-positive top ylim on a ' - 'log-scaled axis.\n' - 'Invalid limit will be ignored.') - top = old_top - if bottom == top: - _api.warn_external( - f"Attempting to set identical bottom == top == {bottom} " - f"results in singular transformations; automatically " - f"expanding.") - reverse = bottom > top - bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top) - bottom, top = self.yaxis.limit_range_for_scale(bottom, top) - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - bottom, top = sorted([bottom, top], reverse=bool(reverse)) - - self._viewLim.intervaly = (bottom, top) - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_y_axes.get_siblings(self): - ax._stale_viewlim_y = False - if auto is not None: - self._autoscaleYon = bool(auto) - - if emit: - self.callbacks.process('ylim_changed', self) - # Call all of the other y-axes that are shared with this one - for other in self._shared_y_axes.get_siblings(self): - if other is not self: - other.set_ylim(self.viewLim.intervaly, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return bottom, top + return self.yaxis._set_lim(bottom, top, emit=emit, auto=auto) get_yscale = _axis_method_wrapper("yaxis", "get_scale") - - def set_yscale(self, value, **kwargs): - """ - Set the y-axis scale. - - Parameters - ---------- - value : {"linear", "log", "symlog", "logit", ...} or `.ScaleBase` - The axis scale type to apply. - - **kwargs - Different keyword arguments are accepted, depending on the scale. - See the respective class keyword arguments: - - - `matplotlib.scale.LinearScale` - - `matplotlib.scale.LogScale` - - `matplotlib.scale.SymmetricalLogScale` - - `matplotlib.scale.LogitScale` - - `matplotlib.scale.FuncScale` - - Notes - ----- - By default, Matplotlib supports the above mentioned scales. - Additionally, custom scales may be registered using - `matplotlib.scale.register_scale`. These scales can then also - be used here. - """ - old_default_lims = (self.yaxis.get_major_locator() - .nonsingular(-np.inf, np.inf)) - g = self.get_shared_y_axes() - for ax in g.get_siblings(self): - ax.yaxis._set_scale(value, **kwargs) - ax._update_transScale() - ax.stale = True - new_default_lims = (self.yaxis.get_major_locator() - .nonsingular(-np.inf, np.inf)) - if old_default_lims != new_default_lims: - # Force autoscaling now, to take advantage of the scale locator's - # nonsingular() before it possibly gets swapped out by the user. - self.autoscale_view(scalex=False) - + set_yscale = _axis_method_wrapper("yaxis", "_set_axes_scale") get_yticks = _axis_method_wrapper("yaxis", "get_ticklocs") set_yticks = _axis_method_wrapper("yaxis", "set_ticks") get_ymajorticklabels = _axis_method_wrapper("yaxis", "get_majorticklabels") get_yminorticklabels = _axis_method_wrapper("yaxis", "get_minorticklabels") get_yticklabels = _axis_method_wrapper("yaxis", "get_ticklabels") set_yticklabels = _axis_method_wrapper( - "yaxis", "_set_ticklabels", + "yaxis", "set_ticklabels", doc_sub={"Axis.set_ticks": "Axes.set_yticks"}) xaxis_date = _axis_method_wrapper("xaxis", "axis_date") @@ -3969,7 +3907,7 @@ def format_xdata(self, x): def format_ydata(self, y): """ - Return *y* formatted as an y-value. + Return *y* formatted as a y-value. This function will use the `.fmt_ydata` attribute if it is not None, else will fall back on the yaxis major formatter. @@ -3979,19 +3917,14 @@ def format_ydata(self, y): def format_coord(self, x, y): """Return a format string formatting the *x*, *y* coordinates.""" - if x is None: - xs = '???' - else: - xs = self.format_xdata(x) - if y is None: - ys = '???' - else: - ys = self.format_ydata(y) - return 'x=%s y=%s' % (xs, ys) + return "x={} y={}".format( + "???" if x is None else self.format_xdata(x), + "???" if y is None else self.format_ydata(y), + ) def minorticks_on(self): """ - Display minor ticks on the axes. + Display minor ticks on the Axes. Displaying minor ticks may reduce performance; you may turn them off using `minorticks_off()` if drawing speed is a problem. @@ -4009,7 +3942,7 @@ def minorticks_on(self): ax.set_minor_locator(mticker.AutoMinorLocator()) def minorticks_off(self): - """Remove minor ticks from the axes.""" + """Remove minor ticks from the Axes.""" self.xaxis.set_minor_locator(mticker.NullLocator()) self.yaxis.set_minor_locator(mticker.NullLocator()) @@ -4017,25 +3950,25 @@ def minorticks_off(self): def can_zoom(self): """ - Return whether this axes supports the zoom box button functionality. + Return whether this Axes supports the zoom box button functionality. """ return True def can_pan(self): """ - Return whether this axes supports any pan/zoom button functionality. + Return whether this Axes supports any pan/zoom button functionality. """ return True def get_navigate(self): """ - Get whether the axes responds to navigation commands + Get whether the Axes responds to navigation commands. """ return self._navigate def set_navigate(self, b): """ - Set whether the axes responds to navigation toolbar commands + Set whether the Axes responds to navigation toolbar commands. Parameters ---------- @@ -4045,16 +3978,16 @@ def set_navigate(self, b): def get_navigate_mode(self): """ - Get the navigation toolbar button status: 'PAN', 'ZOOM', or None + Get the navigation toolbar button status: 'PAN', 'ZOOM', or None. """ return self._navigate_mode def set_navigate_mode(self, b): """ - Set the navigation toolbar button status; + Set the navigation toolbar button status. - .. warning :: - this is not a user-API function. + .. warning:: + This is not a user-API function. """ self._navigate_mode = b @@ -4093,41 +4026,14 @@ def _set_view(self, view): self.set_xlim((xmin, xmax)) self.set_ylim((ymin, ymax)) - def _set_view_from_bbox(self, bbox, direction='in', - mode=None, twinx=False, twiny=False): + def _prepare_view_from_bbox(self, bbox, direction='in', + mode=None, twinx=False, twiny=False): """ - Update view from a selection bbox. + Helper function to prepare the new bounds from a bbox. - .. note:: - - Intended to be overridden by new projection types, but if not, the - default implementation sets the view limits to the bbox directly. - - Parameters - ---------- - bbox : 4-tuple or 3 tuple - * If bbox is a 4 tuple, it is the selected bounding box limits, - in *display* coordinates. - * If bbox is a 3 tuple, it is an (xp, yp, scl) triple, where - (xp, yp) is the center of zooming and scl the scale factor to - zoom by. - - direction : str - The direction to apply the bounding box. - * `'in'` - The bounding box describes the view directly, i.e., - it zooms in. - * `'out'` - The bounding box describes the size to make the - existing view, i.e., it zooms out. - - mode : str or None - The selection mode, whether to apply the bounding box in only the - `'x'` direction, `'y'` direction or both (`None`). - - twinx : bool - Whether this axis is twinned in the *x*-direction. - - twiny : bool - Whether this axis is twinned in the *y*-direction. + This helper function returns the new x and y bounds from the zoom + bbox. This a convenience method to abstract the bbox logic + out of the base setter. """ if len(bbox) == 3: xp, yp, scl = bbox # Zooming code @@ -4184,7 +4090,7 @@ def _set_view_from_bbox(self, bbox, direction='in', [xmin0, xmax0, xmin, xmax]) # To screen space. factor = (sxmax0 - sxmin0) / (sxmax - sxmin) # Unzoom factor. # Move original bounds away by - # (factor) x (distance between unzoom box and axes bbox). + # (factor) x (distance between unzoom box and Axes bbox). sxmin1 = sxmin0 - factor * (sxmin - sxmin0) sxmax1 = sxmax0 + factor * (sxmax0 - sxmax) # And back to data space. @@ -4198,6 +4104,46 @@ def _set_view_from_bbox(self, bbox, direction='in', symax1 = symax0 + factor * (symax0 - symax) new_ybound = y_trf.inverted().transform([symin1, symax1]) + return new_xbound, new_ybound + + def _set_view_from_bbox(self, bbox, direction='in', + mode=None, twinx=False, twiny=False): + """ + Update view from a selection bbox. + + .. note:: + + Intended to be overridden by new projection types, but if not, the + default implementation sets the view limits to the bbox directly. + + Parameters + ---------- + bbox : 4-tuple or 3 tuple + * If bbox is a 4 tuple, it is the selected bounding box limits, + in *display* coordinates. + * If bbox is a 3 tuple, it is an (xp, yp, scl) triple, where + (xp, yp) is the center of zooming and scl the scale factor to + zoom by. + + direction : str + The direction to apply the bounding box. + * `'in'` - The bounding box describes the view directly, i.e., + it zooms in. + * `'out'` - The bounding box describes the size to make the + existing view, i.e., it zooms out. + + mode : str or None + The selection mode, whether to apply the bounding box in only the + `'x'` direction, `'y'` direction or both (`None`). + + twinx : bool + Whether this axis is twinned in the *x*-direction. + + twiny : bool + Whether this axis is twinned in the *y*-direction. + """ + new_xbound, new_ybound = self._prepare_view_from_bbox( + bbox, direction=direction, mode=mode, twinx=twinx, twiny=twiny) if not twinx and mode != "y": self.set_xbound(new_xbound) self.set_autoscalex_on(False) @@ -4238,22 +4184,13 @@ def end_pan(self): """ del self._pan_start - def drag_pan(self, button, key, x, y): + def _get_pan_points(self, button, key, x, y): """ - Called when the mouse moves during a pan operation. + Helper function to return the new points after a pan. - Parameters - ---------- - button : `.MouseButton` - The pressed mouse button. - key : str or None - The pressed key, if any. - x, y : float - The mouse coordinates in display coords. - - Notes - ----- - This is intended to be overridden by new projection types. + This helper function returns the points on the axis after a pan has + occurred. This is a convenience method to abstract the pan logic + out of the base setter. """ def format_deltas(key, dx, dy): if key == 'control': @@ -4307,22 +4244,37 @@ def format_deltas(key, dx, dy): points = result.get_points().astype(object) # Just ignore invalid limits (typically, underflow in log-scale). points[~valid] = None - self.set_xlim(points[:, 0]) - self.set_ylim(points[:, 1]) + return points + + def drag_pan(self, button, key, x, y): + """ + Called when the mouse moves during a pan operation. + + Parameters + ---------- + button : `.MouseButton` + The pressed mouse button. + key : str or None + The pressed key, if any. + x, y : float + The mouse coordinates in display coords. + + Notes + ----- + This is intended to be overridden by new projection types. + """ + points = self._get_pan_points(button, key, x, y) + if points is not None: + self.set_xlim(points[:, 0]) + self.set_ylim(points[:, 1]) def get_children(self): # docstring inherited. return [ - *self.collections, - *self.patches, - *self.lines, - *self.texts, - *self.artists, + *self._children, *self.spines.values(), - *self._get_axis_list(), + *self._axis_map.values(), self.title, self._left_title, self._right_title, - *self.tables, - *self.images, *self.child_axes, *([self.legend_] if self.legend_ is not None else []), self.patch, @@ -4337,7 +4289,7 @@ def contains(self, mouseevent): def contains_point(self, point): """ - Return whether *point* (pair of pixel coordinates) is inside the axes + Return whether *point* (pair of pixel coordinates) is inside the Axes patch. """ return self.patch.contains_point(point, radius=1.0) @@ -4353,26 +4305,31 @@ def get_default_bbox_extra_artists(self): artists = self.get_children() + for axis in self._axis_map.values(): + # axis tight bboxes are calculated separately inside + # Axes.get_tightbbox() using for_layout_only=True + artists.remove(axis) if not (self.axison and self._frameon): # don't do bbox on spines if frame not on. for spine in self.spines.values(): artists.remove(spine) - if not self.axison: - for _axis in self._get_axis_list(): - artists.remove(_axis) - artists.remove(self.title) artists.remove(self._left_title) artists.remove(self._right_title) - return [artist for artist in artists - if (artist.get_visible() and artist.get_in_layout())] + # always include types that do not internally implement clipping + # to Axes. may have clip_on set to True and clip_box equivalent + # to ax.bbox but then ignore these properties during draws. + noclip = (_AxesBase, maxis.Axis, + offsetbox.AnnotationBbox, offsetbox.OffsetBox) + return [a for a in artists if a.get_visible() and a.get_in_layout() + and (isinstance(a, noclip) or not a._fully_clipped_to_axes())] - def get_tightbbox(self, renderer, call_axes_locator=True, + def get_tightbbox(self, renderer=None, call_axes_locator=True, bbox_extra_artists=None, *, for_layout_only=False): """ - Return the tight bounding box of the axes, including axis and their + Return the tight bounding box of the Axes, including axis and their decorators (xlabel, title, etc). Artists that have ``artist.set_in_layout(False)`` are not included @@ -4386,7 +4343,7 @@ def get_tightbbox(self, renderer, call_axes_locator=True, bbox_extra_artists : list of `.Artist` or ``None`` List of artists to include in the tight bounding box. If - ``None`` (default), then all artist children of the axes are + ``None`` (default), then all artist children of the Axes are included in the tight bounding box. call_axes_locator : bool, default: True @@ -4394,7 +4351,7 @@ def get_tightbbox(self, renderer, call_axes_locator=True, ``_axes_locator`` attribute, which is necessary to get the correct bounding box. ``call_axes_locator=False`` can be used if the caller is only interested in the relative size of the tightbbox - compared to the axes bbox. + compared to the Axes bbox. for_layout_only : default: False The bounding box will *not* include the x-extent of the title and @@ -4413,36 +4370,21 @@ def get_tightbbox(self, renderer, call_axes_locator=True, """ bb = [] + if renderer is None: + renderer = self.figure._get_renderer() if not self.get_visible(): return None locator = self.get_axes_locator() - if locator and call_axes_locator: - pos = locator(self, renderer) - self.apply_aspect(pos) - else: - self.apply_aspect() - - if self.axison: - if self.xaxis.get_visible(): - try: - bb_xaxis = self.xaxis.get_tightbbox( - renderer, for_layout_only=for_layout_only) - except TypeError: - # in case downstream library has redefined axis: - bb_xaxis = self.xaxis.get_tightbbox(renderer) - if bb_xaxis: - bb.append(bb_xaxis) - if self.yaxis.get_visible(): - try: - bb_yaxis = self.yaxis.get_tightbbox( - renderer, for_layout_only=for_layout_only) - except TypeError: - # in case downstream library has redefined axis: - bb_yaxis = self.yaxis.get_tightbbox(renderer) - if bb_yaxis: - bb.append(bb_yaxis) + self.apply_aspect( + locator(self, renderer) if locator and call_axes_locator else None) + + for axis in self._axis_map.values(): + if self.axison and axis.get_visible(): + ba = martist._get_tightbbox_for_layout_only(axis, renderer) + if ba: + bb.append(ba) self._update_title_position(renderer) axbbox = self.get_window_extent(renderer) bb.append(axbbox) @@ -4463,17 +4405,6 @@ def get_tightbbox(self, renderer, call_axes_locator=True, bbox_artists = self.get_default_bbox_extra_artists() for a in bbox_artists: - # Extra check here to quickly see if clipping is on and - # contained in the axes. If it is, don't get the tightbbox for - # this artist because this can be expensive: - clip_extent = a._get_clipping_extent_bbox() - if clip_extent is not None: - clip_extent = mtransforms.Bbox.intersection( - clip_extent, axbbox) - if np.all(clip_extent.extents == axbbox.extents): - # clip extent is inside the axes bbox so don't check - # this artist - continue bbox = a.get_tightbbox(renderer) if (bbox is not None and 0 < bbox.width < np.inf @@ -4483,18 +4414,24 @@ def get_tightbbox(self, renderer, call_axes_locator=True, [b for b in bb if b.width != 0 or b.height != 0]) def _make_twin_axes(self, *args, **kwargs): - """Make a twinx axes of self. This is used for twinx and twiny.""" - # Typically, SubplotBase._make_twin_axes is called instead of this. + """Make a twinx Axes of self. This is used for twinx and twiny.""" if 'sharex' in kwargs and 'sharey' in kwargs: - raise ValueError("Twinned Axes may share only one axis") - ax2 = self.figure.add_axes( - self.get_position(True), *args, **kwargs, - axes_locator=_TransformedBoundsLocator( - [0, 0, 1, 1], self.transAxes)) + # The following line is added in v2.2 to avoid breaking Seaborn, + # which currently uses this internal API. + if kwargs["sharex"] is not self and kwargs["sharey"] is not self: + raise ValueError("Twinned Axes may share only one axis") + ss = self.get_subplotspec() + if ss: + twin = self.figure.add_subplot(ss, *args, **kwargs) + else: + twin = self.figure.add_axes( + self.get_position(True), *args, **kwargs, + axes_locator=_TransformedBoundsLocator( + [0, 0, 1, 1], self.transAxes)) self.set_adjustable('datalim') - ax2.set_adjustable('datalim') - self._twinned_axes.join(self, ax2) - return ax2 + twin.set_adjustable('datalim') + self._twinned_axes.join(self, twin) + return twin def twinx(self): """ @@ -4514,7 +4451,7 @@ def twinx(self): Notes ----- For those who are 'picking' artists while using twinx, pick - events are only called for the artists in the top-most axes. + events are only called for the artists in the top-most Axes. """ ax2 = self._make_twin_axes(sharex=self) ax2.yaxis.tick_right() @@ -4544,7 +4481,7 @@ def twiny(self): Notes ----- For those who are 'picking' artists while using twiny, pick - events are only called for the artists in the top-most axes. + events are only called for the artists in the top-most Axes. """ ax2 = self._make_twin_axes(sharey=self) ax2.xaxis.tick_top() @@ -4556,9 +4493,119 @@ def twiny(self): return ax2 def get_shared_x_axes(self): - """Return a reference to the shared axes Grouper object for x axes.""" - return self._shared_x_axes + """Return an immutable view on the shared x-axes Grouper.""" + return cbook.GrouperView(self._shared_axes["x"]) def get_shared_y_axes(self): - """Return a reference to the shared axes Grouper object for y axes.""" - return self._shared_y_axes + """Return an immutable view on the shared y-axes Grouper.""" + return cbook.GrouperView(self._shared_axes["y"]) + + def label_outer(self): + """ + Only show "outer" labels and tick labels. + + x-labels are only kept for subplots on the last row (or first row, if + labels are on the top side); y-labels only for subplots on the first + column (or last column, if labels are on the right side). + """ + self._label_outer_xaxis(check_patch=False) + self._label_outer_yaxis(check_patch=False) + + def _label_outer_xaxis(self, *, check_patch): + # see documentation in label_outer. + if check_patch and not isinstance(self.patch, mpl.patches.Rectangle): + return + ss = self.get_subplotspec() + if not ss: + return + label_position = self.xaxis.get_label_position() + if not ss.is_first_row(): # Remove top label/ticklabels/offsettext. + if label_position == "top": + self.set_xlabel("") + self.xaxis.set_tick_params(which="both", labeltop=False) + if self.xaxis.offsetText.get_position()[1] == 1: + self.xaxis.offsetText.set_visible(False) + if not ss.is_last_row(): # Remove bottom label/ticklabels/offsettext. + if label_position == "bottom": + self.set_xlabel("") + self.xaxis.set_tick_params(which="both", labelbottom=False) + if self.xaxis.offsetText.get_position()[1] == 0: + self.xaxis.offsetText.set_visible(False) + + def _label_outer_yaxis(self, *, check_patch): + # see documentation in label_outer. + if check_patch and not isinstance(self.patch, mpl.patches.Rectangle): + return + ss = self.get_subplotspec() + if not ss: + return + label_position = self.yaxis.get_label_position() + if not ss.is_first_col(): # Remove left label/ticklabels/offsettext. + if label_position == "left": + self.set_ylabel("") + self.yaxis.set_tick_params(which="both", labelleft=False) + if self.yaxis.offsetText.get_position()[0] == 0: + self.yaxis.offsetText.set_visible(False) + if not ss.is_last_col(): # Remove right label/ticklabels/offsettext. + if label_position == "right": + self.set_ylabel("") + self.yaxis.set_tick_params(which="both", labelright=False) + if self.yaxis.offsetText.get_position()[0] == 1: + self.yaxis.offsetText.set_visible(False) + + +def _draw_rasterized(figure, artists, renderer): + """ + A helper function for rasterizing the list of artists. + + The bookkeeping to track if we are or are not in rasterizing mode + with the mixed-mode backends is relatively complicated and is now + handled in the matplotlib.artist.allow_rasterization decorator. + + This helper defines the absolute minimum methods and attributes on a + shim class to be compatible with that decorator and then uses it to + rasterize the list of artists. + + This is maybe too-clever, but allows us to re-use the same code that is + used on normal artists to participate in the "are we rasterizing" + accounting. + + Please do not use this outside of the "rasterize below a given zorder" + functionality of Axes. + + Parameters + ---------- + figure : matplotlib.figure.Figure + The figure all of the artists belong to (not checked). We need this + because we can at the figure level suppress composition and insert each + rasterized artist as its own image. + + artists : List[matplotlib.artist.Artist] + The list of Artists to be rasterized. These are assumed to all + be in the same Figure. + + renderer : matplotlib.backendbases.RendererBase + The currently active renderer + + Returns + ------- + None + + """ + class _MinimalArtist: + def get_rasterized(self): + return True + + def get_agg_filter(self): + return None + + def __init__(self, figure, artists): + self.figure = figure + self.artists = artists + + @martist.allow_rasterization + def draw(self, renderer): + for a in self.artists: + a.draw(renderer) + + return _MinimalArtist(figure, artists).draw(renderer) diff --git a/lib/matplotlib/axes/_secondary_axes.py b/lib/matplotlib/axes/_secondary_axes.py index 98a612013a16..5a65fee1542d 100644 --- a/lib/matplotlib/axes/_secondary_axes.py +++ b/lib/matplotlib/axes/_secondary_axes.py @@ -1,9 +1,11 @@ +import numbers + import numpy as np -from matplotlib import _api -import matplotlib.docstring as docstring +from matplotlib import _api, _docstring import matplotlib.ticker as mticker from matplotlib.axes._base import _AxesBase, _TransformedBoundsLocator +from matplotlib.axis import Axis class SecondaryAxis(_AxesBase): @@ -17,7 +19,7 @@ def __init__(self, parent, orientation, location, functions, **kwargs): While there is no need for this to be private, it should really be called by those higher level functions. """ - + _api.check_in_list(["x", "y"], orientation=orientation) self._functions = functions self._parent = parent self._orientation = orientation @@ -28,7 +30,7 @@ def __init__(self, parent, orientation, location, functions, **kwargs): self._axis = self.xaxis self._locstrings = ['top', 'bottom'] self._otherstrings = ['left', 'right'] - elif self._orientation == 'y': + else: # 'y' super().__init__(self._parent.figure, [0, 1., 0.0001, 1], **kwargs) self._axis = self.yaxis self._locstrings = ['right', 'left'] @@ -40,11 +42,7 @@ def __init__(self, parent, orientation, location, functions, **kwargs): self.set_functions(functions) # styling: - if self._orientation == 'x': - otheraxis = self.yaxis - else: - otheraxis = self.xaxis - + otheraxis = self.yaxis if self._orientation == 'x' else self.xaxis otheraxis.set_major_locator(mticker.NullLocator()) otheraxis.set_ticks_position('none') @@ -63,8 +61,8 @@ def set_alignment(self, align): Parameters ---------- - align : str - either 'top' or 'bottom' for orientation='x' or + align : {'top', 'bottom', 'left', 'right'} + Either 'top' or 'bottom' for orientation='x' or 'left' or 'right' for orientation='y' axis. """ _api.check_in_list(self._locstrings, align=align) @@ -92,23 +90,22 @@ def set_location(self, location): # This puts the rectangle into figure-relative coordinates. if isinstance(location, str): - if location in ['top', 'right']: - self._pos = 1. - elif location in ['bottom', 'left']: - self._pos = 0. - else: - raise ValueError( - f"location must be {self._locstrings[0]!r}, " - f"{self._locstrings[1]!r}, or a float, not {location!r}") - else: + _api.check_in_list(self._locstrings, location=location) + self._pos = 1. if location in ('top', 'right') else 0. + elif isinstance(location, numbers.Real): self._pos = location + else: + raise ValueError( + f"location must be {self._locstrings[0]!r}, " + f"{self._locstrings[1]!r}, or a float, not {location!r}") + self._loc = location if self._orientation == 'x': # An x-secondary axes is like an inset axes from x = 0 to x = 1 and # from y = pos to y = pos + eps, in the parent's transAxes coords. bounds = [0, self._pos, 1., 1e-10] - else: + else: # 'y' bounds = [self._pos, 0, 1e-10, 1] # this locator lets the axes move in the parent axes coordinates. @@ -123,18 +120,9 @@ def apply_aspect(self, position=None): self._set_lims() super().apply_aspect(position) - def set_ticks(self, ticks, *, minor=False): - """ - Set the x ticks with list of *ticks* - - Parameters - ---------- - ticks : list - List of x-axis tick locations. - minor : bool, default: False - If ``False`` sets major ticks, if ``True`` sets minor ticks. - """ - ret = self._axis.set_ticks(ticks, minor=minor) + @_docstring.copy(Axis.set_ticks) + def set_ticks(self, ticks, labels=None, *, minor=False, **kwargs): + ret = self._axis.set_ticks(ticks, labels, minor=minor, **kwargs) self.stale = True self._ticks_set = True return ret @@ -170,9 +158,7 @@ def set_functions(self, functions): 'and the second being the inverse') self._set_scale() - # Should be changed to draw(self, renderer) once the deprecation of - # renderer=None and of inframe expires. - def draw(self, *args, **kwargs): + def draw(self, renderer): """ Draw the secondary axes. @@ -184,7 +170,7 @@ def draw(self, *args, **kwargs): self._set_lims() # this sets the scale in case the parent has set its scale. self._set_scale() - super().draw(*args, **kwargs) + super().draw(renderer) def _set_scale(self): """ @@ -194,22 +180,18 @@ def _set_scale(self): if self._orientation == 'x': pscale = self._parent.xaxis.get_scale() set_scale = self.set_xscale - if self._orientation == 'y': + else: # 'y' pscale = self._parent.yaxis.get_scale() set_scale = self.set_yscale if pscale == self._parentscale: return - if pscale == 'log': - defscale = 'functionlog' - else: - defscale = 'function' - if self._ticks_set: ticks = self._axis.get_ticklocs() # need to invert the roles here for the ticks to line up. - set_scale(defscale, functions=self._functions[::-1]) + set_scale('functionlog' if pscale == 'log' else 'function', + functions=self._functions[::-1]) # OK, set_scale sets the locators, but if we've called # axsecond.set_ticks, we want to keep those. @@ -227,7 +209,7 @@ def _set_lims(self): if self._orientation == 'x': lims = self._parent.get_xlim() set_lim = self.set_xlim - if self._orientation == 'y': + else: # 'y' lims = self._parent.get_ylim() set_lim = self.set_ylim order = lims[0] < lims[1] @@ -258,7 +240,7 @@ def set_color(self, color): self.spines.bottom.set_color(color) self.spines.top.set_color(color) self.xaxis.label.set_color(color) - else: + else: # 'y' self.tick_params(axis='y', colors=color) self.spines.left.set_color(color) self.spines.right.set_color(color) @@ -284,7 +266,8 @@ def set_color(self, color): If a 2-tuple of functions, the user specifies the transform function and its inverse. i.e. ``functions=(lambda x: 2 / x, lambda x: 2 / x)`` would be an - reciprocal transform with a factor of 2. + reciprocal transform with a factor of 2. Both functions must accept + numpy arrays as input. The user can also directly supply a subclass of `.transforms.Transform` so long as it has an inverse. @@ -301,4 +284,4 @@ def set_color(self, color): **kwargs : `~matplotlib.axes.Axes` properties. Other miscellaneous axes parameters. ''' -docstring.interpd.update(_secax_docstring=_secax_docstring) +_docstring.interpd.update(_secax_docstring=_secax_docstring) diff --git a/lib/matplotlib/axes/_subplots.py b/lib/matplotlib/axes/_subplots.py deleted file mode 100644 index d8595213e42f..000000000000 --- a/lib/matplotlib/axes/_subplots.py +++ /dev/null @@ -1,211 +0,0 @@ -import functools - -from matplotlib import _api, docstring -import matplotlib.artist as martist -from matplotlib.axes._axes import Axes -from matplotlib.gridspec import GridSpec, SubplotSpec - - -class SubplotBase: - """ - Base class for subplots, which are :class:`Axes` instances with - additional methods to facilitate generating and manipulating a set - of :class:`Axes` within a figure. - """ - - def __init__(self, fig, *args, **kwargs): - """ - Parameters - ---------- - fig : `matplotlib.figure.Figure` - - *args : tuple (*nrows*, *ncols*, *index*) or int - The array of subplots in the figure has dimensions ``(nrows, - ncols)``, and *index* is the index of the subplot being created. - *index* starts at 1 in the upper left corner and increases to the - right. - - If *nrows*, *ncols*, and *index* are all single digit numbers, then - *args* can be passed as a single 3-digit number (e.g. 234 for - (2, 3, 4)). - - **kwargs - Keyword arguments are passed to the Axes (sub)class constructor. - """ - # _axes_class is set in the subplot_class_factory - self._axes_class.__init__(self, fig, [0, 0, 1, 1], **kwargs) - # This will also update the axes position. - self.set_subplotspec(SubplotSpec._from_subplot_args(fig, args)) - - def __reduce__(self): - # get the first axes class which does not inherit from a subplotbase - axes_class = next( - c for c in type(self).__mro__ - if issubclass(c, Axes) and not issubclass(c, SubplotBase)) - return (_picklable_subplot_class_constructor, - (axes_class,), - self.__getstate__()) - - @_api.deprecated( - "3.4", alternative="get_subplotspec", - addendum="(get_subplotspec returns a SubplotSpec instance.)") - def get_geometry(self): - """Get the subplot geometry, e.g., (2, 2, 3).""" - rows, cols, num1, num2 = self.get_subplotspec().get_geometry() - return rows, cols, num1 + 1 # for compatibility - - @_api.deprecated("3.4", alternative="set_subplotspec") - def change_geometry(self, numrows, numcols, num): - """Change subplot geometry, e.g., from (1, 1, 1) to (2, 2, 3).""" - self._subplotspec = GridSpec(numrows, numcols, - figure=self.figure)[num - 1] - self.update_params() - self.set_position(self.figbox) - - def get_subplotspec(self): - """Return the `.SubplotSpec` instance associated with the subplot.""" - return self._subplotspec - - def set_subplotspec(self, subplotspec): - """Set the `.SubplotSpec`. instance associated with the subplot.""" - self._subplotspec = subplotspec - self._set_position(subplotspec.get_position(self.figure)) - - def get_gridspec(self): - """Return the `.GridSpec` instance associated with the subplot.""" - return self._subplotspec.get_gridspec() - - @_api.deprecated( - "3.4", alternative="get_position()") - @property - def figbox(self): - return self.get_position() - - @_api.deprecated("3.4", alternative="get_gridspec().nrows") - @property - def numRows(self): - return self.get_gridspec().nrows - - @_api.deprecated("3.4", alternative="get_gridspec().ncols") - @property - def numCols(self): - return self.get_gridspec().ncols - - @_api.deprecated("3.4") - def update_params(self): - """Update the subplot position from ``self.figure.subplotpars``.""" - # Now a no-op, as figbox/numRows/numCols are (deprecated) auto-updating - # properties. - - @_api.deprecated("3.4", alternative="ax.get_subplotspec().is_first_row()") - def is_first_row(self): - return self.get_subplotspec().rowspan.start == 0 - - @_api.deprecated("3.4", alternative="ax.get_subplotspec().is_last_row()") - def is_last_row(self): - return self.get_subplotspec().rowspan.stop == self.get_gridspec().nrows - - @_api.deprecated("3.4", alternative="ax.get_subplotspec().is_first_col()") - def is_first_col(self): - return self.get_subplotspec().colspan.start == 0 - - @_api.deprecated("3.4", alternative="ax.get_subplotspec().is_last_col()") - def is_last_col(self): - return self.get_subplotspec().colspan.stop == self.get_gridspec().ncols - - def label_outer(self): - """ - Only show "outer" labels and tick labels. - - x-labels are only kept for subplots on the last row; y-labels only for - subplots on the first column. - """ - ss = self.get_subplotspec() - lastrow = ss.is_last_row() - firstcol = ss.is_first_col() - if not lastrow: - for label in self.get_xticklabels(which="both"): - label.set_visible(False) - self.xaxis.get_offset_text().set_visible(False) - self.set_xlabel("") - if not firstcol: - for label in self.get_yticklabels(which="both"): - label.set_visible(False) - self.yaxis.get_offset_text().set_visible(False) - self.set_ylabel("") - - def _make_twin_axes(self, *args, **kwargs): - """Make a twinx axes of self. This is used for twinx and twiny.""" - if 'sharex' in kwargs and 'sharey' in kwargs: - # The following line is added in v2.2 to avoid breaking Seaborn, - # which currently uses this internal API. - if kwargs["sharex"] is not self and kwargs["sharey"] is not self: - raise ValueError("Twinned Axes may share only one axis") - twin = self.figure.add_subplot(self.get_subplotspec(), *args, **kwargs) - self.set_adjustable('datalim') - twin.set_adjustable('datalim') - self._twinned_axes.join(self, twin) - return twin - - -# this here to support cartopy which was using a private part of the -# API to register their Axes subclasses. - -# In 3.1 this should be changed to a dict subclass that warns on use -# In 3.3 to a dict subclass that raises a useful exception on use -# In 3.4 should be removed - -# The slow timeline is to give cartopy enough time to get several -# release out before we break them. -_subplot_classes = {} - - -@functools.lru_cache(None) -def subplot_class_factory(axes_class=None): - """ - 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 - not have to be created for every type of Axes. - """ - if axes_class is None: - _api.warn_deprecated( - "3.3", message="Support for passing None to subplot_class_factory " - "is deprecated since %(since)s; explicitly pass the default Axes " - "class instead. This will become an error %(removal)s.") - axes_class = Axes - - try: - # Avoid creating two different instances of GeoAxesSubplot... - # Only a temporary backcompat fix. This should be removed in - # 3.4 - return next(cls for cls in SubplotBase.__subclasses__() - if cls.__bases__ == (SubplotBase, axes_class)) - except StopIteration: - # if we have already wrapped this class, declare victory! - if issubclass(axes_class, SubplotBase): - return axes_class - - return type("%sSubplot" % axes_class.__name__, - (SubplotBase, axes_class), - {'_axes_class': axes_class}) - - -Subplot = subplot_class_factory(Axes) # Provided for backward compatibility. - - -def _picklable_subplot_class_constructor(axes_class): - """ - 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) - - -docstring.interpd.update(Axes_kwdoc=martist.kwdoc(Axes)) -docstring.dedent_interpd(Axes.__init__) - -docstring.interpd.update(Subplot_kwdoc=martist.kwdoc(Axes)) diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 83384fd37646..3724cc811e08 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -1,17 +1,18 @@ """ -Classes for the ticks and x and y axis. +Classes for the ticks and x- and y-axis. """ import datetime import functools import logging +from numbers import Number import numpy as np import matplotlib as mpl -from matplotlib import _api +from matplotlib import _api, cbook import matplotlib.artist as martist -import matplotlib.cbook as cbook +import matplotlib.colors as mcolors import matplotlib.lines as mlines import matplotlib.scale as mscale import matplotlib.text as mtext @@ -54,30 +55,29 @@ class Tick(martist.Artist): The right/top tick label. """ - @_api.delete_parameter("3.3", "label") - def __init__(self, axes, loc, label=None, - size=None, # points - width=None, - color=None, - tickdir=None, - pad=None, - labelsize=None, - labelcolor=None, - zorder=None, - gridOn=None, # defaults to axes.grid depending on - # axes.grid.which - tick1On=True, - tick2On=True, - label1On=True, - label2On=False, - major=True, - labelrotation=0, - grid_color=None, - grid_linestyle=None, - grid_linewidth=None, - grid_alpha=None, - **kw # Other Line2D kwargs applied to gridlines. - ): + def __init__( + self, axes, loc, *, + size=None, # points + width=None, + color=None, + tickdir=None, + pad=None, + labelsize=None, + labelcolor=None, + zorder=None, + gridOn=None, # defaults to axes.grid depending on axes.grid.which + tick1On=True, + tick2On=True, + label1On=True, + label2On=False, + major=True, + labelrotation=0, + grid_color=None, + grid_linestyle=None, + grid_linewidth=None, + grid_alpha=None, + **kwargs, # Other Line2D kwargs applied to gridlines. + ): """ bbox is the Bound2D bounding box in display coords of the Axes loc is the tick location in data coords @@ -144,11 +144,14 @@ def __init__(self, axes, loc, label=None, grid_linestyle = mpl.rcParams["grid.linestyle"] if grid_linewidth is None: grid_linewidth = mpl.rcParams["grid.linewidth"] - if grid_alpha is None: + if grid_alpha is None and not mcolors._has_alpha_channel(grid_color): + # alpha precedence: kwarg > color alpha > rcParams['grid.alpha'] + # Note: only resolve to rcParams if the color does not have alpha + # otherwise `grid(color=(1, 1, 1, 0.5))` would work like + # grid(color=(1, 1, 1, 0.5), alpha=rcParams['grid.alpha']) + # so the that the rcParams default would override color alpha. grid_alpha = mpl.rcParams["grid.alpha"] - grid_kw = {k[5:]: v for k, v in kw.items()} - - self.apply_tickdir(tickdir) + grid_kw = {k[5:]: v for k, v in kwargs.items()} self.tick1line = mlines.Line2D( [], [], @@ -170,22 +173,15 @@ def __init__(self, axes, loc, label=None, GRIDLINE_INTERPOLATION_STEPS self.label1 = mtext.Text( np.nan, np.nan, - fontsize=labelsize, color=labelcolor, visible=label1On) + fontsize=labelsize, color=labelcolor, visible=label1On, + rotation=self._labelrotation[1]) self.label2 = mtext.Text( np.nan, np.nan, - fontsize=labelsize, color=labelcolor, visible=label2On) - for meth, attr in [("_get_tick1line", "tick1line"), - ("_get_tick2line", "tick2line"), - ("_get_gridline", "gridline"), - ("_get_text1", "label1"), - ("_get_text2", "label2")]: - overridden_method = _api.deprecate_method_override( - getattr(__class__, meth), self, since="3.3", message="Relying " - f"on {meth} to initialize Tick.{attr} is deprecated since " - f"%(since)s and will not work %(removal)s; please directly " - f"set the attribute in the subclass' __init__ instead.") - if overridden_method: - setattr(self, attr, overridden_method()) + fontsize=labelsize, color=labelcolor, visible=label2On, + rotation=self._labelrotation[1]) + + self._apply_tickdir(tickdir) + for artist in [self.tick1line, self.tick2line, self.gridline, self.label1, self.label2]: self._set_artist_props(artist) @@ -193,7 +189,7 @@ def __init__(self, axes, loc, label=None, self.update_position(loc) @property - @_api.deprecated("3.1", alternative="Tick.label1", pending=True) + @_api.deprecated("3.1", alternative="Tick.label1", removal="3.8") def label(self): return self.label1 @@ -209,21 +205,23 @@ def _set_labelrotation(self, labelrotation): _api.check_in_list(['auto', 'default'], labelrotation=mode) self._labelrotation = (mode, angle) - def apply_tickdir(self, tickdir): + def _apply_tickdir(self, tickdir): """Set tick direction. Valid values are 'out', 'in', 'inout'.""" + # This method is responsible for updating `_pad`, and, in subclasses, + # for setting the tick{1,2}line markers as well. From the user + # perspective this should always be called though _apply_params, which + # further updates ticklabel positions using the new pads. if tickdir is None: tickdir = mpl.rcParams[f'{self.__name__}.direction'] _api.check_in_list(['in', 'out', 'inout'], tickdir=tickdir) self._tickdir = tickdir self._pad = self._base_pad + self.get_tick_padding() - self.stale = True - # Subclass overrides should compute _tickmarkers as appropriate here. def get_tickdir(self): return self._tickdir def get_tick_padding(self): - """Get the length of the tick outside of the axes.""" + """Get the length of the tick outside of the Axes.""" padding = { 'in': 0.0, 'inout': 0.5, @@ -242,6 +240,7 @@ def set_clip_path(self, clippath, transform=None): self.gridline.set_clip_path(clippath, transform) self.stale = True + @_api.deprecated("3.6") def get_pad_pixels(self): return self.figure.dpi * self._base_pad / 72 @@ -279,13 +278,13 @@ def _get_text2(self): """Get the default Text 2 instance.""" def _get_tick1line(self): - """Get the default line2D instance for tick1.""" + """Get the default `.Line2D` instance for tick1.""" def _get_tick2line(self): - """Get the default line2D instance for tick2.""" + """Get the default `.Line2D` instance for tick2.""" def _get_gridline(self): - """Get the default grid Line2d instance for this tick.""" + """Get the default grid `.Line2D` instance for this tick.""" def get_loc(self): """Return the tick location (data coords) as a scalar.""" @@ -349,52 +348,50 @@ def get_view_interval(self): """ raise NotImplementedError('Derived must override') - def _apply_params(self, **kw): + def _apply_params(self, **kwargs): for name, target in [("gridOn", self.gridline), ("tick1On", self.tick1line), ("tick2On", self.tick2line), ("label1On", self.label1), ("label2On", self.label2)]: - if name in kw: - target.set_visible(kw.pop(name)) - if any(k in kw for k in ['size', 'width', 'pad', 'tickdir']): - self._size = kw.pop('size', self._size) + if name in kwargs: + target.set_visible(kwargs.pop(name)) + if any(k in kwargs for k in ['size', 'width', 'pad', 'tickdir']): + self._size = kwargs.pop('size', self._size) # Width could be handled outside this block, but it is # convenient to leave it here. - self._width = kw.pop('width', self._width) - self._base_pad = kw.pop('pad', self._base_pad) - # apply_tickdir uses _size and _base_pad to make _pad, - # and also makes _tickmarkers. - self.apply_tickdir(kw.pop('tickdir', self._tickdir)) - self.tick1line.set_marker(self._tickmarkers[0]) - self.tick2line.set_marker(self._tickmarkers[1]) + self._width = kwargs.pop('width', self._width) + self._base_pad = kwargs.pop('pad', self._base_pad) + # _apply_tickdir uses _size and _base_pad to make _pad, and also + # sets the ticklines markers. + self._apply_tickdir(kwargs.pop('tickdir', self._tickdir)) for line in (self.tick1line, self.tick2line): line.set_markersize(self._size) line.set_markeredgewidth(self._width) - # _get_text1_transform uses _pad from apply_tickdir. + # _get_text1_transform uses _pad from _apply_tickdir. trans = self._get_text1_transform()[0] self.label1.set_transform(trans) trans = self._get_text2_transform()[0] self.label2.set_transform(trans) - tick_kw = {k: v for k, v in kw.items() if k in ['color', 'zorder']} - if 'color' in kw: - tick_kw['markeredgecolor'] = kw['color'] + tick_kw = {k: v for k, v in kwargs.items() if k in ['color', 'zorder']} + if 'color' in kwargs: + tick_kw['markeredgecolor'] = kwargs['color'] self.tick1line.set(**tick_kw) self.tick2line.set(**tick_kw) for k, v in tick_kw.items(): setattr(self, '_' + k, v) - if 'labelrotation' in kw: - self._set_labelrotation(kw.pop('labelrotation')) + if 'labelrotation' in kwargs: + self._set_labelrotation(kwargs.pop('labelrotation')) self.label1.set(rotation=self._labelrotation[1]) self.label2.set(rotation=self._labelrotation[1]) - label_kw = {k[5:]: v for k, v in kw.items() + label_kw = {k[5:]: v for k, v in kwargs.items() if k in ['labelsize', 'labelcolor']} self.label1.set(**label_kw) self.label2.set(**label_kw) - grid_kw = {k[5:]: v for k, v in kw.items() + grid_kw = {k[5:]: v for k, v in kwargs.items() if k in _gridline_param_names} self.gridline.set(**grid_kw) @@ -419,20 +416,13 @@ class XTick(Tick): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # x in data coords, y in axes coords + ax = self.axes self.tick1line.set( - xdata=[0], ydata=[0], - transform=self.axes.get_xaxis_transform(which="tick1"), - marker=self._tickmarkers[0], - ) + data=([0], [0]), transform=ax.get_xaxis_transform("tick1")) self.tick2line.set( - xdata=[0], ydata=[1], - transform=self.axes.get_xaxis_transform(which="tick2"), - marker=self._tickmarkers[1], - ) + data=([0], [1]), transform=ax.get_xaxis_transform("tick2")) self.gridline.set( - xdata=[0, 0], ydata=[0, 1], - transform=self.axes.get_xaxis_transform(which="grid"), - ) + data=([0, 0], [0, 1]), transform=ax.get_xaxis_transform("grid")) # the y loc is 3 points below the min of y axis trans, va, ha = self._get_text1_transform() self.label1.set( @@ -451,15 +441,16 @@ def _get_text1_transform(self): def _get_text2_transform(self): return self.axes.get_xaxis_text2_transform(self._pad) - def apply_tickdir(self, tickdir): + def _apply_tickdir(self, tickdir): # docstring inherited - super().apply_tickdir(tickdir) - self._tickmarkers = { + super()._apply_tickdir(tickdir) + mark1, mark2 = { 'out': (mlines.TICKDOWN, mlines.TICKUP), 'in': (mlines.TICKUP, mlines.TICKDOWN), 'inout': ('|', '|'), }[self._tickdir] - self.stale = True + self.tick1line.set_marker(mark1) + self.tick2line.set_marker(mark2) def update_position(self, loc): """Set the location of tick in data coords with scalar *loc*.""" @@ -486,20 +477,13 @@ class YTick(Tick): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # x in axes coords, y in data coords + ax = self.axes self.tick1line.set( - xdata=[0], ydata=[0], - transform=self.axes.get_yaxis_transform(which="tick1"), - marker=self._tickmarkers[0], - ) + data=([0], [0]), transform=ax.get_yaxis_transform("tick1")) self.tick2line.set( - xdata=[1], ydata=[0], - transform=self.axes.get_yaxis_transform(which="tick2"), - marker=self._tickmarkers[1], - ) + data=([1], [0]), transform=ax.get_yaxis_transform("tick2")) self.gridline.set( - xdata=[0, 1], ydata=[0, 0], - transform=self.axes.get_yaxis_transform(which="grid"), - ) + data=([0, 1], [0, 0]), transform=ax.get_yaxis_transform("grid")) # the y loc is 3 points below the min of y axis trans, va, ha = self._get_text1_transform() self.label1.set( @@ -518,15 +502,16 @@ def _get_text1_transform(self): def _get_text2_transform(self): return self.axes.get_yaxis_text2_transform(self._pad) - def apply_tickdir(self, tickdir): + def _apply_tickdir(self, tickdir): # docstring inherited - super().apply_tickdir(tickdir) - self._tickmarkers = { + super()._apply_tickdir(tickdir) + mark1, mark2 = { 'out': (mlines.TICKLEFT, mlines.TICKRIGHT), 'in': (mlines.TICKRIGHT, mlines.TICKLEFT), 'inout': ('_', '_'), }[self._tickdir] - self.stale = True + self.tick1line.set_marker(mark1) + self.tick2line.set_marker(mark2) def update_position(self, loc): """Set the location of tick in data coords with scalar *loc*.""" @@ -558,6 +543,8 @@ class Ticker: def __init__(self): self._locator = None self._formatter = None + self._locator_is_default = True + self._formatter_is_default = True @property def locator(self): @@ -645,11 +632,15 @@ class Axis(martist.Artist): The minor ticks. """ OFFSETTEXTPAD = 3 + # The class used in _get_tick() to create tick instances. Must either be + # overwritten in subclasses, or subclasses must reimplement _get_tick(). + _tick_class = None def __str__(self): return "{}({},{})".format( type(self).__name__, *self.axes.transAxes.transform((0, 0))) + @_api.make_keyword_only("3.6", name="pickradius") def __init__(self, axes, pickradius=15): """ Parameters @@ -670,7 +661,7 @@ def __init__(self, axes, pickradius=15): self.axes = axes self.major = Ticker() self.minor = Ticker() - self.callbacks = cbook.CallbackRegistry() + self.callbacks = cbook.CallbackRegistry(signals=["units"]) self._autolabelpos = True @@ -693,7 +684,39 @@ def __init__(self, axes, pickradius=15): self._minor_tick_kw = dict() self.clear() - self._set_scale('linear') + self._autoscale_on = True + + @property + def isDefault_majloc(self): + return self.major._locator_is_default + + @isDefault_majloc.setter + def isDefault_majloc(self, value): + self.major._locator_is_default = value + + @property + def isDefault_majfmt(self): + return self.major._formatter_is_default + + @isDefault_majfmt.setter + def isDefault_majfmt(self, value): + self.major._formatter_is_default = value + + @property + def isDefault_minloc(self): + return self.minor._locator_is_default + + @isDefault_minloc.setter + def isDefault_minloc(self, value): + self.minor._locator_is_default = value + + @property + def isDefault_minfmt(self): + return self.minor._formatter_is_default + + @isDefault_minfmt.setter + def isDefault_minfmt(self, value): + self.minor._formatter_is_default = value # During initialization, Axis objects often create ticks that are later # unused; this turns out to be a very slow step. Instead, use a custom @@ -751,9 +774,68 @@ def _set_scale(self, value, **kwargs): self.isDefault_majfmt = True self.isDefault_minfmt = True + # This method is directly wrapped by Axes.set_{x,y}scale. + def _set_axes_scale(self, value, **kwargs): + """ + Set this Axis' scale. + + Parameters + ---------- + value : {"linear", "log", "symlog", "logit", ...} or `.ScaleBase` + The axis scale type to apply. + + **kwargs + Different keyword arguments are accepted, depending on the scale. + See the respective class keyword arguments: + + - `matplotlib.scale.LinearScale` + - `matplotlib.scale.LogScale` + - `matplotlib.scale.SymmetricalLogScale` + - `matplotlib.scale.LogitScale` + - `matplotlib.scale.FuncScale` + + Notes + ----- + By default, Matplotlib supports the above-mentioned scales. + Additionally, custom scales may be registered using + `matplotlib.scale.register_scale`. These scales can then also + be used here. + """ + name, = [name for name, axis in self.axes._axis_map.items() + if axis is self] # The axis name. + old_default_lims = (self.get_major_locator() + .nonsingular(-np.inf, np.inf)) + g = self.axes._shared_axes[name] + for ax in g.get_siblings(self.axes): + ax._axis_map[name]._set_scale(value, **kwargs) + ax._update_transScale() + ax.stale = True + new_default_lims = (self.get_major_locator() + .nonsingular(-np.inf, np.inf)) + if old_default_lims != new_default_lims: + # Force autoscaling now, to take advantage of the scale locator's + # nonsingular() before it possibly gets swapped out by the user. + self.axes.autoscale_view( + **{f"scale{k}": k == name for k in self.axes._axis_names}) + def limit_range_for_scale(self, vmin, vmax): return self._scale.limit_range_for_scale(vmin, vmax, self.get_minpos()) + def _get_autoscale_on(self): + """Return whether this Axis is autoscaled.""" + return self._autoscale_on + + def _set_autoscale_on(self, b): + """ + Set whether this Axis is autoscaled when drawing or by + `.Axes.autoscale_view`. + + Parameters + ---------- + b : bool + """ + self._autoscale_on = b + def get_children(self): return [self.label, self.offsetText, *self.get_major_ticks(), *self.get_minor_ticks()] @@ -783,13 +865,21 @@ def clear(self): - units - registered callbacks """ + self.label._reset_visual_defaults() + # The above resets the label formatting using text rcParams, + # so we then update the formatting using axes rcParams + self.label.set_color(mpl.rcParams['axes.labelcolor']) + self.label.set_fontsize(mpl.rcParams['axes.labelsize']) + self.label.set_fontweight(mpl.rcParams['axes.labelweight']) + self.offsetText._reset_visual_defaults() + self.labelpad = mpl.rcParams['axes.labelpad'] - self.label.set_text('') # self.set_label_text would change isDefault_ + self._init() self._set_scale('linear') # Clear the callback registry for this axis, or it may "leak" - self.callbacks = cbook.CallbackRegistry() + self.callbacks = cbook.CallbackRegistry(signals=["units"]) # whether the grids are on self._major_tick_kw['gridOn'] = ( @@ -805,11 +895,6 @@ def clear(self): self.set_units(None) self.stale = True - @_api.deprecated("3.4", alternative="Axis.clear()") - def cla(self): - """Clear this axis.""" - return self.clear() - def reset_ticks(self): """ Re-initialize the major and minor Tick lists. @@ -830,15 +915,21 @@ def reset_ticks(self): except AttributeError: pass - def set_tick_params(self, which='major', reset=False, **kw): + def set_tick_params(self, which='major', reset=False, **kwargs): """ Set appearance parameters for ticks, ticklabels, and gridlines. For documentation of keyword arguments, see :meth:`matplotlib.axes.Axes.tick_params`. + + See Also + -------- + .Axis.get_tick_params + View the current style settings for ticks, ticklabels, and + gridlines. """ _api.check_in_list(['major', 'minor', 'both'], which=which) - kwtrans = self._translate_tick_kw(kw) + kwtrans = self._translate_tick_params(kwargs) # the kwargs are stored in self._major/minor_tick_kw so that any # future new ticks will automatically get them @@ -869,49 +960,123 @@ def set_tick_params(self, which='major', reset=False, **kw): self.stale = True + def get_tick_params(self, which='major'): + """ + Get appearance parameters for ticks, ticklabels, and gridlines. + + .. versionadded:: 3.7 + + Parameters + ---------- + which : {'major', 'minor'}, default: 'major' + The group of ticks for which the parameters are retrieved. + + Returns + ------- + dict + Properties for styling tick elements added to the axis. + + Notes + ----- + This method returns the appearance parameters for styling *new* + elements added to this axis and may be different from the values + on current elements if they were modified directly by the user + (e.g., via ``set_*`` methods on individual tick objects). + + Examples + -------- + :: + + >>> ax.yaxis.set_tick_params(labelsize=30, labelcolor='red', + direction='out', which='major') + >>> ax.yaxis.get_tick_params(which='major') + {'direction': 'out', + 'left': True, + 'right': False, + 'labelleft': True, + 'labelright': False, + 'gridOn': False, + 'labelsize': 30, + 'labelcolor': 'red'} + >>> ax.yaxis.get_tick_params(which='minor') + {'left': True, + 'right': False, + 'labelleft': True, + 'labelright': False, + 'gridOn': False} + + + """ + _api.check_in_list(['major', 'minor'], which=which) + if which == 'major': + return self._translate_tick_params( + self._major_tick_kw, reverse=True + ) + return self._translate_tick_params(self._minor_tick_kw, reverse=True) + @staticmethod - def _translate_tick_kw(kw): + def _translate_tick_params(kw, reverse=False): + """ + Translate the kwargs supported by `.Axis.set_tick_params` to kwargs + supported by `.Tick._apply_params`. + + In particular, this maps axis specific names like 'top', 'left' + to the generic tick1, tick2 logic of the axis. Additionally, there + are some other name translations. + + Returns a new dict of translated kwargs. + + Note: Use reverse=True to translate from those supported by + `.Tick._apply_params` back to those supported by + `.Axis.set_tick_params`. + """ + kw_ = {**kw} + # The following lists may be moved to a more accessible location. - kwkeys = ['size', 'width', 'color', 'tickdir', 'pad', - 'labelsize', 'labelcolor', 'zorder', 'gridOn', - 'tick1On', 'tick2On', 'label1On', 'label2On', - 'length', 'direction', 'left', 'bottom', 'right', 'top', - 'labelleft', 'labelbottom', 'labelright', 'labeltop', - 'labelrotation'] + _gridline_param_names - kwtrans = {} - if 'length' in kw: - kwtrans['size'] = kw.pop('length') - if 'direction' in kw: - kwtrans['tickdir'] = kw.pop('direction') - if 'rotation' in kw: - kwtrans['labelrotation'] = kw.pop('rotation') - if 'left' in kw: - kwtrans['tick1On'] = kw.pop('left') - if 'bottom' in kw: - kwtrans['tick1On'] = kw.pop('bottom') - if 'right' in kw: - kwtrans['tick2On'] = kw.pop('right') - if 'top' in kw: - kwtrans['tick2On'] = kw.pop('top') - if 'labelleft' in kw: - kwtrans['label1On'] = kw.pop('labelleft') - if 'labelbottom' in kw: - kwtrans['label1On'] = kw.pop('labelbottom') - if 'labelright' in kw: - kwtrans['label2On'] = kw.pop('labelright') - if 'labeltop' in kw: - kwtrans['label2On'] = kw.pop('labeltop') - if 'colors' in kw: - c = kw.pop('colors') + allowed_keys = [ + 'size', 'width', 'color', 'tickdir', 'pad', + 'labelsize', 'labelcolor', 'zorder', 'gridOn', + 'tick1On', 'tick2On', 'label1On', 'label2On', + 'length', 'direction', 'left', 'bottom', 'right', 'top', + 'labelleft', 'labelbottom', 'labelright', 'labeltop', + 'labelrotation', + *_gridline_param_names] + + keymap = { + # tick_params key -> axis key + 'length': 'size', + 'direction': 'tickdir', + 'rotation': 'labelrotation', + 'left': 'tick1On', + 'bottom': 'tick1On', + 'right': 'tick2On', + 'top': 'tick2On', + 'labelleft': 'label1On', + 'labelbottom': 'label1On', + 'labelright': 'label2On', + 'labeltop': 'label2On', + } + if reverse: + kwtrans = { + oldkey: kw_.pop(newkey) + for oldkey, newkey in keymap.items() if newkey in kw_ + } + else: + kwtrans = { + newkey: kw_.pop(oldkey) + for oldkey, newkey in keymap.items() if oldkey in kw_ + } + if 'colors' in kw_: + c = kw_.pop('colors') kwtrans['color'] = c kwtrans['labelcolor'] = c # Maybe move the checking up to the caller of this method. - for key in kw: - if key not in kwkeys: + for key in kw_: + if key not in allowed_keys: raise ValueError( "keyword %s is not recognized; valid keywords are %s" - % (key, kwkeys)) - kwtrans.update(kw) + % (key, allowed_keys)) + kwtrans.update(kw_) return kwtrans def set_clip_path(self, clippath, transform=None): @@ -921,7 +1086,7 @@ def set_clip_path(self, clippath, transform=None): self.stale = True def get_view_interval(self): - """Return the view limits ``(min, max)`` of this axis.""" + """Return the ``(min, max)`` view limits of this axis.""" raise NotImplementedError('Derived must override') def set_view_interval(self, vmin, vmax, ignore=False): @@ -940,7 +1105,7 @@ def set_view_interval(self, vmin, vmax, ignore=False): raise NotImplementedError('Derived must override') def get_data_interval(self): - """Return the Interval instance for this axis data limits.""" + """Return the ``(min, max)`` data limits of this axis.""" raise NotImplementedError('Derived must override') def set_data_interval(self, vmin, vmax, ignore=False): @@ -976,10 +1141,9 @@ def set_inverted(self, inverted): the top for the y-axis; the "inverse" direction is increasing to the left for the x-axis and to the bottom for the y-axis. """ - # Currently, must be implemented in subclasses using set_xlim/set_ylim - # rather than generically using set_view_interval, so that shared - # axes get updated as well. - raise NotImplementedError('Derived must override') + a, b = self.get_view_interval() + # cast to bool to avoid bad interaction between python 3.8 and np.bool_ + self._set_lim(*sorted((a, b), reverse=bool(inverted)), auto=None) def set_default_intervals(self): """ @@ -993,32 +1157,102 @@ def set_default_intervals(self): # interface provides a hook for custom types to register # default limits through the AxisInfo.default_limits # attribute, and the derived code below will check for that - # and use it if is available (else just use 0..1) + # and use it if it's available (else just use 0..1) + + def _set_lim(self, v0, v1, *, emit=True, auto): + """ + Set view limits. + + This method is a helper for the Axes ``set_xlim``, ``set_ylim``, and + ``set_zlim`` methods. + + Parameters + ---------- + v0, v1 : float + The view limits. (Passing *v0* as a (low, high) pair is not + supported; normalization must occur in the Axes setters.) + emit : bool, default: True + Whether to notify observers of limit change. + auto : bool or None, default: False + Whether to turn on autoscaling of the x-axis. True turns on, False + turns off, None leaves unchanged. + """ + name, = [name for name, axis in self.axes._axis_map.items() + if axis is self] # The axis name. + + self.axes._process_unit_info([(name, (v0, v1))], convert=False) + v0 = self.axes._validate_converted_limits(v0, self.convert_units) + v1 = self.axes._validate_converted_limits(v1, self.convert_units) + + if v0 is None or v1 is None: + # Axes init calls set_xlim(0, 1) before get_xlim() can be called, + # so only grab the limits if we really need them. + old0, old1 = self.get_view_interval() + if v0 is None: + v0 = old0 + if v1 is None: + v1 = old1 + + if self.get_scale() == 'log' and (v0 <= 0 or v1 <= 0): + # Axes init calls set_xlim(0, 1) before get_xlim() can be called, + # so only grab the limits if we really need them. + old0, old1 = self.get_view_interval() + if v0 <= 0: + _api.warn_external(f"Attempt to set non-positive {name}lim on " + f"a log-scaled axis will be ignored.") + v0 = old0 + if v1 <= 0: + _api.warn_external(f"Attempt to set non-positive {name}lim on " + f"a log-scaled axis will be ignored.") + v1 = old1 + if v0 == v1: + _api.warn_external( + f"Attempting to set identical low and high {name}lims " + f"makes transformation singular; automatically expanding.") + reverse = bool(v0 > v1) # explicit cast needed for python3.8+np.bool_. + v0, v1 = self.get_major_locator().nonsingular(v0, v1) + v0, v1 = self.limit_range_for_scale(v0, v1) + v0, v1 = sorted([v0, v1], reverse=bool(reverse)) + + self.set_view_interval(v0, v1, ignore=True) + # Mark viewlims as no longer stale without triggering an autoscale. + for ax in self.axes._shared_axes[name].get_siblings(self.axes): + ax._stale_viewlims[name] = False + if auto is not None: + self._set_autoscale_on(bool(auto)) + + if emit: + self.axes.callbacks.process(f"{name}lim_changed", self.axes) + # Call all of the other axes that are shared with this one + for other in self.axes._shared_axes[name].get_siblings(self.axes): + if other is not self.axes: + other._axis_map[name]._set_lim( + v0, v1, emit=False, auto=auto) + if other.figure != self.figure: + other.figure.canvas.draw_idle() + + self.stale = True + return v0, v1 def _set_artist_props(self, a): if a is None: return a.set_figure(self.figure) + @_api.deprecated("3.6") def get_ticklabel_extents(self, renderer): - """ - Get the extents of the tick labels on either side - of the axes. - """ - + """Get the extents of the tick labels on either side of the axes.""" ticks_to_draw = self._update_ticks() - ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes(ticks_to_draw, - renderer) - - if len(ticklabelBoxes): - bbox = mtransforms.Bbox.union(ticklabelBoxes) + tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer) + if len(tlb1): + bbox1 = mtransforms.Bbox.union(tlb1) else: - bbox = mtransforms.Bbox.from_extents(0, 0, 0, 0) - if len(ticklabelBoxes2): - bbox2 = mtransforms.Bbox.union(ticklabelBoxes2) + bbox1 = mtransforms.Bbox.from_extents(0, 0, 0, 0) + if len(tlb2): + bbox2 = mtransforms.Bbox.union(tlb2) else: bbox2 = mtransforms.Bbox.from_extents(0, 0, 0, 0) - return bbox, bbox2 + return bbox1, bbox2 def _update_ticks(self): """ @@ -1063,14 +1297,16 @@ def _update_ticks(self): return ticks_to_draw - def _get_tick_bboxes(self, ticks, renderer): + def _get_ticklabel_bboxes(self, ticks, renderer=None): """Return lists of bboxes for ticks' label1's and label2's.""" + if renderer is None: + renderer = self.figure._get_renderer() return ([tick.label1.get_window_extent(renderer) for tick in ticks if tick.label1.get_visible()], [tick.label2.get_window_extent(renderer) for tick in ticks if tick.label2.get_visible()]) - def get_tightbbox(self, renderer, *, for_layout_only=False): + def get_tightbbox(self, renderer=None, *, for_layout_only=False): """ Return a bounding box that encloses the axis. It only accounts tick labels, axis label, and offsetText. @@ -1082,24 +1318,23 @@ def get_tightbbox(self, renderer, *, for_layout_only=False): """ if not self.get_visible(): return - + if renderer is None: + renderer = self.figure._get_renderer() ticks_to_draw = self._update_ticks() self._update_label_position(renderer) # go back to just this axis's tick labels - ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes( - ticks_to_draw, renderer) + tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer) - self._update_offset_text_position(ticklabelBoxes, ticklabelBoxes2) + self._update_offset_text_position(tlb1, tlb2) self.offsetText.set_text(self.major.formatter.get_offset()) bboxes = [ *(a.get_window_extent(renderer) for a in [self.offsetText] if a.get_visible()), - *ticklabelBoxes, - *ticklabelBoxes2, + *tlb1, *tlb2, ] # take care of label if self.label.get_visible(): @@ -1139,22 +1374,16 @@ def draw(self, renderer, *args, **kwargs): renderer.open_group(__name__, gid=self.get_gid()) ticks_to_draw = self._update_ticks() - ticklabelBoxes, ticklabelBoxes2 = self._get_tick_bboxes(ticks_to_draw, - renderer) + tlb1, tlb2 = self._get_ticklabel_bboxes(ticks_to_draw, renderer) for tick in ticks_to_draw: tick.draw(renderer) - # scale up the axis label box to also find the neighbors, not - # just the tick labels that actually overlap note we need a - # *copy* of the axis label box because we don't want to scale - # the actual bbox - + # Shift label away from axes to avoid overlapping ticklabels. self._update_label_position(renderer) - self.label.draw(renderer) - self._update_offset_text_position(ticklabelBoxes, ticklabelBoxes2) + self._update_offset_text_position(tlb1, tlb2) self.offsetText.set_text(self.major.formatter.get_offset()) self.offsetText.draw(renderer) @@ -1177,10 +1406,11 @@ def get_offset_text(self): def get_pickradius(self): """Return the depth of the axis used by the picker.""" - return self.pickradius + return self._pickradius def get_majorticklabels(self): """Return this Axis' major tick labels, as a list of `~.text.Text`.""" + self._update_ticks() 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()] @@ -1188,6 +1418,7 @@ def get_majorticklabels(self): def get_minorticklabels(self): """Return this Axis' minor tick labels, as a list of `~.text.Text`.""" + self._update_ticks() 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()] @@ -1210,13 +1441,6 @@ def get_ticklabels(self, minor=False, which=None): Returns ------- list of `~matplotlib.text.Text` - - Notes - ----- - The tick label strings are not populated until a ``draw`` method has - been called. - - See also: `~.pyplot.draw` and `~.FigureCanvasBase.draw`. """ if which is not None: if which == 'minor': @@ -1262,24 +1486,38 @@ def get_majorticklocs(self): def get_minorticklocs(self): """Return this Axis' minor tick locations in data coordinates.""" # Remove minor ticks duplicating major ticks. - major_locs = self.major.locator() - minor_locs = self.minor.locator() - transform = self._scale.get_transform() - tr_minor_locs = transform.transform(minor_locs) - tr_major_locs = transform.transform(major_locs) - lo, hi = sorted(transform.transform(self.get_view_interval())) - # Use the transformed view limits as scale. 1e-5 is the default rtol - # for np.isclose. - tol = (hi - lo) * 1e-5 + minor_locs = np.asarray(self.minor.locator()) if self.remove_overlapping_locs: - minor_locs = [ - loc for loc, tr_loc in zip(minor_locs, tr_minor_locs) - if ~np.isclose(tr_loc, tr_major_locs, atol=tol, rtol=0).any()] + major_locs = self.major.locator() + transform = self._scale.get_transform() + tr_minor_locs = transform.transform(minor_locs) + tr_major_locs = transform.transform(major_locs) + lo, hi = sorted(transform.transform(self.get_view_interval())) + # Use the transformed view limits as scale. 1e-5 is the default + # rtol for np.isclose. + tol = (hi - lo) * 1e-5 + mask = np.isclose(tr_minor_locs[:, None], tr_major_locs[None, :], + atol=tol, rtol=0).any(axis=1) + minor_locs = minor_locs[~mask] return minor_locs - @_api.make_keyword_only("3.3", "minor") - def get_ticklocs(self, minor=False): - """Return this Axis' tick locations in data coordinates.""" + def get_ticklocs(self, *, minor=False): + """ + Return this Axis' tick locations in data coordinates. + + The locations are not clipped to the current axis limits and hence + may contain locations that are not visible in the output. + + Parameters + ---------- + minor : bool, default: False + True to return the minor tick directions, + False to return the major tick directions. + + Returns + ------- + numpy array of tick locations + """ return self.get_minorticklocs() if minor else self.get_majorticklocs() def get_ticks_direction(self, minor=False): @@ -1305,7 +1543,12 @@ def get_ticks_direction(self, minor=False): def _get_tick(self, major): """Return the default tick instance.""" - raise NotImplementedError('derived must override') + if self._tick_class is None: + raise NotImplementedError( + f"The Axis subclass {self.__class__.__name__} must define " + "_tick_class or reimplement _get_tick()") + tick_kw = self._major_tick_kw if major else self._minor_tick_kw + return self._tick_class(self.axes, 0, major=major, **tick_kw) def _get_tick_label_size(self, axis_name): """ @@ -1375,17 +1618,17 @@ def get_minor_ticks(self, numticks=None): return self.minorTicks[:numticks] - def grid(self, b=None, which='major', **kwargs): + def grid(self, visible=None, which='major', **kwargs): """ Configure the grid lines. Parameters ---------- - b : bool or None - Whether to show the grid lines. If any *kwargs* are supplied, - it is assumed you want the grid on and *b* will be set to True. + visible : bool or None + Whether to show the grid lines. If any *kwargs* are supplied, it + is assumed you want the grid on and *visible* will be set to True. - If *b* is *None* and there are no *kwargs*, this toggles the + If *visible* is *None* and there are no *kwargs*, this toggles the visibility of the lines. which : {'major', 'minor', 'both'} @@ -1396,47 +1639,31 @@ def grid(self, b=None, which='major', **kwargs): grid(color='r', linestyle='-', linewidth=2) """ - TOGGLE = object() - UNSET = object() - visible = kwargs.pop('visible', UNSET) - - if b is None: - if visible is UNSET: - if kwargs: # grid(color='r') - b = True - else: # grid() - b = TOGGLE - else: # grid(visible=v) - b = visible - else: - if visible is not UNSET and bool(b) != bool(visible): - # grid(True, visible=False), grid(False, visible=True) - raise ValueError( - "'b' and 'visible' specify inconsistent grid visibilities") - if kwargs and not b: # something false-like but not None - # grid(0, visible=True) + if kwargs: + if visible is None: + visible = True + elif not visible: # something false-like but not None _api.warn_external('First parameter to grid() is false, ' 'but line properties are supplied. The ' 'grid will be enabled.') - b = True - + visible = True which = which.lower() _api.check_in_list(['major', 'minor', 'both'], which=which) - gridkw = {'grid_' + item[0]: item[1] for item in kwargs.items()} + gridkw = {f'grid_{name}': value for name, value in kwargs.items()} if which in ['minor', 'both']: gridkw['gridOn'] = (not self._minor_tick_kw['gridOn'] - if b is TOGGLE else b) + if visible is None else visible) self.set_tick_params(which='minor', **gridkw) if which in ['major', 'both']: gridkw['gridOn'] = (not self._major_tick_kw['gridOn'] - if b is TOGGLE else b) + if visible is None else visible) self.set_tick_params(which='major', **gridkw) self.stale = True def update_units(self, data): """ Introspect *data* for units converter and update the - axis.converter instance if necessary. Return *True* + ``axis.converter`` instance if necessary. Return *True* if *data* is registered for unit conversion. """ converter = munits.registry.get_converter(data) @@ -1449,7 +1676,7 @@ def update_units(self, data): if default is not None and self.units is None: self.set_units(default) - if neednew: + elif neednew: self._update_axisinfo() self.stale = True return True @@ -1522,23 +1749,19 @@ def set_units(self, u): """ if u == self.units: return - if self is self.axes.xaxis: - shared = [ - ax.xaxis - for ax in self.axes.get_shared_x_axes().get_siblings(self.axes) - ] - elif self is self.axes.yaxis: - shared = [ - ax.yaxis - for ax in self.axes.get_shared_y_axes().get_siblings(self.axes) - ] + for name, axis in self.axes._axis_map.items(): + if self is axis: + shared = [ + getattr(ax, f"{name}axis") + for ax + in self.axes._shared_axes[name].get_siblings(self.axes)] + break else: shared = [self] for axis in shared: axis.units = u axis._update_axisinfo() axis.callbacks.process('units') - axis.callbacks.process('units finalize') axis.stale = True def get_units(self): @@ -1670,32 +1893,59 @@ def set_pickradius(self, pickradius): Parameters ---------- - pickradius : float + pickradius : float + The acceptance radius for containment tests. + See also `.Axis.contains`. """ - self.pickradius = pickradius + if not isinstance(pickradius, Number) or pickradius < 0: + raise ValueError("pick radius should be a distance") + self._pickradius = pickradius + + pickradius = property( + get_pickradius, set_pickradius, doc="The acceptance radius for " + "containment tests. See also `.Axis.contains`.") - # Helper for set_ticklabels. Defining it here makes it pickleable. + # Helper for set_ticklabels. Defining it here makes it picklable. @staticmethod def _format_with_dict(tickd, x, pos): return tickd.get(x, "") - def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): + @_api.rename_parameter("3.7", "ticklabels", "labels") + def set_ticklabels(self, labels, *, minor=False, fontdict=None, **kwargs): r""" - Set the text values of the tick labels. + [*Discouraged*] Set this Axis' tick labels with list of string labels. + + .. admonition:: Discouraged - .. warning:: - This method should only be used after fixing the tick positions - using `.Axis.set_ticks`. Otherwise, the labels may end up in - unexpected positions. + The use of this method is discouraged, because of the dependency on + tick positions. In most cases, you'll want to use + ``Axes.set_[x/y/z]ticks(positions, labels)`` or ``Axis.set_ticks`` + instead. + + If you are using this method, you should always fix the tick + positions before, e.g. by using `.Axis.set_ticks` or by explicitly + setting a `~.ticker.FixedLocator`. Otherwise, ticks are free to + move and the labels may end up in unexpected positions. Parameters ---------- - ticklabels : sequence of str or of `.Text`\s + labels : sequence of str or of `.Text`\s Texts for labeling each tick location in the sequence set by `.Axis.set_ticks`; the number of labels must match the number of locations. + minor : bool If True, set minor ticks instead of major ticks. + + fontdict : dict, optional + A dictionary controlling the appearance of the ticklabels. + The default *fontdict* is:: + + {'fontsize': rcParams['axes.titlesize'], + 'fontweight': rcParams['axes.titleweight'], + 'verticalalignment': 'baseline', + 'horizontalalignment': loc} + **kwargs Text properties. @@ -1705,24 +1955,27 @@ def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): For each tick, includes ``tick.label1`` if it is visible, then ``tick.label2`` if it is visible, in that order. """ - ticklabels = [t.get_text() if hasattr(t, 'get_text') else t - for t in ticklabels] + try: + labels = [t.get_text() if hasattr(t, 'get_text') else t + for t in labels] + except TypeError: + raise TypeError(f"{labels:=} must be a sequence") from None locator = (self.get_minor_locator() if minor else self.get_major_locator()) if isinstance(locator, mticker.FixedLocator): - # Passing [] as a list of ticklabels is often used as a way to - # remove all tick labels, so only error for > 0 ticklabels - if len(locator.locs) != len(ticklabels) and len(ticklabels) != 0: + # Passing [] as a list of labels is often used as a way to + # remove all tick labels, so only error for > 0 labels + if len(locator.locs) != len(labels) and len(labels) != 0: raise ValueError( "The number of FixedLocator locations" f" ({len(locator.locs)}), usually from a call to" " set_ticks, does not match" - f" the number of ticklabels ({len(ticklabels)}).") - tickd = {loc: lab for loc, lab in zip(locator.locs, ticklabels)} + f" the number of labels ({len(labels)}).") + tickd = {loc: lab for loc, lab in zip(locator.locs, labels)} func = functools.partial(self._format_with_dict, tickd) formatter = mticker.FuncFormatter(func) else: - formatter = mticker.FixedFormatter(ticklabels) + formatter = mticker.FixedFormatter(labels) if minor: self.set_minor_formatter(formatter) @@ -1734,15 +1987,17 @@ def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): ticks = self.get_major_ticks(len(locs)) ret = [] + if fontdict is not None: + kwargs.update(fontdict) for pos, (loc, tick) in enumerate(zip(locs, ticks)): tick.update_position(loc) tick_label = formatter(loc, pos) # deal with label1 tick.label1.set_text(tick_label) - tick.label1.update(kwargs) + tick.label1._internal_update(kwargs) # deal with label2 tick.label2.set_text(tick_label) - tick.label2.update(kwargs) + tick.label2._internal_update(kwargs) # only return visible tick labels if tick.label1.get_visible(): ret.append(tick.label1) @@ -1752,51 +2007,36 @@ def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): self.stale = True return ret - # Wrapper around set_ticklabels used to generate Axes.set_x/ytickabels; can - # go away once the API of Axes.set_x/yticklabels becomes consistent. - @_api.make_keyword_only("3.3", "fontdict") - def _set_ticklabels(self, labels, fontdict=None, minor=False, **kwargs): - """ - Set this Axis' labels with list of string labels. - - .. warning:: - This method should only be used after fixing the tick positions - using `.Axis.set_ticks`. Otherwise, the labels may end up in - unexpected positions. - - Parameters - ---------- - labels : list of str - The label texts. - - fontdict : dict, optional - A dictionary controlling the appearance of the ticklabels. - The default *fontdict* is:: - - {'fontsize': rcParams['axes.titlesize'], - 'fontweight': rcParams['axes.titleweight'], - 'verticalalignment': 'baseline', - 'horizontalalignment': loc} - - minor : bool, default: False - Whether to set the minor ticklabels rather than the major ones. - - Returns - ------- - list of `~.Text` - The labels. + def _set_tick_locations(self, ticks, *, minor=False): + # see docstring of set_ticks - Other Parameters - ---------------- - **kwargs : `~.text.Text` properties. - """ - if fontdict is not None: - kwargs.update(fontdict) - return self.set_ticklabels(labels, minor=minor, **kwargs) + # XXX if the user changes units, the information will be lost here + ticks = self.convert_units(ticks) + locator = mticker.FixedLocator(ticks) # validate ticks early. + for name, axis in self.axes._axis_map.items(): + if self is axis: + shared = [ + getattr(ax, f"{name}axis") + for ax + in self.axes._shared_axes[name].get_siblings(self.axes)] + break + else: + shared = [self] + if len(ticks): + for axis in shared: + # set_view_interval maintains any preexisting inversion. + axis.set_view_interval(min(ticks), max(ticks)) + self.axes.stale = True + if minor: + self.set_minor_locator(locator) + return self.get_minor_ticks(len(ticks)) + else: + self.set_major_locator(locator) + return self.get_major_ticks(len(ticks)) - def set_ticks(self, ticks, *, minor=False): + def set_ticks(self, ticks, labels=None, *, minor=False, **kwargs): """ - Set this Axis' tick locations. + Set this Axis' tick locations and optionally labels. If necessary, the view limits of the Axis are expanded so that all given ticks are visible. @@ -1804,9 +2044,22 @@ def set_ticks(self, ticks, *, minor=False): Parameters ---------- ticks : list of floats - List of tick locations. + List of tick locations. The axis `.Locator` is replaced by a + `~.ticker.FixedLocator`. + + Some tick formatters will not label arbitrary tick positions; + e.g. log formatters only label decade ticks by default. In + such a case you can set a formatter explicitly on the axis + using `.Axis.set_major_formatter` or provide formatted + *labels* yourself. + labels : list of str, optional + List of tick labels. If not set, the labels are generated with + the axis tick `.Formatter`. minor : bool, default: False If ``False``, set the major ticks; if ``True``, the minor ticks. + **kwargs + `.Text` properties for the labels. These take effect only if you + pass *labels*. In other cases, please use `~.Axes.tick_params`. Notes ----- @@ -1815,50 +2068,24 @@ def set_ticks(self, ticks, *, minor=False): other limits, you should set the limits explicitly after setting the ticks. """ - # XXX if the user changes units, the information will be lost here - ticks = self.convert_units(ticks) - if self is self.axes.xaxis: - shared = [ - ax.xaxis - for ax in self.axes.get_shared_x_axes().get_siblings(self.axes) - ] - elif self is self.axes.yaxis: - shared = [ - ax.yaxis - for ax in self.axes.get_shared_y_axes().get_siblings(self.axes) - ] - elif hasattr(self.axes, "zaxis") and self is self.axes.zaxis: - shared = [ - ax.zaxis - for ax in self.axes._shared_z_axes.get_siblings(self.axes) - ] - else: - shared = [self] - for axis in shared: - if len(ticks) > 1: - xleft, xright = axis.get_view_interval() - if xright > xleft: - axis.set_view_interval(min(ticks), max(ticks)) - else: - axis.set_view_interval(max(ticks), min(ticks)) - self.axes.stale = True - if minor: - self.set_minor_locator(mticker.FixedLocator(ticks)) - return self.get_minor_ticks(len(ticks)) - else: - self.set_major_locator(mticker.FixedLocator(ticks)) - return self.get_major_ticks(len(ticks)) + if labels is None and kwargs: + raise ValueError('labels argument cannot be None when ' + 'kwargs are passed') + result = self._set_tick_locations(ticks, minor=minor) + if labels is not None: + self.set_ticklabels(labels, minor=minor, **kwargs) + return result def _get_tick_boxes_siblings(self, renderer): """ Get the bounding boxes for this `.axis` and its siblings as set by `.Figure.align_xlabels` or `.Figure.align_ylabels`. - By default it just gets bboxes for self. + By default, it just gets bboxes for *self*. """ # Get the Grouper keeping track of x or y label groups for this figure. axis_names = [ - name for name, axis in self.axes._get_axis_map().items() + name for name, axis in self.axes._axis_map.items() if name in self.figure._align_label_groups and axis is self] if len(axis_names) != 1: return [], [] @@ -1866,11 +2093,11 @@ def _get_tick_boxes_siblings(self, renderer): grouper = self.figure._align_label_groups[axis_name] bboxes = [] bboxes2 = [] - # If we want to align labels from other axes: + # If we want to align labels from other Axes: for ax in grouper.get_siblings(self.axes): - axis = ax._get_axis_map()[axis_name] + axis = getattr(ax, f"{axis_name}axis") ticks_to_draw = axis._update_ticks() - tlb, tlb2 = axis._get_tick_bboxes(ticks_to_draw, renderer) + tlb, tlb2 = axis._get_ticklabel_bboxes(ticks_to_draw, renderer) bboxes.extend(tlb) bboxes2.extend(tlb2) return bboxes, bboxes2 @@ -1889,16 +2116,6 @@ def _update_offset_text_position(self, bboxes, bboxes2): """ raise NotImplementedError('Derived must override') - @_api.deprecated("3.3") - def pan(self, numsteps): - """Pan by *numsteps* (can be positive or negative).""" - self.major.locator.pan(numsteps) - - @_api.deprecated("3.3") - def zoom(self, direction): - """Zoom in/out on axis; if *direction* is >0 zoom in, else zoom out.""" - self.major.locator.zoom(direction) - def axis_date(self, tz=None): """ Set up axis ticks and labels to treat data along this Axis as dates. @@ -2011,9 +2228,17 @@ def setter(self, vmin, vmax, ignore=False): class XAxis(Axis): __name__ = 'xaxis' axis_name = 'x' #: Read-only name identifying the axis. + _tick_class = XTick def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._init() + + def _init(self): + """ + Initialize the label and offsetText instance values and + `label_position` / `offset_text_position`. + """ # x in axes coords, y in display coords (to be updated at draw time by # _update_label_positions and _update_offset_text_position). self.label.set( @@ -2023,6 +2248,7 @@ def __init__(self, *args, **kwargs): self.axes.transAxes, mtransforms.IdentityTransform()), ) self.label_position = 'bottom' + self.offsetText.set( x=1, y=0, verticalalignment='top', horizontalalignment='right', @@ -2034,7 +2260,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 @@ -2047,17 +2273,10 @@ def contains(self, mouseevent): return False, {} (l, b), (r, t) = self.axes.transAxes.transform([(0, 0), (1, 1)]) inaxis = 0 <= xaxes <= 1 and ( - b - self.pickradius < y < b or - t < y < t + self.pickradius) + b - self._pickradius < y < b or + t < y < t + self._pickradius) return inaxis, {} - def _get_tick(self, major): - if major: - tick_kw = self._major_tick_kw - else: - tick_kw = self._minor_tick_kw - return XTick(self.axes, 0, major=major, **tick_kw) - def set_label_position(self, position): """ Set the label position (top or bottom) @@ -2088,10 +2307,9 @@ def _update_label_position(self, renderer): if self.label_position == 'bottom': try: spine = self.axes.spines['bottom'] - spinebbox = spine.get_transform().transform_path( - spine.get_path()).get_extents() + spinebbox = spine.get_window_extent() except KeyError: - # use axes if spine doesn't exist + # use Axes if spine doesn't exist spinebbox = self.axes.bbox bbox = mtransforms.Bbox.union(bboxes + [spinebbox]) bottom = bbox.y0 @@ -2099,14 +2317,12 @@ def _update_label_position(self, renderer): self.label.set_position( (x, bottom - self.labelpad * self.figure.dpi / 72) ) - else: try: spine = self.axes.spines['top'] - spinebbox = spine.get_transform().transform_path( - spine.get_path()).get_extents() + spinebbox = spine.get_window_extent() except KeyError: - # use axes if spine doesn't exist + # use Axes if spine doesn't exist spinebbox = self.axes.bbox bbox = mtransforms.Bbox.union(bboxes2 + [spinebbox]) top = bbox.y1 @@ -2139,26 +2355,27 @@ def _update_offset_text_position(self, bboxes, bboxes2): y = top + self.OFFSETTEXTPAD * self.figure.dpi / 72 self.offsetText.set_position((x, y)) + @_api.deprecated("3.6") def get_text_heights(self, renderer): """ Return how much space should be reserved for text above and below the - axes, as a pair of floats. + Axes, as a pair of floats. """ bbox, bbox2 = self.get_ticklabel_extents(renderer) # MGDTODO: Need a better way to get the pad - padPixels = self.majorTicks[0].get_pad_pixels() + pad_pixels = self.majorTicks[0].get_pad_pixels() above = 0.0 if bbox2.height: - above += bbox2.height + padPixels + above += bbox2.height + pad_pixels below = 0.0 if bbox.height: - below += bbox.height + padPixels + below += bbox.height + pad_pixels if self.get_label_position() == 'top': - above += self.label.get_window_extent(renderer).height + padPixels + above += self.label.get_window_extent(renderer).height + pad_pixels else: - below += self.label.get_window_extent(renderer).height + padPixels + below += self.label.get_window_extent(renderer).height + pad_pixels return above, below def set_ticks_position(self, position): @@ -2203,7 +2420,7 @@ def set_ticks_position(self, position): def tick_top(self): """ - Move ticks and ticklabels (if present) to the top of the axes. + Move ticks and ticklabels (if present) to the top of the Axes. """ label = True if 'label1On' in self._major_tick_kw: @@ -2215,7 +2432,7 @@ def tick_top(self): def tick_bottom(self): """ - Move ticks and ticklabels (if present) to the bottom of the axes. + Move ticks and ticklabels (if present) to the bottom of the Axes. """ label = True if 'label1On' in self._major_tick_kw: @@ -2241,34 +2458,22 @@ def get_ticks_position(self): def get_minpos(self): return self.axes.dataLim.minposx - def set_inverted(self, inverted): - # docstring inherited - a, b = self.get_view_interval() - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - self.axes.set_xlim(sorted((a, b), reverse=bool(inverted)), auto=None) - def set_default_intervals(self): # docstring inherited - xmin, xmax = 0., 1. - dataMutated = self.axes.dataLim.mutatedx() - viewMutated = self.axes.viewLim.mutatedx() - if not dataMutated or not viewMutated: + # only change view if dataLim has not changed and user has + # not changed the view: + if (not self.axes.dataLim.mutatedx() and + not self.axes.viewLim.mutatedx()): if self.converter is not None: info = self.converter.axisinfo(self.units, self) if info.default_limits is not None: - valmin, valmax = info.default_limits - xmin = self.converter.convert(valmin, self.units, self) - xmax = self.converter.convert(valmax, self.units, self) - if not dataMutated: - self.axes.dataLim.intervalx = xmin, xmax - if not viewMutated: - self.axes.viewLim.intervalx = xmin, xmax + xmin, xmax = self.convert_units(info.default_limits) + self.axes.viewLim.intervalx = xmin, xmax self.stale = True def get_tick_space(self): - ends = mtransforms.Bbox.from_bounds(0, 0, 1, 1) - ends = ends.transformed(self.axes.transAxes - - self.figure.dpi_scale_trans) + ends = mtransforms.Bbox.unit().transformed( + self.axes.transAxes - self.figure.dpi_scale_trans) length = ends.width * 72 # There is a heuristic here that the aspect ratio of tick text # is no more than 3:1 @@ -2282,9 +2487,17 @@ def get_tick_space(self): class YAxis(Axis): __name__ = 'yaxis' axis_name = 'y' #: Read-only name identifying the axis. + _tick_class = YTick def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._init() + + def _init(self): + """ + Initialize the label and offsetText instance values and + `label_position` / `offset_text_position`. + """ # x in display coords, y in axes coords (to be updated at draw time by # _update_label_positions and _update_offset_text_position). self.label.set( @@ -2320,17 +2533,10 @@ def contains(self, mouseevent): return False, {} (l, b), (r, t) = self.axes.transAxes.transform([(0, 0), (1, 1)]) inaxis = 0 <= yaxes <= 1 and ( - l - self.pickradius < x < l or - r < x < r + self.pickradius) + l - self._pickradius < x < l or + r < x < r + self._pickradius) return inaxis, {} - def _get_tick(self, major): - if major: - tick_kw = self._major_tick_kw - else: - tick_kw = self._minor_tick_kw - return YTick(self.axes, 0, major=major, **tick_kw) - def set_label_position(self, position): """ Set the label position (left or right) @@ -2357,15 +2563,13 @@ def _update_label_position(self, renderer): # get bounding boxes for this axis and any siblings # that have been set by `fig.align_ylabels()` bboxes, bboxes2 = self._get_tick_boxes_siblings(renderer=renderer) - x, y = self.label.get_position() if self.label_position == 'left': try: spine = self.axes.spines['left'] - spinebbox = spine.get_transform().transform_path( - spine.get_path()).get_extents() + spinebbox = spine.get_window_extent() except KeyError: - # use axes if spine doesn't exist + # use Axes if spine doesn't exist spinebbox = self.axes.bbox bbox = mtransforms.Bbox.union(bboxes + [spinebbox]) left = bbox.x0 @@ -2376,14 +2580,13 @@ def _update_label_position(self, renderer): else: try: spine = self.axes.spines['right'] - spinebbox = spine.get_transform().transform_path( - spine.get_path()).get_extents() + spinebbox = spine.get_window_extent() except KeyError: - # use axes if spine doesn't exist + # use Axes if spine doesn't exist spinebbox = self.axes.bbox + bbox = mtransforms.Bbox.union(bboxes2 + [spinebbox]) right = bbox.x1 - self.label.set_position( (right + self.labelpad * self.figure.dpi / 72, y) ) @@ -2393,8 +2596,13 @@ def _update_offset_text_position(self, bboxes, bboxes2): Update the offset_text position based on the sequence of bounding boxes of all the ticklabels """ - x, y = self.offsetText.get_position() - top = self.axes.bbox.ymax + x, _ = self.offsetText.get_position() + if 'outline' in self.axes.spines: + # Special case for colorbars: + bbox = self.axes.spines['outline'].get_window_extent() + else: + bbox = self.axes.bbox + top = bbox.ymax self.offsetText.set_position( (x, top + self.OFFSETTEXTPAD * self.figure.dpi / 72) ) @@ -2412,22 +2620,23 @@ def set_offset_position(self, position): self.offsetText.set_position((x, y)) self.stale = True + @_api.deprecated("3.6") def get_text_widths(self, renderer): bbox, bbox2 = self.get_ticklabel_extents(renderer) # MGDTODO: Need a better way to get the pad - padPixels = self.majorTicks[0].get_pad_pixels() + pad_pixels = self.majorTicks[0].get_pad_pixels() left = 0.0 if bbox.width: - left += bbox.width + padPixels + left += bbox.width + pad_pixels right = 0.0 if bbox2.width: - right += bbox2.width + padPixels + right += bbox2.width + pad_pixels if self.get_label_position() == 'left': - left += self.label.get_window_extent(renderer).width + padPixels + left += self.label.get_window_extent(renderer).width + pad_pixels else: - right += self.label.get_window_extent(renderer).width + padPixels + right += self.label.get_window_extent(renderer).width + pad_pixels return left, right def set_ticks_position(self, position): @@ -2468,7 +2677,7 @@ def set_ticks_position(self, position): def tick_right(self): """ - Move ticks and ticklabels (if present) to the right of the axes. + Move ticks and ticklabels (if present) to the right of the Axes. """ label = True if 'label1On' in self._major_tick_kw: @@ -2481,7 +2690,7 @@ def tick_right(self): def tick_left(self): """ - Move ticks and ticklabels (if present) to the left of the axes. + Move ticks and ticklabels (if present) to the left of the Axes. """ label = True if 'label1On' in self._major_tick_kw: @@ -2508,34 +2717,22 @@ def get_ticks_position(self): def get_minpos(self): return self.axes.dataLim.minposy - def set_inverted(self, inverted): - # docstring inherited - a, b = self.get_view_interval() - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - self.axes.set_ylim(sorted((a, b), reverse=bool(inverted)), auto=None) - def set_default_intervals(self): # docstring inherited - ymin, ymax = 0., 1. - dataMutated = self.axes.dataLim.mutatedy() - viewMutated = self.axes.viewLim.mutatedy() - if not dataMutated or not viewMutated: + # only change view if dataLim has not changed and user has + # not changed the view: + if (not self.axes.dataLim.mutatedy() and + not self.axes.viewLim.mutatedy()): if self.converter is not None: info = self.converter.axisinfo(self.units, self) if info.default_limits is not None: - valmin, valmax = info.default_limits - ymin = self.converter.convert(valmin, self.units, self) - ymax = self.converter.convert(valmax, self.units, self) - if not dataMutated: - self.axes.dataLim.intervaly = ymin, ymax - if not viewMutated: - self.axes.viewLim.intervaly = ymin, ymax + ymin, ymax = self.convert_units(info.default_limits) + self.axes.viewLim.intervaly = ymin, ymax self.stale = True def get_tick_space(self): - ends = mtransforms.Bbox.from_bounds(0, 0, 1, 1) - ends = ends.transformed(self.axes.transAxes - - self.figure.dpi_scale_trans) + ends = mtransforms.Bbox.unit().transformed( + self.axes.transAxes - self.figure.dpi_scale_trans) length = ends.height * 72 # Having a spacing of at least 2 just looks good. size = self._get_tick_label_size('y') * 2 diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 32e069ed69cf..6532355f7efe 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -26,30 +26,30 @@ """ from collections import namedtuple -from contextlib import contextmanager, suppress +from contextlib import ExitStack, contextmanager, nullcontext from enum import Enum, IntEnum import functools import importlib import inspect import io +import itertools import logging import os -import re import sys import time -import traceback from weakref import WeakKeyDictionary import numpy as np import matplotlib as mpl from matplotlib import ( - _api, backend_tools as tools, cbook, colors, docstring, textpath, - tight_bbox, transforms, widgets, get_backend, is_interactive, rcParams) + _api, backend_tools as tools, cbook, colors, _docstring, text, + _tight_bbox, transforms, widgets, get_backend, is_interactive, rcParams) from matplotlib._pylab_helpers import Gcf from matplotlib.backend_managers import ToolManager from matplotlib.cbook import _setattr_cm from matplotlib.path import Path +from matplotlib.texmanager import TexManager from matplotlib.transforms import Affine2D from matplotlib._enums import JoinStyle, CapStyle @@ -69,6 +69,7 @@ 'svgz': 'Scalable Vector Graphics', 'tif': 'Tagged Image File Format', 'tiff': 'Tagged Image File Format', + 'webp': 'WebP Image Format', } _default_backends = { 'eps': 'matplotlib.backends.backend_ps', @@ -84,6 +85,7 @@ 'svgz': 'matplotlib.backends.backend_svg', 'tif': 'matplotlib.backends.backend_agg', 'tiff': 'matplotlib.backends.backend_agg', + 'webp': 'matplotlib.backends.backend_agg', } @@ -98,13 +100,15 @@ def _safe_pyplot_import(): current_framework = cbook._get_running_interactive_framework() if current_framework is None: raise # No, something else went wrong, likely with the install... - backend_mapping = {'qt5': 'qt5agg', - 'qt4': 'qt4agg', - 'gtk3': 'gtk3agg', - 'wx': 'wxagg', - 'tk': 'tkagg', - 'macosx': 'macosx', - 'headless': 'agg'} + backend_mapping = { + 'qt': 'qtagg', + 'gtk3': 'gtk3agg', + 'gtk4': 'gtk4agg', + 'wx': 'wxagg', + 'tk': 'tkagg', + 'macosx': 'macosx', + 'headless': 'agg', + } backend = backend_mapping[current_framework] rcParams["backend"] = mpl.rcParamsOrig["backend"] = backend import matplotlib.pyplot as plt # Now this should succeed. @@ -149,26 +153,26 @@ class RendererBase: 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 - give a highly capable backend): + functionality (though just implementing `draw_path` alone would give a + highly capable backend): - * :meth:`draw_path` - * :meth:`draw_image` - * :meth:`draw_gouraud_triangle` + * `draw_path` + * `draw_image` + * `draw_gouraud_triangles` The following methods *should* be implemented in the backend for optimization reasons: - * :meth:`draw_text` - * :meth:`draw_markers` - * :meth:`draw_path_collection` - * :meth:`draw_quad_mesh` + * `draw_text` + * `draw_markers` + * `draw_path_collection` + * `draw_quad_mesh` """ def __init__(self): super().__init__() self._texmanager = None - self._text2path = textpath.TextToPath() + self._text2path = text.TextToPath() self._raster_depth = 0 self._rasterizing = False @@ -195,10 +199,9 @@ def draw_markers(self, gc, marker_path, marker_trans, path, """ Draw a marker at each of *path*'s vertices (excluding control points). - This provides a fallback implementation of draw_markers that - makes multiple calls to :meth:`draw_path`. Some backends may - want to override this method in order to draw the marker only - once and reuse it multiple times. + The base (fallback) implementation makes multiple calls to `draw_path`. + Backends may want to override this method in order to draw the marker + only once and reuse it multiple times. Parameters ---------- @@ -218,38 +221,38 @@ def draw_markers(self, gc, marker_path, marker_trans, path, rgbFace) def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offsetTrans, facecolors, edgecolors, + offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): """ - Draw a collection of paths selecting drawing properties from - the lists *facecolors*, *edgecolors*, *linewidths*, - *linestyles* and *antialiaseds*. *offsets* is a list of - offsets to apply to each of the paths. The offsets in - *offsets* are first transformed by *offsetTrans* before being - applied. - - *offset_position* may be either "screen" or "data" depending on the - space that the offsets are in; "data" is deprecated. - - This provides a fallback implementation of - :meth:`draw_path_collection` that makes multiple calls to - :meth:`draw_path`. Some backends may want to override this in - order to render each set of path data only once, and then - reference that path multiple times with the different offsets, - colors, styles etc. The generator methods - :meth:`_iter_collection_raw_paths` and - :meth:`_iter_collection` are provided to help with (and - standardize) the implementation across backends. It is highly - recommended to use those generators, so that changes to the - behavior of :meth:`draw_path_collection` can be made globally. + Draw a collection of *paths*. + + Each path is first transformed by the corresponding entry + in *all_transforms* (a list of (3, 3) matrices) and then by + *master_transform*. They are then translated by the corresponding + entry in *offsets*, which has been first transformed by *offset_trans*. + + *facecolors*, *edgecolors*, *linewidths*, *linestyles*, and + *antialiased* are lists that set the corresponding properties. + + *offset_position* is unused now, but the argument is kept for + backwards compatibility. + + The base (fallback) implementation makes multiple calls to `draw_path`. + Backends may want to override this in order to render each set of + path data only once, and then reference that path multiple times with + the different offsets, colors, styles etc. The generator methods + `_iter_collection_raw_paths` and `_iter_collection` are provided to + help with (and standardize) the implementation across backends. It + is highly recommended to use those generators, so that changes to the + behavior of `draw_path_collection` can be made globally. """ path_ids = self._iter_collection_raw_paths(master_transform, paths, all_transforms) for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, master_transform, all_transforms, list(path_ids), offsets, - offsetTrans, facecolors, edgecolors, linewidths, linestyles, + gc, list(path_ids), offsets, offset_trans, + facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): path, transform = path_id # Only apply another translation if we have an offset, else we @@ -266,13 +269,14 @@ def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, coordinates, offsets, offsetTrans, facecolors, antialiased, edgecolors): """ - Fallback implementation of :meth:`draw_quad_mesh` that generates paths - and then calls :meth:`draw_path_collection`. + Draw a quadmesh. + + The base (fallback) implementation converts the quadmesh to paths and + then calls `draw_path_collection`. """ from matplotlib.collections import QuadMesh - paths = QuadMesh.convert_mesh_to_paths( - meshWidth, meshHeight, coordinates) + paths = QuadMesh._convert_mesh_to_paths(coordinates) if edgecolors is None: edgecolors = facecolors @@ -282,6 +286,7 @@ def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, gc, master_transform, paths, [], offsets, offsetTrans, facecolors, edgecolors, linewidths, [], [antialiased], [None], 'screen') + @_api.deprecated("3.7", alternative="draw_gouraud_triangles") def draw_gouraud_triangle(self, gc, points, colors, transform): """ Draw a Gouraud-shaded triangle. @@ -306,32 +311,31 @@ def draw_gouraud_triangles(self, gc, triangles_array, colors_array, Parameters ---------- - points : (N, 3, 2) array-like + gc : `.GraphicsContextBase` + The graphics context. + triangles_array : (N, 3, 2) array-like Array of *N* (x, y) points for the triangles. - colors : (N, 3, 4) array-like + colors_array : (N, 3, 4) array-like Array of *N* RGBA colors for each point of the triangles. transform : `matplotlib.transforms.Transform` An affine transform to apply to the points. """ - transform = transform.frozen() - for tri, col in zip(triangles_array, colors_array): - self.draw_gouraud_triangle(gc, tri, col, transform) + raise NotImplementedError def _iter_collection_raw_paths(self, master_transform, paths, all_transforms): """ - Helper method (along with :meth:`_iter_collection`) to implement - :meth:`draw_path_collection` in a space-efficient manner. + Helper method (along with `_iter_collection`) to implement + `draw_path_collection` in a memory-efficient manner. - This method yields all of the base path/transform - combinations, given a master transform, a list of paths and - list of transforms. + This method yields all of the base path/transform combinations, given a + master transform, a list of paths and list of transforms. The arguments should be exactly what is passed in to - :meth:`draw_path_collection`. + `draw_path_collection`. - The backend should take each yielded path and transform and - create an object that can be referenced (reused) later. + The backend should take each yielded path and transform and create an + object that can be referenced (reused) later. """ Npaths = len(paths) Ntransforms = len(all_transforms) @@ -351,8 +355,8 @@ def _iter_collection_uses_per_path(self, paths, all_transforms, offsets, facecolors, edgecolors): """ Compute how many times each raw path object returned by - _iter_collection_raw_paths would be used when calling - _iter_collection. This is intended for the backend to decide + `_iter_collection_raw_paths` would be used when calling + `_iter_collection`. This is intended for the backend to decide on the tradeoff between using the paths in-line and storing them once and reusing. Rounds up in case the number of uses is not the same for every path. @@ -364,24 +368,22 @@ def _iter_collection_uses_per_path(self, paths, all_transforms, N = max(Npath_ids, len(offsets)) return (N + Npath_ids - 1) // Npath_ids - def _iter_collection(self, gc, master_transform, all_transforms, - path_ids, offsets, offsetTrans, facecolors, + def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): """ - Helper method (along with :meth:`_iter_collection_raw_paths`) to - implement :meth:`draw_path_collection` in a space-efficient manner. + Helper method (along with `_iter_collection_raw_paths`) to implement + `draw_path_collection` in a memory-efficient manner. - This method yields all of the path, offset and graphics - context combinations to draw the path collection. The caller - should already have looped over the results of - :meth:`_iter_collection_raw_paths` to draw this collection. + This method yields all of the path, offset and graphics context + combinations to draw the path collection. The caller should already + have looped over the results of `_iter_collection_raw_paths` to draw + this collection. The arguments should be the same as that passed into - :meth:`draw_path_collection`, with the exception of - *path_ids*, which is a list of arbitrary objects that the - backend will use to reference one of the paths created in the - :meth:`_iter_collection_raw_paths` stage. + `draw_path_collection`, with the exception of *path_ids*, which is a + list of arbitrary objects that the backend will use to reference one of + the paths created in the `_iter_collection_raw_paths` stage. Each yielded result is of the form:: @@ -391,7 +393,6 @@ def _iter_collection(self, gc, master_transform, all_transforms, *path_ids*; *gc* is a graphics context and *rgbFace* is a color to use for filling the path. """ - Ntransforms = len(all_transforms) Npaths = len(path_ids) Noffsets = len(offsets) N = max(Npaths, Noffsets) @@ -399,74 +400,55 @@ def _iter_collection(self, gc, master_transform, all_transforms, Nedgecolors = len(edgecolors) Nlinewidths = len(linewidths) Nlinestyles = len(linestyles) - Naa = len(antialiaseds) Nurls = len(urls) - if offset_position == "data": - _api.warn_deprecated( - "3.3", message="Support for offset_position='data' is " - "deprecated since %(since)s and will be removed %(removal)s.") - if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0: return - if Noffsets: - toffsets = offsetTrans.transform(offsets) gc0 = self.new_gc() gc0.copy_properties(gc) - if Nfacecolors == 0: - rgbFace = None + def cycle_or_default(seq, default=None): + # Cycle over *seq* if it is not empty; else always yield *default*. + return (itertools.cycle(seq) if len(seq) + else itertools.repeat(default)) + + pathids = cycle_or_default(path_ids) + toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0)) + fcs = cycle_or_default(facecolors) + ecs = cycle_or_default(edgecolors) + lws = cycle_or_default(linewidths) + lss = cycle_or_default(linestyles) + aas = cycle_or_default(antialiaseds) + urls = cycle_or_default(urls) if Nedgecolors == 0: gc0.set_linewidth(0.0) - xo, yo = 0, 0 - for i in range(N): - path_id = path_ids[i % Npaths] - if Noffsets: - xo, yo = toffsets[i % Noffsets] - if offset_position == 'data': - if Ntransforms: - transform = ( - Affine2D(all_transforms[i % Ntransforms]) + - master_transform) - else: - transform = master_transform - (xo, yo), (xp, yp) = transform.transform( - [(xo, yo), (0, 0)]) - xo = -(xp - xo) - yo = -(yp - yo) + for pathid, (xo, yo), fc, ec, lw, ls, aa, url in itertools.islice( + zip(pathids, toffsets, fcs, ecs, lws, lss, aas, urls), N): if not (np.isfinite(xo) and np.isfinite(yo)): continue - if Nfacecolors: - rgbFace = facecolors[i % Nfacecolors] if Nedgecolors: if Nlinewidths: - gc0.set_linewidth(linewidths[i % Nlinewidths]) + gc0.set_linewidth(lw) if Nlinestyles: - gc0.set_dashes(*linestyles[i % Nlinestyles]) - fg = edgecolors[i % Nedgecolors] - if len(fg) == 4: - if fg[3] == 0.0: - gc0.set_linewidth(0) - else: - gc0.set_foreground(fg) + gc0.set_dashes(*ls) + if len(ec) == 4 and ec[3] == 0.0: + gc0.set_linewidth(0) else: - gc0.set_foreground(fg) - if rgbFace is not None and len(rgbFace) == 4: - if rgbFace[3] == 0: - rgbFace = None - gc0.set_antialiased(antialiaseds[i % Naa]) + gc0.set_foreground(ec) + if fc is not None and len(fc) == 4 and fc[3] == 0: + fc = None + gc0.set_antialiased(aa) if Nurls: - gc0.set_url(urls[i % Nurls]) - - yield xo, yo, path_id, gc0, rgbFace + gc0.set_url(url) + yield xo, yo, pathid, gc0, fc gc0.restore() def get_image_magnification(self): """ - Get the factor by which to magnify images passed to :meth:`draw_image`. + Get the factor by which to magnify images passed to `draw_image`. Allows a backend to have images at a different resolution to other artists. """ @@ -494,14 +476,13 @@ def draw_image(self, gc, x, y, im, transform=None): transform : `matplotlib.transforms.Affine2DBase` If and only if the concrete backend is written such that - :meth:`option_scale_image` returns ``True``, an affine - transformation (i.e., an `.Affine2DBase`) *may* be passed to - :meth:`draw_image`. The translation vector of the transformation - is given in physical units (i.e., dots or pixels). Note that - the transformation does not override *x* and *y*, and has to be - applied *before* translating the result by *x* and *y* (this can - be accomplished by adding *x* and *y* to the translation vector - defined by *transform*). + `option_scale_image` returns ``True``, an affine transformation + (i.e., an `.Affine2DBase`) *may* be passed to `draw_image`. The + translation vector of the transformation is given in physical units + (i.e., dots or pixels). Note that the transformation does not + override *x* and *y*, and has to be applied *before* translating + the result by *x* and *y* (this can be accomplished by adding *x* + and *y* to the translation vector defined by *transform*). """ raise NotImplementedError @@ -517,20 +498,37 @@ def option_image_nocomposite(self): def option_scale_image(self): """ - Return whether arbitrary affine transformations in :meth:`draw_image` - are supported (True for most vector backends). + Return whether arbitrary affine transformations in `draw_image` are + supported (True for most vector backends). """ return False - @_api.delete_parameter("3.3", "ismath") - def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): + def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): """ + Draw a TeX instance. + + Parameters + ---------- + gc : `.GraphicsContextBase` + The graphics context. + x : float + The x location of the text in display coords. + y : float + The y location of the text baseline in display coords. + s : str + The TeX text string. + prop : `~matplotlib.font_manager.FontProperties` + The font properties. + angle : float + The rotation angle in degrees anti-clockwise. + mtext : `matplotlib.text.Text` + The original text object to be rendered. """ self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): """ - Draw the text instance. + Draw a text instance. Parameters ---------- @@ -542,10 +540,12 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): The y location of the text baseline in display coords. s : str The text string. - prop : `matplotlib.font_manager.FontProperties` + prop : `~matplotlib.font_manager.FontProperties` The font properties. angle : float The rotation angle in degrees anti-clockwise. + ismath : bool or "TeX" + If True, use mathtext parser. If "TeX", use tex for rendering. mtext : `matplotlib.text.Text` The original text object to be rendered. @@ -571,12 +571,18 @@ def _get_text_path_transform(self, x, y, s, prop, angle, ismath): Parameters ---------- - prop : `matplotlib.font_manager.FontProperties` - The font property. + x : float + The x location of the text in display coords. + y : float + The y location of the text baseline in display coords. s : str The text to be converted. + prop : `~matplotlib.font_manager.FontProperties` + The font property. + angle : float + Angle in degrees to render the text at. ismath : bool or "TeX" - If True, use mathtext parser. If "TeX", use *usetex* mode. + If True, use mathtext parser. If "TeX", use tex for rendering. """ text2path = self._text2path @@ -601,18 +607,24 @@ def _get_text_path_transform(self, x, y, s, prop, angle, ismath): def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): """ - Draw the text by converting them to paths using textpath module. + Draw the text by converting them to paths using `.TextToPath`. Parameters ---------- - prop : `matplotlib.font_manager.FontProperties` - The font property. + gc : `.GraphicsContextBase` + The graphics context. + x : float + The x location of the text in display coords. + y : float + The y location of the text baseline in display coords. s : str The text to be converted. - usetex : bool - Whether to use usetex mode. + prop : `~matplotlib.font_manager.FontProperties` + The font property. + angle : float + Angle in degrees to render the text at. ismath : bool or "TeX" - If True, use mathtext parser. If "TeX", use *usetex* mode. + If True, use mathtext parser. If "TeX", use tex for rendering. """ path, transform = self._get_text_path_transform( x, y, s, prop, angle, ismath) @@ -626,13 +638,12 @@ def get_text_width_height_descent(self, s, prop, ismath): to the baseline), in display coords, of the string *s* with `.FontProperties` *prop*. """ + fontsize = prop.get_size_in_points() + if ismath == 'TeX': - # todo: handle props - texmanager = self._text2path.get_texmanager() - fontsize = prop.get_size_in_points() - w, h, d = texmanager.get_text_width_height_descent( + # todo: handle properties + return self.get_texmanager().get_text_width_height_descent( s, fontsize, renderer=self) - return w, h, d dpi = self.points_to_pixels(72) if ismath: @@ -641,8 +652,7 @@ def get_text_width_height_descent(self, s, prop, ismath): flags = self._text2path._get_hinting_flag() font = self._text2path._get_font(prop) - size = prop.get_size_in_points() - font.set_size(size, dpi) + font.set_size(fontsize, dpi) # the width and height of unrotated string font.set_text(s, 0.0, flags=flags) w, h = font.get_width_height() @@ -656,7 +666,7 @@ def flipy(self): """ Return whether y values increase from top to bottom. - Note that this only affects drawing of texts and images. + Note that this only affects drawing of texts. """ return True @@ -667,7 +677,6 @@ def get_canvas_width_height(self): def get_texmanager(self): """Return the `.TexManager` instance.""" if self._texmanager is None: - from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager @@ -835,12 +844,9 @@ def get_dashes(self): """ Return the dash style as an (offset, dash-list) pair. - The dash list is a even-length list that gives the ink on, ink off in - points. See p. 107 of to PostScript `blue book`_ for more info. + See `.set_dashes` for details. Default value is (None, None). - - .. _blue book: https://www-cdf.fnal.gov/offline/PostScript/BLUEBOOK.PDF """ return self._dashes @@ -904,7 +910,7 @@ def set_antialiased(self, b): # Use ints to make life easier on extension code trying to read the gc. self._antialiased = int(bool(b)) - @docstring.interpd + @_docstring.interpd def set_capstyle(self, cs): """ Set how to draw endpoints of lines. @@ -930,24 +936,28 @@ def set_dashes(self, dash_offset, dash_list): Parameters ---------- - dash_offset : float or None - The offset (usually 0). + dash_offset : float + Distance, in points, into the dash pattern at which to + start the pattern. It is usually set to 0. dash_list : array-like or None - The on-off sequence as points. + The on-off sequence as points. None specifies a solid line. All + values must otherwise be non-negative (:math:`\\ge 0`). Notes ----- - ``(None, None)`` specifies a solid line. - - See p. 107 of to PostScript `blue book`_ for more info. - - .. _blue book: https://www-cdf.fnal.gov/offline/PostScript/BLUEBOOK.PDF + See p. 666 of the PostScript + `Language Reference + `_ + for more info. """ if dash_list is not None: dl = np.asarray(dash_list) if np.any(dl < 0.0): raise ValueError( - "All values in the dash list must be positive") + "All values in the dash list must be non-negative") + if dl.size and not np.any(dl > 0.0): + raise ValueError( + 'At least one value in the dash list must be positive') self._dashes = dash_offset, dash_list def set_foreground(self, fg, isRGBA=False): @@ -970,7 +980,7 @@ def set_foreground(self, fg, isRGBA=False): else: self._rgb = colors.to_rgba(fg) - @docstring.interpd + @_docstring.interpd def set_joinstyle(self, js): """ Set how to draw connections between line segments. @@ -1228,9 +1238,10 @@ def _on_timer(self): class Event: """ - A Matplotlib event. Attach additional attributes as defined in - :meth:`FigureCanvasBase.mpl_connect`. The following attributes - are defined and shown with their default values + A Matplotlib event. + + The following attributes are defined and shown with their default values. + Subclasses may define additional attributes. Attributes ---------- @@ -1241,28 +1252,33 @@ class Event: guiEvent The GUI event that triggered the Matplotlib event. """ + def __init__(self, name, canvas, guiEvent=None): self.name = name self.canvas = canvas self.guiEvent = guiEvent + def _process(self): + """Generate an event with name ``self.name`` on ``self.canvas``.""" + self.canvas.callbacks.process(self.name, self) + class DrawEvent(Event): """ - An event triggered by a draw operation on the canvas + An event triggered by a draw operation on the canvas. - In most backends callbacks subscribed to this callback will be - fired after the rendering is complete but before the screen is - updated. Any extra artists drawn to the canvas's renderer will - be reflected without an explicit call to ``blit``. + In most backends, callbacks subscribed to this event will be fired after + the rendering is complete but before the screen is updated. Any extra + artists drawn to the canvas's renderer will be reflected without an + explicit call to ``blit``. .. warning:: Calling ``canvas.draw`` and ``canvas.blit`` in these callbacks may not be safe with all backends and may cause infinite recursion. - In addition to the `Event` attributes, the following event - attributes are defined: + A DrawEvent has a number of special attributes in addition to those defined + by the parent `Event` class. Attributes ---------- @@ -1276,10 +1292,10 @@ def __init__(self, name, canvas, renderer): class ResizeEvent(Event): """ - An event triggered by a canvas resize + An event triggered by a canvas resize. - In addition to the `Event` attributes, the following event - attributes are defined: + A ResizeEvent has a number of special attributes in addition to those + defined by the parent `Event` class. Attributes ---------- @@ -1288,6 +1304,7 @@ class ResizeEvent(Event): height : int Height of the canvas in pixels. """ + def __init__(self, name, canvas): super().__init__(name, canvas) self.width, self.height = canvas.get_width_height() @@ -1301,44 +1318,37 @@ class LocationEvent(Event): """ An event that has a screen location. - The following additional attributes are defined and shown with - their default values. - - In addition to the `Event` attributes, the following - event attributes are defined: + A LocationEvent has a number of special attributes in addition to those + defined by the parent `Event` class. Attributes ---------- - x : int - x position - pixels from left of canvas. - y : int - y position - pixels from bottom of canvas. + x, y : int or None + Event location in pixels from bottom left of canvas. inaxes : `~.axes.Axes` or None The `~.axes.Axes` instance over which the mouse is, if any. - xdata : float or None - x data coordinate of the mouse. - ydata : float or None - y data coordinate of the mouse. + xdata, ydata : float or None + Data coordinates of the mouse within *inaxes*, or *None* if the mouse + is not over an Axes. + modifiers : frozenset + The keyboard modifiers currently being pressed (except for KeyEvent). """ - lastevent = None # the last event that was triggered before this one + lastevent = None # The last event processed so far. - def __init__(self, name, canvas, x, y, guiEvent=None): - """ - (*x*, *y*) in figure coords ((0, 0) = bottom left). - """ + def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None): super().__init__(name, canvas, guiEvent=guiEvent) # x position - pixels from left of canvas self.x = int(x) if x is not None else x # y position - pixels from right of canvas self.y = int(y) if y is not None else y - self.inaxes = None # the Axes instance if mouse us over axes + self.inaxes = None # the Axes instance the mouse is over self.xdata = None # x coord of mouse in data coords self.ydata = None # y coord of mouse in data coords + self.modifiers = frozenset(modifiers if modifiers is not None else []) if x is None or y is None: - # cannot check if event was in axes if no (x, y) info - self._update_enter_leave() + # cannot check if event was in Axes if no (x, y) info return if self.canvas.mouse_grabber is None: @@ -1356,34 +1366,6 @@ def __init__(self, name, canvas, x, y, guiEvent=None): self.xdata = xdata self.ydata = ydata - self._update_enter_leave() - - def _update_enter_leave(self): - """Process the figure/axes enter leave events.""" - if LocationEvent.lastevent is not None: - last = LocationEvent.lastevent - if last.inaxes != self.inaxes: - # process axes enter/leave events - try: - if last.inaxes is not None: - last.canvas.callbacks.process('axes_leave_event', last) - except Exception: - pass - # See ticket 2901582. - # I think this is a valid exception to the rule - # against catching all exceptions; if anything goes - # wrong, we simply want to move on and process the - # current event. - if self.inaxes is not None: - self.canvas.callbacks.process('axes_enter_event', self) - - else: - # process a figure enter event - if self.inaxes is not None: - self.canvas.callbacks.process('axes_enter_event', self) - - LocationEvent.lastevent = self - class MouseButton(IntEnum): LEFT = 1 @@ -1395,23 +1377,25 @@ class MouseButton(IntEnum): class MouseEvent(LocationEvent): """ - A mouse event ('button_press_event', - 'button_release_event', - 'scroll_event', - 'motion_notify_event'). + A mouse event ('button_press_event', 'button_release_event', \ +'scroll_event', 'motion_notify_event'). - In addition to the `Event` and `LocationEvent` - attributes, the following attributes are defined: + A MouseEvent has a number of special attributes in addition to those + defined by the parent `Event` and `LocationEvent` classes. Attributes ---------- button : None or `MouseButton` or {'up', 'down'} The button pressed. 'up' and 'down' are used for scroll events. + Note that LEFT and RIGHT actually refer to the "primary" and "secondary" buttons, i.e. if the user inverts their left and right buttons ("left-handed setting") then the LEFT button will be the one physically on the right. + If this is unset, *name* is "scroll_event", and *step* is nonzero, then + this will be set to "up" or "down" depending on the sign of *step*. + key : None or str The key pressed when the mouse event triggered, e.g. 'shift'. See `KeyEvent`. @@ -1420,7 +1404,9 @@ class MouseEvent(LocationEvent): This key is currently obtained from the last 'key_press_event' or 'key_release_event' that occurred within the canvas. Thus, if the last change of keyboard state occurred while the canvas did not have - focus, this attribute will be wrong. + focus, this attribute will be wrong. On the other hand, the + ``modifiers`` attribute should always be correct, but it can only + report on modifier keys. step : float The number of scroll steps (positive for 'up', negative for 'down'). @@ -1442,22 +1428,21 @@ def on_press(event): """ def __init__(self, name, canvas, x, y, button=None, key=None, - step=0, dblclick=False, guiEvent=None): - """ - (*x*, *y*) in figure coords ((0, 0) = bottom left) - button pressed None, 1, 2, 3, 'up', 'down' - """ + step=0, dblclick=False, guiEvent=None, *, modifiers=None): + super().__init__( + name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers) if button in MouseButton.__members__.values(): button = MouseButton(button) + if name == "scroll_event" and button is None: + if step > 0: + button = "up" + elif step < 0: + button = "down" self.button = button self.key = key self.step = step self.dblclick = dblclick - # super-init is deferred to the end because it calls back on - # 'axes_enter_event', which requires a fully initialized event. - super().__init__(name, canvas, x, y, guiEvent=guiEvent) - def __str__(self): return (f"{self.name}: " f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) " @@ -1467,11 +1452,14 @@ def __str__(self): class PickEvent(Event): """ - A pick event, fired when the user picks a location on the canvas + A pick event. + + This event is fired when the user picks a location on the canvas sufficiently close to an artist that has been made pickable with `.Artist.set_picker`. - Attrs: all the `Event` attributes plus + A PickEvent has a number of special attributes in addition to those defined + by the parent `Event` class. Attributes ---------- @@ -1482,8 +1470,8 @@ class PickEvent(Event): (see `.Artist.set_picker`). other Additional attributes may be present depending on the type of the - picked object; e.g., a `~.Line2D` pick may define different extra - attributes than a `~.PatchCollection` pick. + picked object; e.g., a `.Line2D` pick may define different extra + attributes than a `.PatchCollection` pick. Examples -------- @@ -1496,12 +1484,15 @@ def on_pick(event): line = event.artist xdata, ydata = line.get_data() ind = event.ind - print('on pick line:', np.array([xdata[ind], ydata[ind]]).T) + print(f'on pick line: {xdata[ind]:.3f}, {ydata[ind]:.3f}') cid = fig.canvas.mpl_connect('pick_event', on_pick) """ + def __init__(self, name, canvas, mouseevent, artist, guiEvent=None, **kwargs): + if guiEvent is None: + guiEvent = mouseevent.guiEvent super().__init__(name, canvas, guiEvent) self.mouseevent = mouseevent self.artist = artist @@ -1512,19 +1503,16 @@ class KeyEvent(LocationEvent): """ A key event (key press, key release). - Attach additional attributes as defined in - :meth:`FigureCanvasBase.mpl_connect`. - - In addition to the `Event` and `LocationEvent` - attributes, the following attributes are defined: + A KeyEvent has a number of special attributes in addition to those defined + by the parent `Event` and `LocationEvent` classes. Attributes ---------- key : None or str - the key(s) pressed. Could be **None**, a single case sensitive ascii - character ("g", "G", "#", etc.), a special key - ("control", "shift", "f1", "up", etc.) or a - combination of the above (e.g., "ctrl+alt+g", "ctrl+alt+G"). + The key(s) pressed. Could be *None*, a single case sensitive Unicode + character ("g", "G", "#", etc.), a special key ("control", "shift", + "f1", "up", etc.) or a combination of the above (e.g., "ctrl+alt+g", + "ctrl+alt+G"). Notes ----- @@ -1542,16 +1530,51 @@ def on_key(event): cid = fig.canvas.mpl_connect('key_press_event', on_key) """ + def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): - self.key = key - # super-init deferred to the end: callback errors if called before super().__init__(name, canvas, x, y, guiEvent=guiEvent) + self.key = key + + +# Default callback for key events. +def _key_handler(event): + # Dead reckoning of key. + if event.name == "key_press_event": + event.canvas._key = event.key + elif event.name == "key_release_event": + event.canvas._key = None + + +# Default callback for mouse events. +def _mouse_handler(event): + # Dead-reckoning of button and key. + if event.name == "button_press_event": + event.canvas._button = event.button + elif event.name == "button_release_event": + event.canvas._button = None + elif event.name == "motion_notify_event" and event.button is None: + event.button = event.canvas._button + if event.key is None: + event.key = event.canvas._key + # Emit axes_enter/axes_leave. + if event.name == "motion_notify_event": + last = LocationEvent.lastevent + last_axes = last.inaxes if last is not None else None + if last_axes != event.inaxes: + if last_axes is not None: + try: + last.canvas.callbacks.process("axes_leave_event", last) + except Exception: + pass # The last canvas may already have been torn down. + if event.inaxes is not None: + event.canvas.callbacks.process("axes_enter_event", event) + LocationEvent.lastevent = ( + None if event.name == "figure_leave_event" else event) def _get_renderer(figure, print_method=None): """ - Get the renderer that would be used to save a `~.Figure`, and cache it on - the figure. + Get the renderer that would be used to save a `.Figure`. If you need a renderer without any active draw methods use renderer._draw_disabled to temporary patch them out at your call site. @@ -1564,35 +1587,32 @@ class Done(Exception): def _draw(renderer): raise Done(renderer) - with cbook._setattr_cm(figure, draw=_draw): - orig_canvas = figure.canvas + with cbook._setattr_cm(figure, draw=_draw), ExitStack() as stack: if print_method is None: fmt = figure.canvas.get_default_filetype() # Even for a canvas' default output type, a canvas switch may be # needed, e.g. for FigureCanvasBase. - print_method = getattr( - figure.canvas._get_output_canvas(None, fmt), f"print_{fmt}") + print_method = stack.enter_context( + figure.canvas._switch_canvas_and_return_print_method(fmt)) try: print_method(io.BytesIO()) except Done as exc: - renderer, = figure._cachedRenderer, = exc.args + renderer, = exc.args return renderer else: raise RuntimeError(f"{print_method} did not call Figure.draw, so " f"no renderer is available") - finally: - figure.canvas = orig_canvas def _no_output_draw(figure): - renderer = _get_renderer(figure) - with renderer._draw_disabled(): - figure.draw(renderer) + # _no_output_draw was promoted to the figure level, but + # keep this here in case someone was calling it... + figure.draw_without_rendering() def _is_non_interactive_terminal_ipython(ip): """ - Return whether we are in a a terminal IPython, but non interactive. + Return whether we are in a terminal IPython, but non interactive. When in _terminal_ IPython, ip.parent will have and `interact` attribute, if this attribute is False we do not setup eventloop integration as the @@ -1604,73 +1624,6 @@ def _is_non_interactive_terminal_ipython(ip): and getattr(ip.parent, 'interact', None) is False) -def _check_savefig_extra_args(func=None, extra_kwargs=()): - """ - Decorator for the final print_* methods that accept keyword arguments. - - If any unused keyword arguments are left, this decorator will warn about - them, and as part of the warning, will attempt to specify the function that - the user actually called, instead of the backend-specific method. If unable - to determine which function the user called, it will specify `.savefig`. - - For compatibility across backends, this does not warn about keyword - arguments added by `FigureCanvasBase.print_figure` for use in a subset of - backends, because the user would not have added them directly. - """ - - if func is None: - return functools.partial(_check_savefig_extra_args, - extra_kwargs=extra_kwargs) - - old_sig = inspect.signature(func) - - @functools.wraps(func) - def wrapper(*args, **kwargs): - name = 'savefig' # Reasonable default guess. - public_api = re.compile( - r'^savefig|print_[A-Za-z0-9]+|_no_output_draw$' - ) - seen_print_figure = False - for frame, line in traceback.walk_stack(None): - if frame is None: - # when called in embedded context may hit frame is None. - break - if re.match(r'\A(matplotlib|mpl_toolkits)(\Z|\.(?!tests\.))', - # Work around sphinx-gallery not setting __name__. - frame.f_globals.get('__name__', '')): - if public_api.match(frame.f_code.co_name): - name = frame.f_code.co_name - if name in ('print_figure', '_no_output_draw'): - seen_print_figure = True - - else: - break - - accepted_kwargs = {*old_sig.parameters, *extra_kwargs} - if seen_print_figure: - for kw in ['dpi', 'facecolor', 'edgecolor', 'orientation', - 'bbox_inches_restore']: - # Ignore keyword arguments that are passed in by print_figure - # for the use of other renderers. - if kw not in accepted_kwargs: - kwargs.pop(kw, None) - - for arg in list(kwargs): - if arg in accepted_kwargs: - continue - _api.warn_deprecated( - '3.3', name=name, - message='%(name)s() got unexpected keyword argument "' - + arg + '" which is no longer supported as of ' - '%(since)s and will become an error ' - '%(removal)s') - kwargs.pop(arg) - - return func(*args, **kwargs) - - return wrapper - - class FigureCanvasBase: """ The canvas the figure renders into. @@ -1681,10 +1634,17 @@ class FigureCanvasBase: A high-level figure instance. """ - # Set to one of {"qt5", "qt4", "gtk3", "wx", "tk", "macosx"} if an + # Set to one of {"qt", "gtk3", "gtk4", "wx", "tk", "macosx"} if an # interactive framework is required, or None otherwise. required_interactive_framework = None + # The manager class instantiated by new_manager. + # (This is defined as a classproperty because the manager class is + # currently defined *after* the canvas class, but one could also assign + # ``FigureCanvasBase.manager_class = FigureManagerBase`` + # after defining both classes.) + manager_class = _api.classproperty(lambda cls: FigureManagerBase) + events = [ 'resize_event', 'draw_event', @@ -1726,9 +1686,13 @@ def __init__(self, figure=None): self._button = None # the button pressed self._key = None # the key pressed self._lastx, self._lasty = None, None - self.mouse_grabber = None # the axes currently grabbing mouse + self.mouse_grabber = None # the Axes currently grabbing mouse self.toolbar = None # NavigationToolbar2 will set me self._is_idle_drawing = False + # We don't want to scale up the figure DPI more than once. + figure._original_dpi = figure.dpi + self._device_pixel_ratio = 1 + super().__init__() # Typically the GUI widget init (if any). callbacks = property(lambda self: self.figure._canvas_callbacks) button_pick_id = property(lambda self: self.figure._button_pick_id) @@ -1753,13 +1717,30 @@ def _fix_ipython_backend2gui(cls): # In case we ever move the patch to IPython and remove these APIs, # don't break on our side. return - rif = getattr(cls, "required_interactive_framework", None) - backend2gui_rif = {"qt5": "qt", "qt4": "qt", "gtk3": "gtk3", - "wx": "wx", "macosx": "osx"}.get(rif) + backend2gui_rif = { + "qt": "qt", + "gtk3": "gtk3", + "gtk4": "gtk4", + "wx": "wx", + "macosx": "osx", + }.get(cls.required_interactive_framework) if backend2gui_rif: if _is_non_interactive_terminal_ipython(ip): ip.enable_gui(backend2gui_rif) + @classmethod + def new_manager(cls, figure, num): + """ + Create a new figure manager for *figure*, using this canvas class. + + Notes + ----- + This method should not be reimplemented in subclasses. If + custom manager creation logic is needed, please reimplement + ``FigureManager.create_with_canvas``. + """ + return cls.manager_class.create_with_canvas(cls, figure, num) + @contextmanager def _idle_draw_cntx(self): self._is_idle_drawing = True @@ -1775,6 +1756,7 @@ def is_saving(self): """ return self._is_saving + @_api.deprecated("3.6", alternative="canvas.figure.pick") def pick(self, mouseevent): if not self.widgetlock.locked(): self.figure.pick(mouseevent) @@ -1783,14 +1765,30 @@ def blit(self, bbox=None): """Blit the canvas in bbox (default entire canvas).""" def resize(self, w, h): - """Set the canvas size in pixels.""" + """ + UNUSED: Set the canvas size in pixels. + + Certain backends may implement a similar method internally, but this is + not a requirement of, nor is it used by, Matplotlib itself. + """ + # The entire method is actually deprecated, but we allow pass-through + # to a parent class to support e.g. QWidget.resize. + if hasattr(super(), "resize"): + return super().resize(w, h) + else: + _api.warn_deprecated("3.6", name="resize", obj_type="method", + alternative="FigureManagerBase.resize") + @_api.deprecated("3.6", alternative=( + "callbacks.process('draw_event', DrawEvent(...))")) def draw_event(self, renderer): """Pass a `DrawEvent` to all functions connected to ``draw_event``.""" s = 'draw_event' event = DrawEvent(s, self, renderer) self.callbacks.process(s, event) + @_api.deprecated("3.6", alternative=( + "callbacks.process('resize_event', ResizeEvent(...))")) def resize_event(self): """ Pass a `ResizeEvent` to all functions connected to ``resize_event``. @@ -1800,6 +1798,8 @@ def resize_event(self): self.callbacks.process(s, event) self.draw_idle() + @_api.deprecated("3.6", alternative=( + "callbacks.process('close_event', CloseEvent(...))")) def close_event(self, guiEvent=None): """ Pass a `CloseEvent` to all functions connected to ``close_event``. @@ -1816,6 +1816,8 @@ def close_event(self, guiEvent=None): # AttributeError occurs on OSX with qt4agg upon exiting # with an open window; 'callbacks' attribute no longer exists. + @_api.deprecated("3.6", alternative=( + "callbacks.process('key_press_event', KeyEvent(...))")) def key_press_event(self, key, guiEvent=None): """ Pass a `KeyEvent` to all functions connected to ``key_press_event``. @@ -1826,6 +1828,8 @@ def key_press_event(self, key, guiEvent=None): s, self, key, self._lastx, self._lasty, guiEvent=guiEvent) self.callbacks.process(s, event) + @_api.deprecated("3.6", alternative=( + "callbacks.process('key_release_event', KeyEvent(...))")) def key_release_event(self, key, guiEvent=None): """ Pass a `KeyEvent` to all functions connected to ``key_release_event``. @@ -1836,6 +1840,8 @@ def key_release_event(self, key, guiEvent=None): self.callbacks.process(s, event) self._key = None + @_api.deprecated("3.6", alternative=( + "callbacks.process('pick_event', PickEvent(...))")) def pick_event(self, mouseevent, artist, **kwargs): """ Callback processing for pick events. @@ -1852,6 +1858,8 @@ def pick_event(self, mouseevent, artist, **kwargs): **kwargs) self.callbacks.process(s, event) + @_api.deprecated("3.6", alternative=( + "callbacks.process('scroll_event', MouseEvent(...))")) def scroll_event(self, x, y, step, guiEvent=None): """ Callback processing for scroll events. @@ -1872,6 +1880,8 @@ def scroll_event(self, x, y, step, guiEvent=None): step=step, guiEvent=guiEvent) self.callbacks.process(s, mouseevent) + @_api.deprecated("3.6", alternative=( + "callbacks.process('button_press_event', MouseEvent(...))")) def button_press_event(self, x, y, button, dblclick=False, guiEvent=None): """ Callback processing for mouse button press events. @@ -1889,6 +1899,8 @@ def button_press_event(self, x, y, button, dblclick=False, guiEvent=None): dblclick=dblclick, guiEvent=guiEvent) self.callbacks.process(s, mouseevent) + @_api.deprecated("3.6", alternative=( + "callbacks.process('button_release_event', MouseEvent(...))")) def button_release_event(self, x, y, button, guiEvent=None): """ Callback processing for mouse button release events. @@ -1913,6 +1925,9 @@ def button_release_event(self, x, y, button, guiEvent=None): self.callbacks.process(s, event) self._button = None + # Also remove _lastx, _lasty when this goes away. + @_api.deprecated("3.6", alternative=( + "callbacks.process('motion_notify_event', MouseEvent(...))")) def motion_notify_event(self, x, y, guiEvent=None): """ Callback processing for mouse movement events. @@ -1938,6 +1953,8 @@ def motion_notify_event(self, x, y, guiEvent=None): guiEvent=guiEvent) self.callbacks.process(s, event) + @_api.deprecated("3.6", alternative=( + "callbacks.process('leave_notify_event', LocationEvent(...))")) def leave_notify_event(self, guiEvent=None): """ Callback processing for the mouse cursor leaving the canvas. @@ -1954,7 +1971,9 @@ def leave_notify_event(self, guiEvent=None): LocationEvent.lastevent = None self._lastx, self._lasty = None, None - def enter_notify_event(self, guiEvent=None, xy=None): + @_api.deprecated("3.6", alternative=( + "callbacks.process('enter_notify_event', LocationEvent(...))")) + def enter_notify_event(self, guiEvent=None, *, xy): """ Callback processing for the mouse cursor entering the canvas. @@ -1968,18 +1987,7 @@ def enter_notify_event(self, guiEvent=None, xy=None): xy : (float, float) The coordinate location of the pointer when the canvas is entered. """ - if xy is not None: - x, y = xy - self._lastx, self._lasty = x, y - else: - x = None - y = None - _api.warn_deprecated( - '3.0', removal='3.5', name='enter_notify_event', - message='Since %(since)s, %(name)s expects a location but ' - 'your backend did not pass one. This will become an error ' - '%(removal)s.') - + self._lastx, self._lasty = x, y = xy event = LocationEvent('figure_enter_event', self, x, y, guiEvent) self.callbacks.process('figure_enter_event', event) @@ -1995,7 +2003,8 @@ def inaxes(self, xy): Returns ------- `~matplotlib.axes.Axes` or None - The topmost visible axes containing the point, or None if no axes. + The topmost visible Axes containing the point, or None if there + is no Axes at the point. """ axes_list = [a for a in self.figure.get_axes() if a.patch.contains_point(xy) and a.get_visible()] @@ -2011,7 +2020,7 @@ def grab_mouse(self, ax): Set the child `~.axes.Axes` which is grabbing the mouse events. Usually called by the widgets themselves. It is an error to call this - if the mouse is already grabbed by another axes. + if the mouse is already grabbed by another Axes. """ if self.mouse_grabber not in (None, ax): raise RuntimeError("Another Axes already grabs mouse input") @@ -2027,14 +2036,32 @@ def release_mouse(self, ax): if self.mouse_grabber is ax: self.mouse_grabber = None + def set_cursor(self, cursor): + """ + Set the current cursor. + + This may have no effect if the backend does not display anything. + + If required by the backend, this method should trigger an update in + the backend event loop after the cursor is set, as this method may be + called e.g. before a long-running task during which the GUI is not + updated. + + Parameters + ---------- + cursor : `.Cursors` + The cursor to display over the canvas. Note: some backends may + change the cursor for the entire window. + """ + def draw(self, *args, **kwargs): """ Render the `.Figure`. - It is important that this method actually walk the artist tree - even if not output is produced because this will trigger - deferred work (like computing limits auto-limits and tick - values) that users may want access to before saving to disk. + This method must walk the artist tree, even if no output is produced, + because it triggers deferred work that users may want to access + before saving output to disk. For example computing limits, + auto-limits, and tick values. """ def draw_idle(self, *args, **kwargs): @@ -2054,12 +2081,80 @@ def draw_idle(self, *args, **kwargs): with self._idle_draw_cntx(): self.draw(*args, **kwargs) - def get_width_height(self): + @property + def device_pixel_ratio(self): + """ + The ratio of physical to logical pixels used for the canvas on screen. + + By default, this is 1, meaning physical and logical pixels are the same + size. Subclasses that support High DPI screens may set this property to + indicate that said ratio is different. All Matplotlib interaction, + unless working directly with the canvas, remains in logical pixels. + + """ + return self._device_pixel_ratio + + def _set_device_pixel_ratio(self, ratio): """ - Return the figure width and height in points or pixels - (depending on the backend), truncated to integers. + Set the ratio of physical to logical pixels used for the canvas. + + Subclasses that support High DPI screens can set this property to + indicate that said ratio is different. The canvas itself will be + created at the physical size, while the client side will use the + logical size. Thus the DPI of the Figure will change to be scaled by + this ratio. Implementations that support High DPI screens should use + physical pixels for events so that transforms back to Axes space are + correct. + + By default, this is 1, meaning physical and logical pixels are the same + size. + + Parameters + ---------- + ratio : float + The ratio of logical to physical pixels used for the canvas. + + Returns + ------- + bool + Whether the ratio has changed. Backends may interpret this as a + signal to resize the window, repaint the canvas, or change any + other relevant properties. """ - return int(self.figure.bbox.width), int(self.figure.bbox.height) + if self._device_pixel_ratio == ratio: + return False + # In cases with mixed resolution displays, we need to be careful if the + # device pixel ratio changes - in this case we need to resize the + # canvas accordingly. Some backends provide events that indicate a + # change in DPI, but those that don't will update this before drawing. + dpi = ratio * self.figure._original_dpi + self.figure._set_dpi(dpi, forward=False) + self._device_pixel_ratio = ratio + return True + + def get_width_height(self, *, physical=False): + """ + Return the figure width and height in integral points or pixels. + + When the figure is used on High DPI screens (and the backend supports + it), the truncation to integers occurs after scaling by the device + pixel ratio. + + Parameters + ---------- + physical : bool, default: False + Whether to return true physical pixels or logical pixels. Physical + pixels may be used by backends that support HiDPI, but still + configure the canvas using its actual size. + + Returns + ------- + width, height : int + The size of the figure, in points or pixels, depending on the + backend. + """ + return tuple(int(size / (1 if physical else self.device_pixel_ratio)) + for size in self.figure.bbox.max) @classmethod def get_supported_filetypes(cls): @@ -2080,21 +2175,30 @@ def get_supported_filetypes_grouped(cls): groupings[name].sort() return groupings - def _get_output_canvas(self, backend, fmt): + @contextmanager + def _switch_canvas_and_return_print_method(self, fmt, backend=None): """ - Set the canvas in preparation for saving the figure. + Context manager temporarily setting the canvas for saving the figure:: + + with canvas._switch_canvas_and_return_print_method(fmt, backend) \\ + as print_method: + # ``print_method`` is a suitable ``print_{fmt}`` method, and + # the figure's canvas is temporarily switched to the method's + # canvas within the with... block. ``print_method`` is also + # wrapped to suppress extra kwargs passed by ``print_figure``. Parameters ---------- - backend : str or None - If not None, switch the figure canvas to the ``FigureCanvas`` class - of the given backend. fmt : str If *backend* is None, then determine a suitable canvas class for saving to format *fmt* -- either the current canvas class, if it supports *fmt*, or whatever `get_registered_canvas_class` returns; switch the figure canvas to that canvas class. + backend : str or None, default: None + If not None, switch the figure canvas to the ``FigureCanvas`` class + of the given backend. """ + canvas = None if backend is not None: # Return a specific canvas class, if requested. canvas_class = ( @@ -2105,16 +2209,34 @@ def _get_output_canvas(self, backend, fmt): f"The {backend!r} backend does not support {fmt} output") elif hasattr(self, f"print_{fmt}"): # Return the current canvas if it supports the requested format. - return self + canvas = self + canvas_class = None # Skip call to switch_backends. else: # Return a default canvas for the requested format, if it exists. canvas_class = get_registered_canvas_class(fmt) if canvas_class: - return self.switch_backends(canvas_class) - # Else report error for unsupported format. - raise ValueError( - "Format {!r} is not supported (supported formats: {})" - .format(fmt, ", ".join(sorted(self.get_supported_filetypes())))) + canvas = self.switch_backends(canvas_class) + if canvas is None: + raise ValueError( + "Format {!r} is not supported (supported formats: {})".format( + fmt, ", ".join(sorted(self.get_supported_filetypes())))) + meth = getattr(canvas, f"print_{fmt}") + mod = (meth.func.__module__ + if hasattr(meth, "func") # partialmethod, e.g. backend_wx. + else meth.__module__) + if mod.startswith(("matplotlib.", "mpl_toolkits.")): + optional_kws = { # Passed by print_figure for other renderers. + "dpi", "facecolor", "edgecolor", "orientation", + "bbox_inches_restore"} + skip = optional_kws - {*inspect.signature(meth).parameters} + print_method = functools.wraps(meth)(lambda *args, **kwargs: meth( + *args, **{k: v for k, v in kwargs.items() if k not in skip})) + else: # Let third-parties do as they see fit. + print_method = meth + try: + yield print_method + finally: + self.figure.canvas = self def print_figure( self, filename, dpi=None, facecolor=None, edgecolor=None, @@ -2182,10 +2304,6 @@ def print_figure( filename = filename.rstrip('.') + '.' + format format = format.lower() - # get canvas object and print method for format - canvas = self._get_output_canvas(backend, format) - print_method = getattr(canvas, 'print_%s' % format) - if dpi is None: dpi = rcParams['savefig.dpi'] if dpi == 'figure': @@ -2193,27 +2311,24 @@ def print_figure( # Remove the figure manager, if any, to avoid resizing the GUI widget. with cbook._setattr_cm(self, manager=None), \ - cbook._setattr_cm(self.figure, dpi=dpi), \ - cbook._setattr_cm(canvas, _is_saving=True): - origfacecolor = self.figure.get_facecolor() - origedgecolor = self.figure.get_edgecolor() - - if facecolor is None: - facecolor = rcParams['savefig.facecolor'] - if cbook._str_equal(facecolor, 'auto'): - facecolor = origfacecolor - if edgecolor is None: - edgecolor = rcParams['savefig.edgecolor'] - if cbook._str_equal(edgecolor, 'auto'): - edgecolor = origedgecolor - - self.figure.set_facecolor(facecolor) - self.figure.set_edgecolor(edgecolor) + self._switch_canvas_and_return_print_method(format, backend) \ + as print_method, \ + cbook._setattr_cm(self.figure, dpi=dpi), \ + cbook._setattr_cm(self.figure.canvas, _device_pixel_ratio=1), \ + cbook._setattr_cm(self.figure.canvas, _is_saving=True), \ + ExitStack() as stack: + + for prop in ["facecolor", "edgecolor"]: + color = locals()[prop] + if color is None: + color = rcParams[f"savefig.{prop}"] + if not cbook._str_equal(color, "auto"): + stack.enter_context(self.figure._cm_set(**{prop: color})) if bbox_inches is None: bbox_inches = rcParams['savefig.bbox'] - if (self.figure.get_constrained_layout() or + if (self.figure.get_layout_engine() is not None or bbox_inches == "tight"): # we need to trigger a draw before printing to make sure # CL works. "tight" also needs a draw to get the right @@ -2223,10 +2338,7 @@ def print_figure( functools.partial( print_method, orientation=orientation) ) - ctx = (renderer._draw_disabled() - if hasattr(renderer, '_draw_disabled') - else suppress()) - with ctx: + with getattr(renderer, "_draw_disabled", nullcontext)(): self.figure.draw(renderer) if bbox_inches: @@ -2238,16 +2350,15 @@ def print_figure( bbox_inches = bbox_inches.padded(pad_inches) # call adjust_bbox to save only the given area - restore_bbox = tight_bbox.adjust_bbox(self.figure, bbox_inches, - canvas.fixed_dpi) + restore_bbox = _tight_bbox.adjust_bbox( + self.figure, bbox_inches, self.figure.canvas.fixed_dpi) _bbox_inches_restore = (bbox_inches, restore_bbox) else: _bbox_inches_restore = None - # we have already done CL above, so turn it off: - cl_state = self.figure.get_constrained_layout() - self.figure.set_constrained_layout(False) + # we have already done layout above, so turn it off: + stack.enter_context(self.figure._cm_set(layout_engine='none')) try: # _get_renderer may change the figure dpi (as vector formats # force the figure dpi to 72), so we need to set it again here. @@ -2263,11 +2374,6 @@ def print_figure( if bbox_inches and restore_bbox: restore_bbox() - self.figure.set_facecolor(origfacecolor) - self.figure.set_edgecolor(origedgecolor) - self.figure.set_canvas(self) - # reset to cached state - self.figure.set_constrained_layout(cl_state) return result @classmethod @@ -2281,26 +2387,6 @@ def get_default_filetype(cls): """ return rcParams['savefig.format'] - @_api.deprecated( - "3.4", alternative="manager.get_window_title or GUI-specific methods") - def get_window_title(self): - """ - Return the title text of the window containing the figure, or None - if there is no window (e.g., a PS backend). - """ - if self.manager is not None: - return self.manager.get_window_title() - - @_api.deprecated( - "3.4", alternative="manager.set_window_title or GUI-specific methods") - def set_window_title(self, title): - """ - Set the title text of the window containing the figure. Note that - this has no effect if there is no window (e.g., a PS backend). - """ - if self.manager is not None: - self.manager.set_window_title(title) - def get_default_filename(self): """ Return a string, which includes extension, suitable for use as @@ -2357,12 +2443,20 @@ def mpl_connect(self, s, func): def func(event: Event) -> Any For the location events (button and key press/release), if the - mouse is over the axes, the ``inaxes`` attribute of the event will + mouse is over the Axes, the ``inaxes`` attribute of the event will be set to the `~matplotlib.axes.Axes` the event occurs is over, and additionally, the variables ``xdata`` and ``ydata`` attributes will be set to the mouse location in data coordinates. See `.KeyEvent` and `.MouseEvent` for more info. + .. note:: + + If func is a method, this only stores a weak reference to the + method. Thus, the figure does not influence the lifetime of + the associated object. Usually, you want to make sure that the + object is kept alive throughout the lifetime of the figure by + holding a reference to it. + Returns ------- cid @@ -2487,7 +2581,7 @@ def key_press_handler(event, canvas=None, toolbar=None): back-compatibility, but, if set, should always be equal to ``event.canvas.toolbar``. """ - # these bindings happen whether you are over an axes or not + # these bindings happen whether you are over an Axes or not if event.key is None: return @@ -2510,7 +2604,6 @@ def key_press_handler(event, canvas=None, toolbar=None): grid_minor_keys = rcParams['keymap.grid_minor'] toggle_yscale_keys = rcParams['keymap.yscale'] toggle_xscale_keys = rcParams['keymap.xscale'] - all_keys = dict.__getitem__(rcParams, 'keymap.all_axes') # toggle fullscreen mode ('f', 'ctrl + f') if event.key in fullscreen_keys: @@ -2551,7 +2644,7 @@ def key_press_handler(event, canvas=None, toolbar=None): if event.inaxes is None: return - # these bindings require the mouse to be over an axes to trigger + # these bindings require the mouse to be over an Axes to trigger def _get_uniform_gridstate(ticks): # Return True/False if all grid lines are on or off, None if they are # not all in the same state. @@ -2563,7 +2656,7 @@ def _get_uniform_gridstate(ticks): return None ax = event.inaxes - # toggle major grids in current axes (default key 'g') + # toggle major grids in current Axes (default key 'g') # Both here and below (for 'G'), we do nothing if *any* grid (major or # minor, x or y) is not in a uniform state, to avoid messing up user # customization. @@ -2585,7 +2678,7 @@ def _get_uniform_gridstate(ticks): ax.grid(x_state, which="major" if x_state else "both", axis="x") ax.grid(y_state, which="major" if y_state else "both", axis="y") canvas.draw_idle() - # toggle major and minor grids in current axes (default key 'G') + # toggle major and minor grids in current Axes (default key 'G') if (event.key in grid_minor_keys # Exclude major grids not in a uniform state. and None not in [_get_uniform_gridstate(ax.xaxis.majorTicks), @@ -2629,29 +2722,6 @@ def _get_uniform_gridstate(ticks): _log.warning(str(exc)) ax.set_xscale('linear') ax.figure.canvas.draw_idle() - # enable navigation for all axes that contain the event (default key 'a') - elif event.key in all_keys: - for a in canvas.figure.get_axes(): - if (event.x is not None and event.y is not None - and a.in_axes(event)): # FIXME: Why only these? - _api.warn_deprecated( - "3.3", message="Toggling axes navigation from the " - "keyboard is deprecated since %(since)s and will be " - "removed %(removal)s.") - a.set_navigate(True) - # enable navigation only for axes with this index (if such an axes exist, - # otherwise do nothing) - elif event.key.isdigit() and event.key != '0': - n = int(event.key) - 1 - if n < len(canvas.figure.get_axes()): - for i, a in enumerate(canvas.figure.get_axes()): - if (event.x is not None and event.y is not None - and a.in_axes(event)): # FIXME: Why only these? - _api.warn_deprecated( - "3.3", message="Toggling axes navigation from the " - "keyboard is deprecated since %(since)s and will be " - "removed %(removal)s.") - a.set_navigate(i == n) def button_press_handler(event, canvas=None, toolbar=None): @@ -2737,7 +2807,8 @@ class FigureManagerBase: figure.canvas.manager.button_press_handler_id) """ - statusbar = _api.deprecated("3.3")(property(lambda self: None)) + _toolbar2_class = None + _toolmanager_toolbar_class = None def __init__(self, canvas, num): self.canvas = canvas @@ -2756,14 +2827,83 @@ def __init__(self, canvas, num): self.toolmanager = (ToolManager(canvas.figure) if mpl.rcParams['toolbar'] == 'toolmanager' else None) - self.toolbar = None + if (mpl.rcParams["toolbar"] == "toolbar2" + and self._toolbar2_class): + self.toolbar = self._toolbar2_class(self.canvas) + elif (mpl.rcParams["toolbar"] == "toolmanager" + and self._toolmanager_toolbar_class): + self.toolbar = self._toolmanager_toolbar_class(self.toolmanager) + else: + self.toolbar = None + + if self.toolmanager: + tools.add_tools_to_manager(self.toolmanager) + if self.toolbar: + tools.add_tools_to_container(self.toolbar) @self.canvas.figure.add_axobserver def notify_axes_change(fig): - # Called whenever the current axes is changed. + # Called whenever the current Axes is changed. if self.toolmanager is None and self.toolbar is not None: self.toolbar.update() + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + """ + Create a manager for a given *figure* using a specific *canvas_class*. + + Backends should override this method if they have specific needs for + setting up the canvas or the manager. + """ + return cls(canvas_class(figure), num) + + @classmethod + def start_main_loop(cls): + """ + Start the main event loop. + + This method is called by `.FigureManagerBase.pyplot_show`, which is the + implementation of `.pyplot.show`. To customize the behavior of + `.pyplot.show`, interactive backends should usually override + `~.FigureManagerBase.start_main_loop`; if more customized logic is + necessary, `~.FigureManagerBase.pyplot_show` can also be overridden. + """ + + @classmethod + def pyplot_show(cls, *, block=None): + """ + Show all figures. This method is the implementation of `.pyplot.show`. + + To customize the behavior of `.pyplot.show`, interactive backends + should usually override `~.FigureManagerBase.start_main_loop`; if more + customized logic is necessary, `~.FigureManagerBase.pyplot_show` can + also be overridden. + + Parameters + ---------- + block : bool, optional + Whether to block by calling ``start_main_loop``. The default, + None, means to block if we are neither in IPython's ``%pylab`` mode + nor in ``interactive`` mode. + """ + managers = Gcf.get_all_fig_managers() + if not managers: + return + for manager in managers: + try: + manager.show() # Emits a warning for non-interactive backend. + except NonGuiException as exc: + _api.warn_external(str(exc)) + if block is None: + # Hack: Are we in IPython's %pylab mode? In pylab mode, IPython + # (>= 0.10) tacks a _needmain attribute onto pyplot.show (always + # set to False). + ipython_pylab = hasattr( + getattr(sys.modules.get("pyplot"), "show", None), "_needmain") + block = not ipython_pylab and not is_interactive() + if block: + cls.start_main_loop() + def show(self): """ For GUI backends, show the figure window and redraw. @@ -2789,24 +2929,7 @@ def full_screen_toggle(self): pass def resize(self, w, h): - """For GUI backends, resize the window (in pixels).""" - - @_api.deprecated( - "3.4", alternative="self.canvas.callbacks.process(event.name, event)") - def key_press(self, event): - """ - Implement the default Matplotlib key bindings defined at - :ref:`key-event-handling`. - """ - if rcParams['toolbar'] != 'toolmanager': - key_press_handler(event) - - @_api.deprecated( - "3.4", alternative="self.canvas.callbacks.process(event.name, event)") - def button_press(self, event): - """The default Matplotlib button actions for extra mouse buttons.""" - if rcParams['toolbar'] != 'toolmanager': - button_press_handler(event) + """For GUI backends, resize the window (in physical pixels).""" def get_window_title(self): """ @@ -2852,9 +2975,6 @@ class NavigationToolbar2: :meth:`save_figure` save the current figure - :meth:`set_cursor` - if you want the pointer icon to change - :meth:`draw_rubberband` (optional) draw the zoom to rect "rubberband" rectangle @@ -2890,8 +3010,7 @@ class NavigationToolbar2: 'Left button pans, Right button zooms\n' 'x/y fixes axis, CTRL fixes aspect', 'move', 'pan'), - ('Zoom', 'Zoom to rectangle\nx/y fixes axis, CTRL fixes aspect', - 'zoom_to_rect', 'zoom'), + ('Zoom', 'Zoom to rectangle\nx/y fixes axis', 'zoom_to_rect', 'zoom'), ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'), (None, None, None, None), ('Save', 'Save the figure', 'filesave', 'save_figure'), @@ -2902,15 +3021,7 @@ def __init__(self, canvas): canvas.toolbar = self self._nav_stack = cbook.Stack() # This cursor will be set after the initial draw. - self._lastCursor = cursors.POINTER - - init = _api.deprecate_method_override( - __class__._init_toolbar, self, allow_empty=True, since="3.3", - addendum="Please fully initialize the toolbar in your subclass' " - "__init__; a fully empty _init_toolbar implementation may be kept " - "for compatibility with earlier versions of Matplotlib.") - if init: - init() + self._last_cursor = tools.Cursors.POINTER self._id_press = self.canvas.mpl_connect( 'button_press_event', self._zoom_pan_handler) @@ -2973,46 +3084,22 @@ def forward(self, *args): self.set_history_buttons() self._update_view() - @_api.deprecated("3.3", alternative="__init__") - def _init_toolbar(self): - """ - This is where you actually build the GUI widgets (called by - __init__). The icons ``home.xpm``, ``back.xpm``, ``forward.xpm``, - ``hand.xpm``, ``zoom_to_rect.xpm`` and ``filesave.xpm`` are standard - across backends (there are ppm versions in CVS also). - - You just need to set the callbacks - - home : self.home - back : self.back - forward : self.forward - hand : self.pan - zoom_to_rect : self.zoom - filesave : self.save_figure - - You only need to define the last one - the others are in the base - class implementation. - - """ - raise NotImplementedError - 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.mode: - if self._lastCursor != cursors.POINTER: - self.set_cursor(cursors.POINTER) - self._lastCursor = cursors.POINTER - else: + if self.mode and event.inaxes and event.inaxes.get_navigate(): if (self.mode == _Mode.ZOOM - and self._lastCursor != cursors.SELECT_REGION): - self.set_cursor(cursors.SELECT_REGION) - self._lastCursor = cursors.SELECT_REGION + and self._last_cursor != tools.Cursors.SELECT_REGION): + self.canvas.set_cursor(tools.Cursors.SELECT_REGION) + self._last_cursor = tools.Cursors.SELECT_REGION elif (self.mode == _Mode.PAN - and self._lastCursor != cursors.MOVE): - self.set_cursor(cursors.MOVE) - self._lastCursor = cursors.MOVE + and self._last_cursor != tools.Cursors.MOVE): + self.canvas.set_cursor(tools.Cursors.MOVE) + self._last_cursor = tools.Cursors.MOVE + elif self._last_cursor != tools.Cursors.POINTER: + self.canvas.set_cursor(tools.Cursors.POINTER) + self._last_cursor = tools.Cursors.POINTER @contextmanager def _wait_cursor_for_draw_cm(self): @@ -3029,10 +3116,10 @@ def _wait_cursor_for_draw_cm(self): time.time(), getattr(self, "_draw_time", -np.inf)) if self._draw_time - last_draw_time > 1: try: - self.set_cursor(cursors.WAIT) + self.canvas.set_cursor(tools.Cursors.WAIT) yield finally: - self.set_cursor(self._lastCursor) + self.canvas.set_cursor(self._last_cursor) else: yield @@ -3056,15 +3143,11 @@ def _mouse_event_to_message(event): if data_str: s = s + '\n' + data_str return s + return "" def mouse_move(self, event): self._update_cursor(event) - - s = self._mouse_event_to_message(event) - if s is not None: - self.set_message(s) - else: - self.set_message(self.mode) + self.set_message(self._mouse_event_to_message(event)) def _zoom_pan_handler(self, event): if self.mode == _Mode.PAN: @@ -3078,20 +3161,15 @@ def _zoom_pan_handler(self, event): elif event.name == "button_release_event": self.release_zoom(event) - @_api.deprecated("3.3") - def press(self, event): - """Called whenever a mouse button is pressed.""" - - @_api.deprecated("3.3") - def release(self, event): - """Callback for mouse button release.""" - def pan(self, *args): """ Toggle the pan/zoom tool. Pan with left button, zoom with right. """ + if not self.canvas.widgetlock.available(self): + self.set_message("pan unavailable") + return if self.mode == _Mode.PAN: self.mode = _Mode.NONE self.canvas.widgetlock.release(self) @@ -3100,7 +3178,6 @@ def pan(self, *args): self.canvas.widgetlock(self) for a in self.canvas.figure.get_axes(): a.set_navigate_mode(self.mode._navigate_mode) - self.set_message(self.mode) _PanInfo = namedtuple("_PanInfo", "button axes cid") @@ -3121,12 +3198,6 @@ def press_pan(self, event): id_drag = self.canvas.mpl_connect("motion_notify_event", self.drag_pan) self._pan_info = self._PanInfo( button=event.button, axes=axes, cid=id_drag) - press = _api.deprecate_method_override( - __class__.press, self, since="3.3", message="Calling an " - "overridden press() at pan start is deprecated since %(since)s " - "and will be removed %(removal)s; override press_pan() instead.") - if press is not None: - press(event) def drag_pan(self, event): """Callback for dragging in pan/zoom mode.""" @@ -3145,17 +3216,14 @@ def release_pan(self, event): 'motion_notify_event', self.mouse_move) for ax in self._pan_info.axes: ax.end_pan() - release = _api.deprecate_method_override( - __class__.press, self, since="3.3", message="Calling an " - "overridden release() at pan stop is deprecated since %(since)s " - "and will be removed %(removal)s; override release_pan() instead.") - if release is not None: - release(event) - self._draw() + self.canvas.draw_idle() self._pan_info = None self.push_current() def zoom(self, *args): + if not self.canvas.widgetlock.available(self): + self.set_message("zoom unavailable") + return """Toggle zoom to rect mode.""" if self.mode == _Mode.ZOOM: self.mode = _Mode.NONE @@ -3165,9 +3233,8 @@ def zoom(self, *args): self.canvas.widgetlock(self) for a in self.canvas.figure.get_axes(): a.set_navigate_mode(self.mode._navigate_mode) - self.set_message(self.mode) - _ZoomInfo = namedtuple("_ZoomInfo", "direction start_xy axes cid") + _ZoomInfo = namedtuple("_ZoomInfo", "direction start_xy axes cid cbar") def press_zoom(self, event): """Callback for mouse button press in zoom to rect mode.""" @@ -3182,15 +3249,16 @@ def press_zoom(self, event): self.push_current() # set the home button to this view id_zoom = self.canvas.mpl_connect( "motion_notify_event", self.drag_zoom) + # A colorbar is one-dimensional, so we extend the zoom rectangle out + # to the edge of the Axes bbox in the other dimension. To do that we + # store the orientation of the colorbar for later. + if hasattr(axes[0], "_colorbar"): + cbar = axes[0]._colorbar.orientation + else: + cbar = None self._zoom_info = self._ZoomInfo( direction="in" if event.button == 1 else "out", - start_xy=(event.x, event.y), axes=axes, cid=id_zoom) - press = _api.deprecate_method_override( - __class__.press, self, since="3.3", message="Calling an " - "overridden press() at zoom start is deprecated since %(since)s " - "and will be removed %(removal)s; override press_zoom() instead.") - if press is not None: - press(event) + start_xy=(event.x, event.y), axes=axes, cid=id_zoom, cbar=cbar) def drag_zoom(self, event): """Callback for dragging in zoom mode.""" @@ -3198,10 +3266,17 @@ def drag_zoom(self, event): 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": + key = event.key + # Force the key on colorbars to extend the short-axis bbox + if self._zoom_info.cbar == "horizontal": + key = "x" + elif self._zoom_info.cbar == "vertical": + key = "y" + if key == "x": y1, y2 = ax.bbox.intervaly - elif event.key == "y": + elif key == "y": x1, x2 = ax.bbox.intervalx + self.draw_rubberband(event, x1, y1, x2, y2) def release_zoom(self, event): @@ -3215,44 +3290,36 @@ def release_zoom(self, event): self.remove_rubberband() start_x, start_y = self._zoom_info.start_xy + key = event.key + # Force the key on colorbars to ignore the zoom-cancel on the + # short-axis side + if self._zoom_info.cbar == "horizontal": + key = "x" + elif self._zoom_info.cbar == "vertical": + key = "y" # Ignore single clicks: 5 pixels is a threshold that allows the user to # "cancel" a zoom action by zooming by less than 5 pixels. - if ((abs(event.x - start_x) < 5 and event.key != "y") - or (abs(event.y - start_y) < 5 and event.key != "x")): - self._draw() + if ((abs(event.x - start_x) < 5 and key != "y") or + (abs(event.y - start_y) < 5 and key != "x")): + self.canvas.draw_idle() self._zoom_info = None - release = _api.deprecate_method_override( - __class__.press, self, since="3.3", message="Calling an " - "overridden release() at zoom stop is deprecated since " - "%(since)s and will be removed %(removal)s; override " - "release_zoom() instead.") - if release is not None: - release(event) return for i, ax in enumerate(self._zoom_info.axes): - # Detect whether this axes is twinned with an earlier axes in the - # list of zoomed axes, to avoid double zooming. + # 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]) ax._set_view_from_bbox( (start_x, start_y, event.x, event.y), - self._zoom_info.direction, event.key, twinx, twiny) + self._zoom_info.direction, key, twinx, twiny) - self._draw() + self.canvas.draw_idle() self._zoom_info = None self.push_current() - release = _api.deprecate_method_override( - __class__.release, self, since="3.3", message="Calling an " - "overridden release() at zoom stop is deprecated since %(since)s " - "and will be removed %(removal)s; override release_zoom() " - "instead.") - if release is not None: - release(event) - def push_current(self): """Push the current view limits and position onto the stack.""" self._nav_stack.push( @@ -3264,33 +3331,10 @@ def push_current(self): for ax in self.canvas.figure.axes})) self.set_history_buttons() - @_api.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) - locators = [] - if xaxis is not None: - locators.append(xaxis.get_major_locator()) - locators.append(xaxis.get_minor_locator()) - if yaxis is not None: - locators.append(yaxis.get_major_locator()) - locators.append(yaxis.get_minor_locator()) - - for loc in locators: - mpl.ticker._if_refresh_overridden_call_and_emit_deprec(loc) - self.canvas.draw_idle() - def _update_view(self): """ Update the viewlim and position from the view and position stack for - each axes. + each Axes. """ nav_info = self._nav_stack() if nav_info is None: @@ -3306,26 +3350,34 @@ def _update_view(self): self.canvas.draw_idle() def configure_subplots(self, *args): - plt = _safe_pyplot_import() - self.subplot_tool = plt.subplot_tool(self.canvas.figure) - self.subplot_tool.figure.canvas.manager.show() + if hasattr(self, "subplot_tool"): + self.subplot_tool.figure.canvas.manager.show() + return + # This import needs to happen here due to circular imports. + from matplotlib.figure import Figure + with mpl.rc_context({"toolbar": "none"}): # No navbar for the toolfig. + manager = type(self.canvas).new_manager(Figure(figsize=(6, 3)), -1) + manager.set_window_title("Subplot configuration tool") + tool_fig = manager.canvas.figure + tool_fig.subplots_adjust(top=0.9) + self.subplot_tool = widgets.SubplotTool(self.canvas.figure, tool_fig) + cid = self.canvas.mpl_connect( + "close_event", lambda e: manager.destroy()) + + def on_tool_fig_close(e): + self.canvas.mpl_disconnect(cid) + del self.subplot_tool + + tool_fig.canvas.mpl_connect("close_event", on_tool_fig_close) + manager.show() + return self.subplot_tool def save_figure(self, *args): """Save the current figure.""" raise NotImplementedError - def set_cursor(self, cursor): - """ - Set the current cursor to one of the :class:`Cursors` enums values. - - If required by the backend, this method should trigger an update in - the backend event loop after the cursor is set, as this method may be - called e.g. before a long-running task during which the GUI is not - updated. - """ - def update(self): - """Reset the axes stack.""" + """Reset the Axes stack.""" self._nav_stack.clear() self.set_history_buttons() @@ -3488,29 +3540,6 @@ def set_message(self, s): raise NotImplementedError -@_api.deprecated("3.3") -class StatusbarBase: - """Base class for the statusbar.""" - def __init__(self, toolmanager): - self.toolmanager = toolmanager - self.toolmanager.toolmanager_connect('tool_message_event', - self._message_cbk) - - def _message_cbk(self, event): - """Capture the 'tool_message_event' and set the message.""" - self.set_message(event.message) - - def set_message(self, s): - """ - Display a message on toolbar or in status bar. - - Parameters - ---------- - s : str - Message text. - """ - - class _Backend: # A backend can be defined by using the following pattern: # @@ -3547,13 +3576,16 @@ def new_figure_manager(cls, num, *args, **kwargs): @classmethod def new_figure_manager_given_figure(cls, num, figure): """Create a new figure manager instance for the given figure.""" - canvas = cls.FigureCanvas(figure) - manager = cls.FigureManager(canvas, num) - return manager + return cls.FigureCanvas.new_manager(figure, num) @classmethod def draw_if_interactive(cls): - if cls.mainloop is not None and is_interactive(): + manager_class = cls.FigureCanvas.manager_class + # Interactive backends reimplement start_main_loop or pyplot_show. + backend_is_interactive = ( + manager_class.start_main_loop != FigureManagerBase.start_main_loop + or manager_class.pyplot_show != FigureManagerBase.pyplot_show) + if backend_is_interactive and is_interactive(): manager = Gcf.get_active() if manager: manager.canvas.draw_idle() @@ -3578,19 +3610,12 @@ def show(cls, *, block=None): if cls.mainloop is None: return if block is None: - # Hack: Are we in IPython's pylab mode? - from matplotlib import pyplot - try: - # IPython versions >= 0.10 tack the _needmain attribute onto - # pyplot.show, and always set it to False, when in %pylab mode. - ipython_pylab = not pyplot.show._needmain - except AttributeError: - ipython_pylab = False + # Hack: Are we in IPython's %pylab mode? In pylab mode, IPython + # (>= 0.10) tacks a _needmain attribute onto pyplot.show (always + # set to False). + ipython_pylab = hasattr( + getattr(sys.modules.get("pyplot"), "show", None), "_needmain") block = not ipython_pylab and not is_interactive() - # TODO: The above is a hack to get the WebAgg backend working with - # ipython's `%pylab` mode until proper integration is implemented. - if get_backend() == "WebAgg": - block = True if block: cls.mainloop() diff --git a/lib/matplotlib/backend_managers.py b/lib/matplotlib/backend_managers.py index 384b453ede1c..ac74ff97a4e8 100644 --- a/lib/matplotlib/backend_managers.py +++ b/lib/matplotlib/backend_managers.py @@ -1,6 +1,4 @@ -from matplotlib import _api, cbook, widgets -from matplotlib.rcsetup import validate_stringlist -import matplotlib.backend_tools as tools +from matplotlib import _api, backend_tools, cbook, widgets class ToolEvent: @@ -175,8 +173,7 @@ def _remove_keys(self, name): for k in self.get_tool_keymap(name): del self._keys[k] - @_api.delete_parameter("3.3", "args") - def update_keymap(self, name, key, *args): + def update_keymap(self, name, key): """ Set the keymap to associate with the specified tool. @@ -188,23 +185,15 @@ def update_keymap(self, name, key, *args): Keys to associate with the tool. """ if name not in self._tools: - raise KeyError('%s not in Tools' % name) + raise KeyError(f'{name!r} not in Tools') self._remove_keys(name) - for key in [key, *args]: - if isinstance(key, str) and validate_stringlist(key) != [key]: - _api.warn_deprecated( - "3.3", message="Passing a list of keys as a single " - "comma-separated string is deprecated since %(since)s and " - "support will be removed %(removal)s; pass keys as a list " - "of strings instead.") - key = validate_stringlist(key) - if isinstance(key, str): - key = [key] - for k in key: - if k in self._keys: - _api.warn_external( - f'Key {k} changed from {self._keys[k]} to {name}') - self._keys[k] = name + if isinstance(key, str): + key = [key] + for k in key: + if k in self._keys: + _api.warn_external( + f'Key {k} changed from {self._keys[k]} to {name}') + self._keys[k] = name def remove_tool(self, name): """ @@ -217,17 +206,20 @@ def remove_tool(self, name): """ tool = self.get_tool(name) - tool.destroy() + destroy = _api.deprecate_method_override( + backend_tools.ToolBase.destroy, tool, since="3.6", + alternative="tool_removed_event") + if destroy is not None: + destroy() - # If is a toggle tool and toggled, untoggle + # If it's a toggle tool and toggled, untoggle if getattr(tool, 'toggled', False): self.trigger_tool(tool, 'toolmanager') self._remove_keys(name) - s = 'tool_removed_event' - event = ToolEvent(s, self, tool) - self._callbacks.process(s, event) + event = ToolEvent('tool_removed_event', self, tool) + self._callbacks.process(event.name, event) del self._tools[name] @@ -243,19 +235,18 @@ def add_tool(self, name, tool, *args, **kwargs): ---------- name : str Name of the tool, treated as the ID, has to be unique. - tool : class_like, i.e. str or type - Reference to find the class of the Tool to added. - - Notes - ----- - args and kwargs get passed directly to the tools constructor. + tool : type + Class of the tool to be added. A subclass will be used + instead if one was registered for the current canvas class. + *args, **kwargs + Passed to the *tool*'s constructor. See Also -------- matplotlib.backend_tools.ToolBase : The base class for tools. """ - tool_cls = self._get_cls_to_instantiate(tool) + tool_cls = backend_tools._find_tool_class(type(self.canvas), tool) if not tool_cls: raise ValueError('Impossible to find class for %s' % str(tool)) @@ -267,11 +258,11 @@ def add_tool(self, name, tool, *args, **kwargs): tool_obj = tool_cls(self, name, *args, **kwargs) self._tools[name] = tool_obj - if tool_cls.default_keymap is not None: - self.update_keymap(name, tool_cls.default_keymap) + if tool_obj.default_keymap is not None: + self.update_keymap(name, tool_obj.default_keymap) # For toggle tools init the radio_group in self._toggled - if isinstance(tool_obj, tools.ToolToggleBase): + if isinstance(tool_obj, backend_tools.ToolToggleBase): # None group is not mutually exclusive, a set is used to keep track # of all toggled tools in this group if tool_obj.radio_group is None: @@ -281,18 +272,15 @@ def add_tool(self, name, tool, *args, **kwargs): # If initially toggled if tool_obj.toggled: - self._handle_toggle(tool_obj, None, None, None) + self._handle_toggle(tool_obj, None, None) tool_obj.set_figure(self.figure) - self._tool_added_event(tool_obj) - return tool_obj + event = ToolEvent('tool_added_event', self, tool_obj) + self._callbacks.process(event.name, event) - def _tool_added_event(self, tool): - s = 'tool_added_event' - event = ToolEvent(s, self, tool) - self._callbacks.process(s, event) + return tool_obj - def _handle_toggle(self, tool, sender, canvasevent, data): + def _handle_toggle(self, tool, canvasevent, data): """ Toggle tools, need to untoggle prior to using other Toggle tool. Called from trigger_tool. @@ -300,8 +288,6 @@ def _handle_toggle(self, tool, sender, canvasevent, data): Parameters ---------- tool : `.ToolBase` - sender : object - Object that wishes to trigger the tool. canvasevent : Event Original Canvas event or None. data : object @@ -337,23 +323,6 @@ def _handle_toggle(self, tool, sender, canvasevent, data): # Keep track of the toggled tool in the radio_group self._toggled[radio_group] = toggled - def _get_cls_to_instantiate(self, callback_class): - # Find the class that corresponds to the tool - if isinstance(callback_class, str): - # FIXME: make more complete searching structure - if callback_class in globals(): - callback_class = globals()[callback_class] - else: - mod = 'backend_tools' - current_module = __import__(mod, - globals(), locals(), [mod], 1) - - callback_class = getattr(current_module, callback_class, False) - if callable(callback_class): - return callback_class - else: - return None - def trigger_tool(self, name, sender=None, canvasevent=None, data=None): """ Trigger a tool and emit the ``tool_trigger_{name}`` event. @@ -376,23 +345,15 @@ def trigger_tool(self, name, sender=None, canvasevent=None, data=None): if sender is None: sender = self - self._trigger_tool(name, sender, canvasevent, data) + if isinstance(tool, backend_tools.ToolToggleBase): + self._handle_toggle(tool, canvasevent, data) + + tool.trigger(sender, canvasevent, data) # Actually trigger Tool. s = 'tool_trigger_%s' % name event = ToolTriggerEvent(s, sender, tool, canvasevent, data) self._callbacks.process(s, event) - def _trigger_tool(self, name, sender=None, canvasevent=None, data=None): - """Actually trigger a tool.""" - tool = self.get_tool(name) - - if isinstance(tool, tools.ToolToggleBase): - self._handle_toggle(tool, sender, canvasevent, data) - - # Important!!! - # This is where the Tool object gets triggered - tool.trigger(sender, canvasevent, data) - def _key_press(self, event): if event.key is None or self.keypresslock.locked(): return @@ -426,10 +387,12 @@ def get_tool(self, name, warn=True): `.ToolBase` or None The tool or None if no tool with the given name exists. """ - if isinstance(name, tools.ToolBase) and name.name in self._tools: + if (isinstance(name, backend_tools.ToolBase) + and name.name in self._tools): return name if name not in self._tools: if warn: - _api.warn_external(f"ToolManager does not control tool {name}") + _api.warn_external( + f"ToolManager does not control tool {name!r}") return None return self._tools[name] diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index a8bad1edab5a..73425f915093 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -11,7 +11,8 @@ `matplotlib.backend_managers.ToolManager` """ -from enum import IntEnum +import enum +import functools import re import time from types import SimpleNamespace @@ -25,11 +26,46 @@ from matplotlib import _api, cbook -class Cursors(IntEnum): # Must subclass int for the macOS backend. +class Cursors(enum.IntEnum): # Must subclass int for the macOS backend. """Backend-independent cursor types.""" - HAND, POINTER, SELECT_REGION, MOVE, WAIT = range(5) + POINTER = enum.auto() + HAND = enum.auto() + SELECT_REGION = enum.auto() + MOVE = enum.auto() + WAIT = enum.auto() + RESIZE_HORIZONTAL = enum.auto() + RESIZE_VERTICAL = enum.auto() cursors = Cursors # Backcompat. + +# _tool_registry, _register_tool_class, and _find_tool_class implement a +# mechanism through which ToolManager.add_tool can determine whether a subclass +# of the requested tool class has been registered (either for the current +# canvas class or for a parent class), in which case that tool subclass will be +# instantiated instead. This is the mechanism used e.g. to allow different +# GUI backends to implement different specializations for ConfigureSubplots. + + +_tool_registry = set() + + +def _register_tool_class(canvas_cls, tool_cls=None): + """Decorator registering *tool_cls* as a tool class for *canvas_cls*.""" + if tool_cls is None: + return functools.partial(_register_tool_class, canvas_cls) + _tool_registry.add((canvas_cls, tool_cls)) + return tool_cls + + +def _find_tool_class(canvas_cls, tool_cls): + """Find a subclass of *tool_cls* registered for *canvas_cls*.""" + for canvas_parent in canvas_cls.__mro__: + for tool_child in _api.recursive_subclasses(tool_cls): + if (canvas_parent, tool_child) in _tool_registry: + return tool_child + return tool_cls + + # Views positions tool _views_positions = 'viewpos' @@ -38,42 +74,33 @@ class ToolBase: """ Base tool class. - A base tool, only implements `trigger` method or not method at all. + A base tool, only implements `trigger` method or no method at all. The tool is instantiated by `matplotlib.backend_managers.ToolManager`. - - Attributes - ---------- - toolmanager : `matplotlib.backend_managers.ToolManager` - ToolManager that controls this Tool. - figure : `FigureCanvas` - Figure instance that is affected by this Tool. - name : str - Used as **Id** of the tool, has to be unique among tools of the same - ToolManager. """ default_keymap = None """ Keymap to associate with this tool. - **String**: List of comma separated keys that will be used to call this - tool when the keypress event of ``self.figure.canvas`` is emitted. + ``list[str]``: List of keys that will trigger this tool when a keypress + event is emitted on ``self.figure.canvas``. Note that this attribute is + looked up on the instance, and can therefore be a property (this is used + e.g. by the built-in tools to load the rcParams at instantiation time). """ description = None """ Description of the Tool. - **String**: If the Tool is included in the Toolbar this text is used - as a Tooltip. + `str`: Tooltip used if the Tool is included in a Toolbar. """ image = None """ Filename of the image. - **String**: Filename of the image to use in the toolbar. If None, the - *name* is used as a label in the toolbar button. + `str`: Filename of the image to use in a Toolbar. If None, the *name* is + used as a label in the toolbar button. """ def __init__(self, toolmanager, name): @@ -81,23 +108,26 @@ def __init__(self, toolmanager, name): self._toolmanager = toolmanager self._figure = None + name = property( + lambda self: self._name, + doc="The tool id (str, must be unique among tools of a tool manager).") + toolmanager = property( + lambda self: self._toolmanager, + doc="The `.ToolManager` that controls this tool.") + canvas = property( + lambda self: self._figure.canvas if self._figure is not None else None, + doc="The canvas of the figure affected by this tool, or None.") + @property def figure(self): + """The Figure affected by this tool, or None.""" return self._figure @figure.setter def figure(self, figure): - self.set_figure(figure) - - @property - def canvas(self): - if not self._figure: - return None - return self._figure.canvas + self._figure = figure - @property - def toolmanager(self): - return self._toolmanager + set_figure = figure.fset def _make_classic_style_pseudo_toolbar(self): """ @@ -108,22 +138,11 @@ def _make_classic_style_pseudo_toolbar(self): """ return SimpleNamespace(canvas=self.canvas) - def set_figure(self, figure): - """ - Assign a figure to the tool. - - Parameters - ---------- - figure : `.Figure` - """ - self._figure = figure - def trigger(self, sender, event, data=None): """ Called when this tool gets used. - This method is called by - `matplotlib.backend_managers.ToolManager.trigger_tool`. + This method is called by `.ToolManager.trigger_tool`. Parameters ---------- @@ -136,17 +155,12 @@ def trigger(self, sender, event, data=None): """ pass - @property - def name(self): - """Tool Id.""" - return self._name - + @_api.deprecated("3.6", alternative="tool_removed_event") def destroy(self): """ Destroy the tool. - This method is called when the tool is removed by - `matplotlib.backend_managers.ToolManager.remove_tool`. + This method is called by `.ToolManager.remove_tool`. """ pass @@ -167,10 +181,10 @@ class ToolToggleBase(ToolBase): """ radio_group = None - """Attribute to group 'radio' like tools (mutually exclusive). + """ + Attribute to group 'radio' like tools (mutually exclusive). - **String** that identifies the group or **None** if not belonging to a - group. + `str` that identifies the group or **None** if not belonging to a group. """ cursor = None @@ -217,7 +231,6 @@ def disable(self, event=None): @property def toggled(self): """State of the toggled tool.""" - return self._toggled def set_figure(self, figure): @@ -239,22 +252,21 @@ def set_figure(self, figure): self._toggled = True -class SetCursorBase(ToolBase): +class ToolSetCursor(ToolBase): """ Change to the current cursor while inaxes. - This tool, keeps track of all `ToolToggleBase` derived tools, and calls - `set_cursor` when a tool gets triggered. + This tool, keeps track of all `ToolToggleBase` derived tools, and updates + the cursor when a tool gets triggered. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._id_drag = None - self._cursor = None + self._current_tool = None self._default_cursor = cursors.POINTER self._last_cursor = self._default_cursor self.toolmanager.toolmanager_connect('tool_added_event', self._add_tool_cbk) - # process current tools for tool in self.toolmanager.tools.values(): self._add_tool(tool) @@ -269,10 +281,9 @@ def set_figure(self, figure): def _tool_trigger_cbk(self, event): if event.tool.toggled: - self._cursor = event.tool.cursor + self._current_tool = event.tool else: - self._cursor = None - + self._current_tool = None self._set_cursor_cbk(event.canvasevent) def _add_tool(self, tool): @@ -288,26 +299,16 @@ def _add_tool_cbk(self, event): self._add_tool(event.tool) def _set_cursor_cbk(self, event): - if not event: + if not event or not self.canvas: return - - if not getattr(event, 'inaxes', False) or not self._cursor: - if self._last_cursor != self._default_cursor: - self.set_cursor(self._default_cursor) - self._last_cursor = self._default_cursor - elif self._cursor: - cursor = self._cursor - if cursor and self._last_cursor != cursor: - self.set_cursor(cursor) - self._last_cursor = cursor - - def set_cursor(self, cursor): - """ - Set the cursor. - - This method has to be implemented per backend. - """ - raise NotImplementedError + if (self._current_tool and getattr(event, "inaxes", None) + and event.inaxes.get_navigate()): + if self._last_cursor != self._current_tool.cursor: + self.canvas.set_cursor(self._current_tool.cursor) + self._last_cursor = self._current_tool.cursor + elif self._last_cursor != self._default_cursor: + self.canvas.set_cursor(self._default_cursor) + self._last_cursor = self._default_cursor class ToolCursorPosition(ToolBase): @@ -335,14 +336,12 @@ def send_message(self, event): from matplotlib.backend_bases import NavigationToolbar2 message = NavigationToolbar2._mouse_event_to_message(event) - if message is None: - message = ' ' self.toolmanager.message_event(message, self) class RubberbandBase(ToolBase): """Draw and remove a rubberband.""" - def trigger(self, sender, event, data): + def trigger(self, sender, event, data=None): """Call `draw_rubberband` or `remove_rubberband` based on data.""" if not self.figure.canvas.widgetlock.available(sender): return @@ -372,7 +371,7 @@ class ToolQuit(ToolBase): """Tool to call the figure manager destroy method.""" description = 'Quit the figure' - default_keymap = mpl.rcParams['keymap.quit'] + default_keymap = property(lambda self: mpl.rcParams['keymap.quit']) def trigger(self, sender, event, data=None): Gcf.destroy_fig(self.figure) @@ -382,47 +381,17 @@ class ToolQuitAll(ToolBase): """Tool to call the figure manager destroy method.""" description = 'Quit all figures' - default_keymap = mpl.rcParams['keymap.quit_all'] + default_keymap = property(lambda self: mpl.rcParams['keymap.quit_all']) def trigger(self, sender, event, data=None): Gcf.destroy_all() -class _ToolEnableAllNavigation(ToolBase): - """Tool to enable all axes for toolmanager interaction.""" - - description = 'Enable all axes toolmanager' - default_keymap = mpl.rcParams['keymap.all_axes'] - - def trigger(self, sender, event, data=None): - mpl.backend_bases.key_press_handler(event, self.figure.canvas, None) - - -@_api.deprecated("3.3") -class ToolEnableAllNavigation(_ToolEnableAllNavigation): - pass - - -class _ToolEnableNavigation(ToolBase): - """Tool to enable a specific axes for toolmanager interaction.""" - - description = 'Enable one axes toolmanager' - default_keymap = ('1', '2', '3', '4', '5', '6', '7', '8', '9') - - def trigger(self, sender, event, data=None): - mpl.backend_bases.key_press_handler(event, self.figure.canvas, None) - - -@_api.deprecated("3.3") -class ToolEnableNavigation(_ToolEnableNavigation): - pass - - class ToolGrid(ToolBase): """Tool to toggle the major grids of the figure.""" description = 'Toggle major grids' - default_keymap = mpl.rcParams['keymap.grid'] + default_keymap = property(lambda self: mpl.rcParams['keymap.grid']) def trigger(self, sender, event, data=None): sentinel = str(uuid.uuid4()) @@ -437,7 +406,7 @@ class ToolMinorGrid(ToolBase): """Tool to toggle the major and minor grids of the figure.""" description = 'Toggle major and minor grids' - default_keymap = mpl.rcParams['keymap.grid_minor'] + default_keymap = property(lambda self: mpl.rcParams['keymap.grid_minor']) def trigger(self, sender, event, data=None): sentinel = str(uuid.uuid4()) @@ -448,16 +417,13 @@ def trigger(self, sender, event, data=None): mpl.backend_bases.key_press_handler(event, self.figure.canvas) -class ToolFullScreen(ToolToggleBase): +class ToolFullScreen(ToolBase): """Tool to toggle full screen.""" description = 'Toggle fullscreen mode' - default_keymap = mpl.rcParams['keymap.fullscreen'] + default_keymap = property(lambda self: mpl.rcParams['keymap.fullscreen']) - def enable(self, event): - self.figure.canvas.manager.full_screen_toggle() - - def disable(self, event): + def trigger(self, sender, event, data=None): self.figure.canvas.manager.full_screen_toggle() @@ -469,11 +435,11 @@ def trigger(self, sender, event, data=None): return super().trigger(sender, event, data) - def enable(self, event): + def enable(self, event=None): self.set_scale(event.inaxes, 'log') self.figure.canvas.draw_idle() - def disable(self, event): + def disable(self, event=None): self.set_scale(event.inaxes, 'linear') self.figure.canvas.draw_idle() @@ -482,7 +448,7 @@ class ToolYScale(AxisScaleBase): """Tool to toggle between linear and logarithmic scales on the Y axis.""" description = 'Toggle scale Y axis' - default_keymap = mpl.rcParams['keymap.yscale'] + default_keymap = property(lambda self: mpl.rcParams['keymap.yscale']) def set_scale(self, ax, scale): ax.set_yscale(scale) @@ -492,7 +458,7 @@ class ToolXScale(AxisScaleBase): """Tool to toggle between linear and logarithmic scales on the X axis.""" description = 'Toggle scale X axis' - default_keymap = mpl.rcParams['keymap.xscale'] + default_keymap = property(lambda self: mpl.rcParams['keymap.xscale']) def set_scale(self, ax, scale): ax.set_xscale(scale) @@ -613,33 +579,6 @@ def update_home_views(self, figure=None): if a not in self.home_views[figure]: self.home_views[figure][a] = a._get_view() - @_api.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) - zaxis = getattr(a, 'zaxis', None) - locators = [] - if xaxis is not None: - locators.append(xaxis.get_major_locator()) - locators.append(xaxis.get_minor_locator()) - if yaxis is not None: - locators.append(yaxis.get_major_locator()) - locators.append(yaxis.get_minor_locator()) - if zaxis is not None: - locators.append(zaxis.get_major_locator()) - locators.append(zaxis.get_minor_locator()) - - for loc in locators: - mpl.ticker._if_refresh_overridden_call_and_emit_deprec(loc) - self.figure.canvas.draw_idle() - def home(self): """Recall the first view and position from the stack.""" self.views[self.figure].home() @@ -673,7 +612,7 @@ class ToolHome(ViewsPositionsBase): description = 'Reset original view' image = 'home' - default_keymap = mpl.rcParams['keymap.home'] + default_keymap = property(lambda self: mpl.rcParams['keymap.home']) _on_trigger = 'home' @@ -682,7 +621,7 @@ class ToolBack(ViewsPositionsBase): description = 'Back to previous view' image = 'back' - default_keymap = mpl.rcParams['keymap.back'] + default_keymap = property(lambda self: mpl.rcParams['keymap.back']) _on_trigger = 'back' @@ -691,7 +630,7 @@ class ToolForward(ViewsPositionsBase): description = 'Forward to next view' image = 'forward' - default_keymap = mpl.rcParams['keymap.forward'] + default_keymap = property(lambda self: mpl.rcParams['keymap.forward']) _on_trigger = 'forward' @@ -707,7 +646,7 @@ class SaveFigureBase(ToolBase): description = 'Save the figure' image = 'filesave' - default_keymap = mpl.rcParams['keymap.save'] + default_keymap = property(lambda self: mpl.rcParams['keymap.save']) class ZoomPanBase(ToolToggleBase): @@ -723,7 +662,7 @@ def __init__(self, *args): self.scrollthresh = .5 # .5 second scroll threshold self.lastscroll = time.time()-self.scrollthresh - def enable(self, event): + def enable(self, event=None): """Connect press/release events and lock the canvas.""" self.figure.canvas.widgetlock(self) self._idPress = self.figure.canvas.mpl_connect( @@ -733,7 +672,7 @@ def enable(self, event): self._idScroll = self.figure.canvas.mpl_connect( 'scroll_event', self.scroll_zoom) - def disable(self, event): + def disable(self, event=None): """Release the canvas and disconnect press/release events.""" self._cancel_action() self.figure.canvas.widgetlock.release(self) @@ -782,7 +721,7 @@ class ToolZoom(ZoomPanBase): description = 'Zoom to rectangle' image = 'zoom_to_rect' - default_keymap = mpl.rcParams['keymap.zoom'] + default_keymap = property(lambda self: mpl.rcParams['keymap.zoom']) cursor = cursors.SELECT_REGION radio_group = 'default' @@ -794,7 +733,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.figure.canvas.draw_idle() self._xypress = None self._button_pressed = None self._ids_zoom = [] @@ -868,7 +807,7 @@ def _release(self, event): self._cancel_action() return - last_a = [] + done_ax = [] for cur_xypress in self._xypress: x, y = event.x, event.y @@ -879,14 +818,9 @@ def _release(self, event): 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) + twinx = any(a.get_shared_x_axes().joined(a, a1) for a1 in done_ax) + twiny = any(a.get_shared_y_axes().joined(a, a1) for a1 in done_ax) + done_ax.append(a) if self._button_pressed == 1: direction = 'in' @@ -906,7 +840,7 @@ def _release(self, event): class ToolPan(ZoomPanBase): """Pan axes with left mouse, zoom with right.""" - default_keymap = mpl.rcParams['keymap.pan'] + default_keymap = property(lambda self: mpl.rcParams['keymap.pan']) description = 'Pan axes with left mouse, zoom with right' image = 'move' cursor = cursors.MOVE @@ -921,7 +855,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.figure.canvas.draw_idle() def _press(self, event): if event.button == 1: @@ -971,13 +905,13 @@ def _mouse_move(self, event): class ToolHelpBase(ToolBase): description = 'Print tool list, shortcuts and description' - default_keymap = mpl.rcParams['keymap.help'] + default_keymap = property(lambda self: mpl.rcParams['keymap.help']) image = 'help' @staticmethod def format_shortcut(key_sequence): """ - Converts a shortcut string from the notation used in rc config to the + Convert a shortcut string from the notation used in rc config to the standard notation for displaying shortcuts, e.g. 'ctrl+a' -> 'Ctrl+A'. """ return (key_sequence if len(key_sequence) == 1 else @@ -1011,7 +945,7 @@ class ToolCopyToClipboardBase(ToolBase): """Tool to copy the figure to the clipboard.""" description = 'Copy the canvas figure to clipboard' - default_keymap = mpl.rcParams['keymap.copy'] + default_keymap = property(lambda self: mpl.rcParams['keymap.copy']) def trigger(self, *args, **kwargs): message = "Copy tool is not available" @@ -1020,30 +954,26 @@ def trigger(self, *args, **kwargs): default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, 'zoom': ToolZoom, 'pan': ToolPan, - 'subplots': 'ToolConfigureSubplots', - 'save': 'ToolSaveFigure', + 'subplots': ConfigureSubplotsBase, + 'save': SaveFigureBase, 'grid': ToolGrid, 'grid_minor': ToolMinorGrid, 'fullscreen': ToolFullScreen, 'quit': ToolQuit, 'quit_all': ToolQuitAll, - 'allnav': _ToolEnableAllNavigation, - 'nav': _ToolEnableNavigation, 'xscale': ToolXScale, 'yscale': ToolYScale, 'position': ToolCursorPosition, _views_positions: ToolViewsPositions, - 'cursor': 'ToolSetCursor', - 'rubberband': 'ToolRubberband', - 'help': 'ToolHelp', - 'copy': 'ToolCopyToClipboard', + 'cursor': ToolSetCursor, + 'rubberband': RubberbandBase, + 'help': ToolHelpBase, + 'copy': ToolCopyToClipboardBase, } -"""Default tools""" default_toolbar_tools = [['navigation', ['home', 'back', 'forward']], ['zoompan', ['pan', 'zoom', 'subplots']], ['io', ['save', 'help']]] -"""Default tools in the toolbar""" def add_tools_to_manager(toolmanager, tools=default_tools): @@ -1055,8 +985,8 @@ def add_tools_to_manager(toolmanager, tools=default_tools): toolmanager : `.backend_managers.ToolManager` Manager to which the tools are added. tools : {str: class_like}, optional - The tools to add in a {name: tool} dict, see `add_tool` for more - info. + The tools to add in a {name: tool} dict, see + `.backend_managers.ToolManager.add_tool` for more info. """ for name, tool in tools.items(): @@ -1070,11 +1000,12 @@ def add_tools_to_container(container, tools=default_toolbar_tools): Parameters ---------- container : Container - `backend_bases.ToolContainerBase` object that will get the tools added. + `.backend_bases.ToolContainerBase` object that will get the tools + added. tools : list, optional List in the form ``[[group1, [tool1, tool2 ...]], [group2, [...]]]`` where the tools ``[tool1, tool2, ...]`` will display in group1. - See `add_tool` for details. + See `.backend_bases.ToolContainerBase.add_tool` for details. """ for group, grouptools in tools: diff --git a/lib/matplotlib/backends/__init__.py b/lib/matplotlib/backends/__init__.py index 6f4015d6ea8e..3e687f85b0be 100644 --- a/lib/matplotlib/backends/__init__.py +++ b/lib/matplotlib/backends/__init__.py @@ -1,2 +1,3 @@ # NOTE: plt.switch_backend() (called at import time) will add a "backend" # attribute here for backcompat. +_QT_FORCE_QT5_BINDING = False diff --git a/lib/matplotlib/backends/_backend_gtk.py b/lib/matplotlib/backends/_backend_gtk.py new file mode 100644 index 000000000000..1fadc49a0d37 --- /dev/null +++ b/lib/matplotlib/backends/_backend_gtk.py @@ -0,0 +1,332 @@ +""" +Common code for GTK3 and GTK4 backends. +""" + +import logging +import sys + +import matplotlib as mpl +from matplotlib import _api, backend_tools, cbook +from matplotlib._pylab_helpers import Gcf +from matplotlib.backend_bases import ( + _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, + TimerBase) +from matplotlib.backend_tools import Cursors + +import gi +# The GTK3/GTK4 backends will have already called `gi.require_version` to set +# the desired GTK. +from gi.repository import Gdk, Gio, GLib, Gtk + + +try: + gi.require_foreign("cairo") +except ImportError as e: + raise ImportError("Gtk-based backends require cairo") from e + +_log = logging.getLogger(__name__) +_application = None # Placeholder + + +def _shutdown_application(app): + # The application might prematurely shut down if Ctrl-C'd out of IPython, + # so close all windows. + for win in app.get_windows(): + win.close() + # The PyGObject wrapper incorrectly thinks that None is not allowed, or we + # would call this: + # Gio.Application.set_default(None) + # Instead, we set this property and ignore default applications with it: + app._created_by_matplotlib = True + global _application + _application = None + + +def _create_application(): + global _application + + if _application is None: + app = Gio.Application.get_default() + if app is None or getattr(app, '_created_by_matplotlib', False): + # display_is_valid returns False only if on Linux and neither X11 + # nor Wayland display can be opened. + if not mpl._c_internal_utils.display_is_valid(): + raise RuntimeError('Invalid DISPLAY variable') + _application = Gtk.Application.new('org.matplotlib.Matplotlib3', + Gio.ApplicationFlags.NON_UNIQUE) + # The activate signal must be connected, but we don't care for + # handling it, since we don't do any remote processing. + _application.connect('activate', lambda *args, **kwargs: None) + _application.connect('shutdown', _shutdown_application) + _application.register() + cbook._setup_new_guiapp() + else: + _application = app + + return _application + + +def mpl_to_gtk_cursor_name(mpl_cursor): + return _api.check_getitem({ + Cursors.MOVE: "move", + Cursors.HAND: "pointer", + Cursors.POINTER: "default", + Cursors.SELECT_REGION: "crosshair", + Cursors.WAIT: "wait", + Cursors.RESIZE_HORIZONTAL: "ew-resize", + Cursors.RESIZE_VERTICAL: "ns-resize", + }, cursor=mpl_cursor) + + +class TimerGTK(TimerBase): + """Subclass of `.TimerBase` using GTK timer events.""" + + def __init__(self, *args, **kwargs): + self._timer = None + super().__init__(*args, **kwargs) + + def _timer_start(self): + # Need to stop it, otherwise we potentially leak a timer id that will + # never be stopped. + self._timer_stop() + self._timer = GLib.timeout_add(self._interval, self._on_timer) + + def _timer_stop(self): + if self._timer is not None: + GLib.source_remove(self._timer) + self._timer = None + + def _timer_set_interval(self): + # Only stop and restart it if the timer has already been started. + if self._timer is not None: + self._timer_stop() + self._timer_start() + + def _on_timer(self): + super()._on_timer() + + # Gtk timeout_add() requires that the callback returns True if it + # is to be called again. + if self.callbacks and not self._single: + return True + else: + self._timer = None + return False + + +class _FigureCanvasGTK(FigureCanvasBase): + _timer_cls = TimerGTK + + +class _FigureManagerGTK(FigureManagerBase): + """ + Attributes + ---------- + canvas : `FigureCanvas` + The FigureCanvas instance + num : int or str + The Figure number + toolbar : Gtk.Toolbar or Gtk.Box + The toolbar + vbox : Gtk.VBox + The Gtk.VBox containing the canvas and toolbar + window : Gtk.Window + The Gtk.Window + """ + + def __init__(self, canvas, num): + self._gtk_ver = gtk_ver = Gtk.get_major_version() + + app = _create_application() + self.window = Gtk.Window() + app.add_window(self.window) + super().__init__(canvas, num) + + if gtk_ver == 3: + self.window.set_wmclass("matplotlib", "Matplotlib") + icon_ext = "png" if sys.platform == "win32" else "svg" + self.window.set_icon_from_file( + str(cbook._get_data_path(f"images/matplotlib.{icon_ext}"))) + + self.vbox = Gtk.Box() + self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) + + if gtk_ver == 3: + self.window.add(self.vbox) + self.vbox.show() + self.canvas.show() + self.vbox.pack_start(self.canvas, True, True, 0) + elif gtk_ver == 4: + self.window.set_child(self.vbox) + self.vbox.prepend(self.canvas) + + # calculate size for window + w, h = self.canvas.get_width_height() + + if self.toolbar is not None: + if gtk_ver == 3: + self.toolbar.show() + self.vbox.pack_end(self.toolbar, False, False, 0) + elif gtk_ver == 4: + sw = Gtk.ScrolledWindow(vscrollbar_policy=Gtk.PolicyType.NEVER) + sw.set_child(self.toolbar) + self.vbox.append(sw) + min_size, nat_size = self.toolbar.get_preferred_size() + h += nat_size.height + + self.window.set_default_size(w, h) + + self._destroying = False + self.window.connect("destroy", lambda *args: Gcf.destroy(self)) + self.window.connect({3: "delete_event", 4: "close-request"}[gtk_ver], + lambda *args: Gcf.destroy(self)) + if mpl.is_interactive(): + self.window.show() + self.canvas.draw_idle() + + self.canvas.grab_focus() + + def destroy(self, *args): + if self._destroying: + # Otherwise, this can be called twice when the user presses 'q', + # which calls Gcf.destroy(self), then this destroy(), then triggers + # Gcf.destroy(self) once again via + # `connect("destroy", lambda *args: Gcf.destroy(self))`. + return + self._destroying = True + self.window.destroy() + self.canvas.destroy() + + @classmethod + def start_main_loop(cls): + global _application + if _application is None: + return + + try: + _application.run() # Quits when all added windows close. + except KeyboardInterrupt: + # Ensure all windows can process their close event from + # _shutdown_application. + context = GLib.MainContext.default() + while context.pending(): + context.iteration(True) + raise + finally: + # Running after quit is undefined, so create a new one next time. + _application = None + + def show(self): + # show the figure window + self.window.show() + self.canvas.draw() + if mpl.rcParams["figure.raise_window"]: + meth_name = {3: "get_window", 4: "get_surface"}[self._gtk_ver] + if getattr(self.window, meth_name)(): + self.window.present() + else: + # If this is called by a callback early during init, + # self.window (a GtkWindow) may not have an associated + # low-level GdkWindow (on GTK3) or GdkSurface (on GTK4) yet, + # and present() would crash. + _api.warn_external("Cannot raise window yet to be setup") + + def full_screen_toggle(self): + is_fullscreen = { + 3: lambda w: (w.get_window().get_state() + & Gdk.WindowState.FULLSCREEN), + 4: lambda w: w.is_fullscreen(), + }[self._gtk_ver] + if is_fullscreen(self.window): + self.window.unfullscreen() + else: + self.window.fullscreen() + + def get_window_title(self): + return self.window.get_title() + + def set_window_title(self, title): + self.window.set_title(title) + + def resize(self, width, height): + width = int(width / self.canvas.device_pixel_ratio) + height = int(height / self.canvas.device_pixel_ratio) + if self.toolbar: + min_size, nat_size = self.toolbar.get_preferred_size() + height += nat_size.height + canvas_size = self.canvas.get_allocation() + if self._gtk_ver >= 4 or canvas_size.width == canvas_size.height == 1: + # A canvas size of (1, 1) cannot exist in most cases, because + # window decorations would prevent such a small window. This call + # must be before the window has been mapped and widgets have been + # sized, so just change the window's starting size. + self.window.set_default_size(width, height) + else: + self.window.resize(width, height) + + +class _NavigationToolbar2GTK(NavigationToolbar2): + # Must be implemented in GTK3/GTK4 backends: + # * __init__ + # * save_figure + + def set_message(self, s): + escaped = GLib.markup_escape_text(s) + self.message.set_markup(f'{escaped}') + + def draw_rubberband(self, event, x0, y0, x1, y1): + height = self.canvas.figure.bbox.height + y1 = height - y1 + y0 = height - y0 + rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] + self.canvas._draw_rubberband(rect) + + def remove_rubberband(self): + self.canvas._draw_rubberband(None) + + def _update_buttons_checked(self): + for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: + button = self._gtk_ids.get(name) + if button: + with button.handler_block(button._signal_handler): + button.set_active(self.mode.name == active) + + def pan(self, *args): + super().pan(*args) + self._update_buttons_checked() + + def zoom(self, *args): + super().zoom(*args) + self._update_buttons_checked() + + def set_history_buttons(self): + can_backward = self._nav_stack._pos > 0 + can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 + if 'Back' in self._gtk_ids: + self._gtk_ids['Back'].set_sensitive(can_backward) + if 'Forward' in self._gtk_ids: + self._gtk_ids['Forward'].set_sensitive(can_forward) + + +class RubberbandGTK(backend_tools.RubberbandBase): + def draw_rubberband(self, x0, y0, x1, y1): + _NavigationToolbar2GTK.draw_rubberband( + self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) + + def remove_rubberband(self): + _NavigationToolbar2GTK.remove_rubberband( + self._make_classic_style_pseudo_toolbar()) + + +class ConfigureSubplotsGTK(backend_tools.ConfigureSubplotsBase): + def trigger(self, *args): + _NavigationToolbar2GTK.configure_subplots(self, None) + + +class _BackendGTK(_Backend): + backend_version = "%s.%s.%s" % ( + Gtk.get_major_version(), + Gtk.get_minor_version(), + Gtk.get_micro_version(), + ) + mainloop = _FigureManagerGTK.start_main_loop diff --git a/lib/matplotlib/backends/_backend_pdf_ps.py b/lib/matplotlib/backends/_backend_pdf_ps.py index 6c4f129b927f..30d952e7fe34 100644 --- a/lib/matplotlib/backends/_backend_pdf_ps.py +++ b/lib/matplotlib/backends/_backend_pdf_ps.py @@ -2,12 +2,14 @@ Common functionality between the PDF and PS backends. """ +from io import BytesIO import functools +from fontTools import subset + import matplotlib as mpl -from matplotlib import _api from .. import font_manager, ft2font -from ..afm import AFM +from .._afm import AFM from ..backend_bases import RendererBase @@ -17,6 +19,39 @@ def _cached_get_afm_from_fname(fname): return AFM(fh) +def get_glyphs_subset(fontfile, characters): + """ + Subset a TTF font + + Reads the named fontfile and restricts the font to the characters. + Returns a serialization of the subset font as file-like object. + + Parameters + ---------- + symbol : str + Path to the font file + characters : str + Continuous set of characters to include in subset + """ + + options = subset.Options(glyph_names=True, recommended_glyphs=True) + + # prevent subsetting FontForge Timestamp and other tables + options.drop_tables += ['FFTM', 'PfEd', 'BDF'] + + # if fontfile is a ttc, specify font number + if fontfile.endswith(".ttc"): + options.font_number = 0 + + with subset.load_font(fontfile, options) as font: + subsetter = subset.Subsetter(options=options) + subsetter.populate(text=characters) + subsetter.subset(font) + fh = BytesIO() + font.save(fh, reorderTables=False) + return fh + + class CharacterTracker: """ Helper for font subsetting by the pdf and ps backends. @@ -28,29 +63,15 @@ class CharacterTracker: def __init__(self): self.used = {} - @_api.deprecated("3.3") - @property - def used_characters(self): - d = {} - for fname, chars in self.used.items(): - realpath, stat_key = mpl.cbook.get_realpath_and_stat(fname) - d[stat_key] = (realpath, chars) - return d - def track(self, font, s): """Record that string *s* is being typeset using font *font*.""" - if isinstance(font, str): - # Unused, can be removed after removal of track_characters. - fname = font - else: - fname = font.fname - self.used.setdefault(fname, set()).update(map(ord, s)) + char_to_font = font._get_fontmap(s) + for _c, _f in char_to_font.items(): + self.used.setdefault(_f.fname, set()).add(ord(_c)) - # Not public, can be removed when pdf/ps merge_used_characters is removed. - def merge(self, other): - """Update self with a font path to character codepoints.""" - for fname, charset in other.items(): - self.used.setdefault(fname, set()).update(charset) + def track_glyph(self, font, glyph): + """Record that codepoint *glyph* is being typeset using font *font*.""" + self.used.setdefault(font.fname, set()).add(glyph) class RendererPDFPSBase(RendererBase): @@ -83,18 +104,9 @@ def get_canvas_width_height(self): def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited if ismath == "TeX": - texmanager = self.get_texmanager() - fontsize = prop.get_size_in_points() - w, h, d = texmanager.get_text_width_height_descent( - s, fontsize, renderer=self) - return w, h, d + return super().get_text_width_height_descent(s, prop, ismath) elif ismath: - # Circular import. - from matplotlib.backends.backend_ps import RendererPS - parse = self._text2path.mathtext_parser.parse( - s, 72, prop, - _force_standard_ps_fonts=(isinstance(self, RendererPS) - and mpl.rcParams["ps.useafm"])) + parse = self._text2path.mathtext_parser.parse(s, 72, prop) return parse.width, parse.height, parse.depth elif mpl.rcParams[self._use_afm_rc_name]: font = self._get_font_afm(prop) @@ -121,8 +133,8 @@ def _get_font_afm(self, prop): return _cached_get_afm_from_fname(fname) def _get_font_ttf(self, prop): - fname = font_manager.findfont(prop) - font = font_manager.get_font(fname) + fnames = font_manager.fontManager._find_fonts_by_props(prop) + font = font_manager.get_font(fnames) font.clear() font.set_size(prop.get_size_in_points(), 72) return font diff --git a/lib/matplotlib/backends/_backend_tk.py b/lib/matplotlib/backends/_backend_tk.py index adf0a9dfc698..4e5c00251a54 100644 --- a/lib/matplotlib/backends/_backend_tk.py +++ b/lib/matplotlib/backends/_backend_tk.py @@ -1,38 +1,39 @@ import uuid +import weakref from contextlib import contextmanager import logging import math import os.path import sys import tkinter as tk -from tkinter.simpledialog import SimpleDialog import tkinter.filedialog +import tkinter.font import tkinter.messagebox +from tkinter.simpledialog import SimpleDialog import numpy as np +from PIL import Image, ImageTk import matplotlib as mpl from matplotlib import _api, backend_tools, cbook, _c_internal_utils from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - StatusbarBase, TimerBase, ToolContainerBase, cursors, _Mode) + TimerBase, ToolContainerBase, cursors, _Mode, + CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) from matplotlib._pylab_helpers import Gcf -from matplotlib.figure import Figure -from matplotlib.widgets import SubplotTool from . import _tkagg _log = logging.getLogger(__name__) - -backend_version = tk.TkVersion - cursord = { cursors.MOVE: "fleur", cursors.HAND: "hand2", cursors.POINTER: "arrow", cursors.SELECT_REGION: "tcross", cursors.WAIT: "watch", - } + cursors.RESIZE_HORIZONTAL: "sb_h_double_arrow", + cursors.RESIZE_VERTICAL: "sb_v_double_arrow", +} @contextmanager @@ -49,6 +50,9 @@ def _restore_foreground_window_at_end(): # Initialize to a non-empty string that is not a Tcl command _blit_tcl_name = "mpl_blit_" + uuid.uuid4().hex +TK_PHOTO_COMPOSITE_OVERLAY = 0 # apply transparency rules pixel-wise +TK_PHOTO_COMPOSITE_SET = 1 # set image buffer directly + def _blit(argsid): """ @@ -56,15 +60,10 @@ def _blit(argsid): *argsid* is a unique string identifier to fetch the correct arguments from the ``_blit_args`` dict, since arguments cannot be passed directly. - - photoimage blanking must occur in the same event and thread as blitting - to avoid flickering. """ - photoimage, dataptr, offsets, bboxptr, blank = _blit_args.pop(argsid) - if blank: - photoimage.blank() - _tkagg.blit( - photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, bboxptr) + photoimage, dataptr, offsets, bboxptr, comp_rule = _blit_args.pop(argsid) + _tkagg.blit(photoimage.tk.interpaddr(), str(photoimage), dataptr, + comp_rule, offsets, bboxptr) def blit(photoimage, aggimage, offsets, bbox=None): @@ -77,7 +76,9 @@ def blit(photoimage, aggimage, offsets, bbox=None): for big-endian ARGB32 (i.e. ARGB8888) data. If *bbox* is passed, it defines the region that gets blitted. That region - will NOT be blanked before blitting. + will be composed with the previous data according to the alpha channel. + Blitting will be clipped to pixels inside the canvas, including silently + doing nothing if the *bbox* region is entirely outside the canvas. Tcl events must be dispatched to trigger a blit from a non-Tcl thread. """ @@ -90,11 +91,13 @@ def blit(photoimage, aggimage, offsets, bbox=None): x2 = min(math.ceil(x2), width) y1 = max(math.floor(y1), 0) y2 = min(math.ceil(y2), height) + if (x1 > x2) or (y1 > y2): + return bboxptr = (x1, x2, y1, y2) - blank = False + comp_rule = TK_PHOTO_COMPOSITE_OVERLAY else: bboxptr = (0, width, 0, height) - blank = True + comp_rule = TK_PHOTO_COMPOSITE_SET # NOTE: _tkagg.blit is thread unsafe and will crash the process if called # from a thread (GH#13293). Instead of blanking and blitting here, @@ -103,7 +106,7 @@ def blit(photoimage, aggimage, offsets, bbox=None): # tkapp.call coerces all arguments to strings, so to avoid string parsing # within _blit, pack up the arguments into a global data structure. - args = photoimage, dataptr, offsets, bboxptr, blank + args = photoimage, dataptr, offsets, bboxptr, comp_rule # Need a unique key to avoid thread races. # Again, make the key a string to avoid string parsing in _blit. argsid = str(id(args)) @@ -158,24 +161,22 @@ def _on_timer(self): class FigureCanvasTk(FigureCanvasBase): required_interactive_framework = "tk" + manager_class = _api.classproperty(lambda cls: FigureManagerTk) - @_api.delete_parameter( - "3.4", "resize_callback", - alternative="get_tk_widget().bind('', ..., True)") - def __init__(self, figure=None, master=None, resize_callback=None): + def __init__(self, figure=None, master=None): super().__init__(figure) - self._idle = True - self._idle_callback = None + self._idle_draw_id = None self._event_loop_id = None - w, h = self.figure.bbox.size.astype(int) + w, h = self.get_width_height(physical=True) self._tkcanvas = tk.Canvas( master=master, background="white", width=w, height=h, borderwidth=0, highlightthickness=0) self._tkphoto = tk.PhotoImage( master=self._tkcanvas, width=w, height=h) self._tkcanvas.create_image(w//2, h//2, image=self._tkphoto) - self._resize_callback = resize_callback self._tkcanvas.bind("", self.resize) + if sys.platform == 'win32': + self._tkcanvas.bind("", self._update_device_pixel_ratio) self._tkcanvas.bind("", self.key_press) self._tkcanvas.bind("", self.motion_notify_event) self._tkcanvas.bind("", self.enter_notify_event) @@ -196,24 +197,55 @@ def __init__(self, figure=None, master=None, resize_callback=None): # Mouse wheel for windows goes to the window with the focus. # Since the canvas won't usually have the focus, bind the # event to the window containing the canvas instead. - # See http://wiki.tcl.tk/3893 (mousewheel) for details + # See https://wiki.tcl-lang.org/3893 (mousewheel) for details root = self._tkcanvas.winfo_toplevel() - root.bind("", self.scroll_event_windows, "+") + + # Prevent long-lived references via tkinter callback structure GH-24820 + weakself = weakref.ref(self) + weakroot = weakref.ref(root) + + def scroll_event_windows(event): + self = weakself() + if self is None: + root = weakroot() + if root is not None: + root.unbind("", scroll_event_windows_id) + return + return self.scroll_event_windows(event) + scroll_event_windows_id = root.bind("", scroll_event_windows, "+") # Can't get destroy events by binding to _tkcanvas. Therefore, bind # to the window and filter. def filter_destroy(event): + self = weakself() + if self is None: + root = weakroot() + if root is not None: + root.unbind("", filter_destroy_id) + return if event.widget is self._tkcanvas: - self.close_event() - root.bind("", filter_destroy, "+") + CloseEvent("close_event", self)._process() + filter_destroy_id = root.bind("", filter_destroy, "+") - self._master = master self._tkcanvas.focus_set() + self._rubberband_rect_black = None + self._rubberband_rect_white = None + + def _update_device_pixel_ratio(self, event=None): + # Tk gives scaling with respect to 72 DPI, but Windows screens are + # scaled vs 96 dpi, and pixel ratio settings are given in whole + # percentages, so round to 2 digits. + ratio = round(self._tkcanvas.tk.call('tk', 'scaling') / (96 / 72), 2) + if self._set_device_pixel_ratio(ratio): + # The easiest way to resize the canvas is to resize the canvas + # widget itself, since we implement all the logic for resizing the + # canvas backing store on that event. + w, h = self.get_width_height(physical=True) + self._tkcanvas.configure(width=w, height=h) + def resize(self, event): width, height = event.width, event.height - if self._resize_callback is not None: - self._resize_callback(event) # compute desired figure size in inches dpival = self.figure.dpi @@ -226,22 +258,21 @@ def resize(self, event): master=self._tkcanvas, width=int(width), height=int(height)) self._tkcanvas.create_image( int(width / 2), int(height / 2), image=self._tkphoto) - self.resize_event() + ResizeEvent("resize_event", self)._process() + self.draw_idle() def draw_idle(self): # docstring inherited - if not self._idle: + if self._idle_draw_id: return - self._idle = False - def idle_draw(*args): try: self.draw() finally: - self._idle = True + self._idle_draw_id = None - self._idle_callback = self._tkcanvas.after_idle(idle_draw) + self._idle_draw_id = self._tkcanvas.after_idle(idle_draw) def get_tk_widget(self): """ @@ -252,75 +283,79 @@ def get_tk_widget(self): """ return self._tkcanvas + def _event_mpl_coords(self, event): + # calling canvasx/canvasy allows taking scrollbars into account (i.e. + # the top of the widget may have been scrolled out of view). + return (self._tkcanvas.canvasx(event.x), + # flipy so y=0 is bottom of canvas + self.figure.bbox.height - self._tkcanvas.canvasy(event.y)) + def motion_notify_event(self, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y - super().motion_notify_event(x, y, guiEvent=event) + MouseEvent("motion_notify_event", self, + *self._event_mpl_coords(event), + modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() def enter_notify_event(self, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y - super().enter_notify_event(guiEvent=event, xy=(x, y)) + LocationEvent("figure_enter_event", self, + *self._event_mpl_coords(event), + modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() - def button_press_event(self, event, dblclick=False): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y - num = getattr(event, 'num', None) + def leave_notify_event(self, event): + LocationEvent("figure_leave_event", self, + *self._event_mpl_coords(event), + modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() - if sys.platform == 'darwin': - # 2 and 3 were reversed on the OSX platform I tested under tkagg. - if num == 2: - num = 3 - elif num == 3: - num = 2 + def button_press_event(self, event, dblclick=False): + # set focus to the canvas so that it can receive keyboard events + self._tkcanvas.focus_set() - super().button_press_event(x, y, num, - dblclick=dblclick, guiEvent=event) + num = getattr(event, 'num', None) + if sys.platform == 'darwin': # 2 and 3 are reversed. + num = {2: 3, 3: 2}.get(num, num) + MouseEvent("button_press_event", self, + *self._event_mpl_coords(event), num, dblclick=dblclick, + modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() def button_dblclick_event(self, event): self.button_press_event(event, dblclick=True) def button_release_event(self, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y - num = getattr(event, 'num', None) - - if sys.platform == 'darwin': - # 2 and 3 were reversed on the OSX platform I tested under tkagg. - if num == 2: - num = 3 - elif num == 3: - num = 2 - - super().button_release_event(x, y, num, guiEvent=event) + if sys.platform == 'darwin': # 2 and 3 are reversed. + num = {2: 3, 3: 2}.get(num, num) + MouseEvent("button_release_event", self, + *self._event_mpl_coords(event), num, + modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() def scroll_event(self, event): - x = event.x - y = self.figure.bbox.height - event.y num = getattr(event, 'num', None) step = 1 if num == 4 else -1 if num == 5 else 0 - super().scroll_event(x, y, step, guiEvent=event) + MouseEvent("scroll_event", self, + *self._event_mpl_coords(event), step=step, + modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() def scroll_event_windows(self, event): """MouseWheel event processor""" # need to find the window that contains the mouse w = event.widget.winfo_containing(event.x_root, event.y_root) - if w == self._tkcanvas: - x = event.x_root - w.winfo_rootx() - y = event.y_root - w.winfo_rooty() - y = self.figure.bbox.height - y - step = event.delta/120. - FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) - - def _get_key(self, event): - unikey = event.char - key = cbook._unikey_or_keysym_to_mplkey(unikey, event.keysym) + if w != self._tkcanvas: + return + x = self._tkcanvas.canvasx(event.x_root - w.winfo_rootx()) + y = (self.figure.bbox.height + - self._tkcanvas.canvasy(event.y_root - w.winfo_rooty())) + step = event.delta / 120 + MouseEvent("scroll_event", self, + x, y, step=step, modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() + @staticmethod + def _mpl_modifiers(event, *, exclude=None): # add modifier keys to the key string. Bit details originate from # http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm # BIT_SHIFT = 0x001; BIT_CAPSLOCK = 0x002; BIT_CONTROL = 0x004; @@ -329,40 +364,43 @@ def _get_key(self, event): # In general, the modifier key is excluded from the modifier flag, # however this is not the case on "darwin", so double check that # we aren't adding repeat modifier flags to a modifier key. - if sys.platform == 'win32': - modifiers = [(2, 'ctrl', 'control'), - (17, 'alt', 'alt'), - (0, 'shift', 'shift'), - ] - elif sys.platform == 'darwin': - modifiers = [(2, 'ctrl', 'control'), - (4, 'alt', 'alt'), - (0, 'shift', 'shift'), - (3, 'super', 'super'), - ] - else: - modifiers = [(2, 'ctrl', 'control'), - (3, 'alt', 'alt'), - (0, 'shift', 'shift'), - (6, 'super', 'super'), - ] + modifiers = [ + ("ctrl", 1 << 2, "control"), + ("alt", 1 << 17, "alt"), + ("shift", 1 << 0, "shift"), + ] if sys.platform == "win32" else [ + ("ctrl", 1 << 2, "control"), + ("alt", 1 << 4, "alt"), + ("shift", 1 << 0, "shift"), + ("super", 1 << 3, "super"), + ] if sys.platform == "darwin" else [ + ("ctrl", 1 << 2, "control"), + ("alt", 1 << 3, "alt"), + ("shift", 1 << 0, "shift"), + ("super", 1 << 6, "super"), + ] + return [name for name, mask, key in modifiers + if event.state & mask and exclude != key] + def _get_key(self, event): + unikey = event.char + key = cbook._unikey_or_keysym_to_mplkey(unikey, event.keysym) if key is not None: - # shift is not added to the keys as this is already accounted for - for bitmask, prefix, key_name in modifiers: - if event.state & (1 << bitmask) and key_name not in key: - if not (prefix == 'shift' and unikey): - key = '{0}+{1}'.format(prefix, key) - - return key + mods = self._mpl_modifiers(event, exclude=key) + # shift is not added to the keys as this is already accounted for. + if "shift" in mods and unikey: + mods.remove("shift") + return "+".join([*mods, key]) def key_press(self, event): - key = self._get_key(event) - FigureCanvasBase.key_press_event(self, key, guiEvent=event) + KeyEvent("key_press_event", self, + self._get_key(event), *self._event_mpl_coords(event), + guiEvent=event)._process() def key_release(self, event): - key = self._get_key(event) - FigureCanvasBase.key_release_event(self, key, guiEvent=event) + KeyEvent("key_release_event", self, + self._get_key(event), *self._event_mpl_coords(event), + guiEvent=event)._process() def new_timer(self, *args, **kwargs): # docstring inherited @@ -370,7 +408,7 @@ def new_timer(self, *args, **kwargs): def flush_events(self): # docstring inherited - self._master.update() + self._tkcanvas.update() def start_event_loop(self, timeout=0): # docstring inherited @@ -382,14 +420,20 @@ def start_event_loop(self, timeout=0): else: self._event_loop_id = self._tkcanvas.after_idle( self.stop_event_loop) - self._master.mainloop() + self._tkcanvas.mainloop() def stop_event_loop(self): # docstring inherited if self._event_loop_id: - self._master.after_cancel(self._event_loop_id) + self._tkcanvas.after_cancel(self._event_loop_id) self._event_loop_id = None - self._master.quit() + self._tkcanvas.quit() + + def set_cursor(self, cursor): + try: + self._tkcanvas.configure(cursor=cursord[cursor]) + except tkinter.TclError: + pass class FigureManagerTk(FigureManagerBase): @@ -414,24 +458,78 @@ def __init__(self, canvas, num, window): self.window.withdraw() # packing toolbar first, because if space is getting low, last packed # widget is getting shrunk first (-> the canvas) - self.toolbar = self._get_toolbar() self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1) - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) + # If the window has per-monitor DPI awareness, then setup a Tk variable + # to store the DPI, which will be updated by the C code, and the trace + # will handle it on the Python side. + window_frame = int(window.wm_frame(), 16) + self._window_dpi = tk.IntVar(master=window, value=96, + name=f'window_dpi{window_frame}') + self._window_dpi_cbname = '' + if _tkagg.enable_dpi_awareness(window_frame, window.tk.interpaddr()): + self._window_dpi_cbname = self._window_dpi.trace_add( + 'write', self._update_window_dpi) self._shown = False - def _get_toolbar(self): - if mpl.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2Tk(self.canvas, self.window) - elif mpl.rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarTk(self.toolmanager, self.window) - else: - toolbar = None - return toolbar + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + # docstring inherited + with _restore_foreground_window_at_end(): + if cbook._get_running_interactive_framework() is None: + cbook._setup_new_guiapp() + _c_internal_utils.Win32_SetProcessDpiAwareness_max() + window = tk.Tk(className="matplotlib") + window.withdraw() + + # Put a Matplotlib icon on the window rather than the default tk + # icon. See https://www.tcl.tk/man/tcl/TkCmd/wm.html#M50 + # + # `ImageTk` can be replaced with `tk` whenever the minimum + # supported Tk version is increased to 8.6, as Tk 8.6+ natively + # supports PNG images. + icon_fname = str(cbook._get_data_path( + 'images/matplotlib.png')) + icon_img = ImageTk.PhotoImage(file=icon_fname, master=window) + + icon_fname_large = str(cbook._get_data_path( + 'images/matplotlib_large.png')) + icon_img_large = ImageTk.PhotoImage( + file=icon_fname_large, master=window) + try: + window.iconphoto(False, icon_img_large, icon_img) + except Exception as exc: + # log the failure (due e.g. to Tk version), but carry on + _log.info('Could not load matplotlib icon: %s', exc) + + canvas = canvas_class(figure, master=window) + manager = cls(canvas, num, window) + if mpl.is_interactive(): + manager.show() + canvas.draw_idle() + return manager + + @classmethod + def start_main_loop(cls): + managers = Gcf.get_all_fig_managers() + if managers: + first_manager = managers[0] + manager_class = type(first_manager) + if manager_class._owns_mainloop: + return + manager_class._owns_mainloop = True + try: + first_manager.window.mainloop() + finally: + manager_class._owns_mainloop = False + + def _update_window_dpi(self, *args): + newdpi = self._window_dpi.get() + self.window.call('tk', 'scaling', newdpi / 72) + if self.toolbar and hasattr(self.toolbar, '_rescale'): + self.toolbar._rescale() + self.canvas._update_device_pixel_ratio() def resize(self, width, height): max_size = 1_400_000 # the measured max on xorg 1.20.8 was 1_409_023 @@ -454,6 +552,7 @@ def destroy(*args): Gcf.destroy(self) self.window.protocol("WM_DELETE_WINDOW", destroy) self.window.deiconify() + self.canvas._tkcanvas.focus_set() else: self.canvas.draw_idle() if mpl.rcParams['figure.raise_window']: @@ -462,24 +561,30 @@ def destroy(*args): self._shown = True def destroy(self, *args): - if self.canvas._idle_callback: - self.canvas._tkcanvas.after_cancel(self.canvas._idle_callback) + if self.canvas._idle_draw_id: + self.canvas._tkcanvas.after_cancel(self.canvas._idle_draw_id) if self.canvas._event_loop_id: self.canvas._tkcanvas.after_cancel(self.canvas._event_loop_id) + if self._window_dpi_cbname: + self._window_dpi.trace_remove('write', self._window_dpi_cbname) # NOTE: events need to be flushed before issuing destroy (GH #9956), - # however, self.window.update() can break user code. This is the - # safest way to achieve a complete draining of the event queue, - # but it may require users to update() on their own to execute the - # completion in obscure corner cases. + # however, self.window.update() can break user code. An async callback + # is the safest way to achieve a complete draining of the event queue, + # but it leaks if no tk event loop is running. Therefore we explicitly + # check for an event loop and choose our best guess. def delayed_destroy(): self.window.destroy() if self._owns_mainloop and not Gcf.get_num_fig_managers(): self.window.quit() - # "after idle after 0" avoids Tcl error/race (GH #19940) - self.window.after_idle(self.window.after, 0, delayed_destroy) + if cbook._get_running_interactive_framework() == "tk": + # "after idle after 0" avoids Tcl error/race (GH #19940) + self.window.after_idle(self.window.after, 0, delayed_destroy) + else: + self.window.update() + delayed_destroy() def get_window_title(self): return self.window.wm_title() @@ -493,24 +598,26 @@ def full_screen_toggle(self): class NavigationToolbar2Tk(NavigationToolbar2, tk.Frame): - """ - Attributes - ---------- - canvas : `FigureCanvas` - The figure canvas on which to operate. - win : tk.Window - The tk.Window which owns this toolbar. - pack_toolbar : bool, default: True - If True, add the toolbar to the parent's pack manager's packing list - during initialization with ``side='bottom'`` and ``fill='x'``. - If you want to use the toolbar with a different layout manager, use - ``pack_toolbar=False``. - """ - def __init__(self, canvas, window, *, pack_toolbar=True): - # Avoid using self.window (prefer self.canvas.get_tk_widget().master), - # so that Tool implementations can reuse the methods. - self.window = window + window = _api.deprecated("3.6", alternative="self.master")( + property(lambda self: self.master)) + def __init__(self, canvas, window=None, *, pack_toolbar=True): + """ + Parameters + ---------- + canvas : `FigureCanvas` + The figure canvas on which to operate. + window : tk.Window + The tk.Window which owns this toolbar. + pack_toolbar : bool, default: True + If True, add the toolbar to the parent's pack manager's packing + list during initialization with ``side="bottom"`` and ``fill="x"``. + If you want to use the toolbar with a different layout manager, use + ``pack_toolbar=False``. + """ + + if window is None: + window = canvas.get_tk_widget().master tk.Frame.__init__(self, master=window, borderwidth=2, width=int(canvas.figure.bbox.width), height=50) @@ -529,22 +636,53 @@ def __init__(self, canvas, window, *, pack_toolbar=True): if tooltip_text is not None: ToolTip.createToolTip(button, tooltip_text) + self._label_font = tkinter.font.Font(root=window, size=10) + # This filler item ensures the toolbar is always at least two text # lines high. Otherwise the canvas gets redrawn as the mouse hovers # over images because those use two-line messages which resize the # toolbar. - label = tk.Label(master=self, + label = tk.Label(master=self, font=self._label_font, text='\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') label.pack(side=tk.RIGHT) self.message = tk.StringVar(master=self) - self._message_label = tk.Label(master=self, textvariable=self.message) + self._message_label = tk.Label(master=self, font=self._label_font, + textvariable=self.message, + justify=tk.RIGHT) self._message_label.pack(side=tk.RIGHT) NavigationToolbar2.__init__(self, canvas) if pack_toolbar: self.pack(side=tk.BOTTOM, fill=tk.X) + def _rescale(self): + """ + Scale all children of the toolbar to current DPI setting. + + Before this is called, the Tk scaling setting will have been updated to + match the new DPI. Tk widgets do not update for changes to scaling, but + all measurements made after the change will match the new scaling. Thus + this function re-applies all the same sizes in points, which Tk will + scale correctly to pixels. + """ + for widget in self.winfo_children(): + if isinstance(widget, (tk.Button, tk.Checkbutton)): + if hasattr(widget, '_image_file'): + # Explicit class because ToolbarTk calls _rescale. + NavigationToolbar2Tk._set_image_for_button(self, widget) + else: + # Text-only button is handled by the font setting instead. + pass + elif isinstance(widget, tk.Frame): + widget.configure(height='18p') + widget.pack_configure(padx='3p') + elif isinstance(widget, tk.Label): + pass # Text is handled by the font setting instead. + else: + _log.warning('Unknown child class %s', widget.winfo_class) + self._label_font.configure(size=10) + def _update_buttons_checked(self): # sync button checkstates to match active mode for text, mode in [('Zoom', _Mode.ZOOM), ('Pan', _Mode.PAN)]: @@ -566,35 +704,117 @@ def set_message(self, s): self.message.set(s) def draw_rubberband(self, event, x0, y0, x1, y1): + # Block copied from remove_rubberband for backend_tools convenience. + if self.canvas._rubberband_rect_white: + self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_white) + if self.canvas._rubberband_rect_black: + self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_black) height = self.canvas.figure.bbox.height y0 = height - y0 y1 = height - y1 - if hasattr(self, "lastrect"): - self.canvas._tkcanvas.delete(self.lastrect) - self.lastrect = self.canvas._tkcanvas.create_rectangle(x0, y0, x1, y1) + self.canvas._rubberband_rect_black = ( + self.canvas._tkcanvas.create_rectangle( + x0, y0, x1, y1)) + self.canvas._rubberband_rect_white = ( + self.canvas._tkcanvas.create_rectangle( + x0, y0, x1, y1, outline='white', dash=(3, 3))) - def release_zoom(self, event): - super().release_zoom(event) - if hasattr(self, "lastrect"): - self.canvas._tkcanvas.delete(self.lastrect) - del self.lastrect + def remove_rubberband(self): + if self.canvas._rubberband_rect_white: + self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_white) + self.canvas._rubberband_rect_white = None + if self.canvas._rubberband_rect_black: + self.canvas._tkcanvas.delete(self.canvas._rubberband_rect_black) + self.canvas._rubberband_rect_black = None - def set_cursor(self, cursor): - window = self.canvas.get_tk_widget().master - try: - window.configure(cursor=cursord[cursor]) - except tkinter.TclError: - pass + lastrect = _api.deprecated("3.6")( + property(lambda self: self.canvas._rubberband_rect_black)) - def _Button(self, text, image_file, toggle, command): - if tk.TkVersion >= 8.6: - PhotoImage = tk.PhotoImage + def _set_image_for_button(self, button): + """ + Set the image for a button based on its pixel size. + + The pixel size is determined by the DPI scaling of the window. + """ + if button._image_file is None: + return + + # Allow _image_file to be relative to Matplotlib's "images" data + # directory. + path_regular = cbook._get_data_path('images', button._image_file) + path_large = path_regular.with_name( + path_regular.name.replace('.png', '_large.png')) + size = button.winfo_pixels('18p') + + # Nested functions because ToolbarTk calls _Button. + def _get_color(color_name): + # `winfo_rgb` returns an (r, g, b) tuple in the range 0-65535 + return button.winfo_rgb(button.cget(color_name)) + + def _is_dark(color): + if isinstance(color, str): + color = _get_color(color) + return max(color) < 65535 / 2 + + def _recolor_icon(image, color): + image_data = np.asarray(image).copy() + black_mask = (image_data[..., :3] == 0).all(axis=-1) + image_data[black_mask, :3] = color + return Image.fromarray(image_data, mode="RGBA") + + # Use the high-resolution (48x48 px) icon if it exists and is needed + with Image.open(path_large if (size > 24 and path_large.exists()) + else path_regular) as im: + # assure a RGBA image as foreground color is RGB + im = im.convert("RGBA") + image = ImageTk.PhotoImage(im.resize((size, size)), master=self) + button._ntimage = image + + # create a version of the icon with the button's text color + foreground = (255 / 65535) * np.array( + button.winfo_rgb(button.cget("foreground"))) + im_alt = _recolor_icon(im, foreground) + image_alt = ImageTk.PhotoImage( + im_alt.resize((size, size)), master=self) + button._ntimage_alt = image_alt + + if _is_dark("background"): + # For Checkbuttons, we need to set `image` and `selectimage` at + # the same time. Otherwise, when updating the `image` option + # (such as when changing DPI), if the old `selectimage` has + # just been overwritten, Tk will throw an error. + image_kwargs = {"image": image_alt} else: - from PIL.ImageTk import PhotoImage - image = (PhotoImage(master=self, file=image_file) - if image_file is not None else None) + image_kwargs = {"image": image} + # Checkbuttons may switch the background to `selectcolor` in the + # checked state, so check separately which image it needs to use in + # that state to still ensure enough contrast with the background. + if ( + isinstance(button, tk.Checkbutton) + and button.cget("selectcolor") != "" + ): + if self._windowingsystem != "x11": + selectcolor = "selectcolor" + else: + # On X11, selectcolor isn't used directly for indicator-less + # buttons. See `::tk::CheckEnter` in the Tk button.tcl source + # code for details. + r1, g1, b1 = _get_color("selectcolor") + r2, g2, b2 = _get_color("activebackground") + selectcolor = ((r1+r2)/2, (g1+g2)/2, (b1+b2)/2) + if _is_dark(selectcolor): + image_kwargs["selectimage"] = image_alt + else: + image_kwargs["selectimage"] = image + + button.configure(**image_kwargs, height='18p', width='18p') + + def _Button(self, text, image_file, toggle, command): if not toggle: - b = tk.Button(master=self, text=text, image=image, command=command) + b = tk.Button( + master=self, text=text, command=command, + relief="flat", overrelief="groove", borderwidth=1, + ) else: # There is a bug in tkinter included in some python 3.6 versions # that without this variable, produces a "visual" toggling of @@ -603,18 +823,24 @@ def _Button(self, text, image_file, toggle, command): # https://bugs.python.org/issue25684 var = tk.IntVar(master=self) b = tk.Checkbutton( - master=self, text=text, image=image, command=command, - indicatoron=False, variable=var) + master=self, text=text, command=command, indicatoron=False, + variable=var, offrelief="flat", overrelief="groove", + borderwidth=1 + ) b.var = var - b._ntimage = image + b._image_file = image_file + if image_file is not None: + # Explicit class because ToolbarTk calls _Button. + NavigationToolbar2Tk._set_image_for_button(self, b) + else: + b.configure(font=self._label_font) b.pack(side=tk.LEFT) return b def _Spacer(self): - # Buttons are 30px high. Make this 26px tall +2px padding to center it. - s = tk.Frame( - master=self, height=26, relief=tk.RIDGE, pady=2, bg="DarkGray") - s.pack(side=tk.LEFT, padx=5) + # Buttons are also 18pt high. + s = tk.Frame(master=self, height='18p', relief=tk.RIDGE, bg='DarkGray') + s.pack(side=tk.LEFT, padx='3p') return s def save_figure(self, *args): @@ -632,7 +858,7 @@ def save_figure(self, *args): # asksaveasfilename dialog when you choose various save types # from the dropdown. Passing in the empty string seems to # work - JDH! - #defaultextension = self.canvas.get_default_filetype() + # defaultextension = self.canvas.get_default_filetype() defaultextension = '' initialdir = os.path.expanduser(mpl.rcParams['savefig.directory']) initialfile = self.canvas.get_default_filename() @@ -696,7 +922,7 @@ def showtip(self, text): if self.tipwindow or not self.text: return x, y, _, _ = self.widget.bbox("insert") - x = x + self.widget.winfo_rootx() + 27 + x = x + self.widget.winfo_rootx() + self.widget.winfo_width() y = y + self.widget.winfo_rooty() self.tipwindow = tw = tk.Toplevel(self.widget) tw.wm_overrideredirect(1) @@ -719,48 +945,53 @@ def hidetip(self): tw.destroy() +@backend_tools._register_tool_class(FigureCanvasTk) class RubberbandTk(backend_tools.RubberbandBase): def draw_rubberband(self, x0, y0, x1, y1): - height = self.figure.canvas.figure.bbox.height - y0 = height - y0 - y1 = height - y1 - if hasattr(self, "lastrect"): - self.figure.canvas._tkcanvas.delete(self.lastrect) - self.lastrect = self.figure.canvas._tkcanvas.create_rectangle( - x0, y0, x1, y1) + NavigationToolbar2Tk.draw_rubberband( + self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) def remove_rubberband(self): - if hasattr(self, "lastrect"): - self.figure.canvas._tkcanvas.delete(self.lastrect) - del self.lastrect + NavigationToolbar2Tk.remove_rubberband( + self._make_classic_style_pseudo_toolbar()) - -class SetCursorTk(backend_tools.SetCursorBase): - def set_cursor(self, cursor): - NavigationToolbar2Tk.set_cursor( - self._make_classic_style_pseudo_toolbar(), cursor) + lastrect = _api.deprecated("3.6")( + property(lambda self: self.figure.canvas._rubberband_rect_black)) class ToolbarTk(ToolContainerBase, tk.Frame): - def __init__(self, toolmanager, window): + def __init__(self, toolmanager, window=None): ToolContainerBase.__init__(self, toolmanager) + if window is None: + window = self.toolmanager.canvas.get_tk_widget().master xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx height, width = 50, xmax - xmin tk.Frame.__init__(self, master=window, width=int(width), height=int(height), borderwidth=2) + self._label_font = tkinter.font.Font(size=10) self._message = tk.StringVar(master=self) - self._message_label = tk.Label(master=self, textvariable=self._message) + self._message_label = tk.Label(master=self, font=self._label_font, + textvariable=self._message) self._message_label.pack(side=tk.RIGHT) self._toolitems = {} self.pack(side=tk.TOP, fill=tk.X) self._groups = {} + def _rescale(self): + return NavigationToolbar2Tk._rescale(self) + def add_toolitem( self, name, group, position, image_file, description, toggle): frame = self._get_groupframe(group) - button = NavigationToolbar2Tk._Button(self, name, image_file, toggle, + buttons = frame.pack_slaves() + if position >= len(buttons) or position < 0: + before = None + else: + before = buttons[position] + button = NavigationToolbar2Tk._Button(frame, name, image_file, toggle, lambda: self._button_click(name)) + button.pack_configure(before=before) if description is not None: ToolTip.createToolTip(button, description) self._toolitems.setdefault(name, []) @@ -772,6 +1003,7 @@ def _get_groupframe(self, group): self._add_separator() frame = tk.Frame(master=self, borderwidth=0) frame.pack(side=tk.LEFT, fill=tk.Y) + frame._label_font = self._label_font self._groups[group] = frame return self._groups[group] @@ -799,59 +1031,20 @@ def set_message(self, s): self._message.set(s) -@_api.deprecated("3.3") -class StatusbarTk(StatusbarBase, tk.Frame): - def __init__(self, window, *args, **kwargs): - StatusbarBase.__init__(self, *args, **kwargs) - xmin, xmax = self.toolmanager.canvas.figure.bbox.intervalx - height, width = 50, xmax - xmin - tk.Frame.__init__(self, master=window, - width=int(width), height=int(height), - borderwidth=2) - self._message = tk.StringVar(master=self) - self._message_label = tk.Label(master=self, textvariable=self._message) - self._message_label.pack(side=tk.RIGHT) - self.pack(side=tk.TOP, fill=tk.X) - - def set_message(self, s): - self._message.set(s) - - +@backend_tools._register_tool_class(FigureCanvasTk) class SaveFigureTk(backend_tools.SaveFigureBase): def trigger(self, *args): NavigationToolbar2Tk.save_figure( self._make_classic_style_pseudo_toolbar()) +@backend_tools._register_tool_class(FigureCanvasTk) class ConfigureSubplotsTk(backend_tools.ConfigureSubplotsBase): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.window = None - def trigger(self, *args): - self.init_window() - self.window.lift() - - def init_window(self): - if self.window: - return - - toolfig = Figure(figsize=(6, 3)) - self.window = tk.Tk() - - canvas = type(self.canvas)(toolfig, master=self.window) - toolfig.subplots_adjust(top=0.9) - SubplotTool(self.figure, toolfig) - canvas.draw() - canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1) - self.window.protocol("WM_DELETE_WINDOW", self.destroy) - - def destroy(self, *args, **kwargs): - if self.window is not None: - self.window.destroy() - self.window = None + NavigationToolbar2Tk.configure_subplots(self) +@backend_tools._register_tool_class(FigureCanvasTk) class HelpTk(backend_tools.ToolHelpBase): def trigger(self, *args): dialog = SimpleDialog( @@ -859,60 +1052,14 @@ def trigger(self, *args): dialog.done = lambda num: dialog.frame.master.withdraw() -backend_tools.ToolSaveFigure = SaveFigureTk -backend_tools.ToolConfigureSubplots = ConfigureSubplotsTk -backend_tools.ToolSetCursor = SetCursorTk -backend_tools.ToolRubberband = RubberbandTk -backend_tools.ToolHelp = HelpTk -backend_tools.ToolCopyToClipboard = backend_tools.ToolCopyToClipboardBase Toolbar = ToolbarTk +FigureManagerTk._toolbar2_class = NavigationToolbar2Tk +FigureManagerTk._toolmanager_toolbar_class = ToolbarTk @_Backend.export class _BackendTk(_Backend): + backend_version = tk.TkVersion + FigureCanvas = FigureCanvasTk FigureManager = FigureManagerTk - - @classmethod - def new_figure_manager_given_figure(cls, num, figure): - """ - Create a new figure manager instance for the given figure. - """ - with _restore_foreground_window_at_end(): - if cbook._get_running_interactive_framework() is None: - cbook._setup_new_guiapp() - window = tk.Tk(className="matplotlib") - window.withdraw() - - # Put a Matplotlib icon on the window rather than the default tk - # icon. Tkinter doesn't allow colour icons on linux systems, but - # tk>=8.5 has a iconphoto command which we call directly. See - # http://mail.python.org/pipermail/tkinter-discuss/2006-November/000954.html - icon_fname = str(cbook._get_data_path( - 'images/matplotlib_128.ppm')) - icon_img = tk.PhotoImage(file=icon_fname, master=window) - try: - window.iconphoto(False, icon_img) - except Exception as exc: - # log the failure (due e.g. to Tk version), but carry on - _log.info('Could not load matplotlib icon: %s', exc) - - canvas = cls.FigureCanvas(figure, master=window) - manager = cls.FigureManager(canvas, num, window) - if mpl.is_interactive(): - manager.show() - canvas.draw_idle() - return manager - - @staticmethod - def mainloop(): - managers = Gcf.get_all_fig_managers() - if managers: - first_manager = managers[0] - manager_class = type(first_manager) - if manager_class._owns_mainloop: - return - manager_class._owns_mainloop = True - try: - first_manager.window.mainloop() - finally: - manager_class._owns_mainloop = False + mainloop = FigureManagerTk.start_main_loop diff --git a/lib/matplotlib/backends/backend_agg.py b/lib/matplotlib/backends/backend_agg.py index a9b0383f4bdd..0fe0fc40c00b 100644 --- a/lib/matplotlib/backends/backend_agg.py +++ b/lib/matplotlib/backends/backend_agg.py @@ -1,5 +1,5 @@ """ -An `Anti-Grain Geometry `_ (AGG) backend. +An `Anti-Grain Geometry`_ (AGG) backend. Features that are implemented: @@ -17,25 +17,21 @@ Still TODO: * integrate screen dpi w/ ppi and text + +.. _Anti-Grain Geometry: http://agg.sourceforge.net/antigrain.com """ -try: - import threading -except ImportError: - import dummy_threading as threading from contextlib import nullcontext from math import radians, cos, sin +import threading import numpy as np -from PIL import Image import matplotlib as mpl from matplotlib import _api, cbook -from matplotlib import colors as mcolors from matplotlib.backend_bases import ( - _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - RendererBase) -from matplotlib.font_manager import findfont, get_font + _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) +from matplotlib.font_manager import fontManager as _fontManager, get_font from matplotlib.ft2font import (LOAD_FORCE_AUTOHINT, LOAD_NO_HINTING, LOAD_DEFAULT, LOAD_NO_AUTOHINT) from matplotlib.mathtext import MathTextParser @@ -44,9 +40,6 @@ from matplotlib.backends._backend_agg import RendererAgg as _RendererAgg -backend_version = 'v2.2' - - def get_hinting_flag(): mapping = { 'default': LOAD_DEFAULT, @@ -109,27 +102,10 @@ def _update_methods(self): self.draw_gouraud_triangles = self._renderer.draw_gouraud_triangles self.draw_image = self._renderer.draw_image self.draw_markers = self._renderer.draw_markers - # This is its own method for the duration of the deprecation of - # offset_position = "data". - # self.draw_path_collection = self._renderer.draw_path_collection + self.draw_path_collection = self._renderer.draw_path_collection self.draw_quad_mesh = self._renderer.draw_quad_mesh self.copy_from_bbox = self._renderer.copy_from_bbox - @_api.deprecated("3.4") - def get_content_extents(self): - orig_img = np.asarray(self.buffer_rgba()) - slice_y, slice_x = cbook._get_nonzero_slices(orig_img[..., 3]) - return (slice_x.start, slice_y.start, - slice_x.stop - slice_x.start, slice_y.stop - slice_y.start) - - @_api.deprecated("3.4") - def tostring_rgba_minimized(self): - extents = self.get_content_extents() - bbox = [[extents[0], self.height - (extents[1] + extents[3])], - [extents[0] + extents[2], self.height - extents[1]]] - region = self.copy_from_bbox(bbox) - return np.array(region), extents - def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited nmax = mpl.rcParams['agg.path.chunksize'] # here at least for testing @@ -150,35 +126,69 @@ def draw_path(self, gc, path, transform, rgbFace=None): c = c[ii0:ii1] c[0] = Path.MOVETO # move to end of last chunk p = Path(v, c) + p.simplify_threshold = path.simplify_threshold try: self._renderer.draw_path(gc, p, transform, rgbFace) - except OverflowError as err: - raise OverflowError( - "Exceeded cell block limit (set 'agg.path.chunksize' " - "rcparam)") from err + except OverflowError: + msg = ( + "Exceeded cell block limit in Agg.\n\n" + "Please reduce the value of " + f"rcParams['agg.path.chunksize'] (currently {nmax}) " + "or increase the path simplification threshold" + "(rcParams['path.simplify_threshold'] = " + f"{mpl.rcParams['path.simplify_threshold']:.2f} by " + "default and path.simplify_threshold = " + f"{path.simplify_threshold:.2f} on the input)." + ) + raise OverflowError(msg) from None else: try: self._renderer.draw_path(gc, path, transform, rgbFace) - except OverflowError as err: - raise OverflowError("Exceeded cell block limit (set " - "'agg.path.chunksize' rcparam)") from err - - def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offsetTrans, facecolors, edgecolors, - linewidths, linestyles, antialiaseds, urls, - offset_position): - if offset_position == "data": - _api.warn_deprecated( - "3.3", message="Support for offset_position='data' is " - "deprecated since %(since)s and will be removed %(removal)s.") - return self._renderer.draw_path_collection( - gc, master_transform, paths, all_transforms, offsets, offsetTrans, - facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, - offset_position) + except OverflowError: + cant_chunk = '' + if rgbFace is not None: + cant_chunk += "- can not split filled path\n" + if gc.get_hatch() is not None: + cant_chunk += "- can not split hatched path\n" + if not path.should_simplify: + cant_chunk += "- path.should_simplify is False\n" + if len(cant_chunk): + msg = ( + "Exceeded cell block limit in Agg, however for the " + "following reasons:\n\n" + f"{cant_chunk}\n" + "we can not automatically split up this path to draw." + "\n\nPlease manually simplify your path." + ) + + else: + inc_threshold = ( + "or increase the path simplification threshold" + "(rcParams['path.simplify_threshold'] = " + f"{mpl.rcParams['path.simplify_threshold']} " + "by default and path.simplify_threshold " + f"= {path.simplify_threshold} " + "on the input)." + ) + if nmax > 100: + msg = ( + "Exceeded cell block limit in Agg. Please reduce " + "the value of rcParams['agg.path.chunksize'] " + f"(currently {nmax}) {inc_threshold}" + ) + else: + msg = ( + "Exceeded cell block limit in Agg. Please set " + "the value of rcParams['agg.path.chunksize'], " + f"(currently {nmax}) to be greater than 100 " + + inc_threshold + ) + + raise OverflowError(msg) from None def draw_mathtext(self, gc, x, y, s, prop, angle): """Draw mathtext using :mod:`matplotlib.mathtext`.""" - ox, oy, width, height, descent, font_image, used_characters = \ + ox, oy, width, height, descent, font_image = \ self.mathtext_parser.parse(s, self.dpi, prop) xd = descent * sin(radians(angle)) @@ -189,18 +199,12 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited - if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) - - flags = get_hinting_flag() - font = self._get_agg_font(prop) - - if font is None: - return None + font = self._prepare_font(prop) # We pass '0' for angle here, since it will be rotated (in raster # space) in the following call to draw_text_image). - font.set_text(s, 0, flags=flags) + font.set_text(s, 0, flags=get_hinting_flag()) font.draw_glyphs_to_bitmap( antialiased=mpl.rcParams['text.antialiased']) d = font.get_descent() / 64.0 @@ -217,27 +221,17 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited - if ismath in ["TeX", "TeX!"]: - if ismath == "TeX!": - _api.warn_deprecated( - "3.3", message="Support for ismath='TeX!' is deprecated " - "since %(since)s and will be removed %(removal)s; use " - "ismath='TeX' instead.") - # todo: handle props - texmanager = self.get_texmanager() - fontsize = prop.get_size_in_points() - w, h, d = texmanager.get_text_width_height_descent( - s, fontsize, renderer=self) - return w, h, d + _api.check_in_list(["TeX", True, False], ismath=ismath) + if ismath == "TeX": + return super().get_text_width_height_descent(s, prop, ismath) if ismath: - ox, oy, width, height, descent, fonts, used_characters = \ + ox, oy, width, height, descent, font_image = \ self.mathtext_parser.parse(s, self.dpi, prop) return width, height, descent - flags = get_hinting_flag() - font = self._get_agg_font(prop) - font.set_text(s, 0.0, flags=flags) + font = self._prepare_font(prop) + font.set_text(s, 0.0, flags=get_hinting_flag()) w, h = font.get_width_height() # width and height of unrotated string d = font.get_descent() w /= 64.0 # convert from subpixels @@ -266,17 +260,14 @@ def get_canvas_width_height(self): # docstring inherited return self.width, self.height - def _get_agg_font(self, prop): + def _prepare_font(self, font_prop): """ - Get the font for text instance t, caching for efficiency + Get the `.FT2Font` for *font_prop*, clear its buffer, and set its size. """ - fname = findfont(prop) - font = get_font(fname) - + font = get_font(_fontManager._find_fonts_by_props(font_prop)) font.clear() - size = prop.get_size_in_points() + size = font_prop.get_size_in_points() font.set_size(size, self.dpi) - return font def points_to_pixels(self, points): @@ -344,7 +335,7 @@ def restore_region(self, region, bbox=None, xy=None): def start_filter(self): """ - Start filtering. It simply create a new canvas (the old one is saved). + Start filtering. It simply creates a new canvas (the old one is saved). """ self._filter_renderers.append(self._renderer) self._renderer = _RendererAgg(int(self.width), int(self.height), @@ -353,7 +344,7 @@ def start_filter(self): def stop_filter(self, post_processing): """ - Save the plot in the current canvas as a image and apply + Save the plot in the current canvas as an image and apply the *post_processing* function. def post_processing(image, dpi): @@ -388,6 +379,8 @@ def post_processing(image, dpi): class FigureCanvasAgg(FigureCanvasBase): # docstring inherited + _lastKey = None # Overwritten per-instance on the first draw. + def copy_from_bbox(self, bbox): renderer = self.get_renderer() return renderer.copy_from_bbox(bbox) @@ -398,7 +391,8 @@ def restore_region(self, region, bbox=None, xy=None): def draw(self): # docstring inherited - self.renderer = self.get_renderer(cleared=True) + self.renderer = self.get_renderer() + self.renderer.clear() # Acquire a lock on the shared font cache. with RendererAgg.lock, \ (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar @@ -408,11 +402,11 @@ def draw(self): # don't forget to call the superclass. super().draw() + @_api.delete_parameter("3.6", "cleared", alternative="renderer.clear()") def get_renderer(self, cleared=False): w, h = self.figure.bbox.size key = w, h, self.figure.dpi - reuse_renderer = (hasattr(self, "renderer") - and getattr(self, "_lastKey", None) == key) + reuse_renderer = (self._lastKey == key) if not reuse_renderer: self.renderer = RendererAgg(w, h, self.figure.dpi) self._lastKey = key @@ -447,8 +441,7 @@ def buffer_rgba(self): """ return self.renderer.buffer_rgba() - @_check_savefig_extra_args - def print_raw(self, filename_or_obj, *args): + def print_raw(self, filename_or_obj): FigureCanvasAgg.draw(self) renderer = self.get_renderer() with cbook.open_file_cm(filename_or_obj, "wb") as fh: @@ -456,9 +449,17 @@ def print_raw(self, filename_or_obj, *args): print_rgba = print_raw - @_check_savefig_extra_args - def print_png(self, filename_or_obj, *args, - metadata=None, pil_kwargs=None): + def _print_pil(self, filename_or_obj, fmt, pil_kwargs, metadata=None): + """ + Draw the canvas, then save it using `.image.imsave` (to which + *pil_kwargs* and *metadata* are forwarded). + """ + FigureCanvasAgg.draw(self) + mpl.image.imsave( + filename_or_obj, self.buffer_rgba(), format=fmt, origin="upper", + dpi=self.figure.dpi, metadata=metadata, pil_kwargs=pil_kwargs) + + def print_png(self, filename_or_obj, *, metadata=None, pil_kwargs=None): """ Write the figure to a PNG file. @@ -505,10 +506,7 @@ def print_png(self, filename_or_obj, *args, If the 'pnginfo' key is present, it completely overrides *metadata*, including the default 'Software' key. """ - FigureCanvasAgg.draw(self) - mpl.image.imsave( - filename_or_obj, self.buffer_rgba(), format="png", origin="upper", - dpi=self.figure.dpi, metadata=metadata, pil_kwargs=pil_kwargs) + self._print_pil(filename_or_obj, "png", pil_kwargs, metadata) def print_to_buffer(self): FigureCanvasAgg.draw(self) @@ -520,85 +518,39 @@ def print_to_buffer(self): # print_figure(), and the latter ensures that `self.figure.dpi` already # matches the dpi kwarg (if any). - @_check_savefig_extra_args( - extra_kwargs=["quality", "optimize", "progressive"]) - @_api.delete_parameter("3.3", "quality", - alternative="pil_kwargs={'quality': ...}") - @_api.delete_parameter("3.3", "optimize", - alternative="pil_kwargs={'optimize': ...}") - @_api.delete_parameter("3.3", "progressive", - alternative="pil_kwargs={'progressive': ...}") - def print_jpg(self, filename_or_obj, *args, pil_kwargs=None, **kwargs): + def print_jpg(self, filename_or_obj, *, pil_kwargs=None): + # savefig() has already applied savefig.facecolor; we now set it to + # white to make imsave() blend semi-transparent figures against an + # assumed white background. + with mpl.rc_context({"savefig.facecolor": "white"}): + self._print_pil(filename_or_obj, "jpeg", pil_kwargs) + + print_jpeg = print_jpg + + def print_tif(self, filename_or_obj, *, pil_kwargs=None): + self._print_pil(filename_or_obj, "tiff", pil_kwargs) + + print_tiff = print_tif + + def print_webp(self, filename_or_obj, *, pil_kwargs=None): + self._print_pil(filename_or_obj, "webp", pil_kwargs) + + print_jpg.__doc__, print_tif.__doc__, print_webp.__doc__ = map( """ - Write the figure to a JPEG file. + Write the figure to a {} file. Parameters ---------- filename_or_obj : str or path-like or file-like The file to write to. - - Other Parameters - ---------------- - quality : int, default: :rc:`savefig.jpeg_quality` - The image quality, on a scale from 1 (worst) to 95 (best). - Values above 95 should be avoided; 100 disables portions of - the JPEG compression algorithm, and results in large files - with hardly any gain in image quality. This parameter is - deprecated. - optimize : bool, default: False - Whether the encoder should make an extra pass over the image - in order to select optimal encoder settings. This parameter is - deprecated. - progressive : bool, default: False - Whether the image should be stored as a progressive JPEG file. - This parameter is deprecated. pil_kwargs : dict, optional Additional keyword arguments that are passed to - `PIL.Image.Image.save` when saving the figure. These take - precedence over *quality*, *optimize* and *progressive*. - """ - # Remove transparency by alpha-blending on an assumed white background. - r, g, b, a = mcolors.to_rgba(self.figure.get_facecolor()) - try: - self.figure.set_facecolor(a * np.array([r, g, b]) + 1 - a) - FigureCanvasAgg.draw(self) - finally: - self.figure.set_facecolor((r, g, b, a)) - if pil_kwargs is None: - pil_kwargs = {} - for k in ["quality", "optimize", "progressive"]: - if k in kwargs: - pil_kwargs.setdefault(k, kwargs.pop(k)) - if "quality" not in pil_kwargs: - quality = pil_kwargs["quality"] = \ - dict.__getitem__(mpl.rcParams, "savefig.jpeg_quality") - if quality not in [0, 75, 95]: # default qualities. - _api.warn_deprecated( - "3.3", name="savefig.jpeg_quality", obj_type="rcParam", - addendum="Set the quality using " - "`pil_kwargs={'quality': ...}`; the future default " - "quality will be 75, matching the default of Pillow and " - "libjpeg.") - pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi)) - # Drop alpha channel now. - return (Image.fromarray(np.asarray(self.buffer_rgba())[..., :3]) - .save(filename_or_obj, format='jpeg', **pil_kwargs)) - - print_jpeg = print_jpg - - @_check_savefig_extra_args - def print_tif(self, filename_or_obj, *, pil_kwargs=None): - FigureCanvasAgg.draw(self) - if pil_kwargs is None: - pil_kwargs = {} - pil_kwargs.setdefault("dpi", (self.figure.dpi, self.figure.dpi)) - return (Image.fromarray(np.asarray(self.buffer_rgba())) - .save(filename_or_obj, format='tiff', **pil_kwargs)) - - print_tiff = print_tif + `PIL.Image.Image.save` when saving the figure. + """.format, ["JPEG", "TIFF", "WebP"]) @_Backend.export class _BackendAgg(_Backend): + backend_version = 'v2.2' FigureCanvas = FigureCanvasAgg FigureManager = FigureManagerBase diff --git a/lib/matplotlib/backends/backend_cairo.py b/lib/matplotlib/backends/backend_cairo.py index 9b72c0408b52..547a2ae9271f 100644 --- a/lib/matplotlib/backends/backend_cairo.py +++ b/lib/matplotlib/backends/backend_cairo.py @@ -1,11 +1,12 @@ """ -A Cairo backend for matplotlib +A Cairo backend for Matplotlib ============================== :Author: Steve Chaplin and others This backend depends on cairocffi or pycairo. """ +import functools import gzip import math @@ -13,47 +14,26 @@ try: import cairo - if cairo.version_info < (1, 11, 0): - # Introduced create_for_data for Py3. + if cairo.version_info < (1, 14, 0): # Introduced set_device_scale. raise ImportError except ImportError: try: import cairocffi as cairo except ImportError as err: raise ImportError( - "cairo backend requires that pycairo>=1.11.0 or cairocffi " + "cairo backend requires that pycairo>=1.14.0 or cairocffi " "is installed") from err import matplotlib as mpl from .. import _api, cbook, font_manager from matplotlib.backend_bases import ( - _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - GraphicsContextBase, RendererBase) + _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, + RendererBase) from matplotlib.font_manager import ttfFontProperty -from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib.transforms import Affine2D -backend_version = cairo.version - - -if cairo.__name__ == "cairocffi": - # Convert a pycairo context to a cairocffi one. - def _to_context(ctx): - if not isinstance(ctx, cairo.Context): - ctx = cairo.Context._from_pointer( - cairo.ffi.cast( - 'cairo_t **', - id(ctx) + object.__basicsize__)[0], - incref=True) - return ctx -else: - # Pass-through a pycairo context. - def _to_context(ctx): - return ctx - - def _append_path(ctx, path, transform, clip=None): for points, code in path.iter_segments( transform, remove_nans=True, clip=clip): @@ -92,55 +72,39 @@ def attr(field): return name, slant, weight -# Mappings used for deprecated properties in RendererCairo, see below. -_f_weights = { - 100: cairo.FONT_WEIGHT_NORMAL, - 200: cairo.FONT_WEIGHT_NORMAL, - 300: cairo.FONT_WEIGHT_NORMAL, - 400: cairo.FONT_WEIGHT_NORMAL, - 500: cairo.FONT_WEIGHT_NORMAL, - 600: cairo.FONT_WEIGHT_BOLD, - 700: cairo.FONT_WEIGHT_BOLD, - 800: cairo.FONT_WEIGHT_BOLD, - 900: cairo.FONT_WEIGHT_BOLD, - 'ultralight': cairo.FONT_WEIGHT_NORMAL, - 'light': cairo.FONT_WEIGHT_NORMAL, - 'normal': cairo.FONT_WEIGHT_NORMAL, - 'medium': cairo.FONT_WEIGHT_NORMAL, - 'regular': cairo.FONT_WEIGHT_NORMAL, - 'semibold': cairo.FONT_WEIGHT_BOLD, - 'bold': cairo.FONT_WEIGHT_BOLD, - 'heavy': cairo.FONT_WEIGHT_BOLD, - 'ultrabold': cairo.FONT_WEIGHT_BOLD, - 'black': cairo.FONT_WEIGHT_BOLD, -} -_f_angles = { - 'italic': cairo.FONT_SLANT_ITALIC, - 'normal': cairo.FONT_SLANT_NORMAL, - 'oblique': cairo.FONT_SLANT_OBLIQUE, -} - - class RendererCairo(RendererBase): - fontweights = _api.deprecated("3.3")(property(lambda self: {*_f_weights})) - fontangles = _api.deprecated("3.3")(property(lambda self: {*_f_angles})) - mathtext_parser = _api.deprecated("3.4")( - property(lambda self: MathTextParser('Cairo'))) - def __init__(self, dpi): self.dpi = dpi self.gc = GraphicsContextCairo(renderer=self) + self.width = None + self.height = None self.text_ctx = cairo.Context( cairo.ImageSurface(cairo.FORMAT_ARGB32, 1, 1)) super().__init__() + def set_context(self, ctx): + surface = ctx.get_target() + if hasattr(surface, "get_width") and hasattr(surface, "get_height"): + size = surface.get_width(), surface.get_height() + elif hasattr(surface, "get_extents"): # GTK4 RecordingSurface. + ext = surface.get_extents() + size = ext.width, ext.height + else: # vector surfaces. + ctx.save() + ctx.reset_clip() + rect, *rest = ctx.copy_clip_rectangle_list() + if rest: + raise TypeError("Cannot infer surface size") + size = rect.width, rect.height + ctx.restore() + self.gc.ctx = ctx + self.width, self.height = size + + @_api.deprecated("3.6", alternative="set_context") def set_ctx_from_surface(self, surface): self.gc.ctx = cairo.Context(surface) - # Although it may appear natural to automatically call - # `self.set_width_height(surface.get_width(), surface.get_height())` - # here (instead of having the caller do so separately), this would fail - # for PDF/PS/SVG surfaces, which have no way to report their extents. + @_api.deprecated("3.6") def set_width_height(self, width, height): self.width = width self.height = height @@ -246,7 +210,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): ctx.save() ctx.select_font_face(*_cairo_font_args_from_font_prop(prop)) - ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72) + ctx.set_font_size(self.points_to_pixels(prop.get_size_in_points())) opts = cairo.FontOptions() opts.set_antialias( cairo.ANTIALIAS_DEFAULT if mpl.rcParams["text.antialiased"] @@ -272,7 +236,7 @@ def _draw_mathtext(self, gc, x, y, s, prop, angle): ctx.move_to(ox, -oy) ctx.select_font_face( *_cairo_font_args_from_font_prop(ttfFontProperty(font))) - ctx.set_font_size(fontsize * self.dpi / 72) + ctx.set_font_size(self.points_to_pixels(fontsize)) ctx.show_text(chr(idx)) for ox, oy, w, h in rects: @@ -304,9 +268,7 @@ def get_text_width_height_descent(self, s, prop, ismath): # save/restore prevents the problem ctx.save() ctx.select_font_face(*_cairo_font_args_from_font_prop(prop)) - # Cairo (says it) uses 1/96 inch user space units, ref: cairo_gstate.c - # but if /96.0 is used the font is too small - ctx.set_font_size(prop.get_size_in_points() * self.dpi / 72) + ctx.set_font_size(self.points_to_pixels(prop.get_size_in_points())) y_bearing, w, h = ctx.text_extents(s)[1:4] ctx.restore() @@ -417,6 +379,18 @@ def __init__(self, slices, data): class FigureCanvasCairo(FigureCanvasBase): + @property + def _renderer(self): + # In theory, _renderer should be set in __init__, but GUI canvas + # subclasses (FigureCanvasFooCairo) don't always interact well with + # multiple inheritance (FigureCanvasFoo inits but doesn't super-init + # FigureCanvasCairo), so initialize it in the getter instead. + if not hasattr(self, "_cached_renderer"): + self._cached_renderer = RendererCairo(self.figure.dpi) + return self._cached_renderer + + def get_renderer(self): + return self._renderer def copy_from_bbox(self, bbox): surface = self._renderer.gc.ctx.get_target() @@ -451,11 +425,9 @@ def restore_region(self, region): surface.mark_dirty_rectangle( slx.start, sly.start, slx.stop - slx.start, sly.stop - sly.start) - @_check_savefig_extra_args def print_png(self, fobj): self._get_printed_image_surface().write_to_png(fobj) - @_check_savefig_extra_args def print_rgba(self, fobj): width, height = self.get_width_height() buf = self._get_printed_image_surface().get_data() @@ -465,28 +437,14 @@ def print_rgba(self, fobj): print_raw = print_rgba def _get_printed_image_surface(self): + self._renderer.dpi = self.figure.dpi width, height = self.get_width_height() - renderer = RendererCairo(self.figure.dpi) - renderer.set_width_height(width, height) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) - renderer.set_ctx_from_surface(surface) - self.figure.draw(renderer) + self._renderer.set_context(cairo.Context(surface)) + self.figure.draw(self._renderer) return surface - def print_pdf(self, fobj, *args, **kwargs): - return self._save(fobj, 'pdf', *args, **kwargs) - - def print_ps(self, fobj, *args, **kwargs): - return self._save(fobj, 'ps', *args, **kwargs) - - def print_svg(self, fobj, *args, **kwargs): - return self._save(fobj, 'svg', *args, **kwargs) - - def print_svgz(self, fobj, *args, **kwargs): - return self._save(fobj, 'svgz', *args, **kwargs) - - @_check_savefig_extra_args - def _save(self, fo, fmt, *, orientation='portrait'): + def _save(self, fmt, fobj, *, orientation='portrait'): # save PDF/PS/SVG dpi = 72 @@ -502,45 +460,62 @@ def _save(self, fo, fmt, *, orientation='portrait'): if not hasattr(cairo, 'PSSurface'): raise RuntimeError('cairo has not been compiled with PS ' 'support enabled') - surface = cairo.PSSurface(fo, width_in_points, height_in_points) + surface = cairo.PSSurface(fobj, width_in_points, height_in_points) elif fmt == 'pdf': if not hasattr(cairo, 'PDFSurface'): raise RuntimeError('cairo has not been compiled with PDF ' 'support enabled') - surface = cairo.PDFSurface(fo, width_in_points, height_in_points) + surface = cairo.PDFSurface(fobj, width_in_points, height_in_points) elif fmt in ('svg', 'svgz'): if not hasattr(cairo, 'SVGSurface'): raise RuntimeError('cairo has not been compiled with SVG ' 'support enabled') if fmt == 'svgz': - if isinstance(fo, str): - fo = gzip.GzipFile(fo, 'wb') + if isinstance(fobj, str): + fobj = gzip.GzipFile(fobj, 'wb') else: - fo = gzip.GzipFile(None, 'wb', fileobj=fo) - surface = cairo.SVGSurface(fo, width_in_points, height_in_points) + fobj = gzip.GzipFile(None, 'wb', fileobj=fobj) + surface = cairo.SVGSurface(fobj, width_in_points, height_in_points) else: raise ValueError("Unknown format: {!r}".format(fmt)) - # surface.set_dpi() can be used - renderer = RendererCairo(self.figure.dpi) - renderer.set_width_height(width_in_points, height_in_points) - renderer.set_ctx_from_surface(surface) - ctx = renderer.gc.ctx + self._renderer.dpi = self.figure.dpi + self._renderer.set_context(cairo.Context(surface)) + ctx = self._renderer.gc.ctx if orientation == 'landscape': ctx.rotate(np.pi / 2) ctx.translate(0, -height_in_points) # Perhaps add an '%%Orientation: Landscape' comment? - self.figure.draw(renderer) + self.figure.draw(self._renderer) ctx.show_page() surface.finish() if fmt == 'svgz': - fo.close() + fobj.close() + + print_pdf = functools.partialmethod(_save, "pdf") + print_ps = functools.partialmethod(_save, "ps") + print_svg = functools.partialmethod(_save, "svg") + print_svgz = functools.partialmethod(_save, "svgz") + + +@_api.deprecated("3.6") +class _RendererGTKCairo(RendererCairo): + def set_context(self, ctx): + if (cairo.__name__ == "cairocffi" + and not isinstance(ctx, cairo.Context)): + ctx = cairo.Context._from_pointer( + cairo.ffi.cast( + 'cairo_t **', + id(ctx) + object.__basicsize__)[0], + incref=True) + self.gc.ctx = ctx @_Backend.export class _BackendCairo(_Backend): + backend_version = cairo.version FigureCanvas = FigureCanvasCairo FigureManager = FigureManagerBase diff --git a/lib/matplotlib/backends/backend_gtk3.py b/lib/matplotlib/backends/backend_gtk3.py index 1dbf5d93a929..dbb0982ee752 100644 --- a/lib/matplotlib/backends/backend_gtk3.py +++ b/lib/matplotlib/backends/backend_gtk3.py @@ -6,12 +6,9 @@ import matplotlib as mpl from matplotlib import _api, backend_tools, cbook -from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( - _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - StatusbarBase, TimerBase, ToolContainerBase, cursors) -from matplotlib.figure import Figure -from matplotlib.widgets import SubplotTool + ToolContainerBase, CloseEvent, KeyEvent, LocationEvent, MouseEvent, + ResizeEvent) try: import gi @@ -28,67 +25,36 @@ raise ImportError from e from gi.repository import Gio, GLib, GObject, Gtk, Gdk +from . import _backend_gtk +from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611 + _BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK, + TimerGTK as TimerGTK3, +) _log = logging.getLogger(__name__) -backend_version = "%s.%s.%s" % ( - Gtk.get_major_version(), Gtk.get_micro_version(), Gtk.get_minor_version()) -try: - _display = Gdk.Display.get_default() - cursord = { - cursors.MOVE: Gdk.Cursor.new_from_name(_display, "move"), - cursors.HAND: Gdk.Cursor.new_from_name(_display, "pointer"), - cursors.POINTER: Gdk.Cursor.new_from_name(_display, "default"), - cursors.SELECT_REGION: Gdk.Cursor.new_from_name(_display, "crosshair"), - cursors.WAIT: Gdk.Cursor.new_from_name(_display, "wait"), - } -except TypeError as exc: - # Happens when running headless. Convert to ImportError to cooperate with - # backend switching. - raise ImportError(exc) from exc - - -class TimerGTK3(TimerBase): - """Subclass of `.TimerBase` using GTK3 timer events.""" - - def __init__(self, *args, **kwargs): - self._timer = None - super().__init__(*args, **kwargs) - - def _timer_start(self): - # Need to stop it, otherwise we potentially leak a timer id that will - # never be stopped. - self._timer_stop() - self._timer = GLib.timeout_add(self._interval, self._on_timer) - - def _timer_stop(self): - if self._timer is not None: - GLib.source_remove(self._timer) - self._timer = None - - def _timer_set_interval(self): - # Only stop and restart it if the timer has already been started - if self._timer is not None: - self._timer_stop() - self._timer_start() - - def _on_timer(self): - super()._on_timer() - - # Gtk timeout_add() requires that the callback returns True if it - # is to be called again. - if self.callbacks and not self._single: - return True - else: - self._timer = None - return False +@_api.caching_module_getattr # module-level deprecations +class __getattr__: + icon_filename = _api.deprecated("3.6", obj_type="")(property( + lambda self: + "matplotlib.png" if sys.platform == "win32" else "matplotlib.svg")) + window_icon = _api.deprecated("3.6", obj_type="")(property( + lambda self: + str(cbook._get_data_path("images", __getattr__("icon_filename"))))) + + +@functools.lru_cache() +def _mpl_to_gtk_cursor(mpl_cursor): + return Gdk.Cursor.new_from_name( + Gdk.Display.get_default(), + _backend_gtk.mpl_to_gtk_cursor_name(mpl_cursor)) -class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase): +class FigureCanvasGTK3(_FigureCanvasGTK, Gtk.DrawingArea): required_interactive_framework = "gtk3" - _timer_cls = TimerGTK3 + manager_class = _api.classproperty(lambda cls: FigureManagerGTK3) # Setting this as a static constant prevents # this resulting expression from leaking event_mask = (Gdk.EventMask.BUTTON_PRESS_MASK @@ -99,28 +65,27 @@ class FigureCanvasGTK3(Gtk.DrawingArea, FigureCanvasBase): | Gdk.EventMask.ENTER_NOTIFY_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.POINTER_MOTION_MASK - | Gdk.EventMask.POINTER_MOTION_HINT_MASK | Gdk.EventMask.SCROLL_MASK) def __init__(self, figure=None): - FigureCanvasBase.__init__(self, figure) - GObject.GObject.__init__(self) + super().__init__(figure=figure) self._idle_draw_id = 0 - self._lastCursor = None self._rubberband_rect = None self.connect('scroll_event', self.scroll_event) self.connect('button_press_event', self.button_press_event) self.connect('button_release_event', self.button_release_event) self.connect('configure_event', self.configure_event) + self.connect('screen-changed', self._update_device_pixel_ratio) + self.connect('notify::scale-factor', self._update_device_pixel_ratio) self.connect('draw', self.on_draw_event) self.connect('draw', self._post_draw) self.connect('key_press_event', self.key_press_event) self.connect('key_release_event', self.key_release_event) self.connect('motion_notify_event', self.motion_notify_event) - self.connect('leave_notify_event', self.leave_notify_event) self.connect('enter_notify_event', self.enter_notify_event) + self.connect('leave_notify_event', self.leave_notify_event) self.connect('size_allocate', self.size_allocate) self.set_events(self.__class__.event_mask) @@ -133,106 +98,136 @@ def __init__(self, figure=None): style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) style_ctx.add_class("matplotlib-canvas") - renderer_init = _api.deprecate_method_override( - __class__._renderer_init, self, allow_empty=True, since="3.3", - addendum="Please initialize the renderer, if needed, in the " - "subclass' __init__; a fully empty _renderer_init implementation " - "may be kept for compatibility with earlier versions of " - "Matplotlib.") - if renderer_init: - renderer_init() - - @_api.deprecated("3.3", alternative="__init__") - def _renderer_init(self): - pass - def destroy(self): - #Gtk.DrawingArea.destroy(self) - self.close_event() + CloseEvent("close_event", self)._process() + + def set_cursor(self, cursor): + # docstring inherited + window = self.get_property("window") + if window is not None: + window.set_cursor(_mpl_to_gtk_cursor(cursor)) + context = GLib.MainContext.default() + context.iteration(True) + + def _mpl_coords(self, event=None): + """ + Convert the position of a GTK event, or of the current cursor position + if *event* is None, to Matplotlib coordinates. + + GTK use logical pixels, but the figure is scaled to physical 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. + """ + if event is None: + window = self.get_window() + t, x, y, state = window.get_device_position( + window.get_display().get_device_manager().get_client_pointer()) + else: + x, y = event.x, event.y + x = x * self.device_pixel_ratio + # flip y so y=0 is bottom of canvas + y = self.figure.bbox.height - y * self.device_pixel_ratio + return x, y def scroll_event(self, widget, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - event.y step = 1 if event.direction == Gdk.ScrollDirection.UP else -1 - FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) + MouseEvent("scroll_event", self, + *self._mpl_coords(event), step=step, + modifiers=self._mpl_modifiers(event.state), + guiEvent=event)._process() return False # finish event propagation? def button_press_event(self, widget, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - event.y - FigureCanvasBase.button_press_event( - self, x, y, event.button, guiEvent=event) + MouseEvent("button_press_event", self, + *self._mpl_coords(event), event.button, + modifiers=self._mpl_modifiers(event.state), + guiEvent=event)._process() return False # finish event propagation? def button_release_event(self, widget, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - event.y - FigureCanvasBase.button_release_event( - self, x, y, event.button, guiEvent=event) + MouseEvent("button_release_event", self, + *self._mpl_coords(event), event.button, + modifiers=self._mpl_modifiers(event.state), + guiEvent=event)._process() return False # finish event propagation? def key_press_event(self, widget, event): - key = self._get_key(event) - FigureCanvasBase.key_press_event(self, key, guiEvent=event) + KeyEvent("key_press_event", self, + self._get_key(event), *self._mpl_coords(), + guiEvent=event)._process() return True # stop event propagation def key_release_event(self, widget, event): - key = self._get_key(event) - FigureCanvasBase.key_release_event(self, key, guiEvent=event) + KeyEvent("key_release_event", self, + self._get_key(event), *self._mpl_coords(), + guiEvent=event)._process() return True # stop event propagation def motion_notify_event(self, widget, event): - if event.is_hint: - t, x, y, state = event.window.get_device_position(event.device) - else: - x, y = event.x, event.y - - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - y - FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) + MouseEvent("motion_notify_event", self, *self._mpl_coords(event), + modifiers=self._mpl_modifiers(event.state), + guiEvent=event)._process() return False # finish event propagation? - def leave_notify_event(self, widget, event): - FigureCanvasBase.leave_notify_event(self, event) - def enter_notify_event(self, widget, event): - x = event.x - # flipy so y=0 is bottom of canvas - y = self.get_allocation().height - event.y - FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y)) + gtk_mods = Gdk.Keymap.get_for_display( + self.get_display()).get_modifier_state() + LocationEvent("figure_enter_event", self, *self._mpl_coords(event), + modifiers=self._mpl_modifiers(gtk_mods), + guiEvent=event)._process() + + def leave_notify_event(self, widget, event): + gtk_mods = Gdk.Keymap.get_for_display( + self.get_display()).get_modifier_state() + LocationEvent("figure_leave_event", self, *self._mpl_coords(event), + modifiers=self._mpl_modifiers(gtk_mods), + guiEvent=event)._process() def size_allocate(self, widget, allocation): dpival = self.figure.dpi - winch = allocation.width / dpival - hinch = allocation.height / dpival + winch = allocation.width * self.device_pixel_ratio / dpival + hinch = allocation.height * self.device_pixel_ratio / dpival self.figure.set_size_inches(winch, hinch, forward=False) - FigureCanvasBase.resize_event(self) + ResizeEvent("resize_event", self)._process() self.draw_idle() + @staticmethod + def _mpl_modifiers(event_state, *, exclude=None): + modifiers = [ + ("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"), + ("alt", Gdk.ModifierType.MOD1_MASK, "alt"), + ("shift", Gdk.ModifierType.SHIFT_MASK, "shift"), + ("super", Gdk.ModifierType.MOD4_MASK, "super"), + ] + return [name for name, mask, key in modifiers + if exclude != key and event_state & mask] + def _get_key(self, event): unikey = chr(Gdk.keyval_to_unicode(event.keyval)) key = cbook._unikey_or_keysym_to_mplkey( - unikey, - Gdk.keyval_name(event.keyval)) - modifiers = [ - (Gdk.ModifierType.CONTROL_MASK, 'ctrl'), - (Gdk.ModifierType.MOD1_MASK, 'alt'), - (Gdk.ModifierType.SHIFT_MASK, 'shift'), - (Gdk.ModifierType.MOD4_MASK, 'super'), - ] - for key_mask, prefix in modifiers: - if event.state & key_mask: - if not (prefix == 'shift' and unikey.isprintable()): - key = '{0}+{1}'.format(prefix, key) - return key + unikey, Gdk.keyval_name(event.keyval)) + mods = self._mpl_modifiers(event.state, exclude=key) + if "shift" in mods and unikey.isprintable(): + mods.remove("shift") + return "+".join([*mods, key]) + + def _update_device_pixel_ratio(self, *args, **kwargs): + # We need to be careful in cases with mixed resolution displays if + # device_pixel_ratio changes. + if self._set_device_pixel_ratio(self.get_scale_factor()): + # The easiest way to resize the canvas is to emit a resize event + # since we implement all the logic for resizing the canvas for that + # event. + self.queue_resize() + self.queue_draw() def configure_event(self, widget, event): if widget.get_property("window") is None: return - w, h = event.width, event.height + w = event.width * self.device_pixel_ratio + h = event.height * self.device_pixel_ratio if w < 3 or h < 3: return # empty fig # resize the figure (in inches) @@ -249,7 +244,8 @@ def _post_draw(self, widget, ctx): if self._rubberband_rect is None: return - x0, y0, w, h = self._rubberband_rect + x0, y0, w, h = (dim / self.device_pixel_ratio + for dim in self._rubberband_rect) x1 = x0 + w y1 = y0 + h @@ -297,157 +293,15 @@ def idle_draw(*args): def flush_events(self): # docstring inherited - Gdk.threads_enter() - while Gtk.events_pending(): - Gtk.main_iteration() - Gdk.flush() - Gdk.threads_leave() - - -class FigureManagerGTK3(FigureManagerBase): - """ - Attributes - ---------- - canvas : `FigureCanvas` - The FigureCanvas instance - num : int or str - The Figure number - toolbar : Gtk.Toolbar - The Gtk.Toolbar - vbox : Gtk.VBox - The Gtk.VBox containing the canvas and toolbar - window : Gtk.Window - The Gtk.Window - - """ - def __init__(self, canvas, num): - self.window = Gtk.Window() - super().__init__(canvas, num) - - self.window.set_wmclass("matplotlib", "Matplotlib") - try: - self.window.set_icon_from_file(window_icon) - except Exception: - # Some versions of gtk throw a glib.GError but not all, so I am not - # sure how to catch it. I am unhappy doing a blanket catch here, - # but am not sure what a better way is - JDH - _log.info('Could not load matplotlib icon: %s', sys.exc_info()[1]) - - self.vbox = Gtk.Box() - self.vbox.set_property("orientation", Gtk.Orientation.VERTICAL) - self.window.add(self.vbox) - self.vbox.show() - - self.canvas.show() - - self.vbox.pack_start(self.canvas, True, True, 0) - # calculate size for window - w = int(self.canvas.figure.bbox.width) - h = int(self.canvas.figure.bbox.height) - - self.toolbar = self._get_toolbar() - - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - - if self.toolbar is not None: - self.toolbar.show() - self.vbox.pack_end(self.toolbar, False, False, 0) - min_size, nat_size = self.toolbar.get_preferred_size() - h += nat_size.height - - self.window.set_default_size(w, h) - - self._destroying = False - self.window.connect("destroy", lambda *args: Gcf.destroy(self)) - self.window.connect("delete_event", lambda *args: Gcf.destroy(self)) - if mpl.is_interactive(): - self.window.show() - self.canvas.draw_idle() - - self.canvas.grab_focus() - - def destroy(self, *args): - if self._destroying: - # Otherwise, this can be called twice when the user presses 'q', - # which calls Gcf.destroy(self), then this destroy(), then triggers - # Gcf.destroy(self) once again via - # `connect("destroy", lambda *args: Gcf.destroy(self))`. - return - self._destroying = True - self.vbox.destroy() - self.window.destroy() - self.canvas.destroy() - if self.toolbar: - self.toolbar.destroy() - - if (Gcf.get_num_fig_managers() == 0 and not mpl.is_interactive() and - Gtk.main_level() >= 1): - Gtk.main_quit() - - def show(self): - # show the figure window - self.window.show() - self.canvas.draw() - if mpl.rcParams['figure.raise_window']: - if self.window.get_window(): - self.window.present() - else: - # If this is called by a callback early during init, - # self.window (a GtkWindow) may not have an associated - # low-level GdkWindow (self.window.get_window()) yet, and - # present() would crash. - _api.warn_external("Cannot raise window yet to be setup") - - def full_screen_toggle(self): - self._full_screen_flag = not self._full_screen_flag - if self._full_screen_flag: - self.window.fullscreen() - else: - self.window.unfullscreen() - _full_screen_flag = False - - def _get_toolbar(self): - # must be inited after the window, drawingArea and figure - # attrs are set - if mpl.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2GTK3(self.canvas, self.window) - elif mpl.rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarGTK3(self.toolmanager) - else: - toolbar = None - return toolbar - - def get_window_title(self): - return self.window.get_title() - - def set_window_title(self, title): - self.window.set_title(title) - - def resize(self, width, height): - """Set the canvas size in pixels.""" - if self.toolbar: - toolbar_size = self.toolbar.size_request() - height += toolbar_size.height - canvas_size = self.canvas.get_allocation() - if canvas_size.width == canvas_size.height == 1: - # A canvas size of (1, 1) cannot exist in most cases, because - # window decorations would prevent such a small window. This call - # must be before the window has been mapped and widgets have been - # sized, so just change the window's starting size. - self.window.set_default_size(width, height) - else: - self.window.resize(width, height) + context = GLib.MainContext.default() + while context.pending(): + context.iteration(True) -class NavigationToolbar2GTK3(NavigationToolbar2, Gtk.Toolbar): - ctx = _api.deprecated("3.3")(property( - lambda self: self.canvas.get_property("window").cairo_create())) - - def __init__(self, canvas, window): - self.win = window +class NavigationToolbar2GTK3(_NavigationToolbar2GTK, Gtk.Toolbar): + @_api.delete_parameter("3.6", "window") + def __init__(self, canvas, window=None): + self._win = window GObject.GObject.__init__(self) self.set_style(Gtk.ToolbarStyle.ICONS) @@ -462,21 +316,16 @@ def __init__(self, canvas, window): str(cbook._get_data_path('images', f'{image_file}-symbolic.svg'))), Gtk.IconSize.LARGE_TOOLBAR) - self._gtk_ids[text] = tbutton = ( + self._gtk_ids[text] = button = ( Gtk.ToggleToolButton() if callback in ['zoom', 'pan'] else Gtk.ToolButton()) - tbutton.set_label(text) - tbutton.set_icon_widget(image) - self.insert(tbutton, -1) + button.set_label(text) + button.set_icon_widget(image) # Save the handler id, so that we can block it as needed. - tbutton._signal_handler = tbutton.connect( + button._signal_handler = button.connect( 'clicked', getattr(self, callback)) - tbutton.set_tooltip_text(tooltip_text) - - toolitem = Gtk.SeparatorToolItem() - self.insert(toolitem, -1) - toolitem.set_draw(False) - toolitem.set_expand(True) + button.set_tooltip_text(tooltip_text) + self.insert(button, -1) # This filler item ensures the toolbar is always at least two text # lines high. Otherwise the canvas gets redrawn as the mouse hovers @@ -487,51 +336,20 @@ def __init__(self, canvas, window): label = Gtk.Label() label.set_markup( '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') + toolitem.set_expand(True) # Push real message to the right. toolitem.add(label) toolitem = Gtk.ToolItem() self.insert(toolitem, -1) self.message = Gtk.Label() + self.message.set_justify(Gtk.Justification.RIGHT) toolitem.add(self.message) self.show_all() - NavigationToolbar2.__init__(self, canvas) - - def set_message(self, s): - escaped = GLib.markup_escape_text(s) - self.message.set_markup(f'{escaped}') + _NavigationToolbar2GTK.__init__(self, canvas) - def set_cursor(self, cursor): - window = self.canvas.get_property("window") - if window is not None: - window.set_cursor(cursord[cursor]) - Gtk.main_iteration() - - def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height - y1 = height - y1 - y0 = height - y0 - rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] - self.canvas._draw_rubberband(rect) - - def remove_rubberband(self): - self.canvas._draw_rubberband(None) - - def _update_buttons_checked(self): - for name, active in [("Pan", "PAN"), ("Zoom", "ZOOM")]: - button = self._gtk_ids.get(name) - if button: - with button.handler_block(button._signal_handler): - button.set_active(self.mode.name == active) - - def pan(self, *args): - super().pan(*args) - self._update_buttons_checked() - - def zoom(self, *args): - super().zoom(*args) - self._update_buttons_checked() + win = _api.deprecated("3.6")(property(lambda self: self._win)) def save_figure(self, *args): dialog = Gtk.FileChooserDialog( @@ -546,7 +364,7 @@ def save_figure(self, *args): ff = Gtk.FileFilter() ff.set_name(name) for fmt in fmts: - ff.add_pattern("*." + fmt) + ff.add_pattern(f'*.{fmt}') dialog.add_filter(ff) if self.canvas.get_default_filetype() in fmts: dialog.set_filter(ff) @@ -556,7 +374,7 @@ def on_notify_filter(*args): name = dialog.get_filter().get_name() fmt = self.canvas.get_supported_filetypes_grouped()[name][0] dialog.set_current_name( - str(Path(dialog.get_current_name()).with_suffix("." + fmt))) + str(Path(dialog.get_current_name()).with_suffix(f'.{fmt}'))) dialog.set_current_folder(mpl.rcParams["savefig.directory"]) dialog.set_current_name(self.canvas.get_default_filename()) @@ -575,15 +393,11 @@ def on_notify_filter(*args): try: self.canvas.figure.savefig(fname, format=fmt) except Exception as e: - error_msg_gtk(str(e), parent=self) - - def set_history_buttons(self): - can_backward = self._nav_stack._pos > 0 - can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 - if 'Back' in self._gtk_ids: - self._gtk_ids['Back'].set_sensitive(can_backward) - if 'Forward' in self._gtk_ids: - self._gtk_ids['Forward'].set_sensitive(can_forward) + dialog = Gtk.MessageDialog( + parent=self.canvas.get_toplevel(), message_format=str(e), + type=Gtk.MessageType.ERROR, buttons=Gtk.ButtonsType.OK) + dialog.run() + dialog.destroy() class ToolbarGTK3(ToolContainerBase, Gtk.Box): @@ -594,6 +408,7 @@ def __init__(self, toolmanager): Gtk.Box.__init__(self) self.set_property('orientation', Gtk.Orientation.HORIZONTAL) self._message = Gtk.Label() + self._message.set_justify(Gtk.Justification.RIGHT) self.pack_end(self._message, False, False, 0) self.show_all() self._groups = {} @@ -602,26 +417,26 @@ def __init__(self, toolmanager): def add_toolitem(self, name, group, position, image_file, description, toggle): if toggle: - tbutton = Gtk.ToggleToolButton() + button = Gtk.ToggleToolButton() else: - tbutton = Gtk.ToolButton() - tbutton.set_label(name) + button = Gtk.ToolButton() + button.set_label(name) if image_file is not None: image = Gtk.Image.new_from_gicon( Gio.Icon.new_for_string(image_file), Gtk.IconSize.LARGE_TOOLBAR) - tbutton.set_icon_widget(image) + button.set_icon_widget(image) if position is None: position = -1 - self._add_button(tbutton, group, position) - signal = tbutton.connect('clicked', self._call_tool, name) - tbutton.set_tooltip_text(description) - tbutton.show_all() + self._add_button(button, group, position) + signal = button.connect('clicked', self._call_tool, name) + button.set_tooltip_text(description) + button.show_all() self._toolitems.setdefault(name, []) - self._toolitems[name].append((tbutton, signal)) + self._toolitems[name].append((button, signal)) def _add_button(self, button, group, position): if group not in self._groups: @@ -647,7 +462,7 @@ def toggle_toolitem(self, name, toggled): def remove_toolitem(self, name): if name not in self._toolitems: - self.toolmanager.message_event('%s Not in toolbar' % name, self) + self.toolmanager.message_event(f'{name} not in toolbar', self) return for group in self._groups: @@ -666,52 +481,14 @@ def set_message(self, s): self._message.set_label(s) -@_api.deprecated("3.3") -class StatusbarGTK3(StatusbarBase, Gtk.Statusbar): - def __init__(self, *args, **kwargs): - StatusbarBase.__init__(self, *args, **kwargs) - Gtk.Statusbar.__init__(self) - self._context = self.get_context_id('message') - - def set_message(self, s): - self.pop(self._context) - self.push(self._context, s) - - -class RubberbandGTK3(backend_tools.RubberbandBase): - def draw_rubberband(self, x0, y0, x1, y1): - NavigationToolbar2GTK3.draw_rubberband( - self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) - - def remove_rubberband(self): - NavigationToolbar2GTK3.remove_rubberband( - self._make_classic_style_pseudo_toolbar()) - - +@backend_tools._register_tool_class(FigureCanvasGTK3) class SaveFigureGTK3(backend_tools.SaveFigureBase): def trigger(self, *args, **kwargs): - - class PseudoToolbar: - canvas = self.figure.canvas - - return NavigationToolbar2GTK3.save_figure(PseudoToolbar()) - - -class SetCursorGTK3(backend_tools.SetCursorBase): - def set_cursor(self, cursor): - NavigationToolbar2GTK3.set_cursor( - self._make_classic_style_pseudo_toolbar(), cursor) - - -class ConfigureSubplotsGTK3(backend_tools.ConfigureSubplotsBase, Gtk.Window): - def _get_canvas(self, fig): - return self.canvas.__class__(fig) - - def trigger(self, *args): - NavigationToolbar2GTK3.configure_subplots( - self._make_classic_style_pseudo_toolbar(), None) + NavigationToolbar2GTK3.save_figure( + self._make_classic_style_pseudo_toolbar()) +@backend_tools._register_tool_class(FigureCanvasGTK3) class HelpGTK3(backend_tools.ToolHelpBase): def _normalize_shortcut(self, key): """ @@ -797,6 +574,7 @@ def trigger(self, *args): self._show_shortcuts_dialog() +@backend_tools._register_tool_class(FigureCanvasGTK3) class ToolCopyToClipboardGTK3(backend_tools.ToolCopyToClipboardBase): def trigger(self, *args, **kwargs): clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) @@ -806,14 +584,7 @@ def trigger(self, *args, **kwargs): clipboard.set_image(pb) -# Define the file to use as the GTk icon -if sys.platform == 'win32': - icon_filename = 'matplotlib.png' -else: - icon_filename = 'matplotlib.svg' -window_icon = str(cbook._get_data_path('images', icon_filename)) - - +@_api.deprecated("3.6") def error_msg_gtk(msg, parent=None): if parent is not None: # find the toplevel Gtk.Window parent = parent.get_toplevel() @@ -828,23 +599,19 @@ def error_msg_gtk(msg, parent=None): dialog.destroy() -backend_tools.ToolSaveFigure = SaveFigureGTK3 -backend_tools.ToolConfigureSubplots = ConfigureSubplotsGTK3 -backend_tools.ToolSetCursor = SetCursorGTK3 -backend_tools.ToolRubberband = RubberbandGTK3 -backend_tools.ToolHelp = HelpGTK3 -backend_tools.ToolCopyToClipboard = ToolCopyToClipboardGTK3 - Toolbar = ToolbarGTK3 +backend_tools._register_tool_class( + FigureCanvasGTK3, _backend_gtk.ConfigureSubplotsGTK) +backend_tools._register_tool_class( + FigureCanvasGTK3, _backend_gtk.RubberbandGTK) -@_Backend.export -class _BackendGTK3(_Backend): +class FigureManagerGTK3(_FigureManagerGTK): + _toolbar2_class = NavigationToolbar2GTK3 + _toolmanager_toolbar_class = ToolbarGTK3 + + +@_BackendGTK.export +class _BackendGTK3(_BackendGTK): FigureCanvas = FigureCanvasGTK3 FigureManager = FigureManagerGTK3 - - @staticmethod - def mainloop(): - if Gtk.main_level() == 0: - cbook._setup_new_guiapp() - Gtk.main() diff --git a/lib/matplotlib/backends/backend_gtk3agg.py b/lib/matplotlib/backends/backend_gtk3agg.py index ecd15327aa02..10aa15ac2a6b 100644 --- a/lib/matplotlib/backends/backend_gtk3agg.py +++ b/lib/matplotlib/backends/backend_gtk3agg.py @@ -1,26 +1,23 @@ import numpy as np -from .. import cbook -try: - from . import backend_cairo -except ImportError as e: - raise ImportError('backend Gtk3Agg requires cairo') from e +from .. import _api, cbook, transforms from . import backend_agg, backend_gtk3 -from .backend_cairo import cairo from .backend_gtk3 import Gtk, _BackendGTK3 -from matplotlib import transforms +import cairo # Presence of cairo is already checked by _backend_gtk. -class FigureCanvasGTK3Agg(backend_gtk3.FigureCanvasGTK3, - backend_agg.FigureCanvasAgg): + +class FigureCanvasGTK3Agg(backend_agg.FigureCanvasAgg, + backend_gtk3.FigureCanvasGTK3): def __init__(self, figure): - backend_gtk3.FigureCanvasGTK3.__init__(self, figure) + super().__init__(figure=figure) self._bbox_queue = [] def on_draw_event(self, widget, ctx): - """GtkDrawable draw event, like expose_event in GTK 2.X.""" + scale = self.device_pixel_ratio allocation = self.get_allocation() - w, h = allocation.width, allocation.height + w = allocation.width * scale + h = allocation.height * scale if not len(self._bbox_queue): Gtk.render_background( @@ -31,8 +28,6 @@ def on_draw_event(self, widget, ctx): else: bbox_queue = self._bbox_queue - ctx = backend_cairo._to_context(ctx) - for bbox in bbox_queue: x = int(bbox.x0) y = h - int(bbox.y1) @@ -43,7 +38,8 @@ def on_draw_event(self, widget, ctx): np.asarray(self.copy_from_bbox(bbox))) image = cairo.ImageSurface.create_for_data( buf.ravel().data, cairo.FORMAT_ARGB32, width, height) - ctx.set_source_surface(image, x, y) + image.set_device_scale(scale, scale) + ctx.set_source_surface(image, x / scale, y / scale) ctx.paint() if len(self._bbox_queue): @@ -57,25 +53,18 @@ def blit(self, bbox=None): if bbox is None: bbox = self.figure.bbox + scale = self.device_pixel_ratio allocation = self.get_allocation() - x = int(bbox.x0) - y = allocation.height - int(bbox.y1) - width = int(bbox.x1) - int(bbox.x0) - height = int(bbox.y1) - int(bbox.y0) + x = int(bbox.x0 / scale) + y = allocation.height - int(bbox.y1 / scale) + width = (int(bbox.x1) - int(bbox.x0)) // scale + height = (int(bbox.y1) - int(bbox.y0)) // scale self._bbox_queue.append(bbox) self.queue_draw_area(x, y, width, height) - def draw(self): - backend_agg.FigureCanvasAgg.draw(self) - super().draw() - - def print_png(self, filename, *args, **kwargs): - # Do this so we can save the resolution of figure in the PNG file - agg = self.switch_backends(backend_agg.FigureCanvasAgg) - return agg.print_png(filename, *args, **kwargs) - +@_api.deprecated("3.6", alternative="backend_gtk3.FigureManagerGTK3") class FigureManagerGTK3Agg(backend_gtk3.FigureManagerGTK3): pass @@ -83,4 +72,3 @@ class FigureManagerGTK3Agg(backend_gtk3.FigureManagerGTK3): @_BackendGTK3.export class _BackendGTK3Cairo(_BackendGTK3): FigureCanvas = FigureCanvasGTK3Agg - FigureManager = FigureManagerGTK3Agg diff --git a/lib/matplotlib/backends/backend_gtk3cairo.py b/lib/matplotlib/backends/backend_gtk3cairo.py index af290902d8a6..49215510c178 100644 --- a/lib/matplotlib/backends/backend_gtk3cairo.py +++ b/lib/matplotlib/backends/backend_gtk3cairo.py @@ -1,33 +1,24 @@ from contextlib import nullcontext -from . import backend_cairo, backend_gtk3 -from .backend_gtk3 import Gtk, _BackendGTK3 +from .backend_cairo import ( # noqa + FigureCanvasCairo, _RendererGTKCairo as RendererGTK3Cairo) +from .backend_gtk3 import Gtk, FigureCanvasGTK3, _BackendGTK3 -class RendererGTK3Cairo(backend_cairo.RendererCairo): - def set_context(self, ctx): - self.gc.ctx = backend_cairo._to_context(ctx) - - -class FigureCanvasGTK3Cairo(backend_gtk3.FigureCanvasGTK3, - backend_cairo.FigureCanvasCairo): - - def __init__(self, figure): - super().__init__(figure) - self._renderer = RendererGTK3Cairo(self.figure.dpi) - +class FigureCanvasGTK3Cairo(FigureCanvasCairo, FigureCanvasGTK3): def on_draw_event(self, widget, ctx): - """GtkDrawable draw event.""" with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar else nullcontext()): self._renderer.set_context(ctx) + scale = self.device_pixel_ratio + # Scale physical drawing to logical size. + ctx.scale(1 / scale, 1 / scale) allocation = self.get_allocation() Gtk.render_background( self.get_style_context(), ctx, allocation.x, allocation.y, allocation.width, allocation.height) - self._renderer.set_width_height( - allocation.width, allocation.height) + self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) diff --git a/lib/matplotlib/backends/backend_gtk4.py b/lib/matplotlib/backends/backend_gtk4.py new file mode 100644 index 000000000000..328819292018 --- /dev/null +++ b/lib/matplotlib/backends/backend_gtk4.py @@ -0,0 +1,610 @@ +import functools +import io +import os + +import matplotlib as mpl +from matplotlib import _api, backend_tools, cbook +from matplotlib.backend_bases import ( + ToolContainerBase, KeyEvent, LocationEvent, MouseEvent, ResizeEvent, + CloseEvent) + +try: + import gi +except ImportError as err: + raise ImportError("The GTK4 backends require PyGObject") from err + +try: + # :raises ValueError: If module/version is already loaded, already + # required, or unavailable. + gi.require_version("Gtk", "4.0") +except ValueError as e: + # in this case we want to re-raise as ImportError so the + # auto-backend selection logic correctly skips. + raise ImportError from e + +from gi.repository import Gio, GLib, Gtk, Gdk, GdkPixbuf +from . import _backend_gtk +from ._backend_gtk import ( # noqa: F401 # pylint: disable=W0611 + _BackendGTK, _FigureCanvasGTK, _FigureManagerGTK, _NavigationToolbar2GTK, + TimerGTK as TimerGTK4, +) + + +class FigureCanvasGTK4(_FigureCanvasGTK, Gtk.DrawingArea): + required_interactive_framework = "gtk4" + supports_blit = False + manager_class = _api.classproperty(lambda cls: FigureManagerGTK4) + _context_is_scaled = False + + def __init__(self, figure=None): + super().__init__(figure=figure) + + self.set_hexpand(True) + self.set_vexpand(True) + + self._idle_draw_id = 0 + self._rubberband_rect = None + + self.set_draw_func(self._draw_func) + self.connect('resize', self.resize_event) + self.connect('notify::scale-factor', self._update_device_pixel_ratio) + + click = Gtk.GestureClick() + click.set_button(0) # All buttons. + click.connect('pressed', self.button_press_event) + click.connect('released', self.button_release_event) + self.add_controller(click) + + key = Gtk.EventControllerKey() + key.connect('key-pressed', self.key_press_event) + key.connect('key-released', self.key_release_event) + self.add_controller(key) + + motion = Gtk.EventControllerMotion() + motion.connect('motion', self.motion_notify_event) + motion.connect('enter', self.enter_notify_event) + motion.connect('leave', self.leave_notify_event) + self.add_controller(motion) + + scroll = Gtk.EventControllerScroll.new( + Gtk.EventControllerScrollFlags.VERTICAL) + scroll.connect('scroll', self.scroll_event) + self.add_controller(scroll) + + self.set_focusable(True) + + css = Gtk.CssProvider() + style = '.matplotlib-canvas { background-color: white; }' + if Gtk.check_version(4, 9, 3) is None: + css.load_from_data(style, -1) + else: + css.load_from_data(style.encode('utf-8')) + style_ctx = self.get_style_context() + style_ctx.add_provider(css, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION) + style_ctx.add_class("matplotlib-canvas") + + def destroy(self): + CloseEvent("close_event", self)._process() + + def set_cursor(self, cursor): + # docstring inherited + self.set_cursor_from_name(_backend_gtk.mpl_to_gtk_cursor_name(cursor)) + + def _mpl_coords(self, xy=None): + """ + Convert the *xy* position of a GTK event, or of the current cursor + position if *xy* is None, to Matplotlib coordinates. + + GTK use logical pixels, but the figure is scaled to physical 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. + """ + if xy is None: + surface = self.get_native().get_surface() + is_over, x, y, mask = surface.get_device_position( + self.get_display().get_default_seat().get_pointer()) + else: + x, y = xy + x = x * self.device_pixel_ratio + # flip y so y=0 is bottom of canvas + y = self.figure.bbox.height - y * self.device_pixel_ratio + return x, y + + def scroll_event(self, controller, dx, dy): + MouseEvent( + "scroll_event", self, *self._mpl_coords(), step=dy, + modifiers=self._mpl_modifiers(controller), + )._process() + return True + + def button_press_event(self, controller, n_press, x, y): + MouseEvent( + "button_press_event", self, *self._mpl_coords((x, y)), + controller.get_current_button(), + modifiers=self._mpl_modifiers(controller), + )._process() + self.grab_focus() + + def button_release_event(self, controller, n_press, x, y): + MouseEvent( + "button_release_event", self, *self._mpl_coords((x, y)), + controller.get_current_button(), + modifiers=self._mpl_modifiers(controller), + )._process() + + def key_press_event(self, controller, keyval, keycode, state): + KeyEvent( + "key_press_event", self, self._get_key(keyval, keycode, state), + *self._mpl_coords(), + )._process() + return True + + def key_release_event(self, controller, keyval, keycode, state): + KeyEvent( + "key_release_event", self, self._get_key(keyval, keycode, state), + *self._mpl_coords(), + )._process() + return True + + def motion_notify_event(self, controller, x, y): + MouseEvent( + "motion_notify_event", self, *self._mpl_coords((x, y)), + modifiers=self._mpl_modifiers(controller), + )._process() + + def enter_notify_event(self, controller, x, y): + LocationEvent( + "figure_enter_event", self, *self._mpl_coords((x, y)), + modifiers=self._mpl_modifiers(), + )._process() + + def leave_notify_event(self, controller): + LocationEvent( + "figure_leave_event", self, *self._mpl_coords(), + modifiers=self._mpl_modifiers(), + )._process() + + def resize_event(self, area, width, height): + self._update_device_pixel_ratio() + dpi = self.figure.dpi + winch = width * self.device_pixel_ratio / dpi + hinch = height * self.device_pixel_ratio / dpi + self.figure.set_size_inches(winch, hinch, forward=False) + ResizeEvent("resize_event", self)._process() + self.draw_idle() + + def _mpl_modifiers(self, controller=None): + if controller is None: + surface = self.get_native().get_surface() + is_over, x, y, event_state = surface.get_device_position( + self.get_display().get_default_seat().get_pointer()) + else: + event_state = controller.get_current_event_state() + mod_table = [ + ("ctrl", Gdk.ModifierType.CONTROL_MASK), + ("alt", Gdk.ModifierType.ALT_MASK), + ("shift", Gdk.ModifierType.SHIFT_MASK), + ("super", Gdk.ModifierType.SUPER_MASK), + ] + return [name for name, mask in mod_table if event_state & mask] + + def _get_key(self, keyval, keycode, state): + unikey = chr(Gdk.keyval_to_unicode(keyval)) + key = cbook._unikey_or_keysym_to_mplkey( + unikey, + Gdk.keyval_name(keyval)) + modifiers = [ + ("ctrl", Gdk.ModifierType.CONTROL_MASK, "control"), + ("alt", Gdk.ModifierType.ALT_MASK, "alt"), + ("shift", Gdk.ModifierType.SHIFT_MASK, "shift"), + ("super", Gdk.ModifierType.SUPER_MASK, "super"), + ] + mods = [ + mod for mod, mask, mod_key in modifiers + if (mod_key != key and state & mask + and not (mod == "shift" and unikey.isprintable()))] + return "+".join([*mods, key]) + + def _update_device_pixel_ratio(self, *args, **kwargs): + # We need to be careful in cases with mixed resolution displays if + # device_pixel_ratio changes. + if self._set_device_pixel_ratio(self.get_scale_factor()): + self.draw() + + def _draw_rubberband(self, rect): + self._rubberband_rect = rect + # TODO: Only update the rubberband area. + self.queue_draw() + + def _draw_func(self, drawing_area, ctx, width, height): + self.on_draw_event(self, ctx) + self._post_draw(self, ctx) + + def _post_draw(self, widget, ctx): + if self._rubberband_rect is None: + return + + lw = 1 + dash = 3 + if not self._context_is_scaled: + x0, y0, w, h = (dim / self.device_pixel_ratio + for dim in self._rubberband_rect) + else: + x0, y0, w, h = self._rubberband_rect + lw *= self.device_pixel_ratio + dash *= self.device_pixel_ratio + x1 = x0 + w + y1 = y0 + h + + # Draw the lines from x0, y0 towards x1, y1 so that the + # dashes don't "jump" when moving the zoom box. + ctx.move_to(x0, y0) + ctx.line_to(x0, y1) + ctx.move_to(x0, y0) + ctx.line_to(x1, y0) + ctx.move_to(x0, y1) + ctx.line_to(x1, y1) + ctx.move_to(x1, y0) + ctx.line_to(x1, y1) + + ctx.set_antialias(1) + ctx.set_line_width(lw) + ctx.set_dash((dash, dash), 0) + ctx.set_source_rgb(0, 0, 0) + ctx.stroke_preserve() + + ctx.set_dash((dash, dash), dash) + ctx.set_source_rgb(1, 1, 1) + ctx.stroke() + + def on_draw_event(self, widget, ctx): + # to be overwritten by GTK4Agg or GTK4Cairo + pass + + def draw(self): + # docstring inherited + if self.is_drawable(): + self.queue_draw() + + def draw_idle(self): + # docstring inherited + if self._idle_draw_id != 0: + return + def idle_draw(*args): + try: + self.draw() + finally: + self._idle_draw_id = 0 + return False + self._idle_draw_id = GLib.idle_add(idle_draw) + + def flush_events(self): + # docstring inherited + context = GLib.MainContext.default() + while context.pending(): + context.iteration(True) + + +class NavigationToolbar2GTK4(_NavigationToolbar2GTK, Gtk.Box): + @_api.delete_parameter("3.6", "window") + def __init__(self, canvas, window=None): + self._win = window + Gtk.Box.__init__(self) + + self.add_css_class('toolbar') + + self._gtk_ids = {} + for text, tooltip_text, image_file, callback in self.toolitems: + if text is None: + self.append(Gtk.Separator()) + continue + image = Gtk.Image.new_from_gicon( + Gio.Icon.new_for_string( + str(cbook._get_data_path('images', + f'{image_file}-symbolic.svg')))) + self._gtk_ids[text] = button = ( + Gtk.ToggleButton() if callback in ['zoom', 'pan'] else + Gtk.Button()) + button.set_child(image) + button.add_css_class('flat') + button.add_css_class('image-button') + # Save the handler id, so that we can block it as needed. + button._signal_handler = button.connect( + 'clicked', getattr(self, callback)) + button.set_tooltip_text(tooltip_text) + self.append(button) + + # This filler item ensures the toolbar is always at least two text + # lines high. Otherwise the canvas gets redrawn as the mouse hovers + # over images because those use two-line messages which resize the + # toolbar. + label = Gtk.Label() + label.set_markup( + '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') + label.set_hexpand(True) # Push real message to the right. + self.append(label) + + self.message = Gtk.Label() + self.message.set_justify(Gtk.Justification.RIGHT) + self.append(self.message) + + _NavigationToolbar2GTK.__init__(self, canvas) + + win = _api.deprecated("3.6")(property(lambda self: self._win)) + + def save_figure(self, *args): + dialog = Gtk.FileChooserNative( + title='Save the figure', + transient_for=self.canvas.get_root(), + action=Gtk.FileChooserAction.SAVE, + modal=True) + self._save_dialog = dialog # Must keep a reference. + + ff = Gtk.FileFilter() + ff.set_name('All files') + ff.add_pattern('*') + dialog.add_filter(ff) + dialog.set_filter(ff) + + formats = [] + default_format = None + for i, (name, fmts) in enumerate( + self.canvas.get_supported_filetypes_grouped().items()): + ff = Gtk.FileFilter() + ff.set_name(name) + for fmt in fmts: + ff.add_pattern(f'*.{fmt}') + dialog.add_filter(ff) + formats.append(name) + if self.canvas.get_default_filetype() in fmts: + default_format = i + # Setting the choice doesn't always work, so make sure the default + # format is first. + formats = [formats[default_format], *formats[:default_format], + *formats[default_format+1:]] + dialog.add_choice('format', 'File format', formats, formats) + dialog.set_choice('format', formats[default_format]) + + dialog.set_current_folder(Gio.File.new_for_path( + os.path.expanduser(mpl.rcParams['savefig.directory']))) + dialog.set_current_name(self.canvas.get_default_filename()) + + @functools.partial(dialog.connect, 'response') + def on_response(dialog, response): + file = dialog.get_file() + fmt = dialog.get_choice('format') + fmt = self.canvas.get_supported_filetypes_grouped()[fmt][0] + dialog.destroy() + self._save_dialog = None + if response != Gtk.ResponseType.ACCEPT: + return + # Save dir for next time, unless empty str (which means use cwd). + if mpl.rcParams['savefig.directory']: + parent = file.get_parent() + mpl.rcParams['savefig.directory'] = parent.get_path() + try: + self.canvas.figure.savefig(file.get_path(), format=fmt) + except Exception as e: + msg = Gtk.MessageDialog( + transient_for=self.canvas.get_root(), + message_type=Gtk.MessageType.ERROR, + buttons=Gtk.ButtonsType.OK, modal=True, + text=str(e)) + msg.show() + + dialog.show() + + +class ToolbarGTK4(ToolContainerBase, Gtk.Box): + _icon_extension = '-symbolic.svg' + + def __init__(self, toolmanager): + ToolContainerBase.__init__(self, toolmanager) + Gtk.Box.__init__(self) + self.set_property('orientation', Gtk.Orientation.HORIZONTAL) + + # Tool items are created later, but must appear before the message. + self._tool_box = Gtk.Box() + self.append(self._tool_box) + self._groups = {} + self._toolitems = {} + + # This filler item ensures the toolbar is always at least two text + # lines high. Otherwise the canvas gets redrawn as the mouse hovers + # over images because those use two-line messages which resize the + # toolbar. + label = Gtk.Label() + label.set_markup( + '\N{NO-BREAK SPACE}\n\N{NO-BREAK SPACE}') + label.set_hexpand(True) # Push real message to the right. + self.append(label) + + self._message = Gtk.Label() + self._message.set_justify(Gtk.Justification.RIGHT) + self.append(self._message) + + def add_toolitem(self, name, group, position, image_file, description, + toggle): + if toggle: + button = Gtk.ToggleButton() + else: + button = Gtk.Button() + button.set_label(name) + button.add_css_class('flat') + + if image_file is not None: + image = Gtk.Image.new_from_gicon( + Gio.Icon.new_for_string(image_file)) + button.set_child(image) + button.add_css_class('image-button') + + if position is None: + position = -1 + + self._add_button(button, group, position) + signal = button.connect('clicked', self._call_tool, name) + button.set_tooltip_text(description) + self._toolitems.setdefault(name, []) + self._toolitems[name].append((button, signal)) + + def _find_child_at_position(self, group, position): + children = [None] + child = self._groups[group].get_first_child() + while child is not None: + children.append(child) + child = child.get_next_sibling() + return children[position] + + def _add_button(self, button, group, position): + if group not in self._groups: + if self._groups: + self._add_separator() + group_box = Gtk.Box() + self._tool_box.append(group_box) + self._groups[group] = group_box + self._groups[group].insert_child_after( + button, self._find_child_at_position(group, position)) + + def _call_tool(self, btn, name): + self.trigger_tool(name) + + def toggle_toolitem(self, name, toggled): + if name not in self._toolitems: + return + for toolitem, signal in self._toolitems[name]: + toolitem.handler_block(signal) + toolitem.set_active(toggled) + toolitem.handler_unblock(signal) + + def remove_toolitem(self, name): + if name not in self._toolitems: + self.toolmanager.message_event(f'{name} not in toolbar', self) + return + + for group in self._groups: + for toolitem, _signal in self._toolitems[name]: + if toolitem in self._groups[group]: + self._groups[group].remove(toolitem) + del self._toolitems[name] + + def _add_separator(self): + sep = Gtk.Separator() + sep.set_property("orientation", Gtk.Orientation.VERTICAL) + self._tool_box.append(sep) + + def set_message(self, s): + self._message.set_label(s) + + +@backend_tools._register_tool_class(FigureCanvasGTK4) +class SaveFigureGTK4(backend_tools.SaveFigureBase): + def trigger(self, *args, **kwargs): + NavigationToolbar2GTK4.save_figure( + self._make_classic_style_pseudo_toolbar()) + + +@backend_tools._register_tool_class(FigureCanvasGTK4) +class HelpGTK4(backend_tools.ToolHelpBase): + def _normalize_shortcut(self, key): + """ + Convert Matplotlib key presses to GTK+ accelerator identifiers. + + Related to `FigureCanvasGTK4._get_key`. + """ + special = { + 'backspace': 'BackSpace', + 'pagedown': 'Page_Down', + 'pageup': 'Page_Up', + 'scroll_lock': 'Scroll_Lock', + } + + parts = key.split('+') + mods = ['<' + mod + '>' for mod in parts[:-1]] + key = parts[-1] + + if key in special: + key = special[key] + elif len(key) > 1: + key = key.capitalize() + elif key.isupper(): + mods += [''] + + return ''.join(mods) + key + + def _is_valid_shortcut(self, key): + """ + Check for a valid shortcut to be displayed. + + - GTK will never send 'cmd+' (see `FigureCanvasGTK4._get_key`). + - The shortcut window only shows keyboard shortcuts, not mouse buttons. + """ + return 'cmd+' not in key and not key.startswith('MouseButton.') + + def trigger(self, *args): + section = Gtk.ShortcutsSection() + + for name, tool in sorted(self.toolmanager.tools.items()): + if not tool.description: + continue + + # Putting everything in a separate group allows GTK to + # automatically split them into separate columns/pages, which is + # useful because we have lots of shortcuts, some with many keys + # that are very wide. + group = Gtk.ShortcutsGroup() + section.append(group) + # A hack to remove the title since we have no group naming. + child = group.get_first_child() + while child is not None: + child.set_visible(False) + child = child.get_next_sibling() + + shortcut = Gtk.ShortcutsShortcut( + accelerator=' '.join( + self._normalize_shortcut(key) + for key in self.toolmanager.get_tool_keymap(name) + if self._is_valid_shortcut(key)), + title=tool.name, + subtitle=tool.description) + group.append(shortcut) + + window = Gtk.ShortcutsWindow( + title='Help', + modal=True, + transient_for=self._figure.canvas.get_root()) + window.set_child(section) + + window.show() + + +@backend_tools._register_tool_class(FigureCanvasGTK4) +class ToolCopyToClipboardGTK4(backend_tools.ToolCopyToClipboardBase): + def trigger(self, *args, **kwargs): + with io.BytesIO() as f: + self.canvas.print_rgba(f) + w, h = self.canvas.get_width_height() + pb = GdkPixbuf.Pixbuf.new_from_data(f.getbuffer(), + GdkPixbuf.Colorspace.RGB, True, + 8, w, h, w*4) + clipboard = self.canvas.get_clipboard() + clipboard.set(pb) + + +backend_tools._register_tool_class( + FigureCanvasGTK4, _backend_gtk.ConfigureSubplotsGTK) +backend_tools._register_tool_class( + FigureCanvasGTK4, _backend_gtk.RubberbandGTK) +Toolbar = ToolbarGTK4 + + +class FigureManagerGTK4(_FigureManagerGTK): + _toolbar2_class = NavigationToolbar2GTK4 + _toolmanager_toolbar_class = ToolbarGTK4 + + +@_BackendGTK.export +class _BackendGTK4(_BackendGTK): + FigureCanvas = FigureCanvasGTK4 + FigureManager = FigureManagerGTK4 diff --git a/lib/matplotlib/backends/backend_gtk4agg.py b/lib/matplotlib/backends/backend_gtk4agg.py new file mode 100644 index 000000000000..d7ee04616e72 --- /dev/null +++ b/lib/matplotlib/backends/backend_gtk4agg.py @@ -0,0 +1,41 @@ +import numpy as np + +from .. import _api, cbook +from . import backend_agg, backend_gtk4 +from .backend_gtk4 import Gtk, _BackendGTK4 + +import cairo # Presence of cairo is already checked by _backend_gtk. + + +class FigureCanvasGTK4Agg(backend_agg.FigureCanvasAgg, + backend_gtk4.FigureCanvasGTK4): + + def on_draw_event(self, widget, ctx): + scale = self.device_pixel_ratio + allocation = self.get_allocation() + + Gtk.render_background( + self.get_style_context(), ctx, + allocation.x, allocation.y, + allocation.width, allocation.height) + + buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32( + np.asarray(self.get_renderer().buffer_rgba())) + height, width, _ = buf.shape + image = cairo.ImageSurface.create_for_data( + buf.ravel().data, cairo.FORMAT_ARGB32, width, height) + image.set_device_scale(scale, scale) + ctx.set_source_surface(image, 0, 0) + ctx.paint() + + return False + + +@_api.deprecated("3.6", alternative="backend_gtk4.FigureManagerGTK4") +class FigureManagerGTK4Agg(backend_gtk4.FigureManagerGTK4): + pass + + +@_BackendGTK4.export +class _BackendGTK4Agg(_BackendGTK4): + FigureCanvas = FigureCanvasGTK4Agg diff --git a/lib/matplotlib/backends/backend_gtk4cairo.py b/lib/matplotlib/backends/backend_gtk4cairo.py new file mode 100644 index 000000000000..83cbd081c26d --- /dev/null +++ b/lib/matplotlib/backends/backend_gtk4cairo.py @@ -0,0 +1,29 @@ +from contextlib import nullcontext + +from .backend_cairo import ( # noqa + FigureCanvasCairo, _RendererGTKCairo as RendererGTK4Cairo) +from .backend_gtk4 import Gtk, FigureCanvasGTK4, _BackendGTK4 + + +class FigureCanvasGTK4Cairo(FigureCanvasCairo, FigureCanvasGTK4): + _context_is_scaled = True + + def on_draw_event(self, widget, ctx): + with (self.toolbar._wait_cursor_for_draw_cm() if self.toolbar + else nullcontext()): + self._renderer.set_context(ctx) + scale = self.device_pixel_ratio + # Scale physical drawing to logical size. + ctx.scale(1 / scale, 1 / scale) + allocation = self.get_allocation() + Gtk.render_background( + self.get_style_context(), ctx, + allocation.x, allocation.y, + allocation.width, allocation.height) + self._renderer.dpi = self.figure.dpi + self.figure.draw(self._renderer) + + +@_BackendGTK4.export +class _BackendGTK4Cairo(_BackendGTK4): + FigureCanvas = FigureCanvasGTK4Cairo diff --git a/lib/matplotlib/backends/backend_macosx.py b/lib/matplotlib/backends/backend_macosx.py index c84087f3b209..2867c13f430b 100644 --- a/lib/matplotlib/backends/backend_macosx.py +++ b/lib/matplotlib/backends/backend_macosx.py @@ -1,11 +1,13 @@ +import os + import matplotlib as mpl -from matplotlib import cbook +from matplotlib import _api, cbook from matplotlib._pylab_helpers import Gcf -from matplotlib.backends import _macosx -from matplotlib.backends.backend_agg import FigureCanvasAgg +from . import _macosx +from .backend_agg import FigureCanvasAgg from matplotlib.backend_bases import ( _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - TimerBase) + ResizeEvent, TimerBase) from matplotlib.figure import Figure from matplotlib.widgets import SubplotTool @@ -15,87 +17,98 @@ class TimerMac(_macosx.Timer, TimerBase): # completely implemented at the C-level (in _macosx.Timer) -class FigureCanvasMac(_macosx.FigureCanvas, FigureCanvasAgg): +class FigureCanvasMac(FigureCanvasAgg, _macosx.FigureCanvas, FigureCanvasBase): # docstring inherited - # Events such as button presses, mouse movements, and key presses - # are handled in the C code and the base class methods - # button_press_event, button_release_event, motion_notify_event, - # key_press_event, and key_release_event are called from there. + # Ideally this class would be `class FCMacAgg(FCAgg, FCMac)` + # (FC=FigureCanvas) where FCMac would be an ObjC-implemented mac-specific + # class also inheriting from FCBase (this is the approach with other GUI + # toolkits). However, writing an extension type inheriting from a Python + # base class is slightly tricky (the extension type must be a heap type), + # and we can just as well lift the FCBase base up one level, keeping it *at + # the end* to have the right method resolution order. + + # Events such as button presses, mouse movements, and key presses are + # handled in C and events (MouseEvent, etc.) are triggered from there. required_interactive_framework = "macosx" _timer_cls = TimerMac + manager_class = _api.classproperty(lambda cls: FigureManagerMac) def __init__(self, figure): - FigureCanvasBase.__init__(self, figure) - width, height = self.get_width_height() - _macosx.FigureCanvas.__init__(self, width, height) - self._dpi_ratio = 1.0 - - def _set_device_scale(self, value): - if self._dpi_ratio != value: - # Need the new value in place before setting figure.dpi, which - # will trigger a resize - self._dpi_ratio, old_value = value, self._dpi_ratio - self.figure.dpi = self.figure.dpi / old_value * self._dpi_ratio - - def _draw(self): - renderer = self.get_renderer(cleared=self.figure.stale) - if self.figure.stale: - self.figure.draw(renderer) - return renderer + super().__init__(figure=figure) + self._draw_pending = False + self._is_drawing = False def draw(self): - # docstring inherited - self.draw_idle() - self.flush_events() + """Render the figure and update the macosx canvas.""" + # 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: + return + with cbook._setattr_cm(self, _is_drawing=True): + super().draw() + self.update() - # draw_idle is provided by _macosx.FigureCanvas + def draw_idle(self): + # docstring inherited + if not (getattr(self, '_draw_pending', False) or + getattr(self, '_is_drawing', False)): + self._draw_pending = True + # Add a singleshot timer to the eventloop that will call back + # into the Python method _draw_idle to take care of the draw + self._single_shot_timer(self._draw_idle) + + def _single_shot_timer(self, callback): + """Add a single shot timer with the given callback""" + # We need to explicitly stop (called from delete) the timer after + # firing, otherwise segfaults will occur when trying to deallocate + # the singleshot timers. + def callback_func(callback, timer): + callback() + del timer + timer = self.new_timer(interval=0) + timer.add_callback(callback_func, callback, timer) + timer.start() + + def _draw_idle(self): + """ + Draw method for singleshot timer + + This draw method can be added to a singleshot timer, which can + accumulate draws while the eventloop is spinning. This method will + then only draw the first time and short-circuit the others. + """ + with self._idle_draw_cntx(): + if not self._draw_pending: + # Short-circuit because our draw request has already been + # taken care of + return + self._draw_pending = False + self.draw() def blit(self, bbox=None): - self.draw_idle() + # docstring inherited + super().blit(bbox) + self.update() def resize(self, width, height): - dpi = self.figure.dpi - width /= dpi - height /= dpi - self.figure.set_size_inches(width * self._dpi_ratio, - height * self._dpi_ratio, - forward=False) - FigureCanvasBase.resize_event(self) + # Size from macOS is logical pixels, dpi is physical. + scale = self.figure.dpi / self.device_pixel_ratio + width /= scale + height /= scale + self.figure.set_size_inches(width, height, forward=False) + ResizeEvent("resize_event", self)._process() self.draw_idle() -class FigureManagerMac(_macosx.FigureManager, FigureManagerBase): - """ - Wrap everything up into a window for the pylab interface - """ - def __init__(self, canvas, num): - _macosx.FigureManager.__init__(self, canvas) - FigureManagerBase.__init__(self, canvas, num) - if mpl.rcParams['toolbar'] == 'toolbar2': - self.toolbar = NavigationToolbar2Mac(canvas) - else: - self.toolbar = None - if self.toolbar is not None: - self.toolbar.update() - - if mpl.is_interactive(): - self.show() - self.canvas.draw_idle() - - def close(self): - Gcf.destroy(self) - - class NavigationToolbar2Mac(_macosx.NavigationToolbar2, NavigationToolbar2): def __init__(self, canvas): - self.canvas = canvas # Needed by the _macosx __init__. data_path = cbook._get_data_path('images') _, tooltips, image_names, _ = zip(*NavigationToolbar2.toolitems) _macosx.NavigationToolbar2.__init__( - self, + self, canvas, tuple(str(data_path / image_name) + ".pdf" for image_name in image_names if image_name is not None), tuple(tooltip for tooltip in tooltips if tooltip is not None)) @@ -104,20 +117,22 @@ def __init__(self, canvas): def draw_rubberband(self, event, x0, y0, x1, y1): self.canvas.set_rubberband(int(x0), int(y0), int(x1), int(y1)) - def release_zoom(self, event): - super().release_zoom(event) + def remove_rubberband(self): self.canvas.remove_rubberband() - def set_cursor(self, cursor): - _macosx.set_cursor(cursor) - def save_figure(self, *args): + directory = os.path.expanduser(mpl.rcParams['savefig.directory']) filename = _macosx.choose_save_file('Save the figure', + directory, self.canvas.get_default_filename()) if filename is None: # Cancel return + # Save dir for next time, unless empty str (which means use cwd). + if mpl.rcParams['savefig.directory']: + mpl.rcParams['savefig.directory'] = os.path.dirname(filename) self.canvas.figure.savefig(filename) + @_api.deprecated("3.6", alternative='configure_subplots()') def prepare_configure_subplots(self): toolfig = Figure(figsize=(6, 3)) canvas = FigureCanvasMac(toolfig) @@ -126,15 +141,44 @@ def prepare_configure_subplots(self): _tool = SubplotTool(self.canvas.figure, toolfig) return canvas - def set_message(self, message): - _macosx.NavigationToolbar2.set_message(self, message.encode('utf-8')) + +class FigureManagerMac(_macosx.FigureManager, FigureManagerBase): + _toolbar2_class = NavigationToolbar2Mac + + def __init__(self, canvas, num): + self._shown = False + _macosx.FigureManager.__init__(self, canvas) + icon_path = str(cbook._get_data_path('images/matplotlib.pdf')) + _macosx.FigureManager.set_icon(icon_path) + FigureManagerBase.__init__(self, canvas, num) + if self.toolbar is not None: + self.toolbar.update() + if mpl.is_interactive(): + self.show() + self.canvas.draw_idle() + + def _close_button_pressed(self): + Gcf.destroy(self) + self.canvas.flush_events() + + @_api.deprecated("3.6") + def close(self): + return self._close_button_pressed() + + @classmethod + def start_main_loop(cls): + _macosx.show() + + def show(self): + if not self._shown: + self._show() + self._shown = True + if mpl.rcParams["figure.raise_window"]: + self._raise() @_Backend.export class _BackendMac(_Backend): FigureCanvas = FigureCanvasMac FigureManager = FigureManagerMac - - @staticmethod - def mainloop(): - _macosx.show() + mainloop = FigureManagerMac.start_main_loop diff --git a/lib/matplotlib/backends/backend_mixed.py b/lib/matplotlib/backends/backend_mixed.py index 54ca8f81ba02..5fadb96a0f73 100644 --- a/lib/matplotlib/backends/backend_mixed.py +++ b/lib/matplotlib/backends/backend_mixed.py @@ -1,8 +1,8 @@ import numpy as np from matplotlib import cbook -from matplotlib.backends.backend_agg import RendererAgg -from matplotlib.tight_bbox import process_figure_for_rasterizing +from .backend_agg import RendererAgg +from matplotlib._tight_bbox import process_figure_for_rasterizing class MixedModeRenderer: @@ -53,7 +53,7 @@ def __init__(self, figure, width, height, dpi, vector_renderer, # the figure dpi before and after the rasterization. Although # this looks ugly, I couldn't find a better solution. -JJL self.figure = figure - self._figdpi = figure.get_dpi() + self._figdpi = figure.dpi self._bbox_inches_restore = bbox_inches_restore @@ -74,7 +74,7 @@ def start_rasterizing(self): `stop_rasterizing` is called) will be drawn with the raster backend. """ # change the dpi of the figure temporarily. - self.figure.set_dpi(self.dpi) + self.figure.dpi = self.dpi if self._bbox_inches_restore: # when tight bbox is used r = process_figure_for_rasterizing(self.figure, self._bbox_inches_restore) @@ -110,7 +110,7 @@ def stop_rasterizing(self): self._raster_renderer = None # restore the figure dpi. - self.figure.set_dpi(self._figdpi) + self.figure.dpi = self._figdpi if self._bbox_inches_restore: # when tight bbox is used r = process_figure_for_rasterizing(self.figure, diff --git a/lib/matplotlib/backends/backend_nbagg.py b/lib/matplotlib/backends/backend_nbagg.py index 0fb8b03ea266..f4ee4b1179f3 100644 --- a/lib/matplotlib/backends/backend_nbagg.py +++ b/lib/matplotlib/backends/backend_nbagg.py @@ -1,4 +1,4 @@ -"""Interactive figures in the IPython notebook""" +"""Interactive figures in the IPython notebook.""" # Note: There is a notebook in # lib/matplotlib/backends/web_backend/nbagg_uat.ipynb to help verify # that changes made maintain expected behaviour. @@ -9,20 +9,16 @@ import pathlib import uuid +from ipykernel.comm import Comm from IPython.display import display, Javascript, HTML -try: - # Jupyter/IPython 4.x or later - from ipykernel.comm import Comm -except ImportError: - # Jupyter/IPython 3.x or earlier - from IPython.kernel.comm import Comm from matplotlib import is_interactive from matplotlib._pylab_helpers import Gcf -from matplotlib.backend_bases import _Backend, NavigationToolbar2 -from matplotlib.backends.backend_webagg_core import ( - FigureCanvasWebAggCore, FigureManagerWebAgg, NavigationToolbar2WebAgg, - TimerTornado) +from matplotlib.backend_bases import _Backend, CloseEvent, NavigationToolbar2 +from .backend_webagg_core import ( + FigureCanvasWebAggCore, FigureManagerWebAgg, NavigationToolbar2WebAgg) +from .backend_webagg_core import ( # noqa: F401 # pylint: disable=W0611 + TimerTornado, TimerAsyncio) def connection_info(): @@ -43,18 +39,13 @@ def connection_info(): return '\n'.join(result) -# Note: Version 3.2 and 4.x icons -# http://fontawesome.io/3.2.1/icons/ -# http://fontawesome.io/ -# the `fa fa-xxx` part targets font-awesome 4, (IPython 3.x) -# the icon-xxx targets font awesome 3.21 (IPython 2.x) -_FONT_AWESOME_CLASSES = { - 'home': 'fa fa-home icon-home', - 'back': 'fa fa-arrow-left icon-arrow-left', - 'forward': 'fa fa-arrow-right icon-arrow-right', - 'zoom_to_rect': 'fa fa-square-o icon-check-empty', - 'move': 'fa fa-arrows icon-move', - 'download': 'fa fa-floppy-o icon-save', +_FONT_AWESOME_CLASSES = { # font-awesome 4 names + 'home': 'fa fa-home', + 'back': 'fa fa-arrow-left', + 'forward': 'fa fa-arrow-right', + 'zoom_to_rect': 'fa fa-square-o', + 'move': 'fa fa-arrows', + 'download': 'fa fa-floppy-o', None: None } @@ -71,12 +62,27 @@ class NavigationIPy(NavigationToolbar2WebAgg): class FigureManagerNbAgg(FigureManagerWebAgg): - ToolbarCls = NavigationIPy + _toolbar2_class = ToolbarCls = NavigationIPy def __init__(self, canvas, num): self._shown = False super().__init__(canvas, num) + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + canvas = canvas_class(figure) + manager = cls(canvas, num) + if is_interactive(): + manager.show() + canvas.draw_idle() + + def destroy(event): + canvas.mpl_disconnect(cid) + Gcf.destroy(manager) + + cid = canvas.mpl_connect('close_event', destroy) + return manager + def display_js(self): # XXX How to do this just once? It has to deal with multiple # browser instances using the same kernel (require.js - but the @@ -90,6 +96,15 @@ def show(self): else: self.canvas.draw_idle() self._shown = True + # plt.figure adds an event which makes the figure in focus the active + # one. Disable this behaviour, as it results in figures being put as + # the active figure after they have been shown, even in non-interactive + # mode. + if hasattr(self, '_cidgcf'): + self.canvas.mpl_disconnect(self._cidgcf) + if not is_interactive(): + from matplotlib._pylab_helpers import Gcf + Gcf.figs.pop(self.num, None) def reshow(self): """ @@ -134,7 +149,7 @@ def clearup_closed(self): if socket.is_open()} if len(self.web_sockets) == 0: - self.canvas.close_event() + CloseEvent("close_event", self.canvas)._process() def remove_comm(self, comm_id): self.web_sockets = {socket for socket in self.web_sockets @@ -142,7 +157,7 @@ def remove_comm(self, comm_id): class FigureCanvasNbAgg(FigureCanvasWebAggCore): - pass + manager_class = FigureManagerNbAgg class CommSocket: @@ -226,42 +241,3 @@ def on_message(self, message): class _BackendNbAgg(_Backend): FigureCanvas = FigureCanvasNbAgg FigureManager = FigureManagerNbAgg - - @staticmethod - def new_figure_manager_given_figure(num, figure): - canvas = FigureCanvasNbAgg(figure) - manager = FigureManagerNbAgg(canvas, num) - if is_interactive(): - manager.show() - figure.canvas.draw_idle() - - def destroy(event): - canvas.mpl_disconnect(cid) - Gcf.destroy(manager) - - cid = canvas.mpl_connect('close_event', destroy) - return manager - - @staticmethod - def show(block=None): - ## TODO: something to do when keyword block==False ? - from matplotlib._pylab_helpers import Gcf - - managers = Gcf.get_all_fig_managers() - if not managers: - return - - interactive = is_interactive() - - for manager in managers: - manager.show() - - # plt.figure adds an event which makes the figure in focus the - # active one. Disable this behaviour, as it results in - # figures being put as the active figure after they have been - # shown, even in non-interactive mode. - if hasattr(manager, '_cidgcf'): - manager.canvas.mpl_disconnect(manager._cidgcf) - - if not interactive: - Gcf.figs.pop(manager.num, None) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 26a12764da38..7bd0afc4567b 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1,10 +1,10 @@ """ -A PDF matplotlib backend -Author: Jouni K Seppänen +A PDF Matplotlib backend. + +Author: Jouni K Seppänen and others. """ import codecs -import collections from datetime import datetime from enum import Enum from functools import total_ordering @@ -13,8 +13,9 @@ import logging import math import os -import re +import string import struct +import sys import time import types import warnings @@ -24,20 +25,17 @@ from PIL import Image import matplotlib as mpl -from matplotlib import _api, _text_layout, cbook +from matplotlib import _api, _text_helpers, _type1font, cbook, dviread from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( - _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - GraphicsContextBase, RendererBase, _no_output_draw) + _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, + RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.figure import Figure -from matplotlib.font_manager import findfont, get_font -from matplotlib.afm import AFM -import matplotlib.type1font as type1font -import matplotlib.dviread as dviread +from matplotlib.font_manager import get_font, fontManager as _fontManager +from matplotlib._afm import AFM from matplotlib.ft2font import (FIXED_WIDTH, ITALIC, LOAD_NO_SCALE, - LOAD_NO_HINTING, KERNING_UNFITTED) -from matplotlib.mathtext import MathTextParser + LOAD_NO_HINTING, KERNING_UNFITTED, FT2Font) from matplotlib.transforms import Affine2D, BboxBase from matplotlib.path import Path from matplotlib.dates import UTC @@ -88,13 +86,18 @@ # TODOs: # -# * encoding of fonts, including mathtext fonts and unicode support +# * encoding of fonts, including mathtext fonts and Unicode support # * TTF support has lots of small TODOs, e.g., how do you know if a font # is serif/sans-serif, or symbolic/non-symbolic? # * draw_quad_mesh +@_api.deprecated("3.6", alternative="a vendored copy of _fill") def fill(strings, linelen=75): + return _fill(strings, linelen=linelen) + + +def _fill(strings, linelen=75): """ Make one string from sequence of strings, with whitespace in between. @@ -115,25 +118,6 @@ def fill(strings, linelen=75): result.append(b' '.join(strings[lasti:])) return b'\n'.join(result) -# PDF strings are supposed to be able to include any eight-bit data, -# except that unbalanced parens and backslashes must be escaped by a -# backslash. However, sf bug #2708559 shows that the carriage return -# character may get read as a newline; these characters correspond to -# \gamma and \Omega in TeX's math font encoding. Escaping them fixes -# the bug. -_string_escape_regex = re.compile(br'([\\()\r\n])') - - -def _string_escape(match): - m = match.group(0) - if m in br'\()': - return b'\\' + m - elif m == b'\n': - return br'\n' - elif m == b'\r': - return br'\r' - assert False - def _create_pdf_info_dict(backend, metadata): """ @@ -246,6 +230,77 @@ def _datetime_to_pdf(d): return r +def _calculate_quad_point_coordinates(x, y, width, height, angle=0): + """ + Calculate the coordinates of rectangle when rotated by angle around x, y + """ + + angle = math.radians(-angle) + sin_angle = math.sin(angle) + cos_angle = math.cos(angle) + a = x + height * sin_angle + b = y + height * cos_angle + c = x + width * cos_angle + height * sin_angle + d = y - width * sin_angle + height * cos_angle + e = x + width * cos_angle + f = y - width * sin_angle + return ((x, y), (e, f), (c, d), (a, b)) + + +def _get_coordinates_of_block(x, y, width, height, angle=0): + """ + Get the coordinates of rotated rectangle and rectangle that covers the + rotated rectangle. + """ + + vertices = _calculate_quad_point_coordinates(x, y, width, + height, angle) + + # Find min and max values for rectangle + # adjust so that QuadPoints is inside Rect + # PDF docs says that QuadPoints should be ignored if any point lies + # outside Rect, but for Acrobat it is enough that QuadPoints is on the + # border of Rect. + + pad = 0.00001 if angle % 90 else 0 + min_x = min(v[0] for v in vertices) - pad + min_y = min(v[1] for v in vertices) - pad + max_x = max(v[0] for v in vertices) + pad + max_y = max(v[1] for v in vertices) + pad + return (tuple(itertools.chain.from_iterable(vertices)), + (min_x, min_y, max_x, max_y)) + + +def _get_link_annotation(gc, x, y, width, height, angle=0): + """ + Create a link annotation object for embedding URLs. + """ + quadpoints, rect = _get_coordinates_of_block(x, y, width, height, angle) + link_annotation = { + 'Type': Name('Annot'), + 'Subtype': Name('Link'), + 'Rect': rect, + 'Border': [0, 0, 0], + 'A': { + 'S': Name('URI'), + 'URI': gc.get_url(), + }, + } + if angle % 90: + # Add QuadPoints + link_annotation['QuadPoints'] = quadpoints + return link_annotation + + +# PDF strings are supposed to be able to include any eight-bit data, except +# that unbalanced parens and backslashes must be escaped by a backslash. +# However, sf bug #2708559 shows that the carriage return character may get +# read as a newline; these characters correspond to \gamma and \Omega in TeX's +# math font encoding. Escaping them fixes the bug. +_str_escapes = str.maketrans({ + '\\': '\\\\', '(': '\\(', ')': '\\)', '\n': '\\n', '\r': '\\r'}) + + def pdfRepr(obj): """Map Python objects to PDF syntax.""" @@ -271,38 +326,36 @@ def pdfRepr(obj): elif isinstance(obj, (int, np.integer)): return b"%d" % obj - # Unicode strings are encoded in UTF-16BE with byte-order mark. + # Non-ASCII Unicode strings are encoded in UTF-16BE with byte-order mark. elif isinstance(obj, str): - try: - # But maybe it's really ASCII? - s = obj.encode('ASCII') - return pdfRepr(s) - except UnicodeEncodeError: - s = codecs.BOM_UTF16_BE + obj.encode('UTF-16BE') - return pdfRepr(s) + return pdfRepr(obj.encode('ascii') if obj.isascii() + else codecs.BOM_UTF16_BE + obj.encode('UTF-16BE')) # Strings are written in parentheses, with backslashes and parens # escaped. Actually balanced parens are allowed, but it is # simpler to escape them all. TODO: cut long strings into lines; # I believe there is some maximum line length in PDF. + # Despite the extra decode/encode, translate is faster than regex. elif isinstance(obj, bytes): - return b'(' + _string_escape_regex.sub(_string_escape, obj) + b')' + return ( + b'(' + + obj.decode('latin-1').translate(_str_escapes).encode('latin-1') + + b')') # Dictionaries. The keys must be PDF names, so if we find strings # there, we make Name objects from them. The values may be # anything, so the caller must ensure that PDF names are # represented as Name objects. elif isinstance(obj, dict): - return fill([ + return _fill([ b"<<", - *[Name(key).pdfRepr() + b" " + pdfRepr(obj[key]) - for key in sorted(obj)], + *[Name(k).pdfRepr() + b" " + pdfRepr(v) for k, v in obj.items()], b">>", ]) # Lists. elif isinstance(obj, (list, tuple)): - return fill([b"[", *[pdfRepr(val) for val in obj], b"]"]) + return _fill([b"[", *[pdfRepr(val) for val in obj], b"]"]) # The null keyword. elif obj is None: @@ -314,13 +367,28 @@ def pdfRepr(obj): # A bounding box elif isinstance(obj, BboxBase): - return fill([pdfRepr(val) for val in obj.bounds]) + return _fill([pdfRepr(val) for val in obj.bounds]) else: raise TypeError("Don't know a PDF representation for {} objects" .format(type(obj))) +def _font_supports_glyph(fonttype, glyph): + """ + Returns True if the font is able to provide codepoint *glyph* in a PDF. + + For a Type 3 font, this method returns True only for single-byte + characters. For Type 42 fonts this method return True if the character is + from the Basic Multilingual Plane. + """ + if fonttype == 3: + return glyph <= 255 + if fonttype == 42: + return glyph <= 65535 + raise NotImplementedError() + + class Reference: """ PDF reference object. @@ -348,7 +416,8 @@ def write(self, contents, file): class Name: """PDF name object.""" __slots__ = ('name',) - _regex = re.compile(r'[^!-~]') + _hexify = {c: '#%02x' % c + for c in {*range(256)} - {*range(ord('!'), ord('~') + 1)}} def __init__(self, name): if isinstance(name, Name): @@ -356,13 +425,13 @@ def __init__(self, name): else: if isinstance(name, bytes): name = name.decode('ascii') - self.name = self._regex.sub(Name.hexify, name).encode('ascii') + self.name = name.translate(self._hexify).encode('ascii') def __repr__(self): return "" % self.name def __str__(self): - return '/' + str(self.name) + return '/' + self.name.decode('ascii') def __eq__(self, other): return isinstance(other, Name) and self.name == other.name @@ -374,6 +443,7 @@ def __hash__(self): return hash(self.name) @staticmethod + @_api.deprecated("3.6") def hexify(match): return '#%02x' % ord(match.group()) @@ -381,8 +451,8 @@ def pdfRepr(self): return b'/' + self.name +@_api.deprecated("3.6") class Operator: - """PDF operator object.""" __slots__ = ('op',) def __init__(self, op): @@ -404,46 +474,52 @@ def pdfRepr(self): return self._x -# PDF operators (not an exhaustive list) -class Op(Operator, Enum): +class Op(Enum): + """PDF operators (not an exhaustive list).""" + close_fill_stroke = b'b' fill_stroke = b'B' fill = b'f' - closepath = b'h', + closepath = b'h' close_stroke = b's' stroke = b'S' endpath = b'n' - begin_text = b'BT', + begin_text = b'BT' end_text = b'ET' curveto = b'c' rectangle = b're' lineto = b'l' - moveto = b'm', + moveto = b'm' concat_matrix = b'cm' use_xobject = b'Do' - setgray_stroke = b'G', + setgray_stroke = b'G' setgray_nonstroke = b'g' setrgb_stroke = b'RG' - setrgb_nonstroke = b'rg', + setrgb_nonstroke = b'rg' setcolorspace_stroke = b'CS' - setcolorspace_nonstroke = b'cs', + setcolorspace_nonstroke = b'cs' setcolor_stroke = b'SCN' setcolor_nonstroke = b'scn' - setdash = b'd', + setdash = b'd' setlinejoin = b'j' setlinecap = b'J' setgstate = b'gs' - gsave = b'q', + gsave = b'q' grestore = b'Q' textpos = b'Td' selectfont = b'Tf' - textmatrix = b'Tm', + textmatrix = b'Tm' show = b'Tj' showkern = b'TJ' setlinewidth = b'w' clip = b'W' shading = b'sh' + op = _api.deprecated('3.6')(property(lambda self: self.value)) + + def pdfRepr(self): + return self.value + @classmethod def paint_path(cls, fill, stroke): """ @@ -681,15 +757,14 @@ def __init__(self, filename, metadata=None): self._soft_mask_states = {} self._soft_mask_seq = (Name(f'SM{i}') for i in itertools.count(1)) self._soft_mask_groups = [] - # reproducible writeHatches needs an ordered dict: - self.hatchPatterns = collections.OrderedDict() + self.hatchPatterns = {} self._hatch_pattern_seq = (Name(f'H{i}') for i in itertools.count(1)) self.gouraudTriangles = [] - self._images = collections.OrderedDict() # reproducible writeImages + self._images = {} self._image_seq = (Name(f'I{i}') for i in itertools.count(1)) - self.markers = collections.OrderedDict() # reproducible writeMarkers + self.markers = {} self.multi_byte_charprocs = {} self.paths = [] @@ -716,11 +791,6 @@ def __init__(self, filename, metadata=None): 'ProcSet': procsets} self.writeObject(self.resourceObject, resources) - @_api.deprecated("3.3") - @property - def used_characters(self): - return self.file._character_tracker.used_characters - def newPage(self, width, height): self.endStream() @@ -732,9 +802,6 @@ def newPage(self, width, height): 'Resources': self.resourceObject, 'MediaBox': [0, 0, 72 * width, 72 * height], 'Contents': contentObject, - 'Group': {'Type': Name('Group'), - 'S': Name('Transparency'), - 'CS': Name('DeviceRGB')}, 'Annots': annotsObject, } pageObject = self.reserveObject('page') @@ -744,8 +811,10 @@ def newPage(self, width, height): self.beginStream(contentObject.id, self.reserveObject('length of content stream')) - # Initialize the pdf graphics state to match the default mpl - # graphics context: currently only the join style needs to be set + # Initialize the pdf graphics state to match the default Matplotlib + # graphics context (colorspace and joinstyle). + self.output(Name('DeviceRGB'), Op.setcolorspace_stroke) + self.output(Name('DeviceRGB'), Op.setcolorspace_nonstroke) self.output(GraphicsContextPdf.joinstyles['round'], Op.setlinejoin) # Clear the list of annotations for the next page @@ -760,6 +829,22 @@ def newTextnote(self, text, positionRect=[-100, -100, 0, 0]): } self.pageAnnotations.append(theNote) + def _get_subsetted_psname(self, ps_name, charmap): + def toStr(n, base): + if n < base: + return string.ascii_uppercase[n] + else: + return ( + toStr(n // base, base) + string.ascii_uppercase[n % base] + ) + + # encode to string using base 26 + hashed = hash(frozenset(charmap.keys())) % ((sys.maxsize + 1) * 2) + prefix = toStr(hashed, 26) + + # get first 6 characters from prefix + return prefix[:6] + "+" + ps_name + def finalize(self): """Write out the various deferred objects and the pdf end matter.""" @@ -811,7 +896,7 @@ def write(self, data): self.currentstream.write(data) def output(self, *data): - self.write(fill([pdfRepr(x) for x in data])) + self.write(_fill([pdfRepr(x) for x in data])) self.write(b'\n') def beginStream(self, id, len, extra=None, png=None): @@ -823,6 +908,11 @@ def endStream(self): self.currentstream.end() self.currentstream = None + def outputStream(self, ref, data, *, extra=None): + self.beginStream(ref.id, None, extra) + self.currentstream.write(data) + self.endStream() + def _write_annotations(self): for annotsObject, annotations in self._annotations: self.writeObject(annotsObject, annotations) @@ -835,20 +925,28 @@ def fontName(self, fontprop): """ if isinstance(fontprop, str): - filename = fontprop + filenames = [fontprop] elif mpl.rcParams['pdf.use14corefonts']: - filename = findfont( - fontprop, fontext='afm', directory=RendererPdf._afm_font_dir) + filenames = _fontManager._find_fonts_by_props( + fontprop, fontext='afm', directory=RendererPdf._afm_font_dir + ) else: - filename = findfont(fontprop) - - Fx = self.fontNames.get(filename) - if Fx is None: - Fx = next(self._internal_font_seq) - self.fontNames[filename] = Fx - _log.debug('Assigning font %s = %r', Fx, filename) - - return Fx + filenames = _fontManager._find_fonts_by_props(fontprop) + first_Fx = None + for fname in filenames: + Fx = self.fontNames.get(fname) + if not first_Fx: + first_Fx = Fx + if Fx is None: + Fx = next(self._internal_font_seq) + self.fontNames[fname] = Fx + _log.debug('Assigning font %s = %r', Fx, fname) + if not first_Fx: + first_Fx = Fx + + # find_fontsprop's first value always adheres to + # findfont's value, so technically no behaviour change + return first_Fx def dviFontName(self, dvifont): """ @@ -861,7 +959,7 @@ def dviFontName(self, dvifont): if dvi_info is not None: return dvi_info.pdfname - tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) + tex_font_map = dviread.PsfontsMap(dviread._find_tex_file('pdftex.map')) psfont = tex_font_map[dvifont.texname] if psfont.filename is None: raise ValueError( @@ -952,7 +1050,7 @@ def _embedTeXFont(self, fontinfo): return fontdictObject # We have a font file to embed - read it in and apply any effects - t1font = type1font.Type1Font(fontinfo.fontfile) + t1font = _type1font.Type1Font(fontinfo.fontfile) if fontinfo.effects: t1font = t1font.transform(fontinfo.effects) fontdict['BaseFont'] = Name(t1font.prop['FontName']) @@ -1027,22 +1125,19 @@ def createType1Descriptor(self, t1font, fontfile): self.writeObject(fontdescObject, descriptor) - self.beginStream(fontfileObject.id, None, - {'Length1': len(t1font.parts[0]), - 'Length2': len(t1font.parts[1]), - 'Length3': 0}) - self.currentstream.write(t1font.parts[0]) - self.currentstream.write(t1font.parts[1]) - self.endStream() + self.outputStream(fontfileObject, b"".join(t1font.parts[:2]), + extra={'Length1': len(t1font.parts[0]), + 'Length2': len(t1font.parts[1]), + 'Length3': 0}) return fontdescObject - def _get_xobject_symbol_name(self, filename, symbol_name): + def _get_xobject_glyph_name(self, filename, glyph_name): Fx = self.fontName(filename) return "-".join([ Fx.name.decode(), os.path.splitext(os.path.basename(filename))[0], - symbol_name]) + glyph_name]) _identityToUnicodeCMap = b"""/CIDInit /ProcSet findresource begin 12 dict begin @@ -1117,7 +1212,6 @@ def get_char_width(charcode): width = font.load_char( s, flags=LOAD_NO_SCALE | LOAD_NO_HINTING).horiAdvance return cvt(width) - with warnings.catch_warnings(): # Ignore 'Required glyph missing from current font' warning # from ft2font: here we're just building the widths table, but @@ -1156,13 +1250,13 @@ def get_char_width(charcode): charprocs = {} for charname in sorted(rawcharprocs): stream = rawcharprocs[charname] - charprocDict = {'Length': len(stream)} + charprocDict = {} # The 2-byte characters are used as XObjects, so they # need extra info in their dictionary if charname in multi_byte_chars: - charprocDict['Type'] = Name('XObject') - charprocDict['Subtype'] = Name('Form') - charprocDict['BBox'] = bbox + charprocDict = {'Type': Name('XObject'), + 'Subtype': Name('Form'), + 'BBox': bbox} # Each glyph includes bounding box information, # but xpdf and ghostscript can't handle it in a # Form XObject (they segfault!!!), so we remove it @@ -1171,14 +1265,12 @@ def get_char_width(charcode): # value. stream = stream[stream.find(b"d1") + 2:] charprocObject = self.reserveObject('charProc') - self.beginStream(charprocObject.id, None, charprocDict) - self.currentstream.write(stream) - self.endStream() + self.outputStream(charprocObject, stream, extra=charprocDict) # Send the glyphs with ccode > 255 to the XObject dictionary, # and the others to the font itself if charname in multi_byte_chars: - name = self._get_xobject_symbol_name(filename, charname) + name = self._get_xobject_glyph_name(filename, charname) self.multi_byte_charprocs[name] = charprocObject else: charprocs[charname] = charprocObject @@ -1201,6 +1293,22 @@ def embedTTFType42(font, characters, descriptor): wObject = self.reserveObject('Type 0 widths') toUnicodeMapObject = self.reserveObject('ToUnicode map') + subset_str = "".join(chr(c) for c in characters) + _log.debug("SUBSET %s characters: %s", filename, subset_str) + fontdata = _backend_pdf_ps.get_glyphs_subset(filename, subset_str) + _log.debug( + "SUBSET %s %d -> %d", filename, + os.stat(filename).st_size, fontdata.getbuffer().nbytes + ) + + # We need this ref for XObjects + full_font = font + + # reload the font object from the subset + # (all the necessary data could probably be obtained directly + # using fontLib.ttLib) + font = FT2Font(fontdata) + cidFontDict = { 'Type': Name('Font'), 'Subtype': Name('CIDFontType2'), @@ -1225,21 +1333,9 @@ def embedTTFType42(font, characters, descriptor): # Make fontfile stream descriptor['FontFile2'] = fontfileObject - length1Object = self.reserveObject('decoded length of a font') - self.beginStream( - fontfileObject.id, - self.reserveObject('length of font stream'), - {'Length1': length1Object}) - with open(filename, 'rb') as fontfile: - length1 = 0 - while True: - data = fontfile.read(4096) - if not data: - break - length1 += len(data) - self.currentstream.write(data) - self.endStream() - self.writeObject(length1Object, length1) + self.outputStream( + fontfileObject, fontdata.getvalue(), + extra={'Length1': fontdata.getbuffer().nbytes}) # Make the 'W' (Widths) array, CidToGidMap and ToUnicode CMap # at the same time @@ -1275,6 +1371,11 @@ def embedTTFType42(font, characters, descriptor): unicode_bfrange = [] for start, end in unicode_groups: + # Ensure the CID map contains only chars from BMP + if start > 65535: + continue + end = min(65535, end) + unicode_bfrange.append( b"<%04x> <%04x> [%s]" % (start, end, @@ -1282,20 +1383,39 @@ def embedTTFType42(font, characters, descriptor): unicode_cmap = (self._identityToUnicodeCMap % (len(unicode_groups), b"\n".join(unicode_bfrange))) + # Add XObjects for unsupported chars + glyph_ids = [] + for ccode in characters: + if not _font_supports_glyph(fonttype, ccode): + gind = full_font.get_char_index(ccode) + glyph_ids.append(gind) + + bbox = [cvt(x, nearest=False) for x in full_font.bbox] + rawcharprocs = _get_pdf_charprocs(filename, glyph_ids) + for charname in sorted(rawcharprocs): + stream = rawcharprocs[charname] + charprocDict = {'Type': Name('XObject'), + 'Subtype': Name('Form'), + 'BBox': bbox} + # Each glyph includes bounding box information, + # but xpdf and ghostscript can't handle it in a + # Form XObject (they segfault!!!), so we remove it + # from the stream here. It's not needed anyway, + # since the Form XObject includes it in its BBox + # value. + stream = stream[stream.find(b"d1") + 2:] + charprocObject = self.reserveObject('charProc') + self.outputStream(charprocObject, stream, extra=charprocDict) + + name = self._get_xobject_glyph_name(filename, charname) + self.multi_byte_charprocs[name] = charprocObject + # CIDToGIDMap stream cid_to_gid_map = "".join(cid_to_gid_map).encode("utf-16be") - self.beginStream(cidToGidMapObject.id, - None, - {'Length': len(cid_to_gid_map)}) - self.currentstream.write(cid_to_gid_map) - self.endStream() + self.outputStream(cidToGidMapObject, cid_to_gid_map) # ToUnicode CMap - self.beginStream(toUnicodeMapObject.id, - None, - {'Length': unicode_cmap}) - self.currentstream.write(unicode_cmap) - self.endStream() + self.outputStream(toUnicodeMapObject, unicode_cmap) descriptor['MaxWidth'] = max_width @@ -1309,7 +1429,11 @@ def embedTTFType42(font, characters, descriptor): # Beginning of main embedTTF function... - ps_name = font.postscript_name.encode('ascii', 'replace') + ps_name = self._get_subsetted_psname( + font.postscript_name, + font.get_charmap() + ) + ps_name = ps_name.encode('ascii', 'replace') ps_name = Name(ps_name) pclt = font.get_sfnt_table('pclt') or {'capHeight': 0, 'xHeight': 0} post = font.get_sfnt_table('post') or {'italicAngle': (0, 0)} @@ -1469,7 +1593,7 @@ def writeHatches(self): # Change origin to match Agg at top-left. 'Matrix': [1, 0, 0, 1, 0, self.height * 72]}) - stroke_rgb, fill_rgb, path = hatch_style + stroke_rgb, fill_rgb, hatch = hatch_style self.output(stroke_rgb[0], stroke_rgb[1], stroke_rgb[2], Op.setrgb_stroke) if fill_rgb is not None: @@ -1481,7 +1605,7 @@ def writeHatches(self): self.output(mpl.rcParams['hatch.linewidth'], Op.setlinewidth) self.output(*self.pathOperations( - Path.hatch(path), + Path.hatch(hatch), Affine2D().scale(sidelen), simplify=False)) self.output(Op.fill_stroke) @@ -1646,16 +1770,20 @@ def _writeImg(self, data, id, smask=None): # Convert to indexed color if there are 256 colors or fewer # This can significantly reduce the file size num_colors = len(img_colors) - img = img.convert(mode='P', dither=Image.NONE, - palette=Image.ADAPTIVE, colors=num_colors) + # These constants were converted to IntEnums and deprecated in + # Pillow 9.2 + dither = getattr(Image, 'Dither', Image).NONE + pmode = getattr(Image, 'Palette', Image).ADAPTIVE + img = img.convert( + mode='P', dither=dither, palette=pmode, colors=num_colors + ) png_data, bit_depth, palette = self._writePng(img) if bit_depth is None or palette is None: raise RuntimeError("invalid PNG header") palette = palette[:num_colors * 3] # Trim padding - palette = pdfRepr(palette) - obj['ColorSpace'] = Verbatim(b'[/Indexed /DeviceRGB ' - + str(num_colors - 1).encode() - + b' ' + palette + b']') + obj['ColorSpace'] = Verbatim( + b'[/Indexed /DeviceRGB %d %s]' + % (num_colors - 1, pdfRepr(palette))) obj['BitsPerComponent'] = bit_depth color_channels = 1 else: @@ -1770,7 +1898,8 @@ def pathOperations(path, transform, clip=None, simplify=None, sketch=None): return [Verbatim(_path.convert_to_string( path, transform, clip, simplify, sketch, 6, - [Op.moveto.op, Op.lineto.op, b'', Op.curveto.op, Op.closepath.op], + [Op.moveto.value, Op.lineto.value, b'', Op.curveto.value, + Op.closepath.value], True))] def writePath(self, path, transform, clip=False, sketch=None): @@ -1844,11 +1973,6 @@ def __init__(self, file, image_dpi, height, width): self.gc = self.new_gc() self.image_dpi = image_dpi - @_api.deprecated("3.4") - @property - def mathtext_parser(self): - return MathTextParser("Pdf") - def finalize(self): self.file.output(*self.gc.finalize()) @@ -1879,15 +2003,6 @@ def check_gc(self, gc, fillcolor=None): gc._fillcolor = orig_fill gc._effective_alphas = orig_alphas - @_api.deprecated("3.3") - def track_characters(self, *args, **kwargs): - """Keep track of which characters are required from each font.""" - self.file._character_tracker.track(*args, **kwargs) - - @_api.deprecated("3.3") - def merge_used_characters(self, *args, **kwargs): - self.file._character_tracker.merge(*args, **kwargs) - def get_image_magnification(self): return self.image_dpi/72.0 @@ -1931,7 +2046,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): self.file.output(self.gc.paint()) def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offsetTrans, facecolors, edgecolors, + offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): # We can only reuse the objects if the presence of fill and @@ -1973,7 +2088,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, if (not can_do_optimization) or (not should_do_optimization): return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, - offsets, offsetTrans, facecolors, edgecolors, + offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) @@ -1989,8 +2104,8 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, output(*self.gc.push()) lastx, lasty = 0, 0 for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, master_transform, all_transforms, path_codes, offsets, - offsetTrans, facecolors, edgecolors, linewidths, linestyles, + gc, path_codes, offsets, offset_trans, + facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): self.check_gc(gc0, rgbFace) @@ -2098,17 +2213,8 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self._text2path.mathtext_parser.parse(s, 72, prop) if gc.get_url() is not None: - link_annotation = { - 'Type': Name('Annot'), - 'Subtype': Name('Link'), - 'Rect': (x, y, x + width, y + height), - 'Border': [0, 0, 0], - 'A': { - 'S': Name('URI'), - 'URI': gc.get_url(), - }, - } - self.file._annotations[-1][1].append(link_annotation) + self.file._annotations[-1][1].append(_get_link_annotation( + gc, x, y, width, height, angle)) fonttype = mpl.rcParams['pdf.fonttype'] @@ -2122,16 +2228,16 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): self.check_gc(gc, gc._rgb) prev_font = None, None oldx, oldy = 0, 0 - type3_multibytes = [] + unsupported_chars = [] self.file.output(Op.begin_text) for font, fontsize, num, ox, oy in glyphs: - self.file._character_tracker.track(font, chr(num)) + self.file._character_tracker.track_glyph(font, num) fontname = font.fname - if fonttype == 3 and num > 255: - # For Type3 fonts, multibyte characters must be emitted - # separately (below). - type3_multibytes.append((font, fontsize, ox, oy, num)) + if not _font_supports_glyph(fonttype, num): + # Unsupported chars (i.e. multibyte in Type 3 or beyond BMP in + # Type 42) must be emitted separately (below). + unsupported_chars.append((font, fontsize, ox, oy, num)) else: self._setup_textpos(ox, oy, 0, oldx, oldy) oldx, oldy = ox, oy @@ -2143,7 +2249,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): Op.show) self.file.output(Op.end_text) - for font, fontsize, ox, oy, num in type3_multibytes: + for font, fontsize, ox, oy, num in unsupported_chars: self._draw_xobject_glyph( font, fontsize, font.get_char_index(num), ox, oy) @@ -2155,8 +2261,7 @@ def draw_mathtext(self, gc, x, y, s, prop, angle): # Pop off the global transformation self.file.output(Op.grestore) - @_api.delete_parameter("3.3", "ismath") - def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): + def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): # docstring inherited texmanager = self.get_texmanager() fontsize = prop.get_size_in_points() @@ -2165,17 +2270,8 @@ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): page, = dvi if gc.get_url() is not None: - link_annotation = { - 'Type': Name('Annot'), - 'Subtype': Name('Link'), - 'Rect': (x, y, x + page.width, y + page.height), - 'Border': [0, 0, 0], - 'A': { - 'S': Name('URI'), - 'URI': gc.get_url(), - }, - } - self.file._annotations[-1][1].append(link_annotation) + self.file._annotations[-1][1].append(_get_link_annotation( + gc, x, y, page.width, page.height, angle)) # Gather font information and do some setup for combining # characters into strings. The variable seq will contain a @@ -2275,55 +2371,52 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if gc.get_url() is not None: font.set_text(s) width, height = font.get_width_height() - link_annotation = { - 'Type': Name('Annot'), - 'Subtype': Name('Link'), - 'Rect': (x, y, x + width / 64, y + height / 64), - 'Border': [0, 0, 0], - 'A': { - 'S': Name('URI'), - 'URI': gc.get_url(), - }, - } - self.file._annotations[-1][1].append(link_annotation) + self.file._annotations[-1][1].append(_get_link_annotation( + gc, x, y, width / 64, height / 64, angle)) - # If fonttype != 3 emit the whole string at once without manual - # kerning. - if fonttype != 3: + # If fonttype is neither 3 nor 42, emit the whole string at once + # without manual kerning. + if fonttype not in [3, 42]: self.file.output(Op.begin_text, self.file.fontName(prop), fontsize, Op.selectfont) self._setup_textpos(x, y, angle) self.file.output(self.encode_string(s, fonttype), Op.show, Op.end_text) - # There is no way to access multibyte characters of Type 3 fonts, as - # they cannot have a CIDMap. Therefore, in this case we break the - # string into chunks, where each chunk contains either a string of - # consecutive 1-byte characters or a single multibyte character. - # A sequence of 1-byte characters is broken into multiple chunks to - # adjust the kerning between adjacent chunks. Each chunk is emitted - # with a separate command: 1-byte characters use the regular text show - # command (TJ) with appropriate kerning between chunks, whereas - # multibyte characters use the XObject command (Do). (If using Type - # 42 fonts, all of this complication is avoided, but of course, - # subsetting those fonts is complex/hard to implement.) + # A sequence of characters is broken into multiple chunks. The chunking + # serves two purposes: + # - For Type 3 fonts, there is no way to access multibyte characters, + # as they cannot have a CIDMap. Therefore, in this case we break + # the string into chunks, where each chunk contains either a string + # of consecutive 1-byte characters or a single multibyte character. + # - A sequence of 1-byte characters is split into chunks to allow for + # kerning adjustments between consecutive chunks. + # + # Each chunk is emitted with a separate command: 1-byte characters use + # the regular text show command (TJ) with appropriate kerning between + # chunks, whereas multibyte characters use the XObject command (Do). else: - # List of (start_x, [prev_kern, char, char, ...]), w/o zero kerns. + # List of (ft_object, start_x, [prev_kern, char, char, ...]), + # w/o zero kerns. singlebyte_chunks = [] - # List of (start_x, glyph_index). + # List of (ft_object, start_x, glyph_index). multibyte_glyphs = [] prev_was_multibyte = True - for item in _text_layout.layout( + prev_font = font + for item in _text_helpers.layout( s, font, kern_mode=KERNING_UNFITTED): - if ord(item.char) <= 255: - if prev_was_multibyte: - singlebyte_chunks.append((item.x, [])) + if _font_supports_glyph(fonttype, ord(item.char)): + if prev_was_multibyte or item.ft_object != prev_font: + singlebyte_chunks.append((item.ft_object, item.x, [])) + prev_font = item.ft_object if item.prev_kern: - singlebyte_chunks[-1][1].append(item.prev_kern) - singlebyte_chunks[-1][1].append(item.char) + singlebyte_chunks[-1][2].append(item.prev_kern) + singlebyte_chunks[-1][2].append(item.char) prev_was_multibyte = False else: - multibyte_glyphs.append((item.x, item.glyph_idx)) + multibyte_glyphs.append( + (item.ft_object, item.x, item.glyph_idx) + ) prev_was_multibyte = True # Do the rotation and global translation as a single matrix # concatenation up front @@ -2333,10 +2426,12 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): -math.sin(a), math.cos(a), x, y, Op.concat_matrix) # Emit all the 1-byte characters in a BT/ET group. - self.file.output(Op.begin_text, - self.file.fontName(prop), fontsize, Op.selectfont) + + self.file.output(Op.begin_text) prev_start_x = 0 - for start_x, kerns_or_chars in singlebyte_chunks: + for ft_object, start_x, kerns_or_chars in singlebyte_chunks: + ft_name = self.file.fontName(ft_object.fname) + self.file.output(ft_name, fontsize, Op.selectfont) self._setup_textpos(start_x, 0, 0, prev_start_x, 0, 0) self.file.output( # See pdf spec "Text space details" for the 1000/fontsize @@ -2348,14 +2443,16 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): prev_start_x = start_x self.file.output(Op.end_text) # Then emit all the multibyte characters, one at a time. - for start_x, glyph_idx in multibyte_glyphs: - self._draw_xobject_glyph(font, fontsize, glyph_idx, start_x, 0) + for ft_object, start_x, glyph_idx in multibyte_glyphs: + self._draw_xobject_glyph( + ft_object, fontsize, glyph_idx, start_x, 0 + ) self.file.output(Op.grestore) def _draw_xobject_glyph(self, font, fontsize, glyph_idx, x, y): """Draw a multibyte character from a Type 3 font as an XObject.""" - symbol_name = font.get_glyph_name(glyph_idx) - name = self.file._get_xobject_symbol_name(font.fname, symbol_name) + glyph_name = font.get_glyph_name(glyph_idx) + name = self.file._get_xobject_glyph_name(font.fname, glyph_name) self.file.output( Op.gsave, 0.001 * fontsize, 0, 0, 0.001 * fontsize, x, y, Op.concat_matrix, @@ -2517,7 +2614,7 @@ def clip_cmd(self, cliprect, clippath): def delta(self, other): """ Copy properties of other into self and return PDF commands - needed to transform self into other. + needed to transform *self* into *other*. """ cmds = [] fill_performed = False @@ -2657,11 +2754,8 @@ def savefig(self, figure=None, **kwargs): Parameters ---------- - figure : `.Figure` or int, optional - Specifies what figure is saved to file. If not specified, the - active figure is saved. If a `.Figure` instance is provided, this - figure is saved. If an int is specified, the figure instance to - save is looked up by number. + figure : `.Figure` or int, default: the active figure + The figure, or index of the figure, that is saved to the file. """ if not isinstance(figure, Figure): if figure is None: @@ -2702,15 +2796,11 @@ class FigureCanvasPdf(FigureCanvasBase): def get_default_filetype(self): return 'pdf' - @_check_savefig_extra_args - @_api.delete_parameter("3.4", "dpi") def print_pdf(self, filename, *, - dpi=None, # dpi to use for images bbox_inches_restore=None, metadata=None): - if dpi is None: # always use this branch after deprecation elapses. - dpi = self.figure.get_dpi() - self.figure.set_dpi(72) # there are 72 pdf points to an inch + dpi = self.figure.dpi + self.figure.dpi = 72 # there are 72 pdf points to an inch width, height = self.figure.get_size_inches() if isinstance(filename, PdfPages): file = filename._file @@ -2733,7 +2823,7 @@ def print_pdf(self, filename, *, file.close() def draw(self): - _no_output_draw(self.figure) + self.figure.draw_without_rendering() return super().draw() diff --git a/lib/matplotlib/backends/backend_pgf.py b/lib/matplotlib/backends/backend_pgf.py index 3139d0fdf8d6..7312f300a57b 100644 --- a/lib/matplotlib/backends/backend_pgf.py +++ b/lib/matplotlib/backends/backend_pgf.py @@ -1,4 +1,3 @@ -import atexit import codecs import datetime import functools @@ -18,8 +17,7 @@ import matplotlib as mpl from matplotlib import _api, cbook, font_manager as fm from matplotlib.backend_bases import ( - _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - GraphicsContextBase, RendererBase, _no_output_draw + _Backend, FigureCanvasBase, FigureManagerBase, RendererBase ) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.backends.backend_pdf import ( @@ -36,58 +34,70 @@ # which is not recognized by TeX. -def get_fontspec(): - """Build fontspec preamble from rc.""" - latex_fontspec = [] - texcommand = mpl.rcParams["pgf.texsystem"] +@_api.caching_module_getattr +class __getattr__: + NO_ESCAPE = _api.deprecated("3.6", obj_type="")( + property(lambda self: _NO_ESCAPE)) + re_mathsep = _api.deprecated("3.6", obj_type="")( + property(lambda self: _split_math.__self__)) - if texcommand != "pdflatex": - latex_fontspec.append("\\usepackage{fontspec}") - if texcommand != "pdflatex" and mpl.rcParams["pgf.rcfonts"]: - families = ["serif", "sans\\-serif", "monospace"] - commands = ["setmainfont", "setsansfont", "setmonofont"] - for family, command in zip(families, commands): - # 1) Forward slashes also work on Windows, so don't mess with - # backslashes. 2) The dirname needs to include a separator. - path = pathlib.Path(fm.findfont(family)) - latex_fontspec.append(r"\%s{%s}[Path=\detokenize{%s}]" % ( - command, path.name, path.parent.as_posix() + "/")) - - return "\n".join(latex_fontspec) +@_api.deprecated("3.6") +def get_fontspec(): + """Build fontspec preamble from rc.""" + with mpl.rc_context({"pgf.preamble": ""}): + return _get_preamble() +@_api.deprecated("3.6") def get_preamble(): """Get LaTeX preamble from rc.""" return mpl.rcParams["pgf.preamble"] -############################################################################### -# This almost made me cry!!! -# In the end, it's better to use only one unit for all coordinates, since the +def _get_preamble(): + """Prepare a LaTeX preamble based on the rcParams configuration.""" + preamble = [mpl.rcParams["pgf.preamble"]] + if mpl.rcParams["pgf.texsystem"] != "pdflatex": + preamble.append("\\usepackage{fontspec}") + if mpl.rcParams["pgf.rcfonts"]: + families = ["serif", "sans\\-serif", "monospace"] + commands = ["setmainfont", "setsansfont", "setmonofont"] + for family, command in zip(families, commands): + # 1) Forward slashes also work on Windows, so don't mess with + # backslashes. 2) The dirname needs to include a separator. + path = pathlib.Path(fm.findfont(family)) + preamble.append(r"\%s{%s}[Path=\detokenize{%s/}]" % ( + command, path.name, path.parent.as_posix())) + preamble.append(mpl.texmanager._usepackage_if_not_loaded( + "underscore", option="strings")) # Documented as "must come last". + return "\n".join(preamble) + + +# It's better to use only one unit for all coordinates, since the # arithmetic in latex seems to produce inaccurate conversions. latex_pt_to_in = 1. / 72.27 latex_in_to_pt = 1. / latex_pt_to_in mpl_pt_to_in = 1. / 72. mpl_in_to_pt = 1. / mpl_pt_to_in -############################################################################### -# helper functions - -NO_ESCAPE = r"(? 3 else 1.0 if has_fill: - writeln(self.fh, - r"\definecolor{currentfill}{rgb}{%f,%f,%f}" - % tuple(rgbFace[:3])) - writeln(self.fh, r"\pgfsetfillcolor{currentfill}") + _writeln(self.fh, + r"\definecolor{currentfill}{rgb}{%f,%f,%f}" + % tuple(rgbFace[:3])) + _writeln(self.fh, r"\pgfsetfillcolor{currentfill}") if has_fill and fillopacity != 1.0: - writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity) + _writeln(self.fh, r"\pgfsetfillopacity{%f}" % fillopacity) # linewidth and color lw = gc.get_linewidth() * mpl_pt_to_in * latex_in_to_pt stroke_rgba = gc.get_rgb() - writeln(self.fh, r"\pgfsetlinewidth{%fpt}" % lw) - writeln(self.fh, - r"\definecolor{currentstroke}{rgb}{%f,%f,%f}" - % stroke_rgba[:3]) - writeln(self.fh, r"\pgfsetstrokecolor{currentstroke}") + _writeln(self.fh, r"\pgfsetlinewidth{%fpt}" % lw) + _writeln(self.fh, + r"\definecolor{currentstroke}{rgb}{%f,%f,%f}" + % stroke_rgba[:3]) + _writeln(self.fh, r"\pgfsetstrokecolor{currentstroke}") if strokeopacity != 1.0: - writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % strokeopacity) + _writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % strokeopacity) # line style dash_offset, dash_list = gc.get_dashes() if dash_list is None: - writeln(self.fh, r"\pgfsetdash{}{0pt}") + _writeln(self.fh, r"\pgfsetdash{}{0pt}") else: - writeln(self.fh, - r"\pgfsetdash{%s}{%fpt}" - % ("".join(r"{%fpt}" % dash for dash in dash_list), - dash_offset)) + _writeln(self.fh, + r"\pgfsetdash{%s}{%fpt}" + % ("".join(r"{%fpt}" % dash for dash in dash_list), + dash_offset)) def _print_pgf_path(self, gc, path, transform, rgbFace=None): f = 1. / self.dpi # check for clip box / ignore clip for filled paths bbox = gc.get_clip_rectangle() if gc else None + maxcoord = 16383 / 72.27 * self.dpi # Max dimensions in LaTeX. if bbox and (rgbFace is None): p1, p2 = bbox.get_points() - clip = (p1[0], p1[1], p2[0], p2[1]) + clip = (max(p1[0], -maxcoord), max(p1[1], -maxcoord), + min(p2[0], maxcoord), min(p2[1], maxcoord)) else: - clip = None + clip = (-maxcoord, -maxcoord, maxcoord, maxcoord) # build path for points, code in path.iter_segments(transform, clip=clip): if code == Path.MOVETO: x, y = tuple(points) - writeln(self.fh, - r"\pgfpathmoveto{\pgfqpoint{%fin}{%fin}}" % - (f * x, f * y)) + _writeln(self.fh, + r"\pgfpathmoveto{\pgfqpoint{%fin}{%fin}}" % + (f * x, f * y)) elif code == Path.CLOSEPOLY: - writeln(self.fh, r"\pgfpathclose") + _writeln(self.fh, r"\pgfpathclose") elif code == Path.LINETO: x, y = tuple(points) - writeln(self.fh, - r"\pgfpathlineto{\pgfqpoint{%fin}{%fin}}" % - (f * x, f * y)) + _writeln(self.fh, + r"\pgfpathlineto{\pgfqpoint{%fin}{%fin}}" % + (f * x, f * y)) elif code == Path.CURVE3: cx, cy, px, py = tuple(points) coords = cx * f, cy * f, px * f, py * f - writeln(self.fh, - r"\pgfpathquadraticcurveto" - r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}" - % coords) + _writeln(self.fh, + r"\pgfpathquadraticcurveto" + r"{\pgfqpoint{%fin}{%fin}}{\pgfqpoint{%fin}{%fin}}" + % coords) elif code == Path.CURVE4: c1x, c1y, c2x, c2y, px, py = tuple(points) coords = c1x * f, c1y * f, c2x * f, c2y * f, px * f, py * f - writeln(self.fh, - r"\pgfpathcurveto" - r"{\pgfqpoint{%fin}{%fin}}" - r"{\pgfqpoint{%fin}{%fin}}" - r"{\pgfqpoint{%fin}{%fin}}" - % coords) + _writeln(self.fh, + r"\pgfpathcurveto" + r"{\pgfqpoint{%fin}{%fin}}" + r"{\pgfqpoint{%fin}{%fin}}" + r"{\pgfqpoint{%fin}{%fin}}" + % coords) + + # apply pgf decorators + sketch_params = gc.get_sketch_params() if gc else None + if sketch_params is not None: + # Only "length" directly maps to "segment length" in PGF's API. + # PGF uses "amplitude" to pass the combined deviation in both x- + # and y-direction, while matplotlib only varies the length of the + # wiggle along the line ("randomness" and "length" parameters) + # and has a separate "scale" argument for the amplitude. + # -> Use "randomness" as PRNG seed to allow the user to force the + # same shape on multiple sketched lines + scale, length, randomness = sketch_params + if scale is not None: + # make matplotlib and PGF rendering visually similar + length *= 0.5 + scale *= 2 + # PGF guarantees that repeated loading is a no-op + _writeln(self.fh, r"\usepgfmodule{decorations}") + _writeln(self.fh, r"\usepgflibrary{decorations.pathmorphing}") + _writeln(self.fh, r"\pgfkeys{/pgf/decoration/.cd, " + f"segment length = {(length * f):f}in, " + f"amplitude = {(scale * f):f}in}}") + _writeln(self.fh, f"\\pgfmathsetseed{{{int(randomness)}}}") + _writeln(self.fh, r"\pgfdecoratecurrentpath{random steps}") def _pgf_path_draw(self, stroke=True, fill=False): actions = [] @@ -617,7 +641,7 @@ def _pgf_path_draw(self, stroke=True, fill=False): actions.append("stroke") if fill: actions.append("fill") - writeln(self.fh, r"\pgfusepath{%s}" % ",".join(actions)) + _writeln(self.fh, r"\pgfusepath{%s}" % ",".join(actions)) def option_scale_image(self): # docstring inherited @@ -646,50 +670,48 @@ def draw_image(self, gc, x, y, im, transform=None): self.image_counter += 1 # reference the image in the pgf picture - writeln(self.fh, r"\begin{pgfscope}") + _writeln(self.fh, r"\begin{pgfscope}") self._print_pgf_clip(gc) f = 1. / self.dpi # from display coords to inch if transform is None: - writeln(self.fh, - r"\pgfsys@transformshift{%fin}{%fin}" % (x * f, y * f)) + _writeln(self.fh, + r"\pgfsys@transformshift{%fin}{%fin}" % (x * f, y * f)) w, h = w * f, h * f else: tr1, tr2, tr3, tr4, tr5, tr6 = transform.frozen().to_values() - writeln(self.fh, - r"\pgfsys@transformcm{%f}{%f}{%f}{%f}{%fin}{%fin}" % - (tr1 * f, tr2 * f, tr3 * f, tr4 * f, - (tr5 + x) * f, (tr6 + y) * f)) + _writeln(self.fh, + r"\pgfsys@transformcm{%f}{%f}{%f}{%f}{%fin}{%fin}" % + (tr1 * f, tr2 * f, tr3 * f, tr4 * f, + (tr5 + x) * f, (tr6 + y) * f)) w = h = 1 # scale is already included in the transform interp = str(transform is None).lower() # interpolation in PDF reader - writeln(self.fh, - r"\pgftext[left,bottom]" - r"{%s[interpolate=%s,width=%fin,height=%fin]{%s}}" % - (_get_image_inclusion_command(), - interp, w, h, fname_img)) - writeln(self.fh, r"\end{pgfscope}") - - def draw_tex(self, gc, x, y, s, prop, angle, ismath="TeX!", mtext=None): + _writeln(self.fh, + r"\pgftext[left,bottom]" + r"{%s[interpolate=%s,width=%fin,height=%fin]{%s}}" % + (_get_image_inclusion_command(), + interp, w, h, fname_img)) + _writeln(self.fh, r"\end{pgfscope}") + + def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): # docstring inherited - self.draw_text(gc, x, y, s, prop, angle, ismath, mtext) + self.draw_text(gc, x, y, s, prop, angle, ismath="TeX", mtext=mtext) def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited # prepare string for tex - s = common_texification(s) - prop_cmds = _font_properties_str(prop) - s = r"%s %s" % (prop_cmds, s) + s = _escape_and_apply_props(s, prop) - writeln(self.fh, r"\begin{pgfscope}") + _writeln(self.fh, r"\begin{pgfscope}") alpha = gc.get_alpha() if alpha != 1.0: - writeln(self.fh, r"\pgfsetfillopacity{%f}" % alpha) - writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % alpha) + _writeln(self.fh, r"\pgfsetfillopacity{%f}" % alpha) + _writeln(self.fh, r"\pgfsetstrokeopacity{%f}" % alpha) rgb = tuple(gc.get_rgb())[:3] - writeln(self.fh, r"\definecolor{textcolor}{rgb}{%f,%f,%f}" % rgb) - writeln(self.fh, r"\pgfsetstrokecolor{textcolor}") - writeln(self.fh, r"\pgfsetfillcolor{textcolor}") + _writeln(self.fh, r"\definecolor{textcolor}{rgb}{%f,%f,%f}" % rgb) + _writeln(self.fh, r"\pgfsetstrokecolor{textcolor}") + _writeln(self.fh, r"\pgfsetfillcolor{textcolor}") s = r"\color{textcolor}" + s dpi = self.figure.dpi @@ -718,15 +740,11 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if angle != 0: text_args.append("rotate=%f" % angle) - writeln(self.fh, r"\pgftext[%s]{%s}" % (",".join(text_args), s)) - writeln(self.fh, r"\end{pgfscope}") + _writeln(self.fh, r"\pgftext[%s]{%s}" % (",".join(text_args), s)) + _writeln(self.fh, r"\end{pgfscope}") def get_text_width_height_descent(self, s, prop, ismath): # docstring inherited - - # check if the math is supposed to be displaystyled - s = common_texification(s) - # get text metrics in units of latex pt, convert to display units w, h, d = (LatexManager._get_cached_or_new() .get_width_height_descent(s, prop)) @@ -750,36 +768,6 @@ def points_to_pixels(self, points): return points * mpl_pt_to_in * self.dpi -@_api.deprecated("3.3", alternative="GraphicsContextBase") -class GraphicsContextPgf(GraphicsContextBase): - pass - - -@_api.deprecated("3.4") -class TmpDirCleaner: - _remaining_tmpdirs = set() - - @_api.classproperty - @_api.deprecated("3.4") - def remaining_tmpdirs(cls): - return cls._remaining_tmpdirs - - @staticmethod - @_api.deprecated("3.4") - def add(tmpdir): - TmpDirCleaner._remaining_tmpdirs.add(tmpdir) - - @staticmethod - @_api.deprecated("3.4") - @atexit.register - def cleanup_remaining_tmpdirs(): - for tmpdir in TmpDirCleaner._remaining_tmpdirs: - error_message = "error deleting tmp directory {}".format(tmpdir) - shutil.rmtree( - tmpdir, - onerror=lambda *args: _log.error(error_message)) - - class FigureCanvasPgf(FigureCanvasBase): filetypes = {"pgf": "LaTeX PGF picture", "pdf": "LaTeX compiled PGF picture", @@ -788,7 +776,6 @@ class FigureCanvasPgf(FigureCanvasBase): def get_default_filetype(self): return 'pdf' - @_check_savefig_extra_args def _print_pgf_to_fh(self, fh, *, bbox_inches_restore=None): header_text = """%% Creator: Matplotlib, PGF backend @@ -799,6 +786,10 @@ def _print_pgf_to_fh(self, fh, *, bbox_inches_restore=None): %% Make sure the required packages are loaded in your preamble %% \\usepackage{pgf} %% +%% Also ensure that all the required font packages are loaded; for instance, +%% the lmodern package is sometimes necessary when using math font. +%% \\usepackage{lmodern} +%% %% Figures using additional raster images can only be included by \\input if %% they are in the same directory as the main LaTeX file. For loading figures %% from other directories you can use the `import` package @@ -811,39 +802,37 @@ def _print_pgf_to_fh(self, fh, *, bbox_inches_restore=None): # append the preamble used by the backend as a comment for debugging header_info_preamble = ["%% Matplotlib used the following preamble"] - for line in get_preamble().splitlines(): - header_info_preamble.append("%% " + line) - for line in get_fontspec().splitlines(): + for line in _get_preamble().splitlines(): header_info_preamble.append("%% " + line) header_info_preamble.append("%%") header_info_preamble = "\n".join(header_info_preamble) # get figure size in inch w, h = self.figure.get_figwidth(), self.figure.get_figheight() - dpi = self.figure.get_dpi() + dpi = self.figure.dpi # create pgfpicture environment and write the pgf code fh.write(header_text) fh.write(header_info_preamble) fh.write("\n") - writeln(fh, r"\begingroup") - writeln(fh, r"\makeatletter") - writeln(fh, r"\begin{pgfpicture}") - writeln(fh, - r"\pgfpathrectangle{\pgfpointorigin}{\pgfqpoint{%fin}{%fin}}" - % (w, h)) - writeln(fh, r"\pgfusepath{use as bounding box, clip}") + _writeln(fh, r"\begingroup") + _writeln(fh, r"\makeatletter") + _writeln(fh, r"\begin{pgfpicture}") + _writeln(fh, + r"\pgfpathrectangle{\pgfpointorigin}{\pgfqpoint{%fin}{%fin}}" + % (w, h)) + _writeln(fh, r"\pgfusepath{use as bounding box, clip}") renderer = MixedModeRenderer(self.figure, w, h, dpi, RendererPgf(self.figure, fh), bbox_inches_restore=bbox_inches_restore) self.figure.draw(renderer) # end the pgfpicture environment - writeln(fh, r"\end{pgfpicture}") - writeln(fh, r"\makeatother") - writeln(fh, r"\endgroup") + _writeln(fh, r"\end{pgfpicture}") + _writeln(fh, r"\makeatother") + _writeln(fh, r"\endgroup") - def print_pgf(self, fname_or_fh, *args, **kwargs): + def print_pgf(self, fname_or_fh, **kwargs): """ Output pgf macros for drawing the figure so it can be included and rendered in latex documents. @@ -851,54 +840,49 @@ def print_pgf(self, fname_or_fh, *args, **kwargs): with cbook.open_file_cm(fname_or_fh, "w", encoding="utf-8") as file: if not cbook.file_requires_unicode(file): file = codecs.getwriter("utf-8")(file) - self._print_pgf_to_fh(file, *args, **kwargs) + self._print_pgf_to_fh(file, **kwargs) - def print_pdf(self, fname_or_fh, *args, metadata=None, **kwargs): + def print_pdf(self, fname_or_fh, *, metadata=None, **kwargs): """Use LaTeX to compile a pgf generated figure to pdf.""" - w, h = self.figure.get_figwidth(), self.figure.get_figheight() + w, h = self.figure.get_size_inches() info_dict = _create_pdf_info_dict('pgf', metadata or {}) - hyperref_options = ','.join( + pdfinfo = ','.join( _metadata_to_str(k, v) for k, v in info_dict.items()) + # print figure to pgf and compile it with latex with TemporaryDirectory() as tmpdir: tmppath = pathlib.Path(tmpdir) - - # print figure to pgf and compile it with latex - self.print_pgf(tmppath / "figure.pgf", *args, **kwargs) - - latexcode = """ -\\PassOptionsToPackage{pdfinfo={%s}}{hyperref} -\\RequirePackage{hyperref} -\\documentclass[12pt]{minimal} -\\usepackage[paperwidth=%fin, paperheight=%fin, margin=0in]{geometry} -%s -%s -\\usepackage{pgf} - -\\begin{document} -\\centering -\\input{figure.pgf} -\\end{document}""" % (hyperref_options, w, h, get_preamble(), get_fontspec()) - (tmppath / "figure.tex").write_text(latexcode, encoding="utf-8") - + self.print_pgf(tmppath / "figure.pgf", **kwargs) + (tmppath / "figure.tex").write_text( + "\n".join([ + r"\documentclass[12pt]{article}", + r"\usepackage[pdfinfo={%s}]{hyperref}" % pdfinfo, + r"\usepackage[papersize={%fin,%fin}, margin=0in]{geometry}" + % (w, h), + r"\usepackage{pgf}", + _get_preamble(), + r"\begin{document}", + r"\centering", + r"\input{figure.pgf}", + r"\end{document}", + ]), encoding="utf-8") texcommand = mpl.rcParams["pgf.texsystem"] cbook._check_and_log_subprocess( [texcommand, "-interaction=nonstopmode", "-halt-on-error", "figure.tex"], _log, cwd=tmpdir) - with (tmppath / "figure.pdf").open("rb") as orig, \ cbook.open_file_cm(fname_or_fh, "wb") as dest: shutil.copyfileobj(orig, dest) # copy file contents to target - def print_png(self, fname_or_fh, *args, **kwargs): + def print_png(self, fname_or_fh, **kwargs): """Use LaTeX to compile a pgf figure to pdf and convert it to png.""" converter = make_pdf_to_png_converter() with TemporaryDirectory() as tmpdir: tmppath = pathlib.Path(tmpdir) pdf_path = tmppath / "figure.pdf" png_path = tmppath / "figure.png" - self.print_pdf(pdf_path, *args, **kwargs) + self.print_pdf(pdf_path, **kwargs) converter(pdf_path, png_path, dpi=self.figure.dpi) with png_path.open("rb") as orig, \ cbook.open_file_cm(fname_or_fh, "wb") as dest: @@ -908,7 +892,7 @@ def get_renderer(self): return RendererPgf(self.figure, None) def draw(self): - _no_output_draw(self.figure) + self.figure.draw_without_rendering() return super().draw() @@ -943,7 +927,6 @@ class PdfPages: '_info_dict', '_metadata', ) - metadata = _api.deprecated('3.3')(property(lambda self: self._metadata)) def __init__(self, filename, *, keep_empty=True, metadata=None): """ @@ -976,53 +959,22 @@ def __init__(self, filename, *, keep_empty=True, metadata=None): self._n_figures = 0 self.keep_empty = keep_empty self._metadata = (metadata or {}).copy() - if metadata: - for key in metadata: - canonical = { - 'creationdate': 'CreationDate', - 'moddate': 'ModDate', - }.get(key.lower(), key.lower().title()) - if canonical != key: - _api.warn_deprecated( - '3.3', message='Support for setting PDF metadata keys ' - 'case-insensitively is deprecated since %(since)s and ' - 'will be removed %(removal)s; ' - f'set {canonical} instead of {key}.') - self._metadata[canonical] = self._metadata.pop(key) self._info_dict = _create_pdf_info_dict('pgf', self._metadata) self._file = BytesIO() def _write_header(self, width_inches, height_inches): - hyperref_options = ','.join( + pdfinfo = ','.join( _metadata_to_str(k, v) for k, v in self._info_dict.items()) - - latex_preamble = get_preamble() - latex_fontspec = get_fontspec() - latex_header = r"""\PassOptionsToPackage{{ - pdfinfo={{ - {metadata} - }} -}}{{hyperref}} -\RequirePackage{{hyperref}} -\documentclass[12pt]{{minimal}} -\usepackage[ - paperwidth={width}in, - paperheight={height}in, - margin=0in -]{{geometry}} -{preamble} -{fontspec} -\usepackage{{pgf}} -\setlength{{\parindent}}{{0pt}} - -\begin{{document}}%% -""".format( - width=width_inches, - height=height_inches, - preamble=latex_preamble, - fontspec=latex_fontspec, - metadata=hyperref_options, - ) + latex_header = "\n".join([ + r"\documentclass[12pt]{article}", + r"\usepackage[pdfinfo={%s}]{hyperref}" % pdfinfo, + r"\usepackage[papersize={%fin,%fin}, margin=0in]{geometry}" + % (width_inches, height_inches), + r"\usepackage{pgf}", + _get_preamble(), + r"\setlength{\parindent}{0pt}", + r"\begin{document}%", + ]) self._file.write(latex_header.encode('utf-8')) def __enter__(self): @@ -1062,11 +1014,8 @@ def savefig(self, figure=None, **kwargs): Parameters ---------- - figure : `.Figure` or int, optional - Specifies what figure is saved to file. If not specified, the - active figure is saved. If a `.Figure` instance is provided, this - figure is saved. If an int is specified, the figure instance to - save is looked up by number. + figure : `.Figure` or int, default: the active figure + The figure, or index of the figure, that is saved to the file. """ if not isinstance(figure, Figure): if figure is None: diff --git a/lib/matplotlib/backends/backend_ps.py b/lib/matplotlib/backends/backend_ps.py index 1ef67c63456b..68dd61e6f126 100644 --- a/lib/matplotlib/backends/backend_ps.py +++ b/lib/matplotlib/backends/backend_ps.py @@ -2,12 +2,13 @@ A PostScript backend, which can produce both PostScript .ps and .eps. """ +import codecs import datetime from enum import Enum -import glob -from io import StringIO, TextIOWrapper +import functools +from io import StringIO +import itertools import logging -import math import os import pathlib import re @@ -18,17 +19,14 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, cbook, _path -from matplotlib import _text_layout -from matplotlib.afm import AFM +from matplotlib import _api, cbook, _path, _text_helpers +from matplotlib._afm import AFM from matplotlib.backend_bases import ( - _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - GraphicsContextBase, RendererBase, _no_output_draw) + _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) from matplotlib.cbook import is_writable_file_like, file_requires_unicode from matplotlib.font_manager import get_font -from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_NO_SCALE +from matplotlib.ft2font import LOAD_NO_SCALE, FT2Font from matplotlib._ttconv import convert_ttf_to_ps -from matplotlib.mathtext import MathTextParser from matplotlib._mathtext_data import uni2type1 from matplotlib.path import Path from matplotlib.texmanager import TexManager @@ -36,19 +34,22 @@ from matplotlib.backends.backend_mixed import MixedModeRenderer from . import _backend_pdf_ps -_log = logging.getLogger(__name__) - -backend_version = 'Level II' -debugPS = 0 +_log = logging.getLogger(__name__) +debugPS = False +@_api.deprecated("3.7") class PsBackendHelper: def __init__(self): self._cached = {} -ps_backend_helper = PsBackendHelper() +@_api.caching_module_getattr +class __getattr__: + # module-level deprecations + ps_backend_helper = _api.deprecated("3.7", obj_type="")( + property(lambda self: PsBackendHelper())) papersize = {'letter': (8.5, 11), @@ -62,9 +63,9 @@ def __init__(self): 'a5': (5.83, 8.27), 'a6': (4.13, 5.83), 'a7': (2.91, 4.13), - 'a8': (2.07, 2.91), - 'a9': (1.457, 2.05), - 'a10': (1.02, 1.457), + 'a8': (2.05, 2.91), + 'a9': (1.46, 2.05), + 'a10': (1.02, 1.46), 'b0': (40.55, 57.32), 'b1': (28.66, 40.55), 'b2': (20.27, 28.66), @@ -87,24 +88,11 @@ def _get_papertype(w, h): return 'a0' -def _num_to_str(val): - if isinstance(val, str): - return val - - ival = int(val) - if val == ival: - return str(ival) - - s = "%1.3f" % val - s = s.rstrip("0") - s = s.rstrip(".") - return s - - def _nums_to_str(*args): - return ' '.join(map(_num_to_str, args)) + return " ".join(f"{arg:1.3f}".rstrip("0").rstrip(".") for arg in args) +@_api.deprecated("3.6", alternative="a vendored copy of this function") def quote_ps_string(s): """ Quote dangerous characters of S for use in a PostScript string constant. @@ -134,16 +122,16 @@ def _move_path_to_path_or_stream(src, dst): shutil.move(src, dst, copy_function=shutil.copyfile) -def _font_to_ps_type3(font_path, glyph_ids): +def _font_to_ps_type3(font_path, chars): """ - Subset *glyph_ids* from the font at *font_path* into a Type 3 font. + Subset *chars* from the font at *font_path* into a Type 3 font. Parameters ---------- font_path : path-like Path to the font to be subsetted. - glyph_ids : list of int - The glyph indices to include in the subsetted font. + chars : str + The characters to include in the subsetted font. Returns ------- @@ -152,6 +140,7 @@ def _font_to_ps_type3(font_path, glyph_ids): verbatim into a PostScript file. """ font = get_font(font_path, hinting_factor=1) + glyph_ids = [font.get_char_index(c) for c in chars] preamble = """\ %!PS-Adobe-3.0 Resource-Font @@ -214,6 +203,58 @@ def _font_to_ps_type3(font_path, glyph_ids): return preamble + "\n".join(entries) + postamble +def _font_to_ps_type42(font_path, chars, fh): + """ + Subset *chars* from the font at *font_path* into a Type 42 font at *fh*. + + Parameters + ---------- + font_path : path-like + Path to the font to be subsetted. + chars : str + The characters to include in the subsetted font. + fh : file-like + Where to write the font. + """ + subset_str = ''.join(chr(c) for c in chars) + _log.debug("SUBSET %s characters: %s", font_path, subset_str) + try: + fontdata = _backend_pdf_ps.get_glyphs_subset(font_path, subset_str) + _log.debug("SUBSET %s %d -> %d", font_path, os.stat(font_path).st_size, + fontdata.getbuffer().nbytes) + + # Give ttconv a subsetted font along with updated glyph_ids. + font = FT2Font(fontdata) + glyph_ids = [font.get_char_index(c) for c in chars] + with TemporaryDirectory() as tmpdir: + tmpfile = os.path.join(tmpdir, "tmp.ttf") + + with open(tmpfile, 'wb') as tmp: + tmp.write(fontdata.getvalue()) + + # TODO: allow convert_ttf_to_ps to input file objects (BytesIO) + convert_ttf_to_ps(os.fsencode(tmpfile), fh, 42, glyph_ids) + except RuntimeError: + _log.warning( + "The PostScript backend does not currently " + "support the selected font.") + raise + + +def _log_if_debug_on(meth): + """ + Wrap `RendererPS` method *meth* to emit a PS comment with the method name, + if the global flag `debugPS` is set. + """ + @functools.wraps(meth) + def wrapper(self, *args, **kwargs): + if debugPS: + self._pswriter.write(f"% {meth.__name__}\n") + return meth(self, *args, **kwargs) + + return wrapper + + class RendererPS(_backend_pdf_ps.RendererPDFPSBase): """ The renderer handles all the drawing primitives using a graphics @@ -223,11 +264,6 @@ class RendererPS(_backend_pdf_ps.RendererPDFPSBase): _afm_font_dir = cbook._get_data_path("fonts/afm") _use_afm_rc_name = "ps.useafm" - mathtext_parser = _api.deprecated("3.4")(property( - lambda self: MathTextParser("PS"))) - used_characters = _api.deprecated("3.3")(property( - lambda self: self._character_tracker.used_characters)) - def __init__(self, width, height, pswriter, imagedpi=72): # Although postscript itself is dpi independent, we need to inform the # image code about a requested dpi to generate high resolution images @@ -253,23 +289,27 @@ def __init__(self, width, height, pswriter, imagedpi=72): self._path_collection_id = 0 self._character_tracker = _backend_pdf_ps.CharacterTracker() - - @_api.deprecated("3.3") - def track_characters(self, *args, **kwargs): - """Keep track of which characters are required from each font.""" - self._character_tracker.track(*args, **kwargs) - - @_api.deprecated("3.3") - def merge_used_characters(self, *args, **kwargs): - self._character_tracker.merge(*args, **kwargs) + self._logwarn_once = functools.lru_cache(None)(_log.warning) + + def _is_transparent(self, rgb_or_rgba): + if rgb_or_rgba is None: + return True # Consistent with rgbFace semantics. + elif len(rgb_or_rgba) == 4: + if rgb_or_rgba[3] == 0: + return True + if rgb_or_rgba[3] != 1: + self._logwarn_once( + "The PostScript backend does not support transparency; " + "partially transparent artists will be rendered opaque.") + return False + else: # len() == 3. + return False def set_color(self, r, g, b, store=True): if (r, g, b) != self.color: - if r == g and r == b: - self._pswriter.write("%1.3f setgray\n" % r) - else: - self._pswriter.write( - "%1.3f %1.3f %1.3f setrgbcolor\n" % (r, g, b)) + self._pswriter.write(f"{r:1.3f} setgray\n" + if r == g == b else + f"{r:1.3f} {g:1.3f} {b:1.3f} setrgbcolor\n") if store: self.color = (r, g, b) @@ -312,11 +352,10 @@ def set_linedash(self, offset, seq, store=True): if np.array_equal(seq, oldseq) and oldo == offset: return - if seq is not None and len(seq): - s = "[%s] %d setdash\n" % (_nums_to_str(*seq), offset) - self._pswriter.write(s) - else: - self._pswriter.write("[] 0 setdash\n") + self._pswriter.write(f"[{_nums_to_str(*seq)}]" + f" {_nums_to_str(offset)} setdash\n" + if seq is not None and len(seq) else + "[] 0 setdash\n") if store: self.linedash = (offset, seq) @@ -344,7 +383,7 @@ def create_hatch(self, hatch): /PaintProc {{ pop - {linewidth:f} setlinewidth + {linewidth:g} setlinewidth {self._convert_path( Path.hatch(hatch), Affine2D().scale(sidelen), simplify=False)} gsave @@ -354,7 +393,7 @@ def create_hatch(self, hatch): }} bind >> matrix - 0.0 {pageheight:f} translate + 0 {pageheight:g} translate makepattern /{name} exch def """) @@ -388,7 +427,7 @@ def _get_clip_cmd(self, gc): key = (path, id(trf)) custom_clip_cmd = self._clip_paths.get(key) if custom_clip_cmd is None: - custom_clip_cmd = "c%x" % len(self._clip_paths) + custom_clip_cmd = "c%d" % len(self._clip_paths) self._pswriter.write(f"""\ /{custom_clip_cmd} {{ {self._convert_path(path, trf, simplify=False)} @@ -400,23 +439,14 @@ def _get_clip_cmd(self, gc): clip.append(f"{custom_clip_cmd}\n") return "".join(clip) + @_log_if_debug_on def draw_image(self, gc, x, y, im, transform=None): # docstring inherited h, w = im.shape[:2] imagecmd = "false 3 colorimage" data = im[::-1, :, :3] # Vertically flipped rgb values. - # data.tobytes().hex() has no spaces, so can be linewrapped by simply - # splitting data every nchars. It's equivalent to textwrap.fill only - # much faster. - nchars = 128 - data = data.tobytes().hex() - hexlines = "\n".join( - [ - data[n * nchars:(n + 1) * nchars] - for n in range(math.ceil(len(data) / nchars)) - ] - ) + hexdata = data.tobytes().hex("\n", -64) # Linewrap to 128 chars. if transform is None: matrix = "1 0 0 1 0 0" @@ -430,18 +460,19 @@ def draw_image(self, gc, x, y, im, transform=None): self._pswriter.write(f"""\ gsave {self._get_clip_cmd(gc)} -{x:f} {y:f} translate +{x:g} {y:g} translate [{matrix}] concat -{xscale:f} {yscale:f} scale +{xscale:g} {yscale:g} scale /DataString {w:d} string def {w:d} {h:d} 8 [ {w:d} 0 0 -{h:d} 0 {h:d} ] {{ currentfile DataString readhexstring pop }} bind {imagecmd} -{hexlines} +{hexdata} grestore """) + @_log_if_debug_on def draw_path(self, gc, path, transform, rgbFace=None): # docstring inherited clip = rgbFace is None and gc.get_hatch_path() is None @@ -449,16 +480,14 @@ def draw_path(self, gc, path, transform, rgbFace=None): ps = self._convert_path(path, transform, clip=clip, simplify=simplify) self._draw_ps(ps, gc, rgbFace) + @_log_if_debug_on def draw_markers( self, gc, marker_path, marker_trans, path, trans, rgbFace=None): # docstring inherited - if debugPS: - self._pswriter.write('% draw_markers \n') - ps_color = ( None - if _is_transparent(rgbFace) + if self._is_transparent(rgbFace) else '%1.3f setgray' % rgbFace[0] if rgbFace[0] == rgbFace[1] == rgbFace[2] else '%1.3f %1.3f %1.3f setrgbcolor' % rgbFace[:3]) @@ -504,8 +533,9 @@ def draw_markers( ps = '\n'.join(ps_cmd) self._draw_ps(ps, gc, rgbFace, fill=False, stroke=False) + @_log_if_debug_on def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offsetTrans, facecolors, edgecolors, + offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): # Is the optimization worth it? Rough calculation: @@ -521,14 +551,14 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, if not should_do_optimization: return RendererBase.draw_path_collection( self, gc, master_transform, paths, all_transforms, - offsets, offsetTrans, facecolors, edgecolors, + offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) path_codes = [] for i, (path, transform) in enumerate(self._iter_collection_raw_paths( master_transform, paths, all_transforms)): - name = 'p%x_%x' % (self._path_collection_id, i) + name = 'p%d_%d' % (self._path_collection_id, i) path_bytes = self._convert_path(path, transform, simplify=False) self._pswriter.write(f"""\ /{name} {{ @@ -540,19 +570,22 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, path_codes.append(name) for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, master_transform, all_transforms, path_codes, offsets, - offsetTrans, facecolors, edgecolors, linewidths, linestyles, + gc, path_codes, offsets, offset_trans, + facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): ps = "%g %g %s" % (xo, yo, path_id) self._draw_ps(ps, gc0, rgbFace) self._path_collection_id += 1 - @_api.delete_parameter("3.3", "ismath") - def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): + @_log_if_debug_on + def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): # docstring inherited + if self._is_transparent(gc.get_rgb()): + return # Special handling for fully transparent. + if not hasattr(self, "psfrag"): - _log.warning( + self._logwarn_once( "The PS backend determines usetex status solely based on " "rcParams['text.usetex'] and does not support having " "usetex=True only for some elements; this element will thus " @@ -570,19 +603,11 @@ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): s = fontcmd % s tex = r'\color[rgb]{%s} %s' % (color, s) - corr = 0 # w/2*(fontsize-10)/10 - if dict.__getitem__(mpl.rcParams, 'text.latex.preview'): - # use baseline alignment! - pos = _nums_to_str(x-corr, y) - self.psfrag.append( - r'\psfrag{%s}[Bl][Bl][1][%f]{\fontsize{%f}{%f}%s}' % ( - thetext, angle, fontsize, fontsize*1.25, tex)) - else: - # 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}' % ( - thetext, angle, fontsize, fontsize*1.25, tex)) + # Stick to the bottom alignment. + pos = _nums_to_str(x, y-bl) + self.psfrag.append( + r'\psfrag{%s}[bl][bl][1][%f]{\fontsize{%f}{%f}%s}' % ( + thetext, angle, fontsize, fontsize*1.25, tex)) self._pswriter.write(f"""\ gsave @@ -593,13 +618,11 @@ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): """) self.textcnt += 1 + @_log_if_debug_on def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited - if debugPS: - self._pswriter.write("% text\n") - - if _is_transparent(gc.get_rgb()): + if self._is_transparent(gc.get_rgb()): return # Special handling for fully transparent. if ismath == 'TeX': @@ -608,13 +631,15 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if ismath: return self.draw_mathtext(gc, x, y, s, prop, angle) + stream = [] # list of (ps_name, x, char_name) + if mpl.rcParams['ps.useafm']: font = self._get_font_afm(prop) + ps_name = (font.postscript_name.encode("ascii", "replace") + .decode("ascii")) scale = 0.001 * prop.get_size_in_points() - thisx = 0 last_name = None # kerns returns 0 for None. - xs_names = [] for c in s: name = uni2type1.get(ord(c), f"uni{ord(c):04X}") try: @@ -625,66 +650,66 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): kern = font.get_kern_dist_from_name(last_name, name) last_name = name thisx += kern * scale - xs_names.append((thisx, name)) + stream.append((ps_name, thisx, name)) thisx += width * scale else: font = self._get_font_ttf(prop) - font.set_text(s, 0, flags=LOAD_NO_HINTING) self._character_tracker.track(font, s) - xs_names = [(item.x, font.get_glyph_name(item.glyph_idx)) - for item in _text_layout.layout(s, font)] - + for item in _text_helpers.layout(s, font): + ps_name = (item.ft_object.postscript_name + .encode("ascii", "replace").decode("ascii")) + glyph_name = item.ft_object.get_glyph_name(item.glyph_idx) + stream.append((ps_name, item.x, glyph_name)) self.set_color(*gc.get_rgb()) - ps_name = (font.postscript_name - .encode("ascii", "replace").decode("ascii")) - self.set_font(ps_name, prop.get_size_in_points()) - thetext = "\n".join(f"{x:f} 0 m /{name:s} glyphshow" - for x, name in xs_names) - self._pswriter.write(f"""\ + + for ps_name, group in itertools. \ + groupby(stream, lambda entry: entry[0]): + self.set_font(ps_name, prop.get_size_in_points(), False) + thetext = "\n".join(f"{x:g} 0 m /{name:s} glyphshow" + for _, x, name in group) + self._pswriter.write(f"""\ gsave {self._get_clip_cmd(gc)} -{x:f} {y:f} translate -{angle:f} rotate +{x:g} {y:g} translate +{angle:g} rotate {thetext} grestore """) + @_log_if_debug_on def draw_mathtext(self, gc, x, y, s, prop, angle): """Draw the math text using matplotlib.mathtext.""" - if debugPS: - self._pswriter.write("% mathtext\n") - width, height, descent, glyphs, rects = \ - self._text2path.mathtext_parser.parse( - s, 72, prop, - _force_standard_ps_fonts=mpl.rcParams["ps.useafm"]) + self._text2path.mathtext_parser.parse(s, 72, prop) self.set_color(*gc.get_rgb()) self._pswriter.write( f"gsave\n" - f"{x:f} {y:f} translate\n" - f"{angle:f} rotate\n") + f"{x:g} {y:g} translate\n" + f"{angle:g} rotate\n") lastfont = None for font, fontsize, num, ox, oy in glyphs: - self._character_tracker.track(font, chr(num)) + self._character_tracker.track_glyph(font, num) if (font.postscript_name, fontsize) != lastfont: lastfont = font.postscript_name, fontsize self._pswriter.write( f"/{font.postscript_name} {fontsize} selectfont\n") - symbol_name = ( + glyph_name = ( font.get_name_char(chr(num)) if isinstance(font, AFM) else font.get_glyph_name(font.get_char_index(num))) self._pswriter.write( - f"{ox:f} {oy:f} moveto\n" - f"/{symbol_name} glyphshow\n") + f"{ox:g} {oy:g} moveto\n" + f"/{glyph_name} glyphshow\n") for ox, oy, w, h in rects: self._pswriter.write(f"{ox} {oy} {w} {h} rectfill\n") self._pswriter.write("grestore\n") + @_log_if_debug_on def draw_gouraud_triangle(self, gc, points, colors, trans): self.draw_gouraud_triangles(gc, points.reshape((1, 3, 2)), colors.reshape((1, 3, 4)), trans) + @_log_if_debug_on def draw_gouraud_triangles(self, gc, points, colors, trans): assert len(points) == len(colors) assert points.ndim == 3 @@ -705,13 +730,13 @@ def draw_gouraud_triangles(self, gc, points, colors, trans): xmin, ymin = points_min xmax, ymax = points_max - streamarr = np.empty( + data = np.empty( shape[0] * shape[1], dtype=[('flags', 'u1'), ('points', '2>u4'), ('colors', '3u1')]) - streamarr['flags'] = 0 - streamarr['points'] = (flat_points - points_min) * factor - streamarr['colors'] = flat_colors[:, :3] * 255.0 - stream = quote_ps_string(streamarr.tobytes()) + data['flags'] = 0 + data['points'] = (flat_points - points_min) * factor + data['colors'] = flat_colors[:, :3] * 255.0 + hexdata = data.tobytes().hex("\n", -64) # Linewrap to 128 chars. self._pswriter.write(f"""\ gsave @@ -721,32 +746,30 @@ def draw_gouraud_triangles(self, gc, points, colors, trans): /BitsPerComponent 8 /BitsPerFlag 8 /AntiAlias true - /Decode [ {xmin:f} {xmax:f} {ymin:f} {ymax:f} 0 1 0 1 0 1 ] - /DataSource ({stream}) + /Decode [ {xmin:g} {xmax:g} {ymin:g} {ymax:g} 0 1 0 1 0 1 ] + /DataSource < +{hexdata} +> >> shfill grestore """) - def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None): + def _draw_ps(self, ps, gc, rgbFace, *, fill=True, stroke=True): """ - Emit the PostScript snippet 'ps' with all the attributes from 'gc' - applied. 'ps' must consist of PostScript commands to construct a path. + Emit the PostScript snippet *ps* with all the attributes from *gc* + applied. *ps* must consist of PostScript commands to construct a path. - The fill and/or stroke kwargs can be set to False if the - 'ps' string already includes filling and/or stroking, in - which case _draw_ps is just supplying properties and - clipping. + The *fill* and/or *stroke* kwargs can be set to False if the *ps* + string already includes filling and/or stroking, in which case + `_draw_ps` is just supplying properties and clipping. """ - # local variable eliminates all repeated attribute lookups write = self._pswriter.write - if debugPS and command: - write("% "+command+"\n") mightstroke = (gc.get_linewidth() > 0 - and not _is_transparent(gc.get_rgb())) + and not self._is_transparent(gc.get_rgb())) if not mightstroke: stroke = False - if _is_transparent(rgbFace): + if self._is_transparent(rgbFace): fill = False hatch = gc.get_hatch() @@ -755,12 +778,12 @@ def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None): self.set_linejoin(gc.get_joinstyle()) self.set_linecap(gc.get_capstyle()) self.set_linedash(*gc.get_dashes()) + if mightstroke or hatch: self.set_color(*gc.get_rgb()[:3]) write('gsave\n') write(self._get_clip_cmd(gc)) - # Jochen, is the strip necessary? - this could be a honking big string write(ps.strip()) write("\n") @@ -784,30 +807,6 @@ def _draw_ps(self, ps, gc, rgbFace, fill=True, stroke=True, command=None): write("grestore\n") -def _is_transparent(rgb_or_rgba): - if rgb_or_rgba is None: - return True # Consistent with rgbFace semantics. - elif len(rgb_or_rgba) == 4: - if rgb_or_rgba[3] == 0: - return True - if rgb_or_rgba[3] != 1: - _log.warning( - "The PostScript backend does not support transparency; " - "partially transparent artists will be rendered opaque.") - return False - else: # len() == 3. - return False - - -@_api.deprecated("3.4", alternative="GraphicsContextBase") -class GraphicsContextPS(GraphicsContextBase): - def get_capstyle(self): - return {'butt': 0, 'round': 1, 'projecting': 2}[super().get_capstyle()] - - def get_joinstyle(self): - return {'miter': 0, 'round': 1, 'bevel': 2}[super().get_joinstyle()] - - class _Orientation(Enum): portrait, landscape = range(2) @@ -823,21 +822,13 @@ class FigureCanvasPS(FigureCanvasBase): def get_default_filetype(self): return 'ps' - def print_ps(self, outfile, *args, **kwargs): - return self._print_ps(outfile, 'ps', *args, **kwargs) - - def print_eps(self, outfile, *args, **kwargs): - return self._print_ps(outfile, 'eps', *args, **kwargs) - - @_api.delete_parameter("3.4", "dpi") def _print_ps( - self, outfile, format, *args, - dpi=None, metadata=None, papertype=None, orientation='portrait', - **kwargs): + self, fmt, outfile, *, + metadata=None, papertype=None, orientation='portrait', + bbox_inches_restore=None, **kwargs): - if dpi is None: # always use this branch after deprecation elapses. - dpi = self.figure.get_dpi() - self.figure.set_dpi(72) # Override the dpi kwarg + dpi = self.figure.dpi + self.figure.dpi = 72 # Override the dpi kwarg dsc_comments = {} if isinstance(outfile, (str, os.PathLike)): @@ -868,28 +859,24 @@ def _print_ps( printer = (self._print_figure_tex if mpl.rcParams['text.usetex'] else self._print_figure) - printer(outfile, format, dpi=dpi, dsc_comments=dsc_comments, - orientation=orientation, papertype=papertype, **kwargs) + printer(fmt, outfile, dpi=dpi, dsc_comments=dsc_comments, + orientation=orientation, papertype=papertype, + bbox_inches_restore=bbox_inches_restore, **kwargs) - @_check_savefig_extra_args def _print_figure( - self, outfile, format, *, + self, fmt, outfile, *, dpi, dsc_comments, orientation, papertype, bbox_inches_restore=None): """ Render the figure to a filesystem path or a file-like object. Parameters are as for `.print_figure`, except that *dsc_comments* is a - all string containing Document Structuring Convention comments, + string containing Document Structuring Convention comments, generated from the *metadata* parameter to `.print_figure`. """ - is_eps = format == 'eps' - if isinstance(outfile, (str, os.PathLike)): - outfile = os.fspath(outfile) - passed_in_file_object = False - elif is_writable_file_like(outfile): - passed_in_file_object = True - else: + is_eps = fmt == 'eps' + if not (isinstance(outfile, (str, os.PathLike)) + or is_writable_file_like(outfile)): raise ValueError("outfile must be a path or a file-like object") # find the appropriate papertype @@ -960,24 +947,15 @@ def print_figure_impl(fh): in ps_renderer._character_tracker.used.items(): if not chars: continue - font = get_font(font_path) - glyph_ids = [font.get_char_index(c) for c in chars] fonttype = mpl.rcParams['ps.fonttype'] # Can't use more than 255 chars from a single Type 3 font. - if len(glyph_ids) > 255: + if len(chars) > 255: fonttype = 42 fh.flush() if fonttype == 3: - fh.write(_font_to_ps_type3(font_path, glyph_ids)) - else: - try: - convert_ttf_to_ps(os.fsencode(font_path), - fh, fonttype, glyph_ids) - except RuntimeError: - _log.warning( - "The PostScript backend does not currently " - "support the selected font.") - raise + fh.write(_font_to_ps_type3(font_path, chars)) + else: # Type 42 only. + _font_to_ps_type42(font_path, chars, fh) print("end", file=fh) print("%%EndProlog", file=fh) @@ -1016,27 +994,14 @@ def print_figure_impl(fh): tmpfile, is_eps, ptype=papertype, bbox=bbox) _move_path_to_path_or_stream(tmpfile, outfile) - else: - # Write directly to outfile. - if passed_in_file_object: - requires_unicode = file_requires_unicode(outfile) - - if not requires_unicode: - fh = TextIOWrapper(outfile, encoding="latin-1") - # Prevent the TextIOWrapper from closing the underlying - # file. - fh.close = lambda: None - else: - fh = outfile - - print_figure_impl(fh) - else: - with open(outfile, 'w', encoding='latin-1') as fh: - print_figure_impl(fh) + else: # Write directly to outfile. + with cbook.open_file_cm(outfile, "w", encoding="latin-1") as file: + if not file_requires_unicode(file): + file = codecs.getwriter("latin-1")(file) + print_figure_impl(file) - @_check_savefig_extra_args def _print_figure_tex( - self, outfile, format, *, + self, fmt, outfile, *, dpi, dsc_comments, orientation, papertype, bbox_inches_restore=None): """ @@ -1046,7 +1011,7 @@ def _print_figure_tex( The rest of the behavior is as for `._print_figure`. """ - is_eps = format == 'eps' + is_eps = fmt == 'eps' width, height = self.figure.get_size_inches() xo = 0 @@ -1070,8 +1035,8 @@ def _print_figure_tex( # write to a temp file, we'll move it to outfile when done with TemporaryDirectory() as tmpdir: - tmpfile = os.path.join(tmpdir, "tmp.ps") - pathlib.Path(tmpfile).write_text( + tmppath = pathlib.Path(tmpdir, "tmp.ps") + tmppath.write_text( f"""\ %!PS-Adobe-3.0 EPSF-3.0 {dsc_comments} @@ -1107,35 +1072,38 @@ def _print_figure_tex( papertype = _get_papertype(width, height) paper_width, paper_height = papersize[papertype] - texmanager = ps_renderer.get_texmanager() - font_preamble = texmanager.get_font_preamble() - custom_preamble = texmanager.get_custom_preamble() - - psfrag_rotated = convert_psfrags(tmpfile, ps_renderer.psfrag, - font_preamble, - custom_preamble, paper_width, - paper_height, - orientation.name) + psfrag_rotated = _convert_psfrags( + tmppath, ps_renderer.psfrag, paper_width, paper_height, + orientation.name) if (mpl.rcParams['ps.usedistiller'] == 'ghostscript' or mpl.rcParams['text.usetex']): _try_distill(gs_distill, - tmpfile, is_eps, ptype=papertype, bbox=bbox, + tmppath, is_eps, ptype=papertype, bbox=bbox, rotated=psfrag_rotated) elif mpl.rcParams['ps.usedistiller'] == 'xpdf': _try_distill(xpdf_distill, - tmpfile, is_eps, ptype=papertype, bbox=bbox, + tmppath, is_eps, ptype=papertype, bbox=bbox, rotated=psfrag_rotated) - _move_path_to_path_or_stream(tmpfile, outfile) + _move_path_to_path_or_stream(tmppath, outfile) + + print_ps = functools.partialmethod(_print_ps, "ps") + print_eps = functools.partialmethod(_print_ps, "eps") def draw(self): - _no_output_draw(self.figure) + self.figure.draw_without_rendering() return super().draw() +@_api.deprecated("3.6") def convert_psfrags(tmpfile, psfrags, font_preamble, custom_preamble, paper_width, paper_height, orientation): + return _convert_psfrags( + pathlib.Path(tmpfile), psfrags, paper_width, paper_height, orientation) + + +def _convert_psfrags(tmppath, psfrags, paper_width, paper_height, orientation): """ When we want to use the LaTeX backend with postscript, we write PSFrag tags to a temporary postscript file, each one marking a position for LaTeX to @@ -1146,8 +1114,9 @@ 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}""\n" - r"\usepackage[dvips]{graphicx}""\n" + mpl.texmanager._usepackage_if_not_loaded("color") + + mpl.texmanager._usepackage_if_not_loaded("graphicx") + + mpl.texmanager._usepackage_if_not_loaded("psfrag") + r"\geometry{papersize={%(width)sin,%(height)sin},margin=0in}" % {"width": paper_width, "height": paper_height} }): @@ -1161,7 +1130,7 @@ def convert_psfrags(tmpfile, psfrags, font_preamble, custom_preamble, % { "psfrags": "\n".join(psfrags), "angle": 90 if orientation == 'landscape' else 0, - "epsfile": pathlib.Path(tmpfile).resolve().as_posix(), + "epsfile": tmppath.resolve().as_posix(), }, fontsize=10) # tex's default fontsize. @@ -1169,7 +1138,7 @@ def convert_psfrags(tmpfile, psfrags, font_preamble, custom_preamble, psfile = os.path.join(tmpdir, "tmp.ps") cbook._check_and_log_subprocess( ['dvips', '-q', '-R0', '-o', psfile, dvifile], _log) - shutil.move(psfile, tmpfile) + shutil.move(psfile, tmppath) # check if the dvips created a ps in landscape paper. Somehow, # above latex+dvips results in a ps file in a landscape mode for a @@ -1178,14 +1147,14 @@ def convert_psfrags(tmpfile, psfrags, font_preamble, custom_preamble, # the generated ps file is in landscape and return this # information. The return value is used in pstoeps step to recover # the correct bounding box. 2010-06-05 JJL - with open(tmpfile) as fh: + with open(tmppath) as fh: psfrag_rotated = "Landscape" in fh.read(1000) return psfrag_rotated -def _try_distill(func, *args, **kwargs): +def _try_distill(func, tmppath, *args, **kwargs): try: - func(*args, **kwargs) + func(str(tmppath), *args, **kwargs) except mpl.ExecutableNotFoundError as exc: _log.warning("%s. Distillation step skipped.", exc) @@ -1219,7 +1188,7 @@ def gs_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False): # the original bbox can be restored during the pstoeps step. if eps: - # For some versions of gs, above steps result in an ps file where the + # For some versions of gs, above steps result in a ps file where the # original bbox is no more correct. Do not adjust bbox for now. pstoeps(tmpfile, bbox, rotated=rotated) @@ -1234,32 +1203,26 @@ def xpdf_distill(tmpfile, eps=False, ptype='letter', bbox=None, rotated=False): mpl._get_executable_info("gs") # Effectively checks for ps2pdf. mpl._get_executable_info("pdftops") - pdffile = tmpfile + '.pdf' - psfile = tmpfile + '.ps' - - # Pass options as `-foo#bar` instead of `-foo=bar` to keep Windows happy - # (https://www.ghostscript.com/doc/9.22/Use.htm#MS_Windows). - cbook._check_and_log_subprocess( - ["ps2pdf", - "-dAutoFilterColorImages#false", - "-dAutoFilterGrayImages#false", - "-sAutoRotatePages#None", - "-sGrayImageFilter#FlateEncode", - "-sColorImageFilter#FlateEncode", - "-dEPSCrop" if eps else "-sPAPERSIZE#%s" % ptype, - tmpfile, pdffile], _log) - cbook._check_and_log_subprocess( - ["pdftops", "-paper", "match", "-level2", pdffile, psfile], _log) - - os.remove(tmpfile) - shutil.move(psfile, tmpfile) - + with TemporaryDirectory() as tmpdir: + tmppdf = pathlib.Path(tmpdir, "tmp.pdf") + tmpps = pathlib.Path(tmpdir, "tmp.ps") + # Pass options as `-foo#bar` instead of `-foo=bar` to keep Windows + # happy (https://ghostscript.com/doc/9.56.1/Use.htm#MS_Windows). + cbook._check_and_log_subprocess( + ["ps2pdf", + "-dAutoFilterColorImages#false", + "-dAutoFilterGrayImages#false", + "-sAutoRotatePages#None", + "-sGrayImageFilter#FlateEncode", + "-sColorImageFilter#FlateEncode", + "-dEPSCrop" if eps else "-sPAPERSIZE#%s" % ptype, + tmpfile, tmppdf], _log) + cbook._check_and_log_subprocess( + ["pdftops", "-paper", "match", "-level2", tmppdf, tmpps], _log) + shutil.move(tmpps, tmpfile) if eps: pstoeps(tmpfile) - for fname in glob.glob(tmpfile+'.*'): - os.remove(fname) - def get_bbox_header(lbrt, rotated=False): """ @@ -1348,8 +1311,8 @@ def pstoeps(tmpfile, bbox=None, rotated=False): # the matplotlib primitives and some abbreviations. # # References: -# http://www.adobe.com/products/postscript/pdfs/PLRM.pdf -# http://www.mactech.com/articles/mactech/Vol.09/09.04/PostscriptTutorial/ +# https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf +# http://preserve.mactech.com/articles/mactech/Vol.09/09.04/PostscriptTutorial # http://www.math.ubc.ca/people/faculty/cass/graphics/text/www/ # @@ -1394,4 +1357,5 @@ def pstoeps(tmpfile, bbox=None, rotated=False): @_Backend.export class _BackendPS(_Backend): + backend_version = 'Level II' FigureCanvas = FigureCanvasPS diff --git a/lib/matplotlib/backends/backend_qt.py b/lib/matplotlib/backends/backend_qt.py new file mode 100644 index 000000000000..fc6639914d35 --- /dev/null +++ b/lib/matplotlib/backends/backend_qt.py @@ -0,0 +1,1032 @@ +import functools +import os +import sys +import traceback + +import matplotlib as mpl +from matplotlib import _api, backend_tools, cbook +from matplotlib._pylab_helpers import Gcf +from matplotlib.backend_bases import ( + _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, + TimerBase, cursors, ToolContainerBase, MouseButton, + CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) +import matplotlib.backends.qt_editor.figureoptions as figureoptions +from . import qt_compat +from .qt_compat import ( + QtCore, QtGui, QtWidgets, __version__, QT_API, + _enum, _to_int, _isdeleted, _maybe_allow_interrupt +) + + +# SPECIAL_KEYS are Qt::Key that do *not* return their Unicode name +# instead they have manually specified names. +SPECIAL_KEYS = { + _to_int(getattr(_enum("QtCore.Qt.Key"), k)): v for k, v in [ + ("Key_Escape", "escape"), + ("Key_Tab", "tab"), + ("Key_Backspace", "backspace"), + ("Key_Return", "enter"), + ("Key_Enter", "enter"), + ("Key_Insert", "insert"), + ("Key_Delete", "delete"), + ("Key_Pause", "pause"), + ("Key_SysReq", "sysreq"), + ("Key_Clear", "clear"), + ("Key_Home", "home"), + ("Key_End", "end"), + ("Key_Left", "left"), + ("Key_Up", "up"), + ("Key_Right", "right"), + ("Key_Down", "down"), + ("Key_PageUp", "pageup"), + ("Key_PageDown", "pagedown"), + ("Key_Shift", "shift"), + # In OSX, the control and super (aka cmd/apple) keys are switched. + ("Key_Control", "control" if sys.platform != "darwin" else "cmd"), + ("Key_Meta", "meta" if sys.platform != "darwin" else "control"), + ("Key_Alt", "alt"), + ("Key_CapsLock", "caps_lock"), + ("Key_F1", "f1"), + ("Key_F2", "f2"), + ("Key_F3", "f3"), + ("Key_F4", "f4"), + ("Key_F5", "f5"), + ("Key_F6", "f6"), + ("Key_F7", "f7"), + ("Key_F8", "f8"), + ("Key_F9", "f9"), + ("Key_F10", "f10"), + ("Key_F10", "f11"), + ("Key_F12", "f12"), + ("Key_Super_L", "super"), + ("Key_Super_R", "super"), + ] +} +# Define which modifier keys are collected on keyboard events. +# Elements are (Qt::KeyboardModifiers, Qt::Key) tuples. +# Order determines the modifier order (ctrl+alt+...) reported by Matplotlib. +_MODIFIER_KEYS = [ + (_to_int(getattr(_enum("QtCore.Qt.KeyboardModifier"), mod)), + _to_int(getattr(_enum("QtCore.Qt.Key"), key))) + for mod, key in [ + ("ControlModifier", "Key_Control"), + ("AltModifier", "Key_Alt"), + ("ShiftModifier", "Key_Shift"), + ("MetaModifier", "Key_Meta"), + ] +] +cursord = { + k: getattr(_enum("QtCore.Qt.CursorShape"), v) for k, v in [ + (cursors.MOVE, "SizeAllCursor"), + (cursors.HAND, "PointingHandCursor"), + (cursors.POINTER, "ArrowCursor"), + (cursors.SELECT_REGION, "CrossCursor"), + (cursors.WAIT, "WaitCursor"), + (cursors.RESIZE_HORIZONTAL, "SizeHorCursor"), + (cursors.RESIZE_VERTICAL, "SizeVerCursor"), + ] +} + + +@_api.caching_module_getattr +class __getattr__: + qApp = _api.deprecated( + "3.6", alternative="QtWidgets.QApplication.instance()")( + property(lambda self: QtWidgets.QApplication.instance())) + + +# lru_cache keeps a reference to the QApplication instance, keeping it from +# being GC'd. +@functools.lru_cache(1) +def _create_qApp(): + app = QtWidgets.QApplication.instance() + + # Create a new QApplication and configure it if none exists yet, as only + # one QApplication can exist at a time. + if app is None: + # display_is_valid returns False only if on Linux and neither X11 + # nor Wayland display can be opened. + if not mpl._c_internal_utils.display_is_valid(): + raise RuntimeError('Invalid DISPLAY variable') + + # Check to make sure a QApplication from a different major version + # of Qt is not instantiated in the process + if QT_API in {'PyQt6', 'PySide6'}: + other_bindings = ('PyQt5', 'PySide2') + elif QT_API in {'PyQt5', 'PySide2'}: + other_bindings = ('PyQt6', 'PySide6') + else: + raise RuntimeError("Should never be here") + + for binding in other_bindings: + mod = sys.modules.get(f'{binding}.QtWidgets') + if mod is not None and mod.QApplication.instance() is not None: + other_core = sys.modules.get(f'{binding}.QtCore') + _api.warn_external( + f'Matplotlib is using {QT_API} which wraps ' + f'{QtCore.qVersion()} however an instantiated ' + f'QApplication from {binding} which wraps ' + f'{other_core.qVersion()} exists. Mixing Qt major ' + 'versions may not work as expected.' + ) + break + try: + QtWidgets.QApplication.setAttribute( + QtCore.Qt.AA_EnableHighDpiScaling) + except AttributeError: # Only for Qt>=5.6, <6. + pass + try: + QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( + QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) + except AttributeError: # Only for Qt>=5.14. + pass + app = QtWidgets.QApplication(["matplotlib"]) + if sys.platform == "darwin": + image = str(cbook._get_data_path('images/matplotlib.svg')) + icon = QtGui.QIcon(image) + app.setWindowIcon(icon) + app.lastWindowClosed.connect(app.quit) + cbook._setup_new_guiapp() + + try: + app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) # Only for Qt<6. + except AttributeError: + pass + + return app + + +class TimerQT(TimerBase): + """Subclass of `.TimerBase` using QTimer events.""" + + def __init__(self, *args, **kwargs): + # Create a new timer and connect the timeout() signal to the + # _on_timer method. + self._timer = QtCore.QTimer() + self._timer.timeout.connect(self._on_timer) + super().__init__(*args, **kwargs) + + def __del__(self): + # The check for deletedness is needed to avoid an error at animation + # shutdown with PySide2. + if not _isdeleted(self._timer): + self._timer_stop() + + def _timer_set_single_shot(self): + self._timer.setSingleShot(self._single) + + def _timer_set_interval(self): + self._timer.setInterval(self._interval) + + def _timer_start(self): + self._timer.start() + + def _timer_stop(self): + self._timer.stop() + + +class FigureCanvasQT(FigureCanvasBase, QtWidgets.QWidget): + required_interactive_framework = "qt" + _timer_cls = TimerQT + manager_class = _api.classproperty(lambda cls: FigureManagerQT) + + buttond = { + getattr(_enum("QtCore.Qt.MouseButton"), k): v for k, v in [ + ("LeftButton", MouseButton.LEFT), + ("RightButton", MouseButton.RIGHT), + ("MiddleButton", MouseButton.MIDDLE), + ("XButton1", MouseButton.BACK), + ("XButton2", MouseButton.FORWARD), + ] + } + + def __init__(self, figure=None): + _create_qApp() + super().__init__(figure=figure) + + self._draw_pending = False + self._is_drawing = False + self._draw_rect_callback = lambda painter: None + self._in_resize_event = False + + self.setAttribute( + _enum("QtCore.Qt.WidgetAttribute").WA_OpaquePaintEvent) + self.setMouseTracking(True) + self.resize(*self.get_width_height()) + + palette = QtGui.QPalette(QtGui.QColor("white")) + self.setPalette(palette) + + def _update_pixel_ratio(self): + if self._set_device_pixel_ratio( + self.devicePixelRatioF() or 1): # rarely, devicePixelRatioF=0 + # The easiest way to resize the canvas is to emit a resizeEvent + # since we implement all the logic for resizing the canvas for + # that event. + event = QtGui.QResizeEvent(self.size(), self.size()) + self.resizeEvent(event) + + def _update_screen(self, screen): + # Handler for changes to a window's attached screen. + self._update_pixel_ratio() + if screen is not None: + screen.physicalDotsPerInchChanged.connect(self._update_pixel_ratio) + screen.logicalDotsPerInchChanged.connect(self._update_pixel_ratio) + + def showEvent(self, event): + # Set up correct pixel ratio, and connect to any signal changes for it, + # once the window is shown (and thus has these attributes). + window = self.window().windowHandle() + window.screenChanged.connect(self._update_screen) + self._update_screen(window.screen()) + + def set_cursor(self, cursor): + # docstring inherited + self.setCursor(_api.check_getitem(cursord, cursor=cursor)) + + def mouseEventCoords(self, pos=None): + """ + Calculate mouse coordinates in physical pixels. + + Qt uses logical pixels, but the figure is scaled to physical + 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. + """ + if pos is None: + pos = self.mapFromGlobal(QtGui.QCursor.pos()) + elif hasattr(pos, "position"): # qt6 QtGui.QEvent + pos = pos.position() + elif hasattr(pos, "pos"): # qt5 QtCore.QEvent + pos = pos.pos() + # (otherwise, it's already a QPoint) + x = pos.x() + # flip y so y=0 is bottom of canvas + y = self.figure.bbox.height / self.device_pixel_ratio - pos.y() + return x * self.device_pixel_ratio, y * self.device_pixel_ratio + + def enterEvent(self, event): + # Force querying of the modifiers, as the cached modifier state can + # have been invalidated while the window was out of focus. + mods = QtWidgets.QApplication.instance().queryKeyboardModifiers() + LocationEvent("figure_enter_event", self, + *self.mouseEventCoords(event), + modifiers=self._mpl_modifiers(mods), + guiEvent=event)._process() + + def leaveEvent(self, event): + QtWidgets.QApplication.restoreOverrideCursor() + LocationEvent("figure_leave_event", self, + *self.mouseEventCoords(), + modifiers=self._mpl_modifiers(), + guiEvent=event)._process() + + def mousePressEvent(self, event): + button = self.buttond.get(event.button()) + if button is not None: + MouseEvent("button_press_event", self, + *self.mouseEventCoords(event), button, + modifiers=self._mpl_modifiers(), + guiEvent=event)._process() + + def mouseDoubleClickEvent(self, event): + button = self.buttond.get(event.button()) + if button is not None: + MouseEvent("button_press_event", self, + *self.mouseEventCoords(event), button, dblclick=True, + modifiers=self._mpl_modifiers(), + guiEvent=event)._process() + + def mouseMoveEvent(self, event): + MouseEvent("motion_notify_event", self, + *self.mouseEventCoords(event), + modifiers=self._mpl_modifiers(), + guiEvent=event)._process() + + def mouseReleaseEvent(self, event): + button = self.buttond.get(event.button()) + if button is not None: + MouseEvent("button_release_event", self, + *self.mouseEventCoords(event), button, + modifiers=self._mpl_modifiers(), + guiEvent=event)._process() + + def wheelEvent(self, event): + # from QWheelEvent::pixelDelta doc: pixelDelta is sometimes not + # provided (`isNull()`) and is unreliable on X11 ("xcb"). + if (event.pixelDelta().isNull() + or QtWidgets.QApplication.instance().platformName() == "xcb"): + steps = event.angleDelta().y() / 120 + else: + steps = event.pixelDelta().y() + if steps: + MouseEvent("scroll_event", self, + *self.mouseEventCoords(event), step=steps, + modifiers=self._mpl_modifiers(), + guiEvent=event)._process() + + def keyPressEvent(self, event): + key = self._get_key(event) + if key is not None: + KeyEvent("key_press_event", self, + key, *self.mouseEventCoords(), + guiEvent=event)._process() + + def keyReleaseEvent(self, event): + key = self._get_key(event) + if key is not None: + KeyEvent("key_release_event", self, + key, *self.mouseEventCoords(), + guiEvent=event)._process() + + def resizeEvent(self, event): + if self._in_resize_event: # Prevent PyQt6 recursion + return + self._in_resize_event = True + try: + w = event.size().width() * self.device_pixel_ratio + h = event.size().height() * self.device_pixel_ratio + dpival = self.figure.dpi + winch = w / dpival + hinch = h / dpival + self.figure.set_size_inches(winch, hinch, forward=False) + # pass back into Qt to let it finish + QtWidgets.QWidget.resizeEvent(self, event) + # emit our resize events + ResizeEvent("resize_event", self)._process() + self.draw_idle() + finally: + self._in_resize_event = False + + def sizeHint(self): + w, h = self.get_width_height() + return QtCore.QSize(w, h) + + def minumumSizeHint(self): + return QtCore.QSize(10, 10) + + @staticmethod + def _mpl_modifiers(modifiers=None, *, exclude=None): + if modifiers is None: + modifiers = QtWidgets.QApplication.instance().keyboardModifiers() + modifiers = _to_int(modifiers) + # get names of the pressed modifier keys + # 'control' is named 'control' when a standalone key, but 'ctrl' when a + # modifier + # bit twiddling to pick out modifier keys from modifiers bitmask, + # if exclude is a MODIFIER, it should not be duplicated in mods + return [SPECIAL_KEYS[key].replace('control', 'ctrl') + for mask, key in _MODIFIER_KEYS + if exclude != key and modifiers & mask] + + def _get_key(self, event): + event_key = event.key() + mods = self._mpl_modifiers(exclude=event_key) + try: + # for certain keys (enter, left, backspace, etc) use a word for the + # key, rather than Unicode + key = SPECIAL_KEYS[event_key] + except KeyError: + # Unicode defines code points up to 0x10ffff (sys.maxunicode) + # QT will use Key_Codes larger than that for keyboard keys that are + # not Unicode characters (like multimedia keys) + # skip these + # if you really want them, you should add them to SPECIAL_KEYS + if event_key > sys.maxunicode: + return None + + key = chr(event_key) + # qt delivers capitalized letters. fix capitalization + # note that capslock is ignored + if 'shift' in mods: + mods.remove('shift') + else: + key = key.lower() + + return '+'.join(mods + [key]) + + def flush_events(self): + # docstring inherited + QtWidgets.QApplication.instance().processEvents() + + def start_event_loop(self, timeout=0): + # docstring inherited + if hasattr(self, "_event_loop") and self._event_loop.isRunning(): + raise RuntimeError("Event loop already running") + self._event_loop = event_loop = QtCore.QEventLoop() + if timeout > 0: + _ = QtCore.QTimer.singleShot(int(timeout * 1000), event_loop.quit) + + with _maybe_allow_interrupt(event_loop): + qt_compat._exec(event_loop) + + def stop_event_loop(self, event=None): + # docstring inherited + if hasattr(self, "_event_loop"): + self._event_loop.quit() + + def draw(self): + """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: + return + with cbook._setattr_cm(self, _is_drawing=True): + super().draw() + 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 + # 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. + # TODO: queued signal connection might be safer than singleShot + if not (getattr(self, '_draw_pending', False) or + getattr(self, '_is_drawing', False)): + self._draw_pending = True + QtCore.QTimer.singleShot(0, self._draw_idle) + + def blit(self, bbox=None): + # docstring inherited + if bbox is None and self.figure: + bbox = self.figure.bbox # Blit the entire canvas if bbox is None. + # repaint uses logical pixels, not physical pixels like the renderer. + l, b, w, h = [int(pt / self.device_pixel_ratio) for pt in bbox.bounds] + t = b + h + self.repaint(l, self.rect().height() - t, w, h) + + def _draw_idle(self): + with self._idle_draw_cntx(): + if not self._draw_pending: + return + self._draw_pending = False + if self.height() < 0 or self.width() < 0: + return + try: + self.draw() + except Exception: + # Uncaught exceptions are fatal for PyQt5, so catch them. + traceback.print_exc() + + def drawRectangle(self, rect): + # Draw the zoom rectangle to the QPainter. _draw_rect_callback needs + # to be called at the end of paintEvent. + if rect is not None: + x0, y0, w, h = [int(pt / self.device_pixel_ratio) for pt in rect] + x1 = x0 + w + y1 = y0 + h + def _draw_rect_callback(painter): + pen = QtGui.QPen( + QtGui.QColor("black"), + 1 / self.device_pixel_ratio + ) + + pen.setDashPattern([3, 3]) + for color, offset in [ + (QtGui.QColor("black"), 0), + (QtGui.QColor("white"), 3), + ]: + pen.setDashOffset(offset) + pen.setColor(color) + painter.setPen(pen) + # Draw the lines from x0, y0 towards x1, y1 so that the + # dashes don't "jump" when moving the zoom box. + painter.drawLine(x0, y0, x0, y1) + painter.drawLine(x0, y0, x1, y0) + painter.drawLine(x0, y1, x1, y1) + painter.drawLine(x1, y0, x1, y1) + else: + def _draw_rect_callback(painter): + return + self._draw_rect_callback = _draw_rect_callback + self.update() + + +class MainWindow(QtWidgets.QMainWindow): + closing = QtCore.Signal() + + def closeEvent(self, event): + self.closing.emit() + super().closeEvent(event) + + +class FigureManagerQT(FigureManagerBase): + """ + Attributes + ---------- + canvas : `FigureCanvas` + The FigureCanvas instance + num : int or str + The Figure number + toolbar : qt.QToolBar + The qt.QToolBar + window : qt.QMainWindow + The qt.QMainWindow + """ + + def __init__(self, canvas, num): + self.window = MainWindow() + super().__init__(canvas, num) + self.window.closing.connect( + # The lambda prevents the event from being immediately gc'd. + lambda: CloseEvent("close_event", self.canvas)._process()) + self.window.closing.connect(self._widgetclosed) + + if sys.platform != "darwin": + image = str(cbook._get_data_path('images/matplotlib.svg')) + icon = QtGui.QIcon(image) + self.window.setWindowIcon(icon) + + self.window._destroying = False + + if self.toolbar: + self.window.addToolBar(self.toolbar) + tbs_height = self.toolbar.sizeHint().height() + else: + tbs_height = 0 + + # resize the main window so it will display the canvas with the + # requested size: + cs = canvas.sizeHint() + cs_height = cs.height() + height = cs_height + tbs_height + self.window.resize(cs.width(), height) + + self.window.setCentralWidget(self.canvas) + + if mpl.is_interactive(): + self.window.show() + self.canvas.draw_idle() + + # 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(_enum("QtCore.Qt.FocusPolicy").StrongFocus) + self.canvas.setFocus() + + self.window.raise_() + + def full_screen_toggle(self): + if self.window.isFullScreen(): + self.window.showNormal() + else: + self.window.showFullScreen() + + def _widgetclosed(self): + if self.window._destroying: + return + self.window._destroying = True + try: + Gcf.destroy(self) + except AttributeError: + pass + # It seems that when the python session is killed, + # Gcf can get destroyed before the Gcf.destroy + # line is run, leading to a useless AttributeError. + + def resize(self, width, height): + # The Qt methods return sizes in 'virtual' pixels so we do need to + # rescale from physical to logical pixels. + width = int(width / self.canvas.device_pixel_ratio) + height = int(height / self.canvas.device_pixel_ratio) + extra_width = self.window.width() - self.canvas.width() + extra_height = self.window.height() - self.canvas.height() + self.canvas.resize(width, height) + self.window.resize(width + extra_width, height + extra_height) + + @classmethod + def start_main_loop(cls): + qapp = QtWidgets.QApplication.instance() + if qapp: + with _maybe_allow_interrupt(qapp): + qt_compat._exec(qapp) + + def show(self): + self.window.show() + if mpl.rcParams['figure.raise_window']: + self.window.activateWindow() + self.window.raise_() + + def destroy(self, *args): + # check for qApp first, as PySide deletes it in its atexit handler + if QtWidgets.QApplication.instance() is None: + return + if self.window._destroying: + return + self.window._destroying = True + if self.toolbar: + self.toolbar.destroy() + self.window.close() + + def get_window_title(self): + return self.window.windowTitle() + + def set_window_title(self, title): + self.window.setWindowTitle(title) + + +class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar): + message = QtCore.Signal(str) + + toolitems = [*NavigationToolbar2.toolitems] + toolitems.insert( + # Add 'customize' action after 'subplots' + [name for name, *_ in toolitems].index("Subplots") + 1, + ("Customize", "Edit axis, curve and image parameters", + "qt4_editor_options", "edit_parameters")) + + def __init__(self, canvas, parent=None, coordinates=True): + """coordinates: should we show the coordinates on the right?""" + QtWidgets.QToolBar.__init__(self, parent) + self.setAllowedAreas(QtCore.Qt.ToolBarArea( + _to_int(_enum("QtCore.Qt.ToolBarArea").TopToolBarArea) | + _to_int(_enum("QtCore.Qt.ToolBarArea").BottomToolBarArea))) + + self.coordinates = coordinates + self._actions = {} # mapping of toolitem method names to QActions. + self._subplot_dialog = None + + for text, tooltip_text, image_file, callback in self.toolitems: + if text is None: + self.addSeparator() + else: + a = self.addAction(self._icon(image_file + '.png'), + text, getattr(self, callback)) + self._actions[callback] = a + if callback in ['zoom', 'pan']: + a.setCheckable(True) + if tooltip_text is not None: + a.setToolTip(tooltip_text) + + # Add the (x, y) location widget at the right side of the toolbar + # The stretch factor is 1 which means any resizing of the toolbar + # will resize this label instead of the buttons. + if self.coordinates: + self.locLabel = QtWidgets.QLabel("", self) + self.locLabel.setAlignment(QtCore.Qt.AlignmentFlag( + _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignRight) | + _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignVCenter))) + self.locLabel.setSizePolicy(QtWidgets.QSizePolicy( + _enum("QtWidgets.QSizePolicy.Policy").Expanding, + _enum("QtWidgets.QSizePolicy.Policy").Ignored, + )) + labelAction = self.addWidget(self.locLabel) + labelAction.setVisible(True) + + NavigationToolbar2.__init__(self, canvas) + + def _icon(self, name): + """ + Construct a `.QIcon` from an image file *name*, including the extension + and relative to Matplotlib's "images" data directory. + """ + # use a high-resolution icon with suffix '_large' if available + # note: user-provided icons may not have '_large' versions + path_regular = cbook._get_data_path('images', name) + path_large = path_regular.with_name( + path_regular.name.replace('.png', '_large.png')) + filename = str(path_large if path_large.exists() else path_regular) + + pm = QtGui.QPixmap(filename) + pm.setDevicePixelRatio( + self.devicePixelRatioF() or 1) # rarely, devicePixelRatioF=0 + if self.palette().color(self.backgroundRole()).value() < 128: + icon_color = self.palette().color(self.foregroundRole()) + mask = pm.createMaskFromColor( + QtGui.QColor('black'), + _enum("QtCore.Qt.MaskMode").MaskOutColor) + pm.fill(icon_color) + pm.setMask(mask) + return QtGui.QIcon(pm) + + def edit_parameters(self): + axes = self.canvas.figure.get_axes() + if not axes: + QtWidgets.QMessageBox.warning( + self.canvas.parent(), "Error", "There are no axes to edit.") + return + elif len(axes) == 1: + ax, = axes + else: + titles = [ + ax.get_label() or + ax.get_title() or + ax.get_title("left") or + ax.get_title("right") or + " - ".join(filter(None, [ax.get_xlabel(), ax.get_ylabel()])) or + f"" + for ax in axes] + duplicate_titles = [ + title for title in titles if titles.count(title) > 1] + for i, ax in enumerate(axes): + if titles[i] in duplicate_titles: + titles[i] += f" (id: {id(ax):#x})" # Deduplicate titles. + item, ok = QtWidgets.QInputDialog.getItem( + self.canvas.parent(), + 'Customize', 'Select axes:', titles, 0, False) + if not ok: + return + ax = axes[titles.index(item)] + figureoptions.figure_edit(ax, self) + + def _update_buttons_checked(self): + # sync button checkstates to match active mode + if 'pan' in self._actions: + self._actions['pan'].setChecked(self.mode.name == 'PAN') + if 'zoom' in self._actions: + self._actions['zoom'].setChecked(self.mode.name == 'ZOOM') + + def pan(self, *args): + super().pan(*args) + self._update_buttons_checked() + + def zoom(self, *args): + super().zoom(*args) + self._update_buttons_checked() + + def set_message(self, s): + self.message.emit(s) + if self.coordinates: + self.locLabel.setText(s) + + def draw_rubberband(self, event, x0, y0, x1, y1): + height = self.canvas.figure.bbox.height + y1 = height - y1 + y0 = height - y0 + rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] + self.canvas.drawRectangle(rect) + + def remove_rubberband(self): + self.canvas.drawRectangle(None) + + def configure_subplots(self): + if self._subplot_dialog is None: + self._subplot_dialog = SubplotToolQt( + self.canvas.figure, self.canvas.parent()) + self.canvas.mpl_connect( + "close_event", lambda e: self._subplot_dialog.reject()) + self._subplot_dialog.update_from_current_subplotpars() + self._subplot_dialog.show() + return self._subplot_dialog + + def save_figure(self, *args): + filetypes = self.canvas.get_supported_filetypes_grouped() + sorted_filetypes = sorted(filetypes.items()) + default_filetype = self.canvas.get_default_filetype() + + startpath = os.path.expanduser(mpl.rcParams['savefig.directory']) + start = os.path.join(startpath, self.canvas.get_default_filename()) + filters = [] + selectedFilter = None + for name, exts in sorted_filetypes: + exts_list = " ".join(['*.%s' % ext for ext in exts]) + filter = '%s (%s)' % (name, exts_list) + if default_filetype in exts: + selectedFilter = filter + filters.append(filter) + filters = ';;'.join(filters) + + 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 != "": + mpl.rcParams['savefig.directory'] = os.path.dirname(fname) + try: + self.canvas.figure.savefig(fname) + except Exception as e: + QtWidgets.QMessageBox.critical( + self, "Error saving file", str(e), + _enum("QtWidgets.QMessageBox.StandardButton").Ok, + _enum("QtWidgets.QMessageBox.StandardButton").NoButton) + + def set_history_buttons(self): + can_backward = self._nav_stack._pos > 0 + can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 + if 'back' in self._actions: + self._actions['back'].setEnabled(can_backward) + if 'forward' in self._actions: + self._actions['forward'].setEnabled(can_forward) + + +class SubplotToolQt(QtWidgets.QDialog): + def __init__(self, targetfig, parent): + super().__init__() + self.setWindowIcon(QtGui.QIcon( + str(cbook._get_data_path("images/matplotlib.png")))) + self.setObjectName("SubplotTool") + self._spinboxes = {} + main_layout = QtWidgets.QHBoxLayout() + self.setLayout(main_layout) + for group, spinboxes, buttons in [ + ("Borders", + ["top", "bottom", "left", "right"], + [("Export values", self._export_values)]), + ("Spacings", + ["hspace", "wspace"], + [("Tight layout", self._tight_layout), + ("Reset", self._reset), + ("Close", self.close)])]: + layout = QtWidgets.QVBoxLayout() + main_layout.addLayout(layout) + box = QtWidgets.QGroupBox(group) + layout.addWidget(box) + inner = QtWidgets.QFormLayout(box) + for name in spinboxes: + self._spinboxes[name] = spinbox = QtWidgets.QDoubleSpinBox() + spinbox.setRange(0, 1) + spinbox.setDecimals(3) + spinbox.setSingleStep(0.005) + spinbox.setKeyboardTracking(False) + spinbox.valueChanged.connect(self._on_value_changed) + inner.addRow(name, spinbox) + layout.addStretch(1) + for name, method in buttons: + button = QtWidgets.QPushButton(name) + # Don't trigger on , which is used to input values. + button.setAutoDefault(False) + button.clicked.connect(method) + layout.addWidget(button) + if name == "Close": + button.setFocus() + self._figure = targetfig + self._defaults = {} + self._export_values_dialog = None + self.update_from_current_subplotpars() + + def update_from_current_subplotpars(self): + self._defaults = {spinbox: getattr(self._figure.subplotpars, name) + for name, spinbox in self._spinboxes.items()} + self._reset() # Set spinbox current values without triggering signals. + + def _export_values(self): + # Explicitly round to 3 decimals (which is also the spinbox precision) + # to avoid numbers of the form 0.100...001. + self._export_values_dialog = QtWidgets.QDialog() + layout = QtWidgets.QVBoxLayout() + self._export_values_dialog.setLayout(layout) + text = QtWidgets.QPlainTextEdit() + text.setReadOnly(True) + layout.addWidget(text) + text.setPlainText( + ",\n".join(f"{attr}={spinbox.value():.3}" + for attr, spinbox in self._spinboxes.items())) + # Adjust the height of the text widget to fit the whole text, plus + # some padding. + size = text.maximumSize() + size.setHeight( + QtGui.QFontMetrics(text.document().defaultFont()) + .size(0, text.toPlainText()).height() + 20) + text.setMaximumSize(size) + self._export_values_dialog.show() + + def _on_value_changed(self): + spinboxes = self._spinboxes + # Set all mins and maxes, so that this can also be used in _reset(). + for lower, higher in [("bottom", "top"), ("left", "right")]: + spinboxes[higher].setMinimum(spinboxes[lower].value() + .001) + spinboxes[lower].setMaximum(spinboxes[higher].value() - .001) + self._figure.subplots_adjust( + **{attr: spinbox.value() for attr, spinbox in spinboxes.items()}) + self._figure.canvas.draw_idle() + + def _tight_layout(self): + self._figure.tight_layout() + for attr, spinbox in self._spinboxes.items(): + spinbox.blockSignals(True) + spinbox.setValue(getattr(self._figure.subplotpars, attr)) + spinbox.blockSignals(False) + self._figure.canvas.draw_idle() + + def _reset(self): + for spinbox, value in self._defaults.items(): + spinbox.setRange(0, 1) + spinbox.blockSignals(True) + spinbox.setValue(value) + spinbox.blockSignals(False) + self._on_value_changed() + + +class ToolbarQt(ToolContainerBase, QtWidgets.QToolBar): + def __init__(self, toolmanager, parent=None): + ToolContainerBase.__init__(self, toolmanager) + QtWidgets.QToolBar.__init__(self, parent) + self.setAllowedAreas(QtCore.Qt.ToolBarArea( + _to_int(_enum("QtCore.Qt.ToolBarArea").TopToolBarArea) | + _to_int(_enum("QtCore.Qt.ToolBarArea").BottomToolBarArea))) + message_label = QtWidgets.QLabel("") + message_label.setAlignment(QtCore.Qt.AlignmentFlag( + _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignRight) | + _to_int(_enum("QtCore.Qt.AlignmentFlag").AlignVCenter))) + message_label.setSizePolicy(QtWidgets.QSizePolicy( + _enum("QtWidgets.QSizePolicy.Policy").Expanding, + _enum("QtWidgets.QSizePolicy.Policy").Ignored, + )) + self._message_action = self.addWidget(message_label) + self._toolitems = {} + self._groups = {} + + def add_toolitem( + self, name, group, position, image_file, description, toggle): + + button = QtWidgets.QToolButton(self) + if image_file: + button.setIcon(NavigationToolbar2QT._icon(self, image_file)) + button.setText(name) + if description: + button.setToolTip(description) + + def handler(): + self.trigger_tool(name) + if toggle: + button.setCheckable(True) + button.toggled.connect(handler) + else: + button.clicked.connect(handler) + + self._toolitems.setdefault(name, []) + self._add_to_group(group, name, button, position) + self._toolitems[name].append((button, handler)) + + def _add_to_group(self, group, name, button, position): + gr = self._groups.get(group, []) + if not gr: + sep = self.insertSeparator(self._message_action) + gr.append(sep) + before = gr[position] + widget = self.insertWidget(before, button) + gr.insert(position, widget) + self._groups[group] = gr + + def toggle_toolitem(self, name, toggled): + if name not in self._toolitems: + return + for button, handler in self._toolitems[name]: + button.toggled.disconnect(handler) + button.setChecked(toggled) + button.toggled.connect(handler) + + def remove_toolitem(self, name): + for button, handler in self._toolitems[name]: + button.setParent(None) + del self._toolitems[name] + + def set_message(self, s): + self.widgetForAction(self._message_action).setText(s) + + +@backend_tools._register_tool_class(FigureCanvasQT) +class ConfigureSubplotsQt(backend_tools.ConfigureSubplotsBase): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._subplot_dialog = None + + def trigger(self, *args): + NavigationToolbar2QT.configure_subplots(self) + + +@backend_tools._register_tool_class(FigureCanvasQT) +class SaveFigureQt(backend_tools.SaveFigureBase): + def trigger(self, *args): + NavigationToolbar2QT.save_figure( + self._make_classic_style_pseudo_toolbar()) + + +@backend_tools._register_tool_class(FigureCanvasQT) +class RubberbandQt(backend_tools.RubberbandBase): + def draw_rubberband(self, x0, y0, x1, y1): + NavigationToolbar2QT.draw_rubberband( + self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) + + def remove_rubberband(self): + NavigationToolbar2QT.remove_rubberband( + self._make_classic_style_pseudo_toolbar()) + + +@backend_tools._register_tool_class(FigureCanvasQT) +class HelpQt(backend_tools.ToolHelpBase): + def trigger(self, *args): + QtWidgets.QMessageBox.information(None, "Help", self._get_help_html()) + + +@backend_tools._register_tool_class(FigureCanvasQT) +class ToolCopyToClipboardQT(backend_tools.ToolCopyToClipboardBase): + def trigger(self, *args, **kwargs): + pixmap = self.canvas.grab() + QtWidgets.QApplication.instance().clipboard().setPixmap(pixmap) + + +FigureManagerQT._toolbar2_class = NavigationToolbar2QT +FigureManagerQT._toolmanager_toolbar_class = ToolbarQt + + +@_Backend.export +class _BackendQT(_Backend): + backend_version = __version__ + FigureCanvas = FigureCanvasQT + FigureManager = FigureManagerQT + mainloop = FigureManagerQT.start_main_loop diff --git a/lib/matplotlib/backends/backend_qt4.py b/lib/matplotlib/backends/backend_qt4.py deleted file mode 100644 index 9443bff90557..000000000000 --- a/lib/matplotlib/backends/backend_qt4.py +++ /dev/null @@ -1,15 +0,0 @@ -from .. import _api -from .backend_qt5 import ( - backend_version, SPECIAL_KEYS, - SUPER, ALT, CTRL, SHIFT, MODIFIER_KEYS, # These are deprecated. - cursord, _create_qApp, _BackendQT5, TimerQT, MainWindow, FigureCanvasQT, - FigureManagerQT, NavigationToolbar2QT, SubplotToolQt, exception_handler) - - -_api.warn_deprecated("3.3", name=__name__, obj_type="backend") - - -@_BackendQT5.export -class _BackendQT4(_BackendQT5): - class FigureCanvas(FigureCanvasQT): - required_interactive_framework = "qt4" diff --git a/lib/matplotlib/backends/backend_qt4agg.py b/lib/matplotlib/backends/backend_qt4agg.py deleted file mode 100644 index 9f028c477643..000000000000 --- a/lib/matplotlib/backends/backend_qt4agg.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -Render to qt from agg -""" - -from .. import _api -from .backend_qt5agg import ( - _BackendQT5Agg, FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT) - - -_api.warn_deprecated("3.3", name=__name__, obj_type="backend") - - -@_BackendQT5Agg.export -class _BackendQT4Agg(_BackendQT5Agg): - class FigureCanvas(FigureCanvasQTAgg): - required_interactive_framework = "qt4" diff --git a/lib/matplotlib/backends/backend_qt4cairo.py b/lib/matplotlib/backends/backend_qt4cairo.py deleted file mode 100644 index 650b8166a6d1..000000000000 --- a/lib/matplotlib/backends/backend_qt4cairo.py +++ /dev/null @@ -1,11 +0,0 @@ -from .. import _api -from .backend_qt5cairo import _BackendQT5Cairo, FigureCanvasQTCairo - - -_api.warn_deprecated("3.3", name=__name__, obj_type="backend") - - -@_BackendQT5Cairo.export -class _BackendQT4Cairo(_BackendQT5Cairo): - class FigureCanvas(FigureCanvasQTCairo): - required_interactive_framework = "qt4" diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 0c2d32e1e8d1..d94062b723f4 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -1,1031 +1,28 @@ -import functools -import os -import signal -import sys -import traceback +from .. import backends -import matplotlib as mpl -from matplotlib import _api, backend_tools, cbook -from matplotlib._pylab_helpers import Gcf -from matplotlib.backend_bases import ( - _Backend, FigureCanvasBase, FigureManagerBase, NavigationToolbar2, - TimerBase, cursors, ToolContainerBase, StatusbarBase, MouseButton) -import matplotlib.backends.qt_editor.figureoptions as figureoptions -from matplotlib.backends.qt_editor._formsubplottool import UiSubplotTool -from . import qt_compat -from .qt_compat import ( - QtCore, QtGui, QtWidgets, __version__, QT_API, - _devicePixelRatioF, _isdeleted, _setDevicePixelRatio, -) - -backend_version = __version__ - -# SPECIAL_KEYS are keys that do *not* return their unicode name -# instead they have manually specified names -SPECIAL_KEYS = {QtCore.Qt.Key_Control: 'control', - QtCore.Qt.Key_Shift: 'shift', - QtCore.Qt.Key_Alt: 'alt', - QtCore.Qt.Key_Meta: 'meta', - QtCore.Qt.Key_Super_L: 'super', - QtCore.Qt.Key_Super_R: 'super', - QtCore.Qt.Key_CapsLock: 'caps_lock', - QtCore.Qt.Key_Return: 'enter', - QtCore.Qt.Key_Left: 'left', - QtCore.Qt.Key_Up: 'up', - QtCore.Qt.Key_Right: 'right', - QtCore.Qt.Key_Down: 'down', - QtCore.Qt.Key_Escape: 'escape', - QtCore.Qt.Key_F1: 'f1', - QtCore.Qt.Key_F2: 'f2', - QtCore.Qt.Key_F3: 'f3', - QtCore.Qt.Key_F4: 'f4', - QtCore.Qt.Key_F5: 'f5', - QtCore.Qt.Key_F6: 'f6', - QtCore.Qt.Key_F7: 'f7', - QtCore.Qt.Key_F8: 'f8', - QtCore.Qt.Key_F9: 'f9', - QtCore.Qt.Key_F10: 'f10', - QtCore.Qt.Key_F11: 'f11', - QtCore.Qt.Key_F12: 'f12', - QtCore.Qt.Key_Home: 'home', - QtCore.Qt.Key_End: 'end', - QtCore.Qt.Key_PageUp: 'pageup', - QtCore.Qt.Key_PageDown: 'pagedown', - QtCore.Qt.Key_Tab: 'tab', - QtCore.Qt.Key_Backspace: 'backspace', - QtCore.Qt.Key_Enter: 'enter', - QtCore.Qt.Key_Insert: 'insert', - QtCore.Qt.Key_Delete: 'delete', - QtCore.Qt.Key_Pause: 'pause', - QtCore.Qt.Key_SysReq: 'sysreq', - QtCore.Qt.Key_Clear: 'clear', } -if sys.platform == 'darwin': - # in OSX, the control and super (aka cmd/apple) keys are switched, so - # switch them back. - SPECIAL_KEYS.update({QtCore.Qt.Key_Control: 'cmd', # cmd/apple key - QtCore.Qt.Key_Meta: 'control', - }) -# Define which modifier keys are collected on keyboard events. -# Elements are (Modifier Flag, Qt Key) tuples. -# Order determines the modifier order (ctrl+alt+...) reported by Matplotlib. -_MODIFIER_KEYS = [ - (QtCore.Qt.ControlModifier, QtCore.Qt.Key_Control), - (QtCore.Qt.AltModifier, QtCore.Qt.Key_Alt), - (QtCore.Qt.ShiftModifier, QtCore.Qt.Key_Shift), - (QtCore.Qt.MetaModifier, QtCore.Qt.Key_Meta), -] -cursord = { - cursors.MOVE: QtCore.Qt.SizeAllCursor, - cursors.HAND: QtCore.Qt.PointingHandCursor, - cursors.POINTER: QtCore.Qt.ArrowCursor, - cursors.SELECT_REGION: QtCore.Qt.CrossCursor, - cursors.WAIT: QtCore.Qt.WaitCursor, - } -SUPER = 0 # Deprecated. -ALT = 1 # Deprecated. -CTRL = 2 # Deprecated. -SHIFT = 3 # Deprecated. -MODIFIER_KEYS = [ # Deprecated. - (SPECIAL_KEYS[key], mod, key) for mod, key in _MODIFIER_KEYS] - - -# make place holder -qApp = None - - -def _create_qApp(): - """ - Only one qApp can exist at a time, so check before creating one. - """ - global qApp - - if qApp is None: - app = QtWidgets.QApplication.instance() - if app is None: - # display_is_valid returns False only if on Linux and neither X11 - # nor Wayland display can be opened. - if not mpl._c_internal_utils.display_is_valid(): - raise RuntimeError('Invalid DISPLAY variable') - try: - QtWidgets.QApplication.setAttribute( - QtCore.Qt.AA_EnableHighDpiScaling) - except AttributeError: # Attribute only exists for Qt>=5.6. - pass - try: - QtWidgets.QApplication.setHighDpiScaleFactorRoundingPolicy( - QtCore.Qt.HighDpiScaleFactorRoundingPolicy.PassThrough) - except AttributeError: # Added in Qt>=5.14. - pass - qApp = QtWidgets.QApplication(["matplotlib"]) - qApp.lastWindowClosed.connect(qApp.quit) - cbook._setup_new_guiapp() - else: - qApp = app - - try: - qApp.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps) - except AttributeError: - pass - - -def _allow_super_init(__init__): - """ - Decorator for ``__init__`` to allow ``super().__init__`` on PyQt4/PySide2. - """ - - if QT_API == "PyQt5": - - return __init__ - - else: - # To work around lack of cooperative inheritance in PyQt4, PySide, - # and PySide2, when calling FigureCanvasQT.__init__, we temporarily - # patch QWidget.__init__ by a cooperative version, that first calls - # QWidget.__init__ with no additional arguments, and then finds the - # next class in the MRO with an __init__ that does support cooperative - # inheritance (i.e., not defined by the PyQt4, PySide, PySide2, sip - # or Shiboken packages), and manually call its `__init__`, once again - # passing the additional arguments. - - qwidget_init = QtWidgets.QWidget.__init__ - - def cooperative_qwidget_init(self, *args, **kwargs): - qwidget_init(self) - mro = type(self).__mro__ - next_coop_init = next( - cls for cls in mro[mro.index(QtWidgets.QWidget) + 1:] - if cls.__module__.split(".")[0] not in [ - "PyQt4", "sip", "PySide", "PySide2", "Shiboken"]) - next_coop_init.__init__(self, *args, **kwargs) - - @functools.wraps(__init__) - def wrapper(self, *args, **kwargs): - with cbook._setattr_cm(QtWidgets.QWidget, - __init__=cooperative_qwidget_init): - __init__(self, *args, **kwargs) - - return wrapper - - -class TimerQT(TimerBase): - """Subclass of `.TimerBase` using QTimer events.""" - - def __init__(self, *args, **kwargs): - # Create a new timer and connect the timeout() signal to the - # _on_timer method. - self._timer = QtCore.QTimer() - self._timer.timeout.connect(self._on_timer) - super().__init__(*args, **kwargs) - - def __del__(self): - # The check for deletedness is needed to avoid an error at animation - # shutdown with PySide2. - if not _isdeleted(self._timer): - self._timer_stop() - - def _timer_set_single_shot(self): - self._timer.setSingleShot(self._single) - - def _timer_set_interval(self): - self._timer.setInterval(self._interval) - - def _timer_start(self): - self._timer.start() - - def _timer_stop(self): - self._timer.stop() - - -class FigureCanvasQT(QtWidgets.QWidget, FigureCanvasBase): - required_interactive_framework = "qt5" - _timer_cls = TimerQT - - # map Qt button codes to MouseEvent's ones: - buttond = {QtCore.Qt.LeftButton: MouseButton.LEFT, - QtCore.Qt.MidButton: MouseButton.MIDDLE, - QtCore.Qt.RightButton: MouseButton.RIGHT, - QtCore.Qt.XButton1: MouseButton.BACK, - QtCore.Qt.XButton2: MouseButton.FORWARD, - } - - @_allow_super_init - def __init__(self, figure=None): - _create_qApp() - super().__init__(figure=figure) - - # We don't want to scale up the figure DPI more than once. - # Note, we don't handle a signal for changing DPI yet. - self.figure._original_dpi = self.figure.dpi - self._update_figure_dpi() - # In cases with mixed resolution displays, we need to be careful if the - # dpi_ratio changes - in this case we need to resize the canvas - # accordingly. - self._dpi_ratio_prev = self._dpi_ratio - - self._draw_pending = False - self._is_drawing = False - self._draw_rect_callback = lambda painter: None - - self.setAttribute(QtCore.Qt.WA_OpaquePaintEvent) - self.setMouseTracking(True) - self.resize(*self.get_width_height()) - - palette = QtGui.QPalette(QtCore.Qt.white) - self.setPalette(palette) - - def _update_figure_dpi(self): - dpi = self._dpi_ratio * self.figure._original_dpi - self.figure._set_dpi(dpi, forward=False) - - @property - def _dpi_ratio(self): - return _devicePixelRatioF(self) - - def _update_pixel_ratio(self): - # We need to be careful in cases with mixed resolution displays if - # dpi_ratio changes. - if self._dpi_ratio != self._dpi_ratio_prev: - # We need to update the figure DPI. - self._update_figure_dpi() - self._dpi_ratio_prev = self._dpi_ratio - # The easiest way to resize the canvas is to emit a resizeEvent - # since we implement all the logic for resizing the canvas for - # that event. - event = QtGui.QResizeEvent(self.size(), self.size()) - self.resizeEvent(event) - # resizeEvent triggers a paintEvent itself, so we exit this one - # (after making sure that the event is immediately handled). - - def _update_screen(self, screen): - # Handler for changes to a window's attached screen. - self._update_pixel_ratio() - if screen is not None: - screen.physicalDotsPerInchChanged.connect(self._update_pixel_ratio) - screen.logicalDotsPerInchChanged.connect(self._update_pixel_ratio) - - def showEvent(self, event): - # Set up correct pixel ratio, and connect to any signal changes for it, - # once the window is shown (and thus has these attributes). - window = self.window().windowHandle() - window.screenChanged.connect(self._update_screen) - self._update_screen(window.screen()) - - def get_width_height(self): - w, h = FigureCanvasBase.get_width_height(self) - return int(w / self._dpi_ratio), int(h / self._dpi_ratio) - - def enterEvent(self, event): - try: - x, y = self.mouseEventCoords(event.pos()) - except AttributeError: - # the event from PyQt4 does not include the position - x = y = None - FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y)) - - def leaveEvent(self, event): - QtWidgets.QApplication.restoreOverrideCursor() - FigureCanvasBase.leave_notify_event(self, guiEvent=event) - - def mouseEventCoords(self, pos): - """ - 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 - 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() - # flip y so y=0 is bottom of canvas - y = self.figure.bbox.height / dpi_ratio - pos.y() - return x * dpi_ratio, y * dpi_ratio - - def mousePressEvent(self, event): - x, y = self.mouseEventCoords(event.pos()) - button = self.buttond.get(event.button()) - if button is not None: - FigureCanvasBase.button_press_event(self, x, y, button, - guiEvent=event) - - def mouseDoubleClickEvent(self, event): - x, y = self.mouseEventCoords(event.pos()) - button = self.buttond.get(event.button()) - if button is not None: - FigureCanvasBase.button_press_event(self, x, y, - button, dblclick=True, - guiEvent=event) - - def mouseMoveEvent(self, event): - x, y = self.mouseEventCoords(event) - FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) - - def mouseReleaseEvent(self, event): - x, y = self.mouseEventCoords(event) - button = self.buttond.get(event.button()) - if button is not None: - FigureCanvasBase.button_release_event(self, x, y, button, - guiEvent=event) - - if QtCore.qVersion() >= "5.": - def wheelEvent(self, event): - x, y = self.mouseEventCoords(event) - # from QWheelEvent::delta doc - if event.pixelDelta().x() == 0 and event.pixelDelta().y() == 0: - steps = event.angleDelta().y() / 120 - else: - steps = event.pixelDelta().y() - if steps: - FigureCanvasBase.scroll_event( - self, x, y, steps, guiEvent=event) - else: - def wheelEvent(self, event): - x = event.x() - # flipy so y=0 is bottom of canvas - y = self.figure.bbox.height - event.y() - # from QWheelEvent::delta doc - steps = event.delta() / 120 - if event.orientation() == QtCore.Qt.Vertical: - FigureCanvasBase.scroll_event( - self, x, y, steps, guiEvent=event) - - def keyPressEvent(self, event): - key = self._get_key(event) - if key is not None: - FigureCanvasBase.key_press_event(self, key, guiEvent=event) - - def keyReleaseEvent(self, event): - key = self._get_key(event) - if key is not None: - FigureCanvasBase.key_release_event(self, key, guiEvent=event) - - def resizeEvent(self, event): - w = event.size().width() * self._dpi_ratio - h = event.size().height() * self._dpi_ratio - dpival = self.figure.dpi - winch = w / dpival - hinch = h / dpival - self.figure.set_size_inches(winch, hinch, forward=False) - # pass back into Qt to let it finish - QtWidgets.QWidget.resizeEvent(self, event) - # emit our resize events - FigureCanvasBase.resize_event(self) - - def sizeHint(self): - w, h = self.get_width_height() - return QtCore.QSize(w, h) - - def minumumSizeHint(self): - return QtCore.QSize(10, 10) - - def _get_key(self, event): - event_key = event.key() - event_mods = int(event.modifiers()) # actually a bitmask - - # get names of the pressed modifier keys - # 'control' is named 'control' when a standalone key, but 'ctrl' when a - # modifier - # bit twiddling to pick out modifier keys from event_mods bitmask, - # if event_key is a MODIFIER, it should not be duplicated in mods - mods = [SPECIAL_KEYS[key].replace('control', 'ctrl') - for mod, key in _MODIFIER_KEYS - if event_key != key and event_mods & mod] - try: - # for certain keys (enter, left, backspace, etc) use a word for the - # key, rather than unicode - key = SPECIAL_KEYS[event_key] - except KeyError: - # unicode defines code points up to 0x10ffff (sys.maxunicode) - # QT will use Key_Codes larger than that for keyboard keys that are - # are not unicode characters (like multimedia keys) - # skip these - # if you really want them, you should add them to SPECIAL_KEYS - if event_key > sys.maxunicode: - return None - - key = chr(event_key) - # qt delivers capitalized letters. fix capitalization - # note that capslock is ignored - if 'shift' in mods: - mods.remove('shift') - else: - key = key.lower() - - return '+'.join(mods + [key]) - - def flush_events(self): - # docstring inherited - qApp.processEvents() - - def start_event_loop(self, timeout=0): - # docstring inherited - if hasattr(self, "_event_loop") and self._event_loop.isRunning(): - raise RuntimeError("Event loop already running") - self._event_loop = event_loop = QtCore.QEventLoop() - if timeout > 0: - timer = QtCore.QTimer.singleShot(int(timeout * 1000), - event_loop.quit) - event_loop.exec_() - - def stop_event_loop(self, event=None): - # docstring inherited - if hasattr(self, "_event_loop"): - self._event_loop.quit() - - def draw(self): - """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: - return - with cbook._setattr_cm(self, _is_drawing=True): - super().draw() - 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 - # 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. - # TODO: queued signal connection might be safer than singleShot - if not (getattr(self, '_draw_pending', False) or - getattr(self, '_is_drawing', False)): - self._draw_pending = True - QtCore.QTimer.singleShot(0, self._draw_idle) - - def blit(self, bbox=None): - # docstring inherited - if bbox is None and self.figure: - bbox = self.figure.bbox # Blit the entire canvas if bbox is None. - # repaint uses logical pixels, not physical pixels like the renderer. - l, b, w, h = [int(pt / self._dpi_ratio) for pt in bbox.bounds] - t = b + h - self.repaint(l, self.rect().height() - t, w, h) - - def _draw_idle(self): - with self._idle_draw_cntx(): - if not self._draw_pending: - return - self._draw_pending = False - if self.height() < 0 or self.width() < 0: - return - try: - self.draw() - except Exception: - # Uncaught exceptions are fatal for PyQt5, so catch them. - traceback.print_exc() - - def drawRectangle(self, rect): - # Draw the zoom rectangle to the QPainter. _draw_rect_callback needs - # to be called at the end of paintEvent. - if rect is not None: - x0, y0, w, h = [int(pt / self._dpi_ratio) for pt in rect] - x1 = x0 + w - y1 = y0 + h - def _draw_rect_callback(painter): - pen = QtGui.QPen(QtCore.Qt.black, 1 / self._dpi_ratio) - pen.setDashPattern([3, 3]) - for color, offset in [ - (QtCore.Qt.black, 0), (QtCore.Qt.white, 3)]: - pen.setDashOffset(offset) - pen.setColor(color) - painter.setPen(pen) - # Draw the lines from x0, y0 towards x1, y1 so that the - # dashes don't "jump" when moving the zoom box. - painter.drawLine(x0, y0, x0, y1) - painter.drawLine(x0, y0, x1, y0) - painter.drawLine(x0, y1, x1, y1) - painter.drawLine(x1, y0, x1, y1) - else: - def _draw_rect_callback(painter): - return - self._draw_rect_callback = _draw_rect_callback - self.update() - - -class MainWindow(QtWidgets.QMainWindow): - closing = QtCore.Signal() - - def closeEvent(self, event): - self.closing.emit() - super().closeEvent(event) - - -class FigureManagerQT(FigureManagerBase): - """ - Attributes - ---------- - canvas : `FigureCanvas` - The FigureCanvas instance - num : int or str - The Figure number - toolbar : qt.QToolBar - The qt.QToolBar - window : qt.QMainWindow - The qt.QMainWindow - """ - - def __init__(self, canvas, num): - self.window = MainWindow() - super().__init__(canvas, num) - self.window.closing.connect(canvas.close_event) - self.window.closing.connect(self._widgetclosed) - - image = str(cbook._get_data_path('images/matplotlib.svg')) - self.window.setWindowIcon(QtGui.QIcon(image)) - - self.window._destroying = False - - self.toolbar = self._get_toolbar(self.canvas, self.window) - - if self.toolmanager: - backend_tools.add_tools_to_manager(self.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) +backends._QT_FORCE_QT5_BINDING = True - if self.toolbar: - self.window.addToolBar(self.toolbar) - tbs_height = self.toolbar.sizeHint().height() - else: - tbs_height = 0 - - # resize the main window so it will display the canvas with the - # requested size: - cs = canvas.sizeHint() - cs_height = cs.height() - height = cs_height + tbs_height - self.window.resize(cs.width(), height) - - self.window.setCentralWidget(self.canvas) - - if mpl.is_interactive(): - self.window.show() - self.canvas.draw_idle() - - # 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.raise_() - - def full_screen_toggle(self): - if self.window.isFullScreen(): - self.window.showNormal() - else: - self.window.showFullScreen() - - def _widgetclosed(self): - if self.window._destroying: - return - self.window._destroying = True - try: - Gcf.destroy(self) - except AttributeError: - pass - # It seems that when the python session is killed, - # Gcf can get destroyed before the Gcf.destroy - # line is run, leading to a useless AttributeError. - - def _get_toolbar(self, canvas, parent): - # must be inited after the window, drawingArea and figure - # attrs are set - if mpl.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2QT(canvas, parent, True) - elif mpl.rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarQt(self.toolmanager, self.window) - else: - toolbar = None - return toolbar - - 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. - extra_width = self.window.width() - self.canvas.width() - extra_height = self.window.height() - self.canvas.height() - self.canvas.resize(width, height) - self.window.resize(width + extra_width, height + extra_height) - - def show(self): - self.window.show() - if mpl.rcParams['figure.raise_window']: - self.window.activateWindow() - self.window.raise_() - - def destroy(self, *args): - # check for qApp first, as PySide deletes it in its atexit handler - if QtWidgets.QApplication.instance() is None: - return - if self.window._destroying: - return - self.window._destroying = True - if self.toolbar: - self.toolbar.destroy() - self.window.close() - - def get_window_title(self): - return self.window.windowTitle() - - def set_window_title(self, title): - self.window.setWindowTitle(title) - - -class NavigationToolbar2QT(NavigationToolbar2, QtWidgets.QToolBar): - message = QtCore.Signal(str) - - toolitems = [*NavigationToolbar2.toolitems] - toolitems.insert( - # Add 'customize' action after 'subplots' - [name for name, *_ in toolitems].index("Subplots") + 1, - ("Customize", "Edit axis, curve and image parameters", - "qt4_editor_options", "edit_parameters")) - - def __init__(self, canvas, parent, coordinates=True): - """coordinates: should we show the coordinates on the right?""" - QtWidgets.QToolBar.__init__(self, parent) - self.setAllowedAreas( - QtCore.Qt.TopToolBarArea | QtCore.Qt.BottomToolBarArea) - - self.coordinates = coordinates - self._actions = {} # mapping of toolitem method names to QActions. - - for text, tooltip_text, image_file, callback in self.toolitems: - if text is None: - self.addSeparator() - else: - a = self.addAction(self._icon(image_file + '.png'), - text, getattr(self, callback)) - self._actions[callback] = a - if callback in ['zoom', 'pan']: - a.setCheckable(True) - if tooltip_text is not None: - a.setToolTip(tooltip_text) - - # Add the (x, y) location widget at the right side of the toolbar - # The stretch factor is 1 which means any resizing of the toolbar - # will resize this label instead of the buttons. - if self.coordinates: - self.locLabel = QtWidgets.QLabel("", self) - self.locLabel.setAlignment( - QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - self.locLabel.setSizePolicy( - QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Ignored)) - labelAction = self.addWidget(self.locLabel) - labelAction.setVisible(True) - - NavigationToolbar2.__init__(self, canvas) - - @_api.deprecated("3.3", alternative="self.canvas.parent()") - @property - def parent(self): - return self.canvas.parent() - - @_api.deprecated("3.3", alternative="self.canvas.setParent()") - @parent.setter - def parent(self, value): - pass - - @_api.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): - """ - Construct a `.QIcon` from an image file *name*, including the extension - and relative to Matplotlib's "images" data directory. - """ - if QtCore.qVersion() >= '5.': - name = name.replace('.png', '_large.png') - pm = QtGui.QPixmap(str(cbook._get_data_path('images', name))) - _setDevicePixelRatio(pm, _devicePixelRatioF(self)) - if self.palette().color(self.backgroundRole()).value() < 128: - icon_color = self.palette().color(self.foregroundRole()) - mask = pm.createMaskFromColor(QtGui.QColor('black'), - QtCore.Qt.MaskOutColor) - pm.fill(icon_color) - pm.setMask(mask) - return QtGui.QIcon(pm) - - def edit_parameters(self): - axes = self.canvas.figure.get_axes() - if not axes: - QtWidgets.QMessageBox.warning( - self.canvas.parent(), "Error", "There are no axes to edit.") - return - elif len(axes) == 1: - ax, = axes - else: - titles = [ - ax.get_label() or - ax.get_title() or - " - ".join(filter(None, [ax.get_xlabel(), ax.get_ylabel()])) or - f"" - for ax in axes] - duplicate_titles = [ - title for title in titles if titles.count(title) > 1] - for i, ax in enumerate(axes): - if titles[i] in duplicate_titles: - titles[i] += f" (id: {id(ax):#x})" # Deduplicate titles. - item, ok = QtWidgets.QInputDialog.getItem( - self.canvas.parent(), - 'Customize', 'Select axes:', titles, 0, False) - if not ok: - return - ax = axes[titles.index(item)] - figureoptions.figure_edit(ax, self) - - def _update_buttons_checked(self): - # sync button checkstates to match active mode - if 'pan' in self._actions: - self._actions['pan'].setChecked(self.mode.name == 'PAN') - if 'zoom' in self._actions: - self._actions['zoom'].setChecked(self.mode.name == 'ZOOM') - - def pan(self, *args): - super().pan(*args) - self._update_buttons_checked() - - def zoom(self, *args): - super().zoom(*args) - self._update_buttons_checked() - - def set_message(self, s): - self.message.emit(s) - if self.coordinates: - self.locLabel.setText(s) - - def set_cursor(self, cursor): - self.canvas.setCursor(cursord[cursor]) - - def draw_rubberband(self, event, x0, y0, x1, y1): - height = self.canvas.figure.bbox.height - y1 = height - y1 - y0 = height - y0 - rect = [int(val) for val in (x0, y0, x1 - x0, y1 - y0)] - self.canvas.drawRectangle(rect) - - def remove_rubberband(self): - self.canvas.drawRectangle(None) - - def configure_subplots(self): - image = str(cbook._get_data_path('images/matplotlib.png')) - dia = SubplotToolQt(self.canvas.figure, self.canvas.parent()) - dia.setWindowIcon(QtGui.QIcon(image)) - dia.exec_() - - def save_figure(self, *args): - filetypes = self.canvas.get_supported_filetypes_grouped() - sorted_filetypes = sorted(filetypes.items()) - default_filetype = self.canvas.get_default_filetype() - - startpath = os.path.expanduser(mpl.rcParams['savefig.directory']) - start = os.path.join(startpath, self.canvas.get_default_filename()) - filters = [] - selectedFilter = None - for name, exts in sorted_filetypes: - exts_list = " ".join(['*.%s' % ext for ext in exts]) - filter = '%s (%s)' % (name, exts_list) - if default_filetype in exts: - selectedFilter = filter - filters.append(filter) - filters = ';;'.join(filters) - - 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 != "": - mpl.rcParams['savefig.directory'] = os.path.dirname(fname) - try: - self.canvas.figure.savefig(fname) - except Exception as e: - QtWidgets.QMessageBox.critical( - self, "Error saving file", str(e), - QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.NoButton) - - def set_history_buttons(self): - can_backward = self._nav_stack._pos > 0 - can_forward = self._nav_stack._pos < len(self._nav_stack._elements) - 1 - if 'back' in self._actions: - self._actions['back'].setEnabled(can_backward) - if 'forward' in self._actions: - self._actions['forward'].setEnabled(can_forward) - - -class SubplotToolQt(UiSubplotTool): - def __init__(self, targetfig, parent): - super().__init__(None) - - self._figure = targetfig - - for lower, higher in [("bottom", "top"), ("left", "right")]: - self._widgets[lower].valueChanged.connect( - lambda val: self._widgets[higher].setMinimum(val + .001)) - self._widgets[higher].valueChanged.connect( - lambda val: self._widgets[lower].setMaximum(val - .001)) - - self._attrs = ["top", "bottom", "left", "right", "hspace", "wspace"] - self._defaults = {attr: vars(self._figure.subplotpars)[attr] - for attr in self._attrs} - - # Set values after setting the range callbacks, but before setting up - # the redraw callbacks. - self._reset() - - for attr in self._attrs: - self._widgets[attr].valueChanged.connect(self._on_value_changed) - for action, method in [("Export values", self._export_values), - ("Tight layout", self._tight_layout), - ("Reset", self._reset), - ("Close", self.close)]: - self._widgets[action].clicked.connect(method) - - def _export_values(self): - # Explicitly round to 3 decimals (which is also the spinbox precision) - # to avoid numbers of the form 0.100...001. - dialog = QtWidgets.QDialog() - layout = QtWidgets.QVBoxLayout() - dialog.setLayout(layout) - text = QtWidgets.QPlainTextEdit() - text.setReadOnly(True) - layout.addWidget(text) - text.setPlainText( - ",\n".join("{}={:.3}".format(attr, self._widgets[attr].value()) - for attr in self._attrs)) - # Adjust the height of the text widget to fit the whole text, plus - # some padding. - size = text.maximumSize() - size.setHeight( - QtGui.QFontMetrics(text.document().defaultFont()) - .size(0, text.toPlainText()).height() + 20) - text.setMaximumSize(size) - dialog.exec_() - - def _on_value_changed(self): - self._figure.subplots_adjust(**{attr: self._widgets[attr].value() - for attr in self._attrs}) - self._figure.canvas.draw_idle() - - def _tight_layout(self): - self._figure.tight_layout() - for attr in self._attrs: - widget = self._widgets[attr] - widget.blockSignals(True) - widget.setValue(vars(self._figure.subplotpars)[attr]) - widget.blockSignals(False) - self._figure.canvas.draw_idle() - - def _reset(self): - for attr, value in self._defaults.items(): - self._widgets[attr].setValue(value) - - -class ToolbarQt(ToolContainerBase, QtWidgets.QToolBar): - def __init__(self, toolmanager, parent): - ToolContainerBase.__init__(self, toolmanager) - QtWidgets.QToolBar.__init__(self, parent) - self.setAllowedAreas( - QtCore.Qt.TopToolBarArea | QtCore.Qt.BottomToolBarArea) - message_label = QtWidgets.QLabel("") - message_label.setAlignment( - QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) - message_label.setSizePolicy( - QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, - QtWidgets.QSizePolicy.Ignored)) - self._message_action = self.addWidget(message_label) - self._toolitems = {} - self._groups = {} - - def add_toolitem( - self, name, group, position, image_file, description, toggle): - - button = QtWidgets.QToolButton(self) - if image_file: - button.setIcon(NavigationToolbar2QT._icon(self, image_file)) - button.setText(name) - if description: - button.setToolTip(description) - - def handler(): - self.trigger_tool(name) - if toggle: - button.setCheckable(True) - button.toggled.connect(handler) - else: - button.clicked.connect(handler) - - self._toolitems.setdefault(name, []) - self._add_to_group(group, name, button, position) - self._toolitems[name].append((button, handler)) - - def _add_to_group(self, group, name, button, position): - gr = self._groups.get(group, []) - if not gr: - sep = self.insertSeparator(self._message_action) - gr.append(sep) - before = gr[position] - widget = self.insertWidget(before, button) - gr.insert(position, widget) - self._groups[group] = gr - - def toggle_toolitem(self, name, toggled): - if name not in self._toolitems: - return - for button, handler in self._toolitems[name]: - button.toggled.disconnect(handler) - button.setChecked(toggled) - button.toggled.connect(handler) - - def remove_toolitem(self, name): - for button, handler in self._toolitems[name]: - button.setParent(None) - del self._toolitems[name] - - def set_message(self, s): - self.widgetForAction(self._message_action).setText(s) - - -@_api.deprecated("3.3") -class StatusbarQt(StatusbarBase, QtWidgets.QLabel): - def __init__(self, window, *args, **kwargs): - StatusbarBase.__init__(self, *args, **kwargs) - QtWidgets.QLabel.__init__(self) - window.statusBar().addWidget(self) - - def set_message(self, s): - self.setText(s) - - -class ConfigureSubplotsQt(backend_tools.ConfigureSubplotsBase): - def trigger(self, *args): - NavigationToolbar2QT.configure_subplots( - self._make_classic_style_pseudo_toolbar()) - - -class SaveFigureQt(backend_tools.SaveFigureBase): - def trigger(self, *args): - NavigationToolbar2QT.save_figure( - self._make_classic_style_pseudo_toolbar()) - - -class SetCursorQt(backend_tools.SetCursorBase): - def set_cursor(self, cursor): - NavigationToolbar2QT.set_cursor( - self._make_classic_style_pseudo_toolbar(), cursor) - - -class RubberbandQt(backend_tools.RubberbandBase): - def draw_rubberband(self, x0, y0, x1, y1): - NavigationToolbar2QT.draw_rubberband( - self._make_classic_style_pseudo_toolbar(), None, x0, y0, x1, y1) - - def remove_rubberband(self): - NavigationToolbar2QT.remove_rubberband( - self._make_classic_style_pseudo_toolbar()) - - -class HelpQt(backend_tools.ToolHelpBase): - def trigger(self, *args): - QtWidgets.QMessageBox.information(None, "Help", self._get_help_html()) - - -class ToolCopyToClipboardQT(backend_tools.ToolCopyToClipboardBase): - def trigger(self, *args, **kwargs): - pixmap = self.canvas.grab() - qApp.clipboard().setPixmap(pixmap) +from .backend_qt import ( # noqa + SPECIAL_KEYS, + # Public API + cursord, _create_qApp, _BackendQT, TimerQT, MainWindow, FigureCanvasQT, + FigureManagerQT, ToolbarQt, NavigationToolbar2QT, SubplotToolQt, + SaveFigureQt, ConfigureSubplotsQt, RubberbandQt, + HelpQt, ToolCopyToClipboardQT, + # internal re-exports + FigureCanvasBase, FigureManagerBase, MouseButton, NavigationToolbar2, + TimerBase, ToolContainerBase, figureoptions, Gcf +) +from . import backend_qt as _backend_qt # noqa -backend_tools.ToolSaveFigure = SaveFigureQt -backend_tools.ToolConfigureSubplots = ConfigureSubplotsQt -backend_tools.ToolSetCursor = SetCursorQt -backend_tools.ToolRubberband = RubberbandQt -backend_tools.ToolHelp = HelpQt -backend_tools.ToolCopyToClipboard = ToolCopyToClipboardQT +@_BackendQT.export +class _BackendQT5(_BackendQT): + pass -@_Backend.export -class _BackendQT5(_Backend): - FigureCanvas = FigureCanvasQT - FigureManager = FigureManagerQT - @staticmethod - def mainloop(): - old_signal = signal.getsignal(signal.SIGINT) - # allow SIGINT exceptions to close the plot window. - is_python_signal_handler = old_signal is not None - if is_python_signal_handler: - signal.signal(signal.SIGINT, signal.SIG_DFL) - try: - qApp.exec_() - finally: - # reset the SIGINT exception handler - if is_python_signal_handler: - signal.signal(signal.SIGINT, old_signal) +def __getattr__(name): + if name == 'qApp': + return _backend_qt.qApp + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 897c28c38f04..8a92fd5135d5 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -1,84 +1,14 @@ """ -Render to qt from agg. +Render to qt from agg """ +from .. import backends -import ctypes +backends._QT_FORCE_QT5_BINDING = True +from .backend_qtagg import ( # noqa: F401, E402 # pylint: disable=W0611 + _BackendQTAgg, FigureCanvasQTAgg, FigureManagerQT, NavigationToolbar2QT, + FigureCanvasAgg, FigureCanvasQT) -from matplotlib.transforms import Bbox -from .. import cbook -from .backend_agg import FigureCanvasAgg -from .backend_qt5 import ( - QtCore, QtGui, QtWidgets, _BackendQT5, FigureCanvasQT, FigureManagerQT, - NavigationToolbar2QT, backend_version) -from .qt_compat import QT_API, _setDevicePixelRatio - - -class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT): - - def __init__(self, figure=None): - # Must pass 'figure' as kwarg to Qt base class. - super().__init__(figure=figure) - - def paintEvent(self, event): - """ - Copy the image from the Agg canvas to the qt.drawable. - - In Qt, all drawing should be done inside of here when a widget is - shown onscreen. - """ - self._draw_idle() # Only does something if a draw is pending. - - # If the canvas does not have a renderer, then give up and wait for - # FigureCanvasAgg.draw(self) to be called. - if not hasattr(self, 'renderer'): - return - - painter = QtGui.QPainter(self) - try: - # See documentation of QRect: bottom() and right() are off - # by 1, so use left() + width() and top() + height(). - rect = event.rect() - # scale rect dimensions using the screen dpi ratio to get - # correct values for the Figure coordinates (rather than - # QT5's coords) - width = rect.width() * self._dpi_ratio - height = rect.height() * self._dpi_ratio - left, top = self.mouseEventCoords(rect.topLeft()) - # shift the "top" by the height of the image to get the - # correct corner for our coordinate system - bottom = top - height - # same with the right side of the image - right = left + width - # create a buffer using the image bounding box - bbox = Bbox([[left, bottom], [right, top]]) - reg = self.copy_from_bbox(bbox) - buf = cbook._unmultiplied_rgba8888_to_premultiplied_argb32( - memoryview(reg)) - - # clear the widget canvas - painter.eraseRect(rect) - - qimage = QtGui.QImage(buf, buf.shape[1], buf.shape[0], - QtGui.QImage.Format_ARGB32_Premultiplied) - _setDevicePixelRatio(qimage, self._dpi_ratio) - # set origin using original QT coordinates - origin = QtCore.QPoint(rect.left(), rect.top()) - painter.drawImage(origin, qimage) - # Adjust the buf reference count to work around a memory - # leak bug in QImage under PySide on Python 3. - if QT_API in ('PySide', 'PySide2'): - ctypes.c_long.from_address(id(buf)).value = 1 - - self._draw_rect_callback(painter) - finally: - painter.end() - - def print_figure(self, *args, **kwargs): - super().print_figure(*args, **kwargs) - self.draw() - - -@_BackendQT5.export -class _BackendQT5Agg(_BackendQT5): - FigureCanvas = FigureCanvasQTAgg +@_BackendQTAgg.export +class _BackendQT5Agg(_BackendQTAgg): + pass diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index 4b6d7305e7c1..a4263f597119 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -1,45 +1,11 @@ -import ctypes +from .. import backends -from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo -from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT -from .qt_compat import QT_API, _setDevicePixelRatio +backends._QT_FORCE_QT5_BINDING = True +from .backend_qtcairo import ( # noqa: F401, E402 # pylint: disable=W0611 + _BackendQTCairo, FigureCanvasQTCairo, FigureCanvasCairo, FigureCanvasQT +) -class FigureCanvasQTCairo(FigureCanvasQT, FigureCanvasCairo): - def __init__(self, figure=None): - super().__init__(figure=figure) - self._renderer = RendererCairo(self.figure.dpi) - self._renderer.set_width_height(-1, -1) # Invalid values. - - def draw(self): - if hasattr(self._renderer.gc, "ctx"): - self.figure.draw(self._renderer) - super().draw() - - def paintEvent(self, event): - dpi_ratio = self._dpi_ratio - width = int(dpi_ratio * self.width()) - height = int(dpi_ratio * self.height()) - if (width, height) != self._renderer.get_canvas_width_height(): - surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) - self._renderer.set_ctx_from_surface(surface) - self._renderer.set_width_height(width, height) - self.figure.draw(self._renderer) - buf = self._renderer.gc.ctx.get_target().get_data() - qimage = QtGui.QImage(buf, width, height, - QtGui.QImage.Format_ARGB32_Premultiplied) - # Adjust the buf reference count to work around a memory leak bug in - # QImage under PySide on Python 3. - if QT_API == 'PySide': - ctypes.c_long.from_address(id(buf)).value = 1 - _setDevicePixelRatio(qimage, dpi_ratio) - painter = QtGui.QPainter(self) - painter.eraseRect(event.rect()) - painter.drawImage(0, 0, qimage) - self._draw_rect_callback(painter) - painter.end() - - -@_BackendQT5.export -class _BackendQT5Cairo(_BackendQT5): - FigureCanvas = FigureCanvasQTCairo +@_BackendQTCairo.export +class _BackendQT5Cairo(_BackendQTCairo): + pass diff --git a/lib/matplotlib/backends/backend_qtagg.py b/lib/matplotlib/backends/backend_qtagg.py new file mode 100644 index 000000000000..f64264d712f7 --- /dev/null +++ b/lib/matplotlib/backends/backend_qtagg.py @@ -0,0 +1,81 @@ +""" +Render to qt from agg. +""" + +import ctypes + +from matplotlib.transforms import Bbox + +from .qt_compat import QT_API, _enum +from .backend_agg import FigureCanvasAgg +from .backend_qt import QtCore, QtGui, _BackendQT, FigureCanvasQT +from .backend_qt import ( # noqa: F401 # pylint: disable=W0611 + FigureManagerQT, NavigationToolbar2QT) + + +class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT): + + def paintEvent(self, event): + """ + Copy the image from the Agg canvas to the qt.drawable. + + In Qt, all drawing should be done inside of here when a widget is + shown onscreen. + """ + self._draw_idle() # Only does something if a draw is pending. + + # If the canvas does not have a renderer, then give up and wait for + # FigureCanvasAgg.draw(self) to be called. + if not hasattr(self, 'renderer'): + return + + painter = QtGui.QPainter(self) + try: + # See documentation of QRect: bottom() and right() are off + # by 1, so use left() + width() and top() + height(). + rect = event.rect() + # scale rect dimensions using the screen dpi ratio to get + # correct values for the Figure coordinates (rather than + # QT5's coords) + width = rect.width() * self.device_pixel_ratio + height = rect.height() * self.device_pixel_ratio + left, top = self.mouseEventCoords(rect.topLeft()) + # shift the "top" by the height of the image to get the + # correct corner for our coordinate system + bottom = top - height + # same with the right side of the image + right = left + width + # create a buffer using the image bounding box + bbox = Bbox([[left, bottom], [right, top]]) + buf = memoryview(self.copy_from_bbox(bbox)) + + if QT_API == "PyQt6": + from PyQt6 import sip + ptr = int(sip.voidptr(buf)) + else: + ptr = buf + + painter.eraseRect(rect) # clear the widget canvas + qimage = QtGui.QImage(ptr, buf.shape[1], buf.shape[0], + _enum("QtGui.QImage.Format").Format_RGBA8888) + qimage.setDevicePixelRatio(self.device_pixel_ratio) + # set origin using original QT coordinates + origin = QtCore.QPoint(rect.left(), rect.top()) + painter.drawImage(origin, qimage) + # Adjust the buf reference count to work around a memory + # leak bug in QImage under PySide. + if QT_API == "PySide2" and QtCore.__version_info__ < (5, 12): + ctypes.c_long.from_address(id(buf)).value = 1 + + self._draw_rect_callback(painter) + finally: + painter.end() + + def print_figure(self, *args, **kwargs): + super().print_figure(*args, **kwargs) + self.draw() + + +@_BackendQT.export +class _BackendQTAgg(_BackendQT): + FigureCanvas = FigureCanvasQTAgg diff --git a/lib/matplotlib/backends/backend_qtcairo.py b/lib/matplotlib/backends/backend_qtcairo.py new file mode 100644 index 000000000000..cca1be012f9e --- /dev/null +++ b/lib/matplotlib/backends/backend_qtcairo.py @@ -0,0 +1,46 @@ +import ctypes + +from .backend_cairo import cairo, FigureCanvasCairo +from .backend_qt import QtCore, QtGui, _BackendQT, FigureCanvasQT +from .qt_compat import QT_API, _enum + + +class FigureCanvasQTCairo(FigureCanvasCairo, FigureCanvasQT): + def draw(self): + if hasattr(self._renderer.gc, "ctx"): + self._renderer.dpi = self.figure.dpi + self.figure.draw(self._renderer) + super().draw() + + def paintEvent(self, event): + width = int(self.device_pixel_ratio * self.width()) + height = int(self.device_pixel_ratio * self.height()) + if (width, height) != self._renderer.get_canvas_width_height(): + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) + self._renderer.set_context(cairo.Context(surface)) + self._renderer.dpi = self.figure.dpi + self.figure.draw(self._renderer) + buf = self._renderer.gc.ctx.get_target().get_data() + if QT_API == "PyQt6": + from PyQt6 import sip + ptr = int(sip.voidptr(buf)) + else: + ptr = buf + qimage = QtGui.QImage( + ptr, width, height, + _enum("QtGui.QImage.Format").Format_ARGB32_Premultiplied) + # Adjust the buf reference count to work around a memory leak bug in + # QImage under PySide. + if QT_API == "PySide2" and QtCore.__version_info__ < (5, 12): + ctypes.c_long.from_address(id(buf)).value = 1 + qimage.setDevicePixelRatio(self.device_pixel_ratio) + painter = QtGui.QPainter(self) + painter.eraseRect(event.rect()) + painter.drawImage(0, 0, qimage) + self._draw_rect_callback(painter) + painter.end() + + +@_BackendQT.export +class _BackendQTCairo(_BackendQT): + FigureCanvas = FigureCanvasQTCairo diff --git a/lib/matplotlib/backends/backend_svg.py b/lib/matplotlib/backends/backend_svg.py index 21a853693b1c..df39e620f888 100644 --- a/lib/matplotlib/backends/backend_svg.py +++ b/lib/matplotlib/backends/backend_svg.py @@ -1,9 +1,9 @@ -from collections import OrderedDict import base64 +import codecs import datetime import gzip import hashlib -from io import BytesIO, StringIO, TextIOWrapper +from io import BytesIO import itertools import logging import os @@ -14,23 +14,19 @@ from PIL import Image import matplotlib as mpl -from matplotlib import _api, cbook +from matplotlib import _api, cbook, font_manager as fm from matplotlib.backend_bases import ( - _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, - RendererBase, _no_output_draw) + _Backend, FigureCanvasBase, FigureManagerBase, RendererBase) from matplotlib.backends.backend_mixed import MixedModeRenderer from matplotlib.colors import rgb2hex from matplotlib.dates import UTC -from matplotlib.font_manager import findfont, get_font -from matplotlib.ft2font import LOAD_NO_HINTING -from matplotlib.mathtext import MathTextParser from matplotlib.path import Path from matplotlib import _path from matplotlib.transforms import Affine2D, Affine2DBase + _log = logging.getLogger(__name__) -backend_version = mpl.__version__ # ---------------------------------------------------------------------- # SimpleXMLWriter class @@ -70,7 +66,12 @@ # -------------------------------------------------------------------- +@_api.deprecated("3.6", alternative="a vendored copy of _escape_cdata") def escape_cdata(s): + return _escape_cdata(s) + + +def _escape_cdata(s): s = s.replace("&", "&") s = s.replace("<", "<") s = s.replace(">", ">") @@ -80,12 +81,22 @@ def escape_cdata(s): _escape_xml_comment = re.compile(r'-(?=-)') +@_api.deprecated("3.6", alternative="a vendored copy of _escape_comment") def escape_comment(s): - s = escape_cdata(s) + return _escape_comment.sub(s) + + +def _escape_comment(s): + s = _escape_cdata(s) return _escape_xml_comment.sub('- ', s) +@_api.deprecated("3.6", alternative="a vendored copy of _escape_attrib") def escape_attrib(s): + return _escape_attrib(s) + + +def _escape_attrib(s): s = s.replace("&", "&") s = s.replace("'", "'") s = s.replace('"', """) @@ -94,7 +105,18 @@ def escape_attrib(s): return s +def _quote_escape_attrib(s): + return ('"' + _escape_cdata(s) + '"' if '"' not in s else + "'" + _escape_cdata(s) + "'" if "'" not in s else + '"' + _escape_attrib(s) + '"') + + +@_api.deprecated("3.6", alternative="a vendored copy of _short_float_fmt") def short_float_fmt(x): + return _short_float_fmt(x) + + +def _short_float_fmt(x): """ Create a short string representation of a float, which is %f formatting with trailing zeros and the decimal point removed. @@ -128,7 +150,7 @@ def __flush(self, indent=True): self.__open = 0 if self.__data: data = ''.join(self.__data) - self.__write(escape_cdata(data)) + self.__write(_escape_cdata(data)) self.__data = [] def start(self, tag, attrib={}, **extra): @@ -151,16 +173,16 @@ def start(self, tag, attrib={}, **extra): An element identifier. """ self.__flush() - tag = escape_cdata(tag) + tag = _escape_cdata(tag) self.__data = [] self.__tags.append(tag) self.__write(self.__indentation[:len(self.__tags) - 1]) self.__write("<%s" % tag) - for k, v in sorted({**attrib, **extra}.items()): + for k, v in {**attrib, **extra}.items(): if v: - k = escape_cdata(k) - v = escape_attrib(v) - self.__write(' %s="%s"' % (k, v)) + k = _escape_cdata(k) + v = _quote_escape_attrib(v) + self.__write(' %s=%s' % (k, v)) self.__open = 1 return len(self.__tags) - 1 @@ -175,7 +197,7 @@ def comment(self, comment): """ self.__flush() self.__write(self.__indentation[:len(self.__tags)]) - self.__write("\n" % escape_comment(comment)) + self.__write("\n" % _escape_comment(comment)) def data(self, text): """ @@ -201,7 +223,7 @@ def end(self, tag=None, indent=True): """ if tag: assert self.__tags, "unbalanced end(%s)" % tag - assert escape_cdata(tag) == self.__tags[-1], \ + assert _escape_cdata(tag) == self.__tags[-1], \ "expected end(%s), got %s" % (self.__tags[-1], tag) else: assert self.__tags, "unbalanced end()" @@ -245,37 +267,54 @@ def flush(self): pass # replaced by the constructor -def generate_transform(transform_list=[]): - if len(transform_list): - output = StringIO() - for type, value in transform_list: - if (type == 'scale' and (value == (1,) or value == (1, 1)) - or type == 'translate' and value == (0, 0) - or type == 'rotate' and value == (0,)): - continue - if type == 'matrix' and isinstance(value, Affine2DBase): - value = value.to_values() - output.write('%s(%s)' % ( - type, ' '.join(short_float_fmt(x) for x in value))) - return output.getvalue() - return '' - - -def generate_css(attrib={}): - if attrib: - output = StringIO() - attrib = sorted(attrib.items()) - for k, v in attrib: - k = escape_attrib(k) - v = escape_attrib(v) - output.write("%s:%s;" % (k, v)) - return output.getvalue() - return '' +def _generate_transform(transform_list): + parts = [] + for type, value in transform_list: + if (type == 'scale' and (value == (1,) or value == (1, 1)) + or type == 'translate' and value == (0, 0) + or type == 'rotate' and value == (0,)): + continue + if type == 'matrix' and isinstance(value, Affine2DBase): + value = value.to_values() + parts.append('%s(%s)' % ( + type, ' '.join(_short_float_fmt(x) for x in value))) + return ' '.join(parts) + + +@_api.deprecated("3.6") +def generate_transform(transform_list=None): + return _generate_transform(transform_list or []) + + +def _generate_css(attrib): + return "; ".join(f"{k}: {v}" for k, v in attrib.items()) + + +@_api.deprecated("3.6") +def generate_css(attrib=None): + return _generate_css(attrib or {}) _capstyle_d = {'projecting': 'square', 'butt': 'butt', 'round': 'round'} +def _check_is_str(info, key): + if not isinstance(info, str): + raise TypeError(f'Invalid type for {key} metadata. Expected str, not ' + f'{type(info)}.') + + +def _check_is_iterable_of_str(infos, key): + if np.iterable(infos): + for info in infos: + if not isinstance(info, str): + raise TypeError(f'Invalid type for {key} metadata. Expected ' + f'iterable of str, not {type(info)}.') + else: + raise TypeError(f'Invalid type for {key} metadata. Expected str or ' + f'iterable of str, not {type(infos)}.') + + class RendererSVG(RendererBase): def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, *, metadata=None): @@ -284,21 +323,25 @@ def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, self.writer = XMLWriter(svgwriter) self.image_dpi = image_dpi # actual dpi at which we rasterize stuff - self._groupd = {} + if basename is None: + basename = getattr(svgwriter, "name", "") + if not isinstance(basename, str): + basename = "" self.basename = basename + + self._groupd = {} self._image_counter = itertools.count() - self._clipd = OrderedDict() + self._clipd = {} self._markers = {} self._path_collection_id = 0 - self._hatchd = OrderedDict() + self._hatchd = {} self._has_gouraud = False self._n_gradients = 0 - self._fonts = OrderedDict() super().__init__() self._glyph_map = dict() - str_height = short_float_fmt(height) - str_width = short_float_fmt(width) + str_height = _short_float_fmt(height) + str_width = _short_float_fmt(width) svgwriter.write(svgProlog) self._start_id = self.writer.start( 'svg', @@ -311,11 +354,6 @@ def __init__(self, width, height, svgwriter, basename=None, image_dpi=72, self._write_metadata(metadata) self._write_default_style() - @_api.deprecated("3.4") - @property - def mathtext_parser(self): - return MathTextParser('SVG') - def finalize(self): self._write_clips() self._write_hatches() @@ -338,7 +376,9 @@ def _write_metadata(self, metadata): writer = self.writer if 'Title' in metadata: - writer.element('title', text=metadata['Title']) + title = metadata['Title'] + _check_is_str(title, 'Title') + writer.element('title', text=title) # Special handling. date = metadata.get('Date', None) @@ -355,14 +395,14 @@ def _write_metadata(self, metadata): elif isinstance(d, (datetime.datetime, datetime.date)): dates.append(d.isoformat()) else: - raise ValueError( - 'Invalid type for Date metadata. ' - 'Expected iterable of str, date, or datetime, ' - 'not {!r}.'.format(type(d))) + raise TypeError( + f'Invalid type for Date metadata. ' + f'Expected iterable of str, date, or datetime, ' + f'not {type(d)}.') else: - raise ValueError('Invalid type for Date metadata. ' - 'Expected str, date, datetime, or iterable ' - 'of the same, not {!r}.'.format(type(date))) + raise TypeError(f'Invalid type for Date metadata. ' + f'Expected str, date, datetime, or iterable ' + f'of the same, not {type(date)}.') metadata['Date'] = '/'.join(dates) elif 'Date' not in metadata: # Do not add `Date` if the user explicitly set `Date` to `None` @@ -394,36 +434,40 @@ def ensure_metadata(mid): writer.element('dc:type', attrib={'rdf:resource': uri}) # Single value only. - for key in ['title', 'coverage', 'date', 'description', 'format', - 'identifier', 'language', 'relation', 'source']: - info = metadata.pop(key.title(), None) + for key in ['Title', 'Coverage', 'Date', 'Description', 'Format', + 'Identifier', 'Language', 'Relation', 'Source']: + info = metadata.pop(key, None) if info is not None: mid = ensure_metadata(mid) - writer.element(f'dc:{key}', text=info) + _check_is_str(info, key) + writer.element(f'dc:{key.lower()}', text=info) # Multiple Agent values. - for key in ['creator', 'contributor', 'publisher', 'rights']: - agents = metadata.pop(key.title(), None) + for key in ['Creator', 'Contributor', 'Publisher', 'Rights']: + agents = metadata.pop(key, None) if agents is None: continue if isinstance(agents, str): agents = [agents] + _check_is_iterable_of_str(agents, key) + # Now we know that we have an iterable of str mid = ensure_metadata(mid) - writer.start(f'dc:{key}') + writer.start(f'dc:{key.lower()}') for agent in agents: writer.start('cc:Agent') writer.element('dc:title', text=agent) writer.end('cc:Agent') - writer.end(f'dc:{key}') + writer.end(f'dc:{key.lower()}') # Multiple values. keywords = metadata.pop('Keywords', None) if keywords is not None: if isinstance(keywords, str): keywords = [keywords] - + _check_is_iterable_of_str(keywords, 'Keywords') + # Now we know that we have an iterable of str mid = ensure_metadata(mid) writer.start('dc:subject') writer.start('rdf:Bag') @@ -441,7 +485,7 @@ def ensure_metadata(mid): def _write_default_style(self): writer = self.writer - default_style = generate_css({ + default_style = _generate_css({ 'stroke-linejoin': 'round', 'stroke-linecap': 'butt'}) writer.start('defs') @@ -458,18 +502,7 @@ def _make_id(self, type, content): return '%s%s' % (type, m.hexdigest()[:10]) def _make_flip_transform(self, transform): - return (transform + - Affine2D() - .scale(1.0, -1.0) - .translate(0.0, self.height)) - - def _get_font(self, prop): - fname = findfont(prop) - font = get_font(fname) - font.clear() - size = prop.get_size_in_points() - font.set_size(size, 72.0) - return font + return transform + Affine2D().scale(1, -1).translate(0, self.height) def _get_hatch(self, gc, rgbFace): """ @@ -528,7 +561,7 @@ def _write_hatches(self): writer.element( 'path', d=path_data, - style=generate_css(hatch_style) + style=_generate_css(hatch_style) ) writer.end('pattern') writer.end('defs') @@ -543,7 +576,7 @@ def _get_style_dict(self, gc, rgbFace): attrib['fill'] = "url(#%s)" % self._get_hatch(gc, rgbFace) if (rgbFace is not None and len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha): - attrib['fill-opacity'] = short_float_fmt(rgbFace[3]) + attrib['fill-opacity'] = _short_float_fmt(rgbFace[3]) else: if rgbFace is None: attrib['fill'] = 'none' @@ -552,25 +585,25 @@ def _get_style_dict(self, gc, rgbFace): attrib['fill'] = rgb2hex(rgbFace) if (len(rgbFace) == 4 and rgbFace[3] != 1.0 and not forced_alpha): - attrib['fill-opacity'] = short_float_fmt(rgbFace[3]) + attrib['fill-opacity'] = _short_float_fmt(rgbFace[3]) if forced_alpha and gc.get_alpha() != 1.0: - attrib['opacity'] = short_float_fmt(gc.get_alpha()) + attrib['opacity'] = _short_float_fmt(gc.get_alpha()) offset, seq = gc.get_dashes() if seq is not None: attrib['stroke-dasharray'] = ','.join( - short_float_fmt(val) for val in seq) - attrib['stroke-dashoffset'] = short_float_fmt(float(offset)) + _short_float_fmt(val) for val in seq) + attrib['stroke-dashoffset'] = _short_float_fmt(float(offset)) linewidth = gc.get_linewidth() if linewidth: rgb = gc.get_rgb() attrib['stroke'] = rgb2hex(rgb) if not forced_alpha and rgb[3] != 1.0: - attrib['stroke-opacity'] = short_float_fmt(rgb[3]) + attrib['stroke-opacity'] = _short_float_fmt(rgb[3]) if linewidth != 1.0: - attrib['stroke-width'] = short_float_fmt(linewidth) + attrib['stroke-width'] = _short_float_fmt(linewidth) if gc.get_joinstyle() != 'round': attrib['stroke-linejoin'] = gc.get_joinstyle() if gc.get_capstyle() != 'butt': @@ -579,9 +612,9 @@ def _get_style_dict(self, gc, rgbFace): return attrib def _get_style(self, gc, rgbFace): - return generate_css(self._get_style_dict(gc, rgbFace)) + return _generate_css(self._get_style_dict(gc, rgbFace)) - def _get_clip(self, gc): + def _get_clip_attrs(self, gc): cliprect = gc.get_clip_rectangle() clippath, clippath_trans = gc.get_clip_path() if clippath is not None: @@ -592,8 +625,7 @@ def _get_clip(self, gc): y = self.height-(y+h) dictkey = (x, y, w, h) else: - return None - + return {} clip = self._clipd.get(dictkey) if clip is None: oid = self._make_id('p', dictkey) @@ -603,7 +635,7 @@ def _get_clip(self, gc): self._clipd[dictkey] = (dictkey, oid) else: clip, oid = clip - return oid + return {'clip-path': f'url(#{oid})'} def _write_clips(self): if not len(self._clipd): @@ -621,10 +653,10 @@ def _write_clips(self): x, y, w, h = clip writer.element( 'rect', - x=short_float_fmt(x), - y=short_float_fmt(y), - width=short_float_fmt(w), - height=short_float_fmt(h)) + x=_short_float_fmt(x), + y=_short_float_fmt(y), + width=_short_float_fmt(w), + height=_short_float_fmt(h)) writer.end('clipPath') writer.end('defs') @@ -663,16 +695,10 @@ def draw_path(self, gc, path, transform, rgbFace=None): path, trans_and_flip, clip=clip, simplify=simplify, sketch=gc.get_sketch_params()) - attrib = {} - attrib['style'] = self._get_style(gc, rgbFace) - - clipid = self._get_clip(gc) - if clipid is not None: - attrib['clip-path'] = 'url(#%s)' % clipid - if gc.get_url() is not None: self.writer.start('a', {'xlink:href': gc.get_url()}) - self.writer.element('path', d=path_data, attrib=attrib) + self.writer.element('path', d=path_data, **self._get_clip_attrs(gc), + style=self._get_style(gc, rgbFace)) if gc.get_url() is not None: self.writer.end('a') @@ -689,9 +715,9 @@ def draw_markers( marker_trans + Affine2D().scale(1.0, -1.0), simplify=False) style = self._get_style_dict(gc, rgbFace) - dictkey = (path_data, generate_css(style)) + dictkey = (path_data, _generate_css(style)) oid = self._markers.get(dictkey) - style = generate_css({k: v for k, v in style.items() + style = _generate_css({k: v for k, v in style.items() if k.startswith('stroke')}) if oid is None: @@ -701,12 +727,7 @@ def draw_markers( writer.end('defs') self._markers[dictkey] = oid - attrib = {} - clipid = self._get_clip(gc) - if clipid is not None: - attrib['clip-path'] = 'url(#%s)' % clipid - writer.start('g', attrib=attrib) - + writer.start('g', **self._get_clip_attrs(gc)) trans_and_flip = self._make_flip_transform(trans) attrib = {'xlink:href': '#%s' % oid} clip = (0, 0, self.width*72, self.height*72) @@ -714,14 +735,14 @@ def draw_markers( trans_and_flip, clip=clip, simplify=False): if len(vertices): x, y = vertices[-2:] - attrib['x'] = short_float_fmt(x) - attrib['y'] = short_float_fmt(y) + attrib['x'] = _short_float_fmt(x) + attrib['y'] = _short_float_fmt(y) attrib['style'] = self._get_style(gc, rgbFace) writer.element('use', attrib=attrib) writer.end('g') def draw_path_collection(self, gc, master_transform, paths, all_transforms, - offsets, offsetTrans, facecolors, edgecolors, + offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): # Is the optimization worth it? Rough calculation: @@ -737,7 +758,7 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, if not should_do_optimization: return super().draw_path_collection( gc, master_transform, paths, all_transforms, - offsets, offsetTrans, facecolors, edgecolors, + offsets, offset_trans, facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position) @@ -755,23 +776,23 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, writer.end('defs') for xo, yo, path_id, gc0, rgbFace in self._iter_collection( - gc, master_transform, all_transforms, path_codes, offsets, - offsetTrans, facecolors, edgecolors, linewidths, linestyles, + gc, path_codes, offsets, offset_trans, + facecolors, edgecolors, linewidths, linestyles, antialiaseds, urls, offset_position): - clipid = self._get_clip(gc0) url = gc0.get_url() if url is not None: writer.start('a', attrib={'xlink:href': url}) - if clipid is not None: - writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) + clip_attrs = self._get_clip_attrs(gc0) + if clip_attrs: + writer.start('g', **clip_attrs) attrib = { 'xlink:href': '#%s' % path_id, - 'x': short_float_fmt(xo), - 'y': short_float_fmt(self.height - yo), + 'x': _short_float_fmt(xo), + 'y': _short_float_fmt(self.height - yo), 'style': self._get_style(gc0, rgbFace) } writer.element('use', attrib=attrib) - if clipid is not None: + if clip_attrs: writer.end('g') if url is not None: writer.end('a') @@ -780,7 +801,9 @@ def draw_path_collection(self, gc, master_transform, paths, all_transforms, def draw_gouraud_triangle(self, gc, points, colors, trans): # docstring inherited + self._draw_gouraud_triangle(gc, points, colors, trans) + def _draw_gouraud_triangle(self, gc, points, colors, trans): # This uses a method described here: # # http://www.svgopen.org/2005/papers/Converting3DFaceToSVG/index.html @@ -849,18 +872,18 @@ def draw_gouraud_triangle(self, gc, points, colors, trans): 'linearGradient', id="GR%x_%d" % (self._n_gradients, i), gradientUnits="userSpaceOnUse", - x1=short_float_fmt(x1), y1=short_float_fmt(y1), - x2=short_float_fmt(xb), y2=short_float_fmt(yb)) + x1=_short_float_fmt(x1), y1=_short_float_fmt(y1), + x2=_short_float_fmt(xb), y2=_short_float_fmt(yb)) writer.element( 'stop', offset='1', - style=generate_css({ + style=_generate_css({ 'stop-color': rgb2hex(avg_color), - 'stop-opacity': short_float_fmt(rgba_color[-1])})) + 'stop-opacity': _short_float_fmt(rgba_color[-1])})) writer.element( 'stop', offset='0', - style=generate_css({'stop-color': rgb2hex(rgba_color), + style=_generate_css({'stop-color': rgb2hex(rgba_color), 'stop-opacity': "0"})) writer.end('linearGradient') @@ -868,9 +891,9 @@ def draw_gouraud_triangle(self, gc, points, colors, trans): writer.end('defs') # triangle formation using "path" - dpath = "M " + short_float_fmt(x1)+',' + short_float_fmt(y1) - dpath += " L " + short_float_fmt(x2) + ',' + short_float_fmt(y2) - dpath += " " + short_float_fmt(x3) + ',' + short_float_fmt(y3) + " Z" + dpath = "M " + _short_float_fmt(x1)+',' + _short_float_fmt(y1) + dpath += " L " + _short_float_fmt(x2) + ',' + _short_float_fmt(y2) + dpath += " " + _short_float_fmt(x3) + ',' + _short_float_fmt(y3) + " Z" writer.element( 'path', @@ -912,17 +935,10 @@ def draw_gouraud_triangle(self, gc, points, colors, trans): def draw_gouraud_triangles(self, gc, triangles_array, colors_array, transform): - attrib = {} - clipid = self._get_clip(gc) - if clipid is not None: - attrib['clip-path'] = 'url(#%s)' % clipid - - self.writer.start('g', attrib=attrib) - + self.writer.start('g', **self._get_clip_attrs(gc)) transform = transform.frozen() for tri, col in zip(triangles_array, colors_array): - self.draw_gouraud_triangle(gc, tri, col, transform) - + self._draw_gouraud_triangle(gc, tri, col, transform) self.writer.end('g') def option_scale_image(self): @@ -940,18 +956,18 @@ def draw_image(self, gc, x, y, im, transform=None): if w == 0 or h == 0: return - attrib = {} - clipid = self._get_clip(gc) - if clipid is not None: - # Can't apply clip-path directly to the image because the - # image has a transformation, which would also be applied - # to the clip-path - self.writer.start('g', attrib={'clip-path': 'url(#%s)' % clipid}) + clip_attrs = self._get_clip_attrs(gc) + if clip_attrs: + # Can't apply clip-path directly to the image because the image has + # a transformation, which would also be applied to the clip-path. + self.writer.start('g', **clip_attrs) - oid = gc.get_gid() url = gc.get_url() if url is not None: self.writer.start('a', attrib={'xlink:href': url}) + + attrib = {} + oid = gc.get_gid() if mpl.rcParams['svg.image_inline']: buf = BytesIO() Image.fromarray(im).save(buf, format="png") @@ -969,7 +985,6 @@ def draw_image(self, gc, x, y, im, transform=None): Image.fromarray(im).save(filename) oid = oid or 'Im_' + self._make_id('image', filename) attrib['xlink:href'] = filename - attrib['id'] = oid if transform is None: @@ -978,16 +993,16 @@ def draw_image(self, gc, x, y, im, transform=None): self.writer.element( 'image', - transform=generate_transform([ + transform=_generate_transform([ ('scale', (1, -1)), ('translate', (0, -h))]), - x=short_float_fmt(x), - y=short_float_fmt(-(self.height - y - h)), - width=short_float_fmt(w), height=short_float_fmt(h), + x=_short_float_fmt(x), + y=_short_float_fmt(-(self.height - y - h)), + width=_short_float_fmt(w), height=_short_float_fmt(h), attrib=attrib) else: alpha = gc.get_alpha() if alpha != 1.0: - attrib['opacity'] = short_float_fmt(alpha) + attrib['opacity'] = _short_float_fmt(alpha) flipped = ( Affine2D().scale(1.0 / w, 1.0 / h) + @@ -997,19 +1012,19 @@ def draw_image(self, gc, x, y, im, transform=None): .scale(1.0, -1.0) .translate(0.0, self.height)) - attrib['transform'] = generate_transform( + attrib['transform'] = _generate_transform( [('matrix', flipped.frozen())]) attrib['style'] = ( 'image-rendering:crisp-edges;' 'image-rendering:pixelated') self.writer.element( 'image', - width=short_float_fmt(w), height=short_float_fmt(h), + width=_short_float_fmt(w), height=_short_float_fmt(h), attrib=attrib) if url is not None: self.writer.end('a') - if clipid is not None: + if clip_attrs: self.writer.end('g') def _update_glyph_map_defs(self, glyph_map_new): @@ -1027,7 +1042,7 @@ def _update_glyph_map_defs(self, glyph_map_new): Path(vertices * 64, codes), simplify=False) writer.element( 'path', id=char_id, d=path_data, - transform=generate_transform([('scale', (1 / 64,))])) + transform=_generate_transform([('scale', (1 / 64,))])) writer.end('defs') self._glyph_map.update(glyph_map_new) @@ -1035,18 +1050,7 @@ def _adjust_char_id(self, char_id): return char_id.replace("%20", "_") def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): - """ - Draw the text by converting them to paths using the textpath module. - - Parameters - ---------- - s : str - text to be converted - prop : `matplotlib.font_manager.FontProperties` - font property - ismath : bool - If True, use mathtext parser. If "TeX", use *usetex* mode. - """ + # docstring inherited writer = self.writer writer.comment(s) @@ -1062,11 +1066,11 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): style['fill'] = color alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3] if alpha != 1: - style['opacity'] = short_float_fmt(alpha) + style['opacity'] = _short_float_fmt(alpha) font_scale = fontsize / text2path.FONT_SCALE attrib = { - 'style': generate_css(style), - 'transform': generate_transform([ + 'style': _generate_css(style), + 'transform': _generate_transform([ ('translate', (x, y)), ('rotate', (-angle,)), ('scale', (font_scale, -font_scale))]), @@ -1083,9 +1087,9 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): for glyph_id, xposition, yposition, scale in glyph_info: attrib = {'xlink:href': '#%s' % glyph_id} if xposition != 0.0: - attrib['x'] = short_float_fmt(xposition) + attrib['x'] = _short_float_fmt(xposition) if yposition != 0.0: - attrib['y'] = short_float_fmt(yposition) + attrib['y'] = _short_float_fmt(yposition) writer.element('use', attrib=attrib) else: @@ -1102,7 +1106,7 @@ def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath, mtext=None): char_id = self._adjust_char_id(char_id) writer.element( 'use', - transform=generate_transform([ + transform=_generate_transform([ ('translate', (xposition, yposition)), ('scale', (scale,)), ]), @@ -1125,20 +1129,50 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): alpha = gc.get_alpha() if gc.get_forced_alpha() else gc.get_rgb()[3] if alpha != 1: - style['opacity'] = short_float_fmt(alpha) + style['opacity'] = _short_float_fmt(alpha) if not ismath: - font = self._get_font(prop) - font.set_text(s, 0.0, flags=LOAD_NO_HINTING) - attrib = {} - style['font-family'] = str(font.family_name) - style['font-weight'] = str(prop.get_weight()).lower() - style['font-stretch'] = str(prop.get_stretch()).lower() - style['font-style'] = prop.get_style().lower() - # Must add "px" to workaround a Firefox bug - style['font-size'] = short_float_fmt(prop.get_size()) + 'px' - attrib['style'] = generate_css(style) + + font_parts = [] + if prop.get_style() != 'normal': + font_parts.append(prop.get_style()) + if prop.get_variant() != 'normal': + font_parts.append(prop.get_variant()) + weight = fm.weight_dict[prop.get_weight()] + if weight != 400: + font_parts.append(f'{weight}') + + def _normalize_sans(name): + return 'sans-serif' if name in ['sans', 'sans serif'] else name + + def _expand_family_entry(fn): + fn = _normalize_sans(fn) + # prepend generic font families with all configured font names + if fn in fm.font_family_aliases: + # get all of the font names and fix spelling of sans-serif + # (we accept 3 ways CSS only supports 1) + for name in fm.FontManager._expand_aliases(fn): + yield _normalize_sans(name) + # whether a generic name or a family name, it must appear at + # least once + yield fn + + def _get_all_quoted_names(prop): + # only quote specific names, not generic names + return [name if name in fm.font_family_aliases else repr(name) + for entry in prop.get_family() + for name in _expand_family_entry(entry)] + + font_parts.extend([ + f'{_short_float_fmt(prop.get_size())}px', + # ensure expansion, quoting, and dedupe of font names + ", ".join(dict.fromkeys(_get_all_quoted_names(prop))) + ]) + style['font'] = ' '.join(font_parts) + if prop.get_stretch() != 'normal': + style['font-stretch'] = prop.get_stretch() + attrib['style'] = _generate_css(style) if mtext and (angle == 0 or mtext.get_rotation_mode() == "anchor"): # If text anchoring can be supported, get the original @@ -1162,20 +1196,18 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): 'center': 'middle'} style['text-anchor'] = ha_mpl_to_svg[mtext.get_ha()] - attrib['x'] = short_float_fmt(ax) - attrib['y'] = short_float_fmt(ay) - attrib['style'] = generate_css(style) - attrib['transform'] = "rotate(%s, %s, %s)" % ( - short_float_fmt(-angle), - short_float_fmt(ax), - short_float_fmt(ay)) - writer.element('text', s, attrib=attrib) + attrib['x'] = _short_float_fmt(ax) + attrib['y'] = _short_float_fmt(ay) + attrib['style'] = _generate_css(style) + attrib['transform'] = _generate_transform([ + ("rotate", (-angle, ax, ay))]) + else: - attrib['transform'] = generate_transform([ + attrib['transform'] = _generate_transform([ ('translate', (x, y)), ('rotate', (-angle,))]) - writer.element('text', s, attrib=attrib) + writer.element('text', s, attrib=attrib) else: writer.comment(s) @@ -1186,8 +1218,8 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): # Apply attributes to 'g', not 'text', because we likely have some # rectangles as well with the same style and transformation. writer.start('g', - style=generate_css(style), - transform=generate_transform([ + style=_generate_css(style), + transform=_generate_transform([ ('translate', (x, y)), ('rotate', (-angle,))]), ) @@ -1195,13 +1227,24 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): writer.start('text') # Sort the characters by font, and output one tspan for each. - spans = OrderedDict() + spans = {} for font, fontsize, thetext, new_x, new_y in glyphs: - style = generate_css({ - 'font-size': short_float_fmt(fontsize) + 'px', - 'font-family': font.family_name, - 'font-style': font.style_name.lower(), - 'font-weight': font.style_name.lower()}) + entry = fm.ttfFontProperty(font) + font_parts = [] + if entry.style != 'normal': + font_parts.append(entry.style) + if entry.variant != 'normal': + font_parts.append(entry.variant) + if entry.weight != 400: + font_parts.append(f'{entry.weight}') + font_parts.extend([ + f'{_short_float_fmt(fontsize)}px', + f'{entry.name!r}', # ensure quoting + ]) + style = {'font': ' '.join(font_parts)} + if entry.stretch != 'normal': + style['font-stretch'] = entry.stretch + style = _generate_css(style) if thetext == 32: thetext = 0xa0 # non-breaking space spans.setdefault(style, []).append((new_x, -new_y, thetext)) @@ -1216,7 +1259,7 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): attrib = { 'style': style, - 'x': ' '.join(short_float_fmt(c[0]) for c in chars), + 'x': ' '.join(_short_float_fmt(c[0]) for c in chars), 'y': ys } @@ -1230,28 +1273,22 @@ def _draw_text_as_text(self, gc, x, y, s, prop, angle, ismath, mtext=None): for x, y, width, height in rects: writer.element( 'rect', - x=short_float_fmt(x), - y=short_float_fmt(-y-1), - width=short_float_fmt(width), - height=short_float_fmt(height) + x=_short_float_fmt(x), + y=_short_float_fmt(-y-1), + width=_short_float_fmt(width), + height=_short_float_fmt(height) ) writer.end('g') - @_api.delete_parameter("3.3", "ismath") - def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): - # docstring inherited - self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") - def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): # docstring inherited - clipid = self._get_clip(gc) - if clipid is not None: + clip_attrs = self._get_clip_attrs(gc) + if clip_attrs: # Cannot apply clip-path directly to the text, because - # is has a transformation - self.writer.start( - 'g', attrib={'clip-path': 'url(#%s)' % clipid}) + # it has a transformation + self.writer.start('g', **clip_attrs) if gc.get_url() is not None: self.writer.start('a', {'xlink:href': gc.get_url()}) @@ -1264,7 +1301,7 @@ def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): if gc.get_url() is not None: self.writer.end('a') - if clipid is not None: + if clip_attrs: self.writer.end('g') def flipy(self): @@ -1286,7 +1323,7 @@ class FigureCanvasSVG(FigureCanvasBase): fixed_dpi = 72 - def print_svg(self, filename, *args, **kwargs): + def print_svg(self, filename, *, bbox_inches_restore=None, metadata=None): """ Parameters ---------- @@ -1319,52 +1356,29 @@ def print_svg(self, filename, *args, **kwargs): __ DC_ """ with cbook.open_file_cm(filename, "w", encoding="utf-8") as fh: - - filename = getattr(fh, 'name', '') - if not isinstance(filename, str): - filename = '' - - if cbook.file_requires_unicode(fh): - detach = False - else: - fh = TextIOWrapper(fh, 'utf-8') - detach = True - - self._print_svg(filename, fh, **kwargs) - - # Detach underlying stream from wrapper so that it remains open in - # the caller. - if detach: - fh.detach() - - def print_svgz(self, filename, *args, **kwargs): + if not cbook.file_requires_unicode(fh): + fh = codecs.getwriter('utf-8')(fh) + dpi = self.figure.dpi + self.figure.dpi = 72 + width, height = self.figure.get_size_inches() + w, h = width * 72, height * 72 + renderer = MixedModeRenderer( + self.figure, width, height, dpi, + RendererSVG(w, h, fh, image_dpi=dpi, metadata=metadata), + bbox_inches_restore=bbox_inches_restore) + self.figure.draw(renderer) + renderer.finalize() + + def print_svgz(self, filename, **kwargs): with cbook.open_file_cm(filename, "wb") as fh, \ gzip.GzipFile(mode='w', fileobj=fh) as gzipwriter: - return self.print_svg(gzipwriter) - - @_check_savefig_extra_args - @_api.delete_parameter("3.4", "dpi") - def _print_svg(self, filename, fh, *, dpi=None, bbox_inches_restore=None, - metadata=None): - if dpi is None: # always use this branch after deprecation elapses. - dpi = self.figure.get_dpi() - self.figure.set_dpi(72) - width, height = self.figure.get_size_inches() - w, h = width * 72, height * 72 - - renderer = MixedModeRenderer( - self.figure, width, height, dpi, - RendererSVG(w, h, fh, filename, dpi, metadata=metadata), - bbox_inches_restore=bbox_inches_restore) - - self.figure.draw(renderer) - renderer.finalize() + return self.print_svg(gzipwriter, **kwargs) def get_default_filetype(self): return 'svg' def draw(self): - _no_output_draw(self.figure) + self.figure.draw_without_rendering() return super().draw() @@ -1380,4 +1394,5 @@ def draw(self): @_Backend.export class _BackendSVG(_Backend): + backend_version = mpl.__version__ FigureCanvas = FigureCanvasSVG diff --git a/lib/matplotlib/backends/backend_template.py b/lib/matplotlib/backends/backend_template.py index 7ed017a0c6eb..915cdeb210bb 100644 --- a/lib/matplotlib/backends/backend_template.py +++ b/lib/matplotlib/backends/backend_template.py @@ -10,9 +10,9 @@ produced. This provides a starting point for backend writers; you can selectively implement drawing methods (`~.RendererTemplate.draw_path`, `~.RendererTemplate.draw_image`, etc.) and slowly see your figure come to life -instead having to have a full blown implementation before getting any results. +instead having to have a full-blown implementation before getting any results. -Copy this file to a directory outside of the Matplotlib source tree, somewhere +Copy this file to a directory outside the Matplotlib source tree, somewhere where Python can import it (by adding the directory to your ``sys.path`` or by packaging it as a normal Python package); if the backend is importable as ``import my.backend`` you can then select it using :: @@ -29,6 +29,7 @@ plt.savefig("figure.xyz") """ +from matplotlib import _api from matplotlib._pylab_helpers import Gcf from matplotlib.backend_bases import ( FigureCanvasBase, FigureManagerBase, GraphicsContextBase, RendererBase) @@ -40,7 +41,7 @@ class RendererTemplate(RendererBase): The renderer handles drawing/rendering operations. This is a minimal do-nothing class that can be used to get started when - writing a new backend. Refer to `backend_bases.RendererBase` for + writing a new backend. Refer to `.backend_bases.RendererBase` for documentation of the methods. """ @@ -62,7 +63,7 @@ def draw_path(self, gc, path, transform, rgbFace=None): # relative timings by leaving it out. backend implementers concerned with # performance will probably want to implement it # def draw_path_collection(self, gc, master_transform, paths, -# all_transforms, offsets, offsetTrans, +# all_transforms, offsets, offset_trans, # facecolors, edgecolors, linewidths, linestyles, # antialiaseds): # pass @@ -100,14 +101,14 @@ def points_to_pixels(self, points): # if backend doesn't have dpi, e.g., postscript or svg return points # elif backend assumes a value for pixels_per_inch - #return points/72.0 * self.dpi.get() * pixels_per_inch/72.0 + # return points/72.0 * self.dpi.get() * pixels_per_inch/72.0 # else - #return points/72.0 * self.dpi.get() + # return points/72.0 * self.dpi.get() class GraphicsContextTemplate(GraphicsContextBase): """ - The graphics context provides the color, line styles, etc... See the cairo + The graphics context provides the color, line styles, etc. See the cairo and postscript backends for examples of mapping the graphics context attributes (cap styles, join styles, line widths, colors) to a particular backend. In cairo this is done by wrapping a cairo.Context object and @@ -121,7 +122,7 @@ class GraphicsContextTemplate(GraphicsContextBase): do the mapping here, you'll need to override several of the setter methods. - The base GraphicsContext stores colors as a RGB tuple on the unit + The base GraphicsContext stores colors as an RGB tuple on the unit interval, e.g., (0.5, 0.0, 1.0). You may need to map this to colors appropriate for your backend. """ @@ -130,47 +131,19 @@ class GraphicsContextTemplate(GraphicsContextBase): ######################################################################## # # The following functions and classes are for pyplot and implement -# window/figure managers, etc... +# window/figure managers, etc. # ######################################################################## -def draw_if_interactive(): - """ - For image backends - is not required. - For GUI backends - this should be overridden if drawing should be done in - interactive python mode. +class FigureManagerTemplate(FigureManagerBase): """ + Helper class for pyplot mode, wraps everything up into a neat bundle. - -def show(*, block=None): - """ - For image backends - is not required. - For GUI backends - show() is usually the last line of a pyplot script and - tells the backend that it is time to draw. In interactive mode, this - should do nothing. + For non-interactive backends, the base class is sufficient. For + interactive backends, see the documentation of the `.FigureManagerBase` + class for the list of methods that can/should be overridden. """ - for manager in Gcf.get_all_fig_managers(): - # do something to display the GUI - pass - - -def new_figure_manager(num, *args, FigureClass=Figure, **kwargs): - """Create a new figure manager instance.""" - # If a main-level app must be created, this (and - # new_figure_manager_given_figure) is the usual place to do it -- see - # backend_wx, backend_wxagg and backend_tkagg for examples. Not all GUIs - # require explicit instantiation of a main-level app (e.g., backend_gtk3) - # for pylab. - thisFig = FigureClass(*args, **kwargs) - return new_figure_manager_given_figure(num, thisFig) - - -def new_figure_manager_given_figure(num, figure): - """Create a new figure manager instance for the given figure.""" - canvas = FigureCanvasTemplate(figure) - manager = FigureManagerTemplate(canvas, num) - return manager class FigureCanvasTemplate(FigureCanvasBase): @@ -190,6 +163,11 @@ class methods button_press_event, button_release_event, A high-level Figure instance """ + # The instantiated manager class. For further customization, + # ``FigureManager.create_with_canvas`` can also be overridden; see the + # wx-based backends for an example. + manager_class = FigureManagerTemplate + def draw(self): """ Draw the figure using the renderer. @@ -209,7 +187,7 @@ def draw(self): # you should add it to the class-scope filetypes dictionary as follows: filetypes = {**FigureCanvasBase.filetypes, 'foo': 'My magic Foo format'} - def print_foo(self, filename, *args, **kwargs): + def print_foo(self, filename, **kwargs): """ Write out format foo. @@ -225,14 +203,6 @@ def get_default_filetype(self): return 'foo' -class FigureManagerTemplate(FigureManagerBase): - """ - Helper class for pyplot mode, wraps everything up into a neat bundle. - - For non-interactive backends, the base class is sufficient. - """ - - ######################################################################## # # Now just provide the standard names that backend.__init__ is expecting diff --git a/lib/matplotlib/backends/backend_tkagg.py b/lib/matplotlib/backends/backend_tkagg.py index 31012d80eab8..f95b6011eadf 100644 --- a/lib/matplotlib/backends/backend_tkagg.py +++ b/lib/matplotlib/backends/backend_tkagg.py @@ -1,7 +1,8 @@ from . import _backend_tk from .backend_agg import FigureCanvasAgg -from ._backend_tk import ( - _BackendTk, FigureCanvasTk, FigureManagerTk, NavigationToolbar2Tk) +from ._backend_tk import _BackendTk, FigureCanvasTk +from ._backend_tk import ( # noqa: F401 # pylint: disable=W0611 + FigureManagerTk, NavigationToolbar2Tk) class FigureCanvasTkAgg(FigureCanvasAgg, FigureCanvasTk): diff --git a/lib/matplotlib/backends/backend_tkcairo.py b/lib/matplotlib/backends/backend_tkcairo.py index a81fd0d92bb8..a6951c03c65a 100644 --- a/lib/matplotlib/backends/backend_tkcairo.py +++ b/lib/matplotlib/backends/backend_tkcairo.py @@ -3,21 +3,17 @@ import numpy as np from . import _backend_tk -from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo +from .backend_cairo import cairo, FigureCanvasCairo from ._backend_tk import _BackendTk, FigureCanvasTk class FigureCanvasTkCairo(FigureCanvasCairo, FigureCanvasTk): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self._renderer = RendererCairo(self.figure.dpi) - def draw(self): width = int(self.figure.bbox.width) height = int(self.figure.bbox.height) surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) - self._renderer.set_ctx_from_surface(surface) - self._renderer.set_width_height(width, height) + self._renderer.set_context(cairo.Context(surface)) + self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) buf = np.reshape(surface.get_data(), (height, width, 4)) _backend_tk.blit( diff --git a/lib/matplotlib/backends/backend_webagg.py b/lib/matplotlib/backends/backend_webagg.py index 7f43187ac65f..17c12c0a2f8e 100644 --- a/lib/matplotlib/backends/backend_webagg.py +++ b/lib/matplotlib/backends/backend_webagg.py @@ -36,19 +36,44 @@ from matplotlib.backend_bases import _Backend from matplotlib._pylab_helpers import Gcf from . import backend_webagg_core as core -from .backend_webagg_core import TimerTornado +from .backend_webagg_core import ( # noqa: F401 # pylint: disable=W0611 + TimerAsyncio, TimerTornado) +@mpl._api.deprecated("3.7") class ServerThread(threading.Thread): def run(self): tornado.ioloop.IOLoop.instance().start() -webagg_server_thread = ServerThread() +webagg_server_thread = threading.Thread( + target=lambda: tornado.ioloop.IOLoop.instance().start()) + + +class FigureManagerWebAgg(core.FigureManagerWebAgg): + _toolbar2_class = core.NavigationToolbar2WebAgg + + @classmethod + def pyplot_show(cls, *, block=None): + WebAggApplication.initialize() + + url = "http://{address}:{port}{prefix}".format( + address=WebAggApplication.address, + port=WebAggApplication.port, + prefix=WebAggApplication.url_prefix) + + if mpl.rcParams['webagg.open_in_browser']: + import webbrowser + if not webbrowser.open(url): + print("To view figure, visit {0}".format(url)) + else: + print("To view figure, visit {0}".format(url)) + + WebAggApplication.start() class FigureCanvasWebAgg(core.FigureCanvasWebAggCore): - pass + manager_class = FigureManagerWebAgg class WebAggApplication(tornado.web.Application): @@ -240,6 +265,14 @@ def random_ports(port, n): @classmethod def start(cls): + import asyncio + try: + asyncio.get_running_loop() + except RuntimeError: + pass + else: + cls.started = True + if cls.started: return @@ -281,8 +314,12 @@ def ipython_inline_display(figure): import tornado.template WebAggApplication.initialize() - if not webagg_server_thread.is_alive(): - webagg_server_thread.start() + import asyncio + try: + asyncio.get_running_loop() + except RuntimeError: + if not webagg_server_thread.is_alive(): + webagg_server_thread.start() fignum = figure.number tpl = Path(core.FigureManagerWebAgg.get_static_file_path(), @@ -299,22 +336,4 @@ def ipython_inline_display(figure): @_Backend.export class _BackendWebAgg(_Backend): FigureCanvas = FigureCanvasWebAgg - FigureManager = core.FigureManagerWebAgg - - @staticmethod - def show(): - WebAggApplication.initialize() - - url = "http://{address}:{port}{prefix}".format( - address=WebAggApplication.address, - port=WebAggApplication.port, - prefix=WebAggApplication.url_prefix) - - if mpl.rcParams['webagg.open_in_browser']: - import webbrowser - if not webbrowser.open(url): - print("To view figure, visit {0}".format(url)) - else: - print("To view figure, visit {0}".format(url)) - - WebAggApplication.start() + FigureManager = FigureManagerWebAgg diff --git a/lib/matplotlib/backends/backend_webagg_core.py b/lib/matplotlib/backends/backend_webagg_core.py index ed0d6173a6fe..57cfa311b8f7 100644 --- a/lib/matplotlib/backends/backend_webagg_core.py +++ b/lib/matplotlib/backends/backend_webagg_core.py @@ -8,8 +8,9 @@ # way over a web socket. # # - `backend_webagg.py` contains a concrete implementation of a basic -# application, implemented with tornado. +# application, implemented with asyncio. +import asyncio import datetime from io import BytesIO, StringIO import json @@ -19,11 +20,11 @@ import numpy as np from PIL import Image -import tornado -from matplotlib import _api, backend_bases +from matplotlib import _api, backend_bases, backend_tools from matplotlib.backends import backend_agg -from matplotlib.backend_bases import _Backend +from matplotlib.backend_bases import ( + _Backend, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) _log = logging.getLogger(__name__) @@ -85,6 +86,8 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def _timer_start(self): + import tornado + self._timer_stop() if self._single: ioloop = tornado.ioloop.IOLoop.instance() @@ -98,6 +101,8 @@ def _timer_start(self): self._timer.start() def _timer_stop(self): + import tornado + if self._timer is None: return elif self._single: @@ -114,8 +119,44 @@ def _timer_set_interval(self): self._timer_start() +class TimerAsyncio(backend_bases.TimerBase): + def __init__(self, *args, **kwargs): + self._task = None + super().__init__(*args, **kwargs) + + async def _timer_task(self, interval): + while True: + try: + await asyncio.sleep(interval) + self._on_timer() + + if self._single: + break + except asyncio.CancelledError: + break + + def _timer_start(self): + self._timer_stop() + + self._task = asyncio.ensure_future( + self._timer_task(max(self.interval / 1_000., 1e-6)) + ) + + def _timer_stop(self): + if self._task is not None: + self._task.cancel() + self._task = None + + def _timer_set_interval(self): + # Only stop and restart it if the timer has already been started + if self._task is not None: + self._timer_stop() + self._timer_start() + + class FigureCanvasWebAggCore(backend_agg.FigureCanvasAgg): - _timer_cls = TimerTornado + manager_class = _api.classproperty(lambda cls: FigureManagerWebAgg) + _timer_cls = TimerAsyncio # Webagg and friends having the right methods, but still # having bugs in practice. Do not advertise that it works until # we can debug this. @@ -123,24 +164,21 @@ class FigureCanvasWebAggCore(backend_agg.FigureCanvasAgg): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - # Set to True when the renderer contains data that is newer # than the PNG buffer. self._png_is_old = True - # Set to True by the `refresh` message so that the next frame # sent to the clients will be a full frame. self._force_full = True - + # The last buffer, for diff mode. + self._last_buff = np.empty((0, 0)) # Store the current image mode so that at any point, clients can # request the information. This should be changed by calling # self.set_image_mode(mode) so that the notification can be given # to the connected clients. self._current_image_mode = 'full' - - # Store the DPI ratio of the browser. This is the scaling that - # occurs automatically for all images on a HiDPI display. - self._dpi_ratio = 1 + # Track mouse events to fill in the x, y position of key events. + self._last_mouse_xy = (None, None) def show(self): # show the figure window @@ -161,6 +199,19 @@ def blit(self, bbox=None): def draw_idle(self): self.send_event("draw") + def set_cursor(self, cursor): + # docstring inherited + cursor = _api.check_getitem({ + backend_tools.Cursors.HAND: 'pointer', + backend_tools.Cursors.POINTER: 'default', + backend_tools.Cursors.SELECT_REGION: 'crosshair', + backend_tools.Cursors.MOVE: 'move', + backend_tools.Cursors.WAIT: 'wait', + backend_tools.Cursors.RESIZE_HORIZONTAL: 'ew-resize', + backend_tools.Cursors.RESIZE_VERTICAL: 'ns-resize', + }, cursor=cursor) + self.send_event('cursor', cursor=cursor) + def set_image_mode(self, mode): """ Set the image mode for any subsequent images which will be sent @@ -179,17 +230,18 @@ def get_diff_image(self): if self._png_is_old: renderer = self.get_renderer() + pixels = np.asarray(renderer.buffer_rgba()) # The buffer is created as type uint32 so that entire # pixels can be compared in one numpy call, rather than # needing to compare each plane separately. - buff = (np.frombuffer(renderer.buffer_rgba(), dtype=np.uint32) - .reshape((renderer.height, renderer.width))) - - # If any pixels have transparency, we need to force a full - # draw as we cannot overlay new on top of old. - pixels = buff.view(dtype=np.uint8).reshape(buff.shape + (4,)) - - if self._force_full or np.any(pixels[:, :, 3] != 255): + buff = pixels.view(np.uint32).squeeze(2) + + if (self._force_full + # If the buffer has changed size we need to do a full draw. + or buff.shape != self._last_buff.shape + # If any pixels have transparency, we need to force a full + # draw as we cannot overlay new on top of old. + or (pixels[:, :, 3] != 255).any()): self.set_image_mode('full') output = buff else: @@ -198,7 +250,7 @@ def get_diff_image(self): output = np.where(diff, buff, 0) # Store the current buffer so we can compute the next diff. - np.copyto(self._last_buff, buff) + self._last_buff = buff.copy() self._force_full = False self._png_is_old = False @@ -207,31 +259,6 @@ def get_diff_image(self): Image.fromarray(data).save(png, format="png") return png.getvalue() - def get_renderer(self, cleared=None): - # Mirrors super.get_renderer, but caches the old one so that we can do - # things such as produce a diff image in get_diff_image. - w, h = self.figure.bbox.size.astype(int) - key = w, h, self.figure.dpi - try: - self._lastKey, self._renderer - except AttributeError: - need_new_renderer = True - else: - need_new_renderer = (self._lastKey != key) - - if need_new_renderer: - self._renderer = backend_agg.RendererAgg( - w, h, self.figure.dpi) - self._lastKey = key - self._last_buff = np.copy(np.frombuffer( - self._renderer.buffer_rgba(), dtype=np.uint32 - ).reshape((self._renderer.height, self._renderer.width))) - - elif cleared: - self._renderer.clear() - - return self._renderer - def handle_event(self, event): e_type = event['type'] handler = getattr(self, 'handle_{0}'.format(e_type), @@ -258,40 +285,36 @@ def _handle_mouse(self, event): x = event['x'] y = event['y'] y = self.get_renderer().height - y - - # Javascript button numbers and matplotlib button numbers are - # off by 1 + self._last_mouse_xy = x, y + # JavaScript button numbers and Matplotlib button numbers are off by 1. button = event['button'] + 1 e_type = event['type'] - guiEvent = event.get('guiEvent', None) - if e_type == 'button_press': - self.button_press_event(x, y, button, guiEvent=guiEvent) + modifiers = event['modifiers'] + guiEvent = event.get('guiEvent') + if e_type in ['button_press', 'button_release']: + MouseEvent(e_type + '_event', self, x, y, button, + modifiers=modifiers, guiEvent=guiEvent)._process() elif e_type == 'dblclick': - self.button_press_event(x, y, button, dblclick=True, - guiEvent=guiEvent) - elif e_type == 'button_release': - self.button_release_event(x, y, button, guiEvent=guiEvent) - elif e_type == 'motion_notify': - self.motion_notify_event(x, y, guiEvent=guiEvent) - elif e_type == 'figure_enter': - self.enter_notify_event(xy=(x, y), guiEvent=guiEvent) - elif e_type == 'figure_leave': - self.leave_notify_event() + MouseEvent('button_press_event', self, x, y, button, dblclick=True, + modifiers=modifiers, guiEvent=guiEvent)._process() elif e_type == 'scroll': - self.scroll_event(x, y, event['step'], guiEvent=guiEvent) + MouseEvent('scroll_event', self, x, y, step=event['step'], + modifiers=modifiers, guiEvent=guiEvent)._process() + elif e_type == 'motion_notify': + MouseEvent(e_type + '_event', self, x, y, + modifiers=modifiers, guiEvent=guiEvent)._process() + elif e_type in ['figure_enter', 'figure_leave']: + LocationEvent(e_type + '_event', self, x, y, + modifiers=modifiers, guiEvent=guiEvent)._process() handle_button_press = handle_button_release = handle_dblclick = \ handle_figure_enter = handle_figure_leave = handle_motion_notify = \ handle_scroll = _handle_mouse def _handle_key(self, event): - key = _handle_key(event['key']) - e_type = event['type'] - guiEvent = event.get('guiEvent', None) - if e_type == 'key_press': - self.key_press_event(key, guiEvent=guiEvent) - elif e_type == 'key_release': - self.key_release_event(key, guiEvent=guiEvent) + KeyEvent(event['type'] + '_event', self, + _handle_key(event['key']), *self._last_mouse_xy, + guiEvent=event.get('guiEvent'))._process() handle_key_press = handle_key_release = _handle_key def handle_toolbar_button(self, event): @@ -311,8 +334,8 @@ def handle_refresh(self, event): self.draw_idle() def handle_resize(self, event): - x, y = event.get('width', 800), event.get('height', 800) - x, y = int(x) * self._dpi_ratio, int(y) * self._dpi_ratio + x = int(event.get('width', 800)) * self.device_pixel_ratio + y = int(event.get('height', 800)) * self.device_pixel_ratio fig = self.figure # An attempt at approximating the figure size in pixels. fig.set_size_inches(x / fig.dpi, y / fig.dpi, forward=False) @@ -321,20 +344,22 @@ def handle_resize(self, event): # identical or within a pixel or so). self._png_is_old = True self.manager.resize(*fig.bbox.size, forward=False) - self.resize_event() + ResizeEvent('resize_event', self)._process() + self.draw_idle() def handle_send_image_mode(self, event): # The client requests notification of what the current image mode is. self.send_event('image_mode', mode=self._current_image_mode) + def handle_set_device_pixel_ratio(self, event): + self._handle_set_device_pixel_ratio(event.get('device_pixel_ratio', 1)) + def handle_set_dpi_ratio(self, event): - dpi_ratio = event.get('dpi_ratio', 1) - if dpi_ratio != self._dpi_ratio: - # We don't want to scale up the figure dpi more than once. - if not hasattr(self.figure, '_original_dpi'): - self.figure._original_dpi = self.figure.dpi - self.figure.dpi = dpi_ratio * self.figure._original_dpi - self._dpi_ratio = dpi_ratio + # This handler is for backwards-compatibility with older ipympl. + self._handle_set_device_pixel_ratio(event.get('dpi_ratio', 1)) + + def _handle_set_device_pixel_ratio(self, device_pixel_ratio): + if self._set_device_pixel_ratio(device_pixel_ratio): self._force_full = True self.draw_idle() @@ -367,7 +392,6 @@ class NavigationToolbar2WebAgg(backend_bases.NavigationToolbar2): def __init__(self, canvas): self.message = '' - self.cursor = 0 super().__init__(canvas) def set_message(self, message): @@ -375,19 +399,11 @@ def set_message(self, message): self.canvas.send_event("message", message=message) self.message = message - def set_cursor(self, cursor): - if cursor != self.cursor: - self.canvas.send_event("cursor", cursor=cursor) - self.cursor = cursor - def draw_rubberband(self, event, x0, y0, x1, y1): - self.canvas.send_event( - "rubberband", x0=x0, y0=y0, x1=x1, y1=y1) + self.canvas.send_event("rubberband", x0=x0, y0=y0, x1=x1, y1=y1) - def release_zoom(self, event): - super().release_zoom(event) - self.canvas.send_event( - "rubberband", x0=-1, y0=-1, x1=-1, y1=-1) + def remove_rubberband(self): + self.canvas.send_event("rubberband", x0=-1, y0=-1, x1=-1, y1=-1) def save_figure(self, *args): """Save the current figure""" @@ -409,24 +425,22 @@ def set_history_buttons(self): class FigureManagerWebAgg(backend_bases.FigureManagerBase): + # This must be None to not break ipympl + _toolbar2_class = None ToolbarCls = NavigationToolbar2WebAgg def __init__(self, canvas, num): self.web_sockets = set() super().__init__(canvas, num) - self.toolbar = self._get_toolbar(canvas) def show(self): pass - def _get_toolbar(self, canvas): - toolbar = self.ToolbarCls(canvas) - return toolbar - def resize(self, w, h, forward=True): self._send_event( 'resize', - size=(w / self.canvas._dpi_ratio, h / self.canvas._dpi_ratio), + size=(w / self.canvas.device_pixel_ratio, + h / self.canvas.device_pixel_ratio), forward=forward) def set_window_title(self, title): diff --git a/lib/matplotlib/backends/backend_wx.py b/lib/matplotlib/backends/backend_wx.py index 9d12357d2523..eeed515aafa2 100644 --- a/lib/matplotlib/backends/backend_wx.py +++ b/lib/matplotlib/backends/backend_wx.py @@ -7,6 +7,7 @@ Copyright (C) Jeremy O'Donoghue & John Hunter, 2003-4. """ +import functools import logging import math import pathlib @@ -18,46 +19,27 @@ import matplotlib as mpl from matplotlib.backend_bases import ( - _Backend, _check_savefig_extra_args, FigureCanvasBase, FigureManagerBase, + _Backend, FigureCanvasBase, FigureManagerBase, GraphicsContextBase, MouseButton, NavigationToolbar2, RendererBase, - StatusbarBase, TimerBase, ToolContainerBase, cursors) + TimerBase, ToolContainerBase, cursors, + CloseEvent, KeyEvent, LocationEvent, MouseEvent, ResizeEvent) from matplotlib import _api, cbook, backend_tools from matplotlib._pylab_helpers import Gcf -from matplotlib.backend_managers import ToolManager -from matplotlib.figure import Figure from matplotlib.path import Path from matplotlib.transforms import Affine2D -from matplotlib.widgets import SubplotTool import wx _log = logging.getLogger(__name__) -# Debugging settings here... -# Debug level set here. If the debug level is less than 5, information -# messages (progressively more info for lower value) are printed. In addition, -# traceback is performed, and pdb activated, for all uncaught exceptions in -# this case -_DEBUG = 5 -_DEBUG_lvls = {1: 'Low ', 2: 'Med ', 3: 'High', 4: 'Error'} - - -@_api.deprecated("3.3") -def DEBUG_MSG(string, lvl=3, o=None): - if lvl >= _DEBUG: - print(f"{_DEBUG_lvls[lvl]}- {string} in {type(o)}") - - # the True dots per inch on the screen; should be display dependent; see -# http://groups.google.com/groups?q=screen+dpi+x11&hl=en&lr=&ie=UTF-8&oe=UTF-8&safe=off&selm=7077.26e81ad5%40swift.cs.tcd.ie&rnum=5 +# http://groups.google.com/d/msg/comp.lang.postscript/-/omHAc9FEuAsJ?hl=en # for some info about screen dpi PIXELS_PER_INCH = 75 -# Delay time for idle checks -IDLE_DELAY = 5 # Documented as deprecated as of Matplotlib 3.1. - +@_api.deprecated("3.6") def error_msg_wx(msg, parent=None): """Signal an error condition with a popup error dialog.""" dialog = wx.MessageDialog(parent=parent, @@ -69,6 +51,15 @@ def error_msg_wx(msg, parent=None): return None +# lru_cache holds a reference to the App and prevents it from being gc'ed. +@functools.lru_cache(1) +def _create_wxapp(): + wxapp = wx.App(False) + wxapp.SetExitOnFrameDelete(True) + cbook._setup_new_guiapp() + return wxapp + + class TimerWx(TimerBase): """Subclass of `.TimerBase` using wx.Timer events.""" @@ -88,6 +79,10 @@ def _timer_set_interval(self): self._timer_start() # Restart with new interval. +@_api.deprecated( + "2.0", name="wx", obj_type="backend", removal="the future", + alternative="wxagg", + addendum="See the Matplotlib usage FAQ for more info on backends.") class RendererWx(RendererBase): """ The renderer handles all the drawing primitives using a graphics @@ -129,7 +124,7 @@ class RendererWx(RendererBase): # wxPython allows for portable font styles, choosing them appropriately for # the target platform. Map some standard font names to the portable styles. - # QUESTION: Is it be wise to agree standard fontnames across all backends? + # QUESTION: Is it wise to agree to standard fontnames across all backends? fontnames = { 'Sans': wx.FONTFAMILY_SWISS, 'Roman': wx.FONTFAMILY_ROMAN, @@ -142,10 +137,6 @@ class RendererWx(RendererBase): def __init__(self, bitmap, dpi): """Initialise a wxWindows renderer instance.""" - _api.warn_deprecated( - "2.0", name="wx", obj_type="backend", removal="the future", - alternative="wxagg", addendum="See the Matplotlib usage FAQ for " - "more info on backends.") super().__init__() _log.debug("%s - __init__()", type(self)) self.width = bitmap.GetWidth() @@ -159,6 +150,7 @@ def flipy(self): # docstring inherited return True + @_api.deprecated("3.6") def offset_text_height(self): return True @@ -280,16 +272,6 @@ def new_gc(self): self.gc.unselect() return self.gc - @_api.deprecated("3.3", alternative=".gc") - def get_gc(self): - """ - Fetch the locally cached gc. - """ - # This is a dirty hack to allow anything with access to a renderer to - # access the current graphics context - assert self.gc is not None, "gc must be defined" - return self.gc - def get_wx_font(self, s, prop): """Return a wx font. Cache font instances for efficiency.""" _log.debug("%s - get_wx_font()", type(self)) @@ -301,7 +283,7 @@ def get_wx_font(self, s, prop): # Font colour is determined by the active wx.Pen # TODO: It may be wise to cache font information self.fontd[key] = font = wx.Font( # Cache the font and gc. - pointSize=int(size + 0.5), + pointSize=round(size), family=self.fontnames.get(prop.get_name(), wx.ROMAN), style=self.fontangles[prop.get_style()], weight=self.fontweights[prop.get_weight()]) @@ -314,14 +296,14 @@ def points_to_pixels(self, points): class GraphicsContextWx(GraphicsContextBase): """ - The graphics context provides the color, line styles, etc... + The graphics context provides the color, line styles, etc. This class stores a reference to a wxMemoryDC, and a wxGraphicsContext that draws to it. Creating a wxGraphicsContext seems to be fairly heavy, so these objects are cached based on the bitmap object that is passed in. - The base GraphicsContext stores colors as a RGB tuple on the unit + The base GraphicsContext stores colors as an RGB tuple on the unit interval, e.g., (0.5, 0.0, 1.0). wxPython uses an int interval, but since wxPython colour management is rather simple, I have not chosen to implement a separate colour manager class. @@ -343,8 +325,7 @@ def __init__(self, bitmap, renderer): dc, gfx_ctx = self._cache.get(bitmap, (None, None)) if dc is None: - dc = wx.MemoryDC() - dc.SelectObject(bitmap) + dc = wx.MemoryDC(bitmap) gfx_ctx = wx.GraphicsContext.Create(dc) gfx_ctx._lastcliprect = None self._cache[bitmap] = dc, gfx_ctx @@ -417,7 +398,7 @@ def set_joinstyle(self, js): self.unselect() def get_wxcolour(self, color): - """Convert a RGB(A) color to a wx.Colour.""" + """Convert an RGB(A) color to a wx.Colour.""" _log.debug("%s - get_wx_color()", type(self)) return wx.Colour(*[int(255 * x) for x in color]) @@ -434,6 +415,7 @@ class _FigureCanvasWxBase(FigureCanvasBase, wx.Panel): required_interactive_framework = "wx" _timer_cls = TimerWx + manager_class = _api.classproperty(lambda cls: FigureManagerWx) keyvald = { wx.WXK_CONTROL: 'control', @@ -513,27 +495,35 @@ def __init__(self, parent, id, figure=None): _log.debug("%s - __init__() - bitmap w:%d h:%d", type(self), w, h) self._isDrawn = False self._rubberband_rect = None - - self.Bind(wx.EVT_SIZE, self._onSize) - self.Bind(wx.EVT_PAINT, self._onPaint) - self.Bind(wx.EVT_CHAR_HOOK, self._onKeyDown) - self.Bind(wx.EVT_KEY_UP, self._onKeyUp) - self.Bind(wx.EVT_LEFT_DOWN, self._onMouseButton) - self.Bind(wx.EVT_LEFT_DCLICK, self._onMouseButton) - self.Bind(wx.EVT_LEFT_UP, self._onMouseButton) - self.Bind(wx.EVT_MIDDLE_DOWN, self._onMouseButton) - self.Bind(wx.EVT_MIDDLE_DCLICK, self._onMouseButton) - self.Bind(wx.EVT_MIDDLE_UP, self._onMouseButton) - self.Bind(wx.EVT_RIGHT_DOWN, self._onMouseButton) - self.Bind(wx.EVT_RIGHT_DCLICK, self._onMouseButton) - self.Bind(wx.EVT_RIGHT_UP, self._onMouseButton) - self.Bind(wx.EVT_MOUSEWHEEL, self._onMouseWheel) - self.Bind(wx.EVT_MOTION, self._onMotion) - self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeave) - self.Bind(wx.EVT_ENTER_WINDOW, self._onEnter) - - self.Bind(wx.EVT_MOUSE_CAPTURE_CHANGED, self._onCaptureLost) - self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self._onCaptureLost) + self._rubberband_pen_black = wx.Pen('BLACK', 1, wx.PENSTYLE_SHORT_DASH) + self._rubberband_pen_white = wx.Pen('WHITE', 1, wx.PENSTYLE_SOLID) + + self.Bind(wx.EVT_SIZE, self._on_size) + self.Bind(wx.EVT_PAINT, self._on_paint) + self.Bind(wx.EVT_CHAR_HOOK, self._on_key_down) + self.Bind(wx.EVT_KEY_UP, self._on_key_up) + self.Bind(wx.EVT_LEFT_DOWN, self._on_mouse_button) + self.Bind(wx.EVT_LEFT_DCLICK, self._on_mouse_button) + self.Bind(wx.EVT_LEFT_UP, self._on_mouse_button) + self.Bind(wx.EVT_MIDDLE_DOWN, self._on_mouse_button) + self.Bind(wx.EVT_MIDDLE_DCLICK, self._on_mouse_button) + self.Bind(wx.EVT_MIDDLE_UP, self._on_mouse_button) + self.Bind(wx.EVT_RIGHT_DOWN, self._on_mouse_button) + self.Bind(wx.EVT_RIGHT_DCLICK, self._on_mouse_button) + self.Bind(wx.EVT_RIGHT_UP, self._on_mouse_button) + self.Bind(wx.EVT_MOUSE_AUX1_DOWN, self._on_mouse_button) + self.Bind(wx.EVT_MOUSE_AUX1_UP, self._on_mouse_button) + self.Bind(wx.EVT_MOUSE_AUX2_DOWN, self._on_mouse_button) + self.Bind(wx.EVT_MOUSE_AUX2_UP, self._on_mouse_button) + self.Bind(wx.EVT_MOUSE_AUX1_DCLICK, self._on_mouse_button) + self.Bind(wx.EVT_MOUSE_AUX2_DCLICK, self._on_mouse_button) + self.Bind(wx.EVT_MOUSEWHEEL, self._on_mouse_wheel) + self.Bind(wx.EVT_MOTION, self._on_motion) + self.Bind(wx.EVT_ENTER_WINDOW, self._on_enter) + self.Bind(wx.EVT_LEAVE_WINDOW, self._on_leave) + + self.Bind(wx.EVT_MOUSE_CAPTURE_CHANGED, self._on_capture_lost) + self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self._on_capture_lost) self.SetBackgroundStyle(wx.BG_STYLE_PAINT) # Reduce flicker. self.SetBackgroundColour(wx.WHITE) @@ -601,11 +591,10 @@ def _get_imagesave_wildcards(self): wildcards = '|'.join(wildcards) return wildcards, extensions, filter_index - @_api.delete_parameter("3.4", "origin") - def gui_repaint(self, drawDC=None, origin='WX'): + def gui_repaint(self, drawDC=None): """ - Performs update of the displayed image on the GUI canvas, using the - supplied wx.PaintDC device context. + Update the displayed image on the GUI canvas, using the supplied + wx.PaintDC device context. The 'WXAgg' backend sets origin accordingly. """ @@ -620,15 +609,16 @@ def gui_repaint(self, drawDC=None, origin='WX'): # DC (see GraphicsContextWx._cache). bmp = (self.bitmap.ConvertToImage().ConvertToBitmap() if wx.Platform == '__WXMSW__' - and isinstance(self.figure._cachedRenderer, RendererWx) + and isinstance(self.figure.canvas.get_renderer(), RendererWx) else self.bitmap) drawDC.DrawBitmap(bmp, 0, 0) if self._rubberband_rect is not None: - x0, y0, x1, y1 = self._rubberband_rect - drawDC.DrawLineList( - [(x0, y0, x1, y0), (x1, y0, x1, y1), - (x0, y0, x0, y1), (x0, y1, x1, y1)], - wx.Pen('BLACK', 1, wx.PENSTYLE_SHORT_DASH)) + # Some versions of wx+python don't support numpy.float64 here. + x0, y0, x1, y1 = map(round, self._rubberband_rect) + rect = [(x0, y0, x1, y0), (x1, y0, x1, y1), + (x0, y0, x0, y1), (x0, y1, x1, y1)] + drawDC.DrawLineList(rect, self._rubberband_pen_white) + drawDC.DrawLineList(rect, self._rubberband_pen_black) filetypes = { **FigureCanvasBase.filetypes, @@ -651,9 +641,9 @@ def print_figure(self, filename, *args, **kwargs): if self._isDrawn: self.draw() - def _onPaint(self, event): + def _on_paint(self, event): """Called when wxPaintEvt is generated.""" - _log.debug("%s - _onPaint()", type(self)) + _log.debug("%s - _on_paint()", type(self)) drawDC = wx.PaintDC(self) if not self._isDrawn: self.draw(drawDC=drawDC) @@ -661,7 +651,7 @@ def _onPaint(self, event): self.gui_repaint(drawDC=drawDC) drawDC.Destroy() - def _onSize(self, event): + def _on_size(self, event): """ Called when wxEventSize is generated. @@ -669,7 +659,7 @@ def _onSize(self, event): is better to take the performance hit and redraw the whole window. """ - _log.debug("%s - _onSize()", type(self)) + _log.debug("%s - _on_size()", type(self)) sz = self.GetParent().GetSizer() if sz: si = sz.GetItem(self) @@ -703,10 +693,25 @@ def _onSize(self, event): # so no need to do anything here except to make sure # the whole background is repainted. self.Refresh(eraseBackground=False) - FigureCanvasBase.resize_event(self) + ResizeEvent("resize_event", self)._process() + self.draw_idle() - def _get_key(self, event): + @staticmethod + def _mpl_modifiers(event=None, *, exclude=None): + mod_table = [ + ("ctrl", wx.MOD_CONTROL, wx.WXK_CONTROL), + ("alt", wx.MOD_ALT, wx.WXK_ALT), + ("shift", wx.MOD_SHIFT, wx.WXK_SHIFT), + ] + if event is not None: + modifiers = event.GetModifiers() + return [name for name, mod, key in mod_table + if modifiers & mod and exclude != key] + else: + return [name for name, mod, key in mod_table + if wx.GetKeyState(key)] + def _get_key(self, event): keyval = event.KeyCode if keyval in self.keyvald: key = self.keyvald[keyval] @@ -717,32 +722,55 @@ def _get_key(self, event): if not event.ShiftDown(): key = key.lower() else: - key = None - - for meth, prefix, key_name in ( - [event.ControlDown, 'ctrl', 'control'], - [event.AltDown, 'alt', 'alt'], - [event.ShiftDown, 'shift', 'shift'],): - if meth() and key_name != key: - if not (key_name == 'shift' and key.isupper()): - key = '{0}+{1}'.format(prefix, key) + return None + mods = self._mpl_modifiers(event, exclude=keyval) + if "shift" in mods and key.isupper(): + mods.remove("shift") + return "+".join([*mods, key]) - return key + def _mpl_coords(self, pos=None): + """ + Convert a wx position, defaulting to the current cursor position, to + Matplotlib coordinates. + """ + if pos is None: + pos = wx.GetMouseState() + x, y = self.ScreenToClient(pos.X, pos.Y) + else: + x, y = pos.X, pos.Y + # flip y so y=0 is bottom of canvas + return x, self.figure.bbox.height - y - def _onKeyDown(self, event): + def _on_key_down(self, event): """Capture key press.""" - key = self._get_key(event) - FigureCanvasBase.key_press_event(self, key, guiEvent=event) + KeyEvent("key_press_event", self, + self._get_key(event), *self._mpl_coords(), + guiEvent=event)._process() if self: event.Skip() - def _onKeyUp(self, event): + def _on_key_up(self, event): """Release key.""" - key = self._get_key(event) - FigureCanvasBase.key_release_event(self, key, guiEvent=event) + KeyEvent("key_release_event", self, + self._get_key(event), *self._mpl_coords(), + guiEvent=event)._process() if self: event.Skip() + def set_cursor(self, cursor): + # docstring inherited + cursor = wx.Cursor(_api.check_getitem({ + cursors.MOVE: wx.CURSOR_HAND, + cursors.HAND: wx.CURSOR_HAND, + cursors.POINTER: wx.CURSOR_ARROW, + cursors.SELECT_REGION: wx.CURSOR_CROSS, + cursors.WAIT: wx.CURSOR_WAIT, + cursors.RESIZE_HORIZONTAL: wx.CURSOR_SIZEWE, + cursors.RESIZE_VERTICAL: wx.CURSOR_SIZENS, + }, cursor=cursor)) + self.SetCursor(cursor) + self.Refresh() + def _set_capture(self, capture=True): """Control wx mouse capture.""" if self.HasCapture(): @@ -750,36 +778,39 @@ def _set_capture(self, capture=True): if capture: self.CaptureMouse() - def _onCaptureLost(self, event): + def _on_capture_lost(self, event): """Capture changed or lost""" self._set_capture(False) - def _onMouseButton(self, event): + def _on_mouse_button(self, event): """Start measuring on an axis.""" event.Skip() self._set_capture(event.ButtonDown() or event.ButtonDClick()) - x = event.X - y = self.figure.bbox.height - event.Y + x, y = self._mpl_coords(event) button_map = { wx.MOUSE_BTN_LEFT: MouseButton.LEFT, wx.MOUSE_BTN_MIDDLE: MouseButton.MIDDLE, wx.MOUSE_BTN_RIGHT: MouseButton.RIGHT, + wx.MOUSE_BTN_AUX1: MouseButton.BACK, + wx.MOUSE_BTN_AUX2: MouseButton.FORWARD, } button = event.GetButton() button = button_map.get(button, button) + modifiers = self._mpl_modifiers(event) if event.ButtonDown(): - self.button_press_event(x, y, button, guiEvent=event) + MouseEvent("button_press_event", self, x, y, button, + modifiers=modifiers, guiEvent=event)._process() elif event.ButtonDClick(): - self.button_press_event(x, y, button, dblclick=True, - guiEvent=event) + MouseEvent("button_press_event", self, x, y, button, + dblclick=True, modifiers=modifiers, + guiEvent=event)._process() elif event.ButtonUp(): - self.button_release_event(x, y, button, guiEvent=event) + MouseEvent("button_release_event", self, x, y, button, + modifiers=modifiers, guiEvent=event)._process() - def _onMouseWheel(self, event): + def _on_mouse_wheel(self, event): """Translate mouse wheel events into matplotlib events""" - # Determine mouse location - x = event.GetX() - y = self.figure.bbox.height - event.GetY() + x, y = self._mpl_coords(event) # Convert delta/rotation/rate into a floating point step size step = event.LinesPerAction * event.WheelRotation / event.WheelDelta # Done handling event @@ -793,26 +824,33 @@ def _onMouseWheel(self, event): return # Return without processing event else: self._skipwheelevent = True - FigureCanvasBase.scroll_event(self, x, y, step, guiEvent=event) + MouseEvent("scroll_event", self, x, y, step=step, + modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() - def _onMotion(self, event): + def _on_motion(self, event): """Start measuring on an axis.""" - x = event.GetX() - y = self.figure.bbox.height - event.GetY() event.Skip() - FigureCanvasBase.motion_notify_event(self, x, y, guiEvent=event) + MouseEvent("motion_notify_event", self, + *self._mpl_coords(event), + modifiers=self._mpl_modifiers(event), + guiEvent=event)._process() - def _onLeave(self, event): - """Mouse has left the window.""" + def _on_enter(self, event): + """Mouse has entered the window.""" event.Skip() - FigureCanvasBase.leave_notify_event(self, guiEvent=event) + LocationEvent("figure_enter_event", self, + *self._mpl_coords(event), + modifiers=self._mpl_modifiers(), + guiEvent=event)._process() - def _onEnter(self, event): - """Mouse has entered the window.""" - x = event.GetX() - y = self.figure.bbox.height - event.GetY() + def _on_leave(self, event): + """Mouse has left the window.""" event.Skip() - FigureCanvasBase.enter_notify_event(self, guiEvent=event, xy=(x, y)) + LocationEvent("figure_leave_event", self, + *self._mpl_coords(event), + modifiers=self._mpl_modifiers(), + guiEvent=event)._process() class FigureCanvasWx(_FigureCanvasWxBase): @@ -829,67 +867,19 @@ def draw(self, drawDC=None): self._isDrawn = True self.gui_repaint(drawDC=drawDC) - def print_bmp(self, filename, *args, **kwargs): - return self._print_image(filename, wx.BITMAP_TYPE_BMP, *args, **kwargs) - - def print_jpeg(self, filename, *args, **kwargs): - return self._print_image(filename, wx.BITMAP_TYPE_JPEG, - *args, **kwargs) - print_jpg = print_jpeg - - def print_pcx(self, filename, *args, **kwargs): - return self._print_image(filename, wx.BITMAP_TYPE_PCX, *args, **kwargs) - - def print_png(self, filename, *args, **kwargs): - return self._print_image(filename, wx.BITMAP_TYPE_PNG, *args, **kwargs) - - def print_tiff(self, filename, *args, **kwargs): - return self._print_image(filename, wx.BITMAP_TYPE_TIF, *args, **kwargs) - print_tif = print_tiff - - def print_xpm(self, filename, *args, **kwargs): - return self._print_image(filename, wx.BITMAP_TYPE_XPM, *args, **kwargs) - - @_check_savefig_extra_args - def _print_image(self, filename, filetype, *, quality=None): - origBitmap = self.bitmap - - self.bitmap = wx.Bitmap(math.ceil(self.figure.bbox.width), - math.ceil(self.figure.bbox.height)) - renderer = RendererWx(self.bitmap, self.figure.dpi) - - gc = renderer.new_gc() - self.figure.draw(renderer) - - # image is the object that we call SaveFile on. - image = self.bitmap - # set the JPEG quality appropriately. Unfortunately, it is only - # possible to set the quality on a wx.Image object. So if we - # are saving a JPEG, convert the wx.Bitmap to a wx.Image, - # and set the quality. - if filetype == wx.BITMAP_TYPE_JPEG: - if quality is None: - quality = dict.__getitem__(mpl.rcParams, - 'savefig.jpeg_quality') - image = self.bitmap.ConvertToImage() - image.SetOption(wx.IMAGE_OPTION_QUALITY, str(quality)) - - # Now that we have rendered into the bitmap, save it to the appropriate - # file type and clean up. - if (cbook.is_writable_file_like(filename) and - not isinstance(image, wx.Image)): - image = image.ConvertToImage() - if not image.SaveFile(filename, filetype): + def _print_image(self, filetype, filename): + bitmap = wx.Bitmap(math.ceil(self.figure.bbox.width), + math.ceil(self.figure.bbox.height)) + self.figure.draw(RendererWx(bitmap, self.figure.dpi)) + saved_obj = (bitmap.ConvertToImage() + if cbook.is_writable_file_like(filename) + else bitmap) + if not saved_obj.SaveFile(filename, filetype): raise RuntimeError(f'Could not save figure to {filename}') - - # Restore everything to normal - self.bitmap = origBitmap - - # Note: draw is required here since bits of state about the - # last renderer are strewn about the artist draw methods. Do - # not remove the draw without first verifying that these have - # been cleaned up. The artist contains() methods will fail - # otherwise. + # draw() is required here since bits of state about the last renderer + # are strewn about the artist draw methods. Do not remove the draw + # without first verifying that these have been cleaned up. The artist + # contains() methods will fail otherwise. if self._isDrawn: self.draw() # The "if self" check avoids a "wrapped C/C++ object has been deleted" @@ -897,9 +887,22 @@ def _print_image(self, filename, filetype, *, quality=None): if self: self.Refresh() + print_bmp = functools.partialmethod( + _print_image, wx.BITMAP_TYPE_BMP) + print_jpeg = print_jpg = functools.partialmethod( + _print_image, wx.BITMAP_TYPE_JPEG) + print_pcx = functools.partialmethod( + _print_image, wx.BITMAP_TYPE_PCX) + print_png = functools.partialmethod( + _print_image, wx.BITMAP_TYPE_PNG) + print_tiff = print_tif = functools.partialmethod( + _print_image, wx.BITMAP_TYPE_TIF) + print_xpm = functools.partialmethod( + _print_image, wx.BITMAP_TYPE_XPM) + class FigureFrameWx(wx.Frame): - def __init__(self, num, fig): + def __init__(self, num, fig, *, canvas_class=None): # On non-Windows platform, explicitly set the position - fix # positioning bug on some Linux platforms if wx.Platform == '__WXMSW__': @@ -909,95 +912,74 @@ def __init__(self, num, fig): super().__init__(parent=None, id=-1, pos=pos) # Frame will be sized later by the Fit method _log.debug("%s - __init__()", type(self)) - self.num = num _set_frame_icon(self) - self.canvas = self.get_canvas(fig) - w, h = map(math.ceil, fig.bbox.size) - self.canvas.SetInitialSize(wx.Size(w, h)) - self.canvas.SetFocus() - self.sizer = wx.BoxSizer(wx.VERTICAL) - self.sizer.Add(self.canvas, 1, wx.TOP | wx.LEFT | wx.EXPAND) - # 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.figmgr = FigureManagerWx(self.canvas, num, self) - - self.toolbar = self._get_toolbar() - - if self.figmgr.toolmanager: - backend_tools.add_tools_to_manager(self.figmgr.toolmanager) - if self.toolbar: - backend_tools.add_tools_to_container(self.toolbar) - - if self.toolbar is not None: - self.toolbar.Realize() - # On Windows platform, default window size is incorrect, so set - # toolbar width to figure width. - tw, th = self.toolbar.GetSize() - fw, fh = self.canvas.GetSize() - # 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.toolbar.SetSize(wx.Size(fw, th)) - self.sizer.Add(self.toolbar, 0, wx.LEFT | wx.EXPAND) - self.SetSizer(self.sizer) - self.Fit() + # The parameter will become required after the deprecation elapses. + if canvas_class is not None: + self.canvas = canvas_class(self, -1, fig) + else: + _api.warn_deprecated( + "3.6", message="The canvas_class parameter will become " + "required after the deprecation period starting in Matplotlib " + "%(since)s elapses.") + self.canvas = self.get_canvas(fig) - self.canvas.SetMinSize((2, 2)) + # Auto-attaches itself to self.canvas.manager + manager = FigureManagerWx(self.canvas, num, self) - self.Bind(wx.EVT_CLOSE, self._onClose) + toolbar = self.canvas.manager.toolbar + if toolbar is not None: + self.SetToolBar(toolbar) - @property - def toolmanager(self): - return self.figmgr.toolmanager + # On Windows, canvas sizing must occur after toolbar addition; + # otherwise the toolbar further resizes the canvas. + w, h = map(math.ceil, fig.bbox.size) + self.canvas.SetInitialSize(wx.Size(w, h)) + self.canvas.SetMinSize((2, 2)) + self.canvas.SetFocus() - def _get_toolbar(self): - if mpl.rcParams['toolbar'] == 'toolbar2': - toolbar = NavigationToolbar2Wx(self.canvas) - elif mpl.rcParams['toolbar'] == 'toolmanager': - toolbar = ToolbarWx(self.toolmanager, self) - else: - toolbar = None - return toolbar + self.Fit() + self.Bind(wx.EVT_CLOSE, self._on_close) + + sizer = _api.deprecated("3.6", alternative="frame.GetSizer()")( + property(lambda self: self.GetSizer())) + figmgr = _api.deprecated("3.6", alternative="frame.canvas.manager")( + property(lambda self: self.canvas.manager)) + num = _api.deprecated("3.6", alternative="frame.canvas.manager.num")( + property(lambda self: self.canvas.manager.num)) + toolbar = _api.deprecated("3.6", alternative="frame.GetToolBar()")( + property(lambda self: self.GetToolBar())) + toolmanager = _api.deprecated( + "3.6", alternative="frame.canvas.manager.toolmanager")( + property(lambda self: self.canvas.manager.toolmanager)) + + @_api.deprecated( + "3.6", alternative="the canvas_class constructor parameter") def get_canvas(self, fig): return FigureCanvasWx(self, -1, fig) + @_api.deprecated("3.6", alternative="frame.canvas.manager") def get_figure_manager(self): _log.debug("%s - get_figure_manager()", type(self)) - return self.figmgr + return self.canvas.manager - def _onClose(self, event): - _log.debug("%s - onClose()", type(self)) - self.canvas.close_event() + def _on_close(self, event): + _log.debug("%s - on_close()", type(self)) + CloseEvent("close_event", self.canvas)._process() self.canvas.stop_event_loop() # set FigureManagerWx.frame to None to prevent repeated attempts to # close this frame from FigureManagerWx.destroy() - self.figmgr.frame = None + self.canvas.manager.frame = None # remove figure manager from Gcf.figs - Gcf.destroy(self.figmgr) + Gcf.destroy(self.canvas.manager) + try: # See issue 2941338. + self.canvas.mpl_disconnect(self.canvas.toolbar._id_drag) + except AttributeError: # If there's no toolbar. + pass # Carry on with close event propagation, frame & children destruction event.Skip() - def GetToolBar(self): - """Override wxFrame::GetToolBar as we don't have managed toolbar""" - return self.toolbar - - def Destroy(self, *args, **kwargs): - try: - self.canvas.mpl_disconnect(self.toolbar._id_drag) - # Rationale for line above: see issue 2941338. - except AttributeError: - pass # classic toolbar lacks the attribute - # The "if self" check avoids a "wrapped C/C++ object has been deleted" - # RuntimeError at exit with e.g. - # MPLBACKEND=wxagg python -c 'from pylab import *; plot()'. - if self and not self.IsBeingDeleted(): - super().Destroy(*args, **kwargs) - # self.toolbar.Destroy() should not be necessary if the close event - # is allowed to propagate. - return True - class FigureManagerWx(FigureManagerBase): """ @@ -1017,20 +999,25 @@ class FigureManagerWx(FigureManagerBase): def __init__(self, canvas, num, frame): _log.debug("%s - __init__()", type(self)) self.frame = self.window = frame - self._initializing = True super().__init__(canvas, num) - self._initializing = False - @property - def toolbar(self): - return self.frame.GetToolBar() + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + # docstring inherited + wxapp = wx.GetApp() or _create_wxapp() + frame = FigureFrameWx(num, figure, canvas_class=canvas_class) + manager = figure.canvas.manager + if mpl.is_interactive(): + manager.frame.Show() + figure.canvas.draw_idle() + return manager - @toolbar.setter - def toolbar(self, value): - # Never allow this, except that base class inits this to None before - # the frame is set up. - if not self._initializing: - raise AttributeError("can't set attribute") + @classmethod + def start_main_loop(cls): + if not wx.App.IsMainLoopRunning(): + wxapp = wx.GetApp() + if wxapp is not None: + wxapp.MainLoop() def show(self): # docstring inherited @@ -1062,9 +1049,9 @@ def set_window_title(self, title): def resize(self, width, height): # docstring inherited - self.canvas.SetInitialSize( - wx.Size(math.ceil(width), math.ceil(height))) - self.window.GetSizer().Fit(self.window) + # Directly using SetClientSize doesn't handle the toolbar on Windows. + self.window.SetSize(self.window.ClientToWindowSize(wx.Size( + math.ceil(width), math.ceil(height)))) def _load_bitmap(filename): @@ -1085,18 +1072,9 @@ def _set_frame_icon(frame): frame.SetIcons(bundle) -cursord = { - cursors.MOVE: wx.CURSOR_HAND, - cursors.HAND: wx.CURSOR_HAND, - cursors.POINTER: wx.CURSOR_ARROW, - cursors.SELECT_REGION: wx.CURSOR_CROSS, - cursors.WAIT: wx.CURSOR_WAIT, -} - - class NavigationToolbar2Wx(NavigationToolbar2, wx.ToolBar): - def __init__(self, canvas, coordinates=True): - wx.ToolBar.__init__(self, canvas.GetParent(), -1) + def __init__(self, canvas, coordinates=True, *, style=wx.TB_BOTTOM): + wx.ToolBar.__init__(self, canvas.GetParent(), -1, style=style) if 'wxMac' in wx.PlatformInfo: self.SetToolBitmapSize((24, 24)) @@ -1120,28 +1098,13 @@ def __init__(self, canvas, coordinates=True): self._coordinates = coordinates if self._coordinates: self.AddStretchableSpace() - self._label_text = wx.StaticText(self) + self._label_text = wx.StaticText(self, style=wx.ALIGN_RIGHT) self.AddControl(self._label_text) self.Realize() NavigationToolbar2.__init__(self, canvas) - self._prevZoomRect = None - # for now, use alternate zoom-rectangle drawing on all - # Macs. N.B. In future versions of wx it may be possible to - # detect Retina displays with window.GetContentScaleFactor() - # and/or dc.GetContentScaleFactor() - self._retinaFix = 'wxMac' in wx.PlatformInfo - - prevZoomRect = _api.deprecate_privatize_attribute("3.3") - retinaFix = _api.deprecate_privatize_attribute("3.3") - savedRetinaImage = _api.deprecate_privatize_attribute("3.3") - wxoverlay = _api.deprecate_privatize_attribute("3.3") - zoomAxes = _api.deprecate_privatize_attribute("3.3") - zoomStartX = _api.deprecate_privatize_attribute("3.3") - zoomStartY = _api.deprecate_privatize_attribute("3.3") - @staticmethod def _icon(name): """ @@ -1149,7 +1112,9 @@ def _icon(name): *name*, including the extension and relative to Matplotlib's "images" data directory. """ - image = np.array(PIL.Image.open(cbook._get_data_path("images", name))) + pilimg = PIL.Image.open(cbook._get_data_path("images", name)) + # ensure RGBA as wx BitMap expects RGBA format + image = np.array(pilimg.convert("RGBA")) try: dark = wx.SystemSettings.GetAppearance().IsDark() except AttributeError: # wxpython < 4.1 @@ -1167,33 +1132,33 @@ def _icon(name): return wx.Bitmap.FromBufferRGBA( image.shape[1], image.shape[0], image.tobytes()) - @_api.deprecated("3.4") - def get_canvas(self, frame, fig): - return type(self.canvas)(frame, -1, fig) + def _update_buttons_checked(self): + if "Pan" in self.wx_ids: + self.ToggleTool(self.wx_ids["Pan"], self.mode.name == "PAN") + if "Zoom" in self.wx_ids: + self.ToggleTool(self.wx_ids["Zoom"], self.mode.name == "ZOOM") def zoom(self, *args): - tool = self.wx_ids['Zoom'] - self.ToggleTool(tool, not self.GetToolState(tool)) super().zoom(*args) + self._update_buttons_checked() def pan(self, *args): - tool = self.wx_ids['Pan'] - self.ToggleTool(tool, not self.GetToolState(tool)) super().pan(*args) + self._update_buttons_checked() def save_figure(self, *args): # Fetch the required filename and file type. filetypes, exts, filter_index = self.canvas._get_imagesave_wildcards() default_file = self.canvas.get_default_filename() - dlg = wx.FileDialog( + dialog = wx.FileDialog( self.canvas.GetParent(), "Save to file", mpl.rcParams["savefig.directory"], default_file, filetypes, wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) - dlg.SetFilterIndex(filter_index) - if dlg.ShowModal() == wx.ID_OK: - path = pathlib.Path(dlg.GetPath()) + dialog.SetFilterIndex(filter_index) + if dialog.ShowModal() == wx.ID_OK: + path = pathlib.Path(dialog.GetPath()) _log.debug('%s - Save file path: %s', type(self), path) - fmt = exts[dlg.GetFilterIndex()] + fmt = exts[dialog.GetFilterIndex()] ext = path.suffix[1:] if ext in self.canvas.get_supported_filetypes() and fmt != ext: # looks like they forgot to set the image type drop @@ -1206,43 +1171,13 @@ def save_figure(self, *args): if mpl.rcParams["savefig.directory"]: mpl.rcParams["savefig.directory"] = str(path.parent) try: - self.canvas.figure.savefig(str(path), format=fmt) + self.canvas.figure.savefig(path, format=fmt) except Exception as e: - error_msg_wx(str(e)) - - def set_cursor(self, cursor): - cursor = wx.Cursor(cursord[cursor]) - self.canvas.SetCursor(cursor) - self.canvas.Update() - - def press_zoom(self, event): - super().press_zoom(event) - if self.mode.name == 'ZOOM': - if not self._retinaFix: - self._wxoverlay = wx.Overlay() - else: - if event.inaxes is not None: - self._savedRetinaImage = self.canvas.copy_from_bbox( - event.inaxes.bbox) - self._zoomStartX = event.xdata - self._zoomStartY = event.ydata - self._zoomAxes = event.inaxes - - def release_zoom(self, event): - super().release_zoom(event) - 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: - self._wxoverlay.Reset() - del self._wxoverlay - else: - del self._savedRetinaImage - if self._prevZoomRect: - self._prevZoomRect.pop(0).remove() - self._prevZoomRect = None - if self._zoomAxes: - self._zoomAxes = None + dialog = wx.MessageDialog( + parent=self.canvas.GetParent(), message=str(e), + caption='Matplotlib error') + dialog.ShowModal() + dialog.Destroy() def draw_rubberband(self, event, x0, y0, x1, y1): height = self.canvas.figure.bbox.height @@ -1266,29 +1201,16 @@ def set_history_buttons(self): self.EnableTool(self.wx_ids['Forward'], can_forward) -@_api.deprecated("3.3") -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. - """ - - def __init__(self, parent, *args, **kwargs): - super().__init__(parent, -1) - self.SetFieldsCount(2) - - def set_function(self, string): - self.SetStatusText("%s" % string, 1) - - # tools for matplotlib.backend_managers.ToolManager: class ToolbarWx(ToolContainerBase, wx.ToolBar): - def __init__(self, toolmanager, parent, style=wx.TB_HORIZONTAL): + def __init__(self, toolmanager, parent=None, style=wx.TB_BOTTOM): + if parent is None: + parent = toolmanager.canvas.GetParent() ToolContainerBase.__init__(self, toolmanager) wx.ToolBar.__init__(self, parent, -1, style=style) self._space = self.AddStretchableSpace() - self._label_text = wx.StaticText(self) + self._label_text = wx.StaticText(self, style=wx.ALIGN_RIGHT) self.AddControl(self._label_text) self._toolitems = {} self._groups = {} # Mapping of groups to the separator after them. @@ -1368,37 +1290,20 @@ def set_message(self, s): self._label_text.SetLabel(s) -@_api.deprecated("3.3") -class StatusbarWx(StatusbarBase, wx.StatusBar): - """For use with ToolManager.""" - def __init__(self, parent, *args, **kwargs): - StatusbarBase.__init__(self, *args, **kwargs) - wx.StatusBar.__init__(self, parent, -1) - self.SetFieldsCount(1) - self.SetStatusText("") - - def set_message(self, s): - self.SetStatusText(s) - - +@backend_tools._register_tool_class(_FigureCanvasWxBase) class ConfigureSubplotsWx(backend_tools.ConfigureSubplotsBase): def trigger(self, *args): - NavigationToolbar2Wx.configure_subplots( - self._make_classic_style_pseudo_toolbar()) + NavigationToolbar2Wx.configure_subplots(self) +@backend_tools._register_tool_class(_FigureCanvasWxBase) class SaveFigureWx(backend_tools.SaveFigureBase): def trigger(self, *args): NavigationToolbar2Wx.save_figure( self._make_classic_style_pseudo_toolbar()) -class SetCursorWx(backend_tools.SetCursorBase): - def set_cursor(self, cursor): - NavigationToolbar2Wx.set_cursor( - self._make_classic_style_pseudo_toolbar(), cursor) - - +@backend_tools._register_tool_class(_FigureCanvasWxBase) class RubberbandWx(backend_tools.RubberbandBase): def draw_rubberband(self, x0, y0, x1, y1): NavigationToolbar2Wx.draw_rubberband( @@ -1431,15 +1336,15 @@ def __init__(self, parent, help_entries): grid_sizer.Add(label, 0, 0, 0) # finalize layout, create button sizer.Add(grid_sizer, 0, wx.ALL, 6) - OK = wx.Button(self, wx.ID_OK) - sizer.Add(OK, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 8) + ok = wx.Button(self, wx.ID_OK) + sizer.Add(ok, 0, wx.ALIGN_CENTER_HORIZONTAL | wx.ALL, 8) self.SetSizer(sizer) sizer.Fit(self) self.Layout() - self.Bind(wx.EVT_CLOSE, self.OnClose) - OK.Bind(wx.EVT_BUTTON, self.OnClose) + self.Bind(wx.EVT_CLOSE, self._on_close) + ok.Bind(wx.EVT_BUTTON, self._on_close) - def OnClose(self, event): + def _on_close(self, event): _HelpDialog._instance = None # remove global reference self.DestroyLater() event.Skip() @@ -1454,12 +1359,14 @@ def show(cls, parent, help_entries): cls._instance.Show() +@backend_tools._register_tool_class(_FigureCanvasWxBase) class HelpWx(backend_tools.ToolHelpBase): def trigger(self, *args): _HelpDialog.show(self.figure.canvas.GetTopLevelParent(), self._get_help_entries()) +@backend_tools._register_tool_class(_FigureCanvasWxBase) class ToolCopyToClipboardWx(backend_tools.ToolCopyToClipboardBase): def trigger(self, *args, **kwargs): if not self.canvas._isDrawn: @@ -1472,45 +1379,12 @@ def trigger(self, *args, **kwargs): wx.TheClipboard.Close() -backend_tools.ToolSaveFigure = SaveFigureWx -backend_tools.ToolConfigureSubplots = ConfigureSubplotsWx -backend_tools.ToolSetCursor = SetCursorWx -backend_tools.ToolRubberband = RubberbandWx -backend_tools.ToolHelp = HelpWx -backend_tools.ToolCopyToClipboard = ToolCopyToClipboardWx +FigureManagerWx._toolbar2_class = NavigationToolbar2Wx +FigureManagerWx._toolmanager_toolbar_class = ToolbarWx @_Backend.export class _BackendWx(_Backend): FigureCanvas = FigureCanvasWx FigureManager = FigureManagerWx - _frame_class = FigureFrameWx - - @classmethod - def new_figure_manager(cls, num, *args, **kwargs): - # Create a wx.App instance if it has not been created so far. - wxapp = wx.GetApp() - if wxapp is None: - wxapp = wx.App(False) - wxapp.SetExitOnFrameDelete(True) - cbook._setup_new_guiapp() - # Retain a reference to the app object so that it does not get - # garbage collected. - _BackendWx._theWxApp = wxapp - return super().new_figure_manager(num, *args, **kwargs) - - @classmethod - def new_figure_manager_given_figure(cls, num, figure): - frame = cls._frame_class(num, figure) - figmgr = frame.get_figure_manager() - if mpl.is_interactive(): - figmgr.frame.Show() - figure.canvas.draw_idle() - return figmgr - - @staticmethod - def mainloop(): - if not wx.App.IsMainLoopRunning(): - wxapp = wx.GetApp() - if wxapp is not None: - wxapp.MainLoop() + mainloop = FigureManagerWx.start_main_loop diff --git a/lib/matplotlib/backends/backend_wxagg.py b/lib/matplotlib/backends/backend_wxagg.py index 106578e7e14b..ca7f91583766 100644 --- a/lib/matplotlib/backends/backend_wxagg.py +++ b/lib/matplotlib/backends/backend_wxagg.py @@ -1,11 +1,14 @@ import wx +from .. import _api from .backend_agg import FigureCanvasAgg -from .backend_wx import ( - _BackendWx, _FigureCanvasWxBase, FigureFrameWx, +from .backend_wx import _BackendWx, _FigureCanvasWxBase, FigureFrameWx +from .backend_wx import ( # noqa: F401 # pylint: disable=W0611 NavigationToolbar2Wx as NavigationToolbar2WxAgg) +@_api.deprecated( + "3.6", alternative="FigureFrameWx(..., canvas_class=FigureCanvasWxAgg)") class FigureFrameWxAgg(FigureFrameWx): def get_canvas(self, fig): return FigureCanvasWxAgg(self, -1, fig) @@ -27,66 +30,32 @@ def draw(self, drawDC=None): Render the figure using agg. """ FigureCanvasAgg.draw(self) - - self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None) + self.bitmap = _rgba_to_wx_bitmap(self.get_renderer().buffer_rgba()) self._isDrawn = True self.gui_repaint(drawDC=drawDC) def blit(self, bbox=None): # docstring inherited + bitmap = _rgba_to_wx_bitmap(self.get_renderer().buffer_rgba()) if bbox is None: - self.bitmap = _convert_agg_to_wx_bitmap(self.get_renderer(), None) - self.gui_repaint() - return - - srcBmp = _convert_agg_to_wx_bitmap(self.get_renderer(), None) - srcDC = wx.MemoryDC() - srcDC.SelectObject(srcBmp) - - destDC = wx.MemoryDC() - destDC.SelectObject(self.bitmap) - - x = int(bbox.x0) - y = int(self.bitmap.GetHeight() - bbox.y1) - destDC.Blit(x, y, int(bbox.width), int(bbox.height), srcDC, x, y) - - destDC.SelectObject(wx.NullBitmap) - srcDC.SelectObject(wx.NullBitmap) + self.bitmap = bitmap + else: + srcDC = wx.MemoryDC(bitmap) + destDC = wx.MemoryDC(self.bitmap) + x = int(bbox.x0) + y = int(self.bitmap.GetHeight() - bbox.y1) + destDC.Blit(x, y, int(bbox.width), int(bbox.height), srcDC, x, y) + destDC.SelectObject(wx.NullBitmap) + srcDC.SelectObject(wx.NullBitmap) self.gui_repaint() -def _convert_agg_to_wx_bitmap(agg, bbox): - """ - Convert the region of the agg buffer bounded by bbox to a wx.Bitmap. If - bbox is None, the entire buffer is converted. - Note: agg must be a backend_agg.RendererAgg instance. - """ - if bbox is None: - # agg => rgba buffer -> bitmap - return wx.Bitmap.FromBufferRGBA(int(agg.width), int(agg.height), - agg.buffer_rgba()) - else: - # agg => rgba buffer -> bitmap => clipped bitmap - srcBmp = wx.Bitmap.FromBufferRGBA(int(agg.width), int(agg.height), - agg.buffer_rgba()) - srcDC = wx.MemoryDC() - srcDC.SelectObject(srcBmp) - - destBmp = wx.Bitmap(int(bbox.width), int(bbox.height)) - destDC = wx.MemoryDC() - destDC.SelectObject(destBmp) - - x = int(bbox.x0) - y = int(int(agg.height) - bbox.y1) - destDC.Blit(0, 0, int(bbox.width), int(bbox.height), srcDC, x, y) - - srcDC.SelectObject(wx.NullBitmap) - destDC.SelectObject(wx.NullBitmap) - - return destBmp +def _rgba_to_wx_bitmap(rgba): + """Convert an RGBA buffer to a wx.Bitmap.""" + h, w, _ = rgba.shape + return wx.Bitmap.FromBufferRGBA(w, h, rgba) @_BackendWx.export class _BackendWxAgg(_BackendWx): FigureCanvas = FigureCanvasWxAgg - _frame_class = FigureFrameWxAgg diff --git a/lib/matplotlib/backends/backend_wxcairo.py b/lib/matplotlib/backends/backend_wxcairo.py index 6cb0b9d68414..0416a187d091 100644 --- a/lib/matplotlib/backends/backend_wxcairo.py +++ b/lib/matplotlib/backends/backend_wxcairo.py @@ -1,17 +1,20 @@ import wx.lib.wxcairo as wxcairo -from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo -from .backend_wx import ( - _BackendWx, _FigureCanvasWxBase, FigureFrameWx, +from .. import _api +from .backend_cairo import cairo, FigureCanvasCairo +from .backend_wx import _BackendWx, _FigureCanvasWxBase, FigureFrameWx +from .backend_wx import ( # noqa: F401 # pylint: disable=W0611 NavigationToolbar2Wx as NavigationToolbar2WxCairo) +@_api.deprecated( + "3.6", alternative="FigureFrameWx(..., canvas_class=FigureCanvasWxCairo)") class FigureFrameWxCairo(FigureFrameWx): def get_canvas(self, fig): return FigureCanvasWxCairo(self, -1, fig) -class FigureCanvasWxCairo(_FigureCanvasWxBase, FigureCanvasCairo): +class FigureCanvasWxCairo(FigureCanvasCairo, _FigureCanvasWxBase): """ The FigureCanvas contains the figure and does event handling. @@ -21,20 +24,11 @@ class FigureCanvasWxCairo(_FigureCanvasWxBase, FigureCanvasCairo): we give a hint as to our preferred minimum size. """ - def __init__(self, parent, id, figure): - # _FigureCanvasWxBase should be fixed to have the same signature as - # every other FigureCanvas and use cooperative inheritance, but in the - # meantime the following will make do. - _FigureCanvasWxBase.__init__(self, parent, id, figure) - FigureCanvasCairo.__init__(self, figure) - self._renderer = RendererCairo(self.figure.dpi) - def draw(self, drawDC=None): - width = int(self.figure.bbox.width) - height = int(self.figure.bbox.height) - surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) - self._renderer.set_ctx_from_surface(surface) - self._renderer.set_width_height(width, height) + size = self.figure.bbox.size.astype(int) + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, *size) + self._renderer.set_context(cairo.Context(surface)) + self._renderer.dpi = self.figure.dpi self.figure.draw(self._renderer) self.bitmap = wxcairo.BitmapFromImageSurface(surface) self._isDrawn = True @@ -44,4 +38,3 @@ def draw(self, drawDC=None): @_BackendWx.export class _BackendWxCairo(_BackendWx): FigureCanvas = FigureCanvasWxCairo - _frame_class = FigureFrameWxCairo diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index b741dc42466e..663671894a74 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -2,173 +2,129 @@ Qt binding and backend selector. The selection logic is as follows: -- if any of PyQt5, PySide2, PyQt4 or PySide have already been imported - (checked in that order), use it; +- if any of PyQt6, PySide6, PyQt5, or PySide2 have already been + imported (checked in that order), use it; - otherwise, if the QT_API environment variable (used by Enthought) is set, use - it to determine which binding to use (but do not change the backend based on - it; i.e. if the Qt5Agg backend is requested but QT_API is set to "pyqt4", - then actually use Qt5 with PyQt5 or PySide2 (whichever can be imported); + it to determine which binding to use; - otherwise, use whatever the rcParams indicate. - -Support for PyQt4 is deprecated. """ -from distutils.version import LooseVersion +import functools +import operator import os import platform import sys +import signal +import socket +import contextlib + +from packaging.version import parse as parse_version import matplotlib as mpl -from matplotlib import _api +from . import _QT_FORCE_QT5_BINDING +QT_API_PYQT6 = "PyQt6" +QT_API_PYSIDE6 = "PySide6" QT_API_PYQT5 = "PyQt5" QT_API_PYSIDE2 = "PySide2" -QT_API_PYQTv2 = "PyQt4v2" -QT_API_PYSIDE = "PySide" -QT_API_PYQT = "PyQt4" # Use the old sip v1 API (Py3 defaults to v2). QT_API_ENV = os.environ.get("QT_API") if QT_API_ENV is not None: QT_API_ENV = QT_API_ENV.lower() -# Mapping of QT_API_ENV to requested binding. ETS does not support PyQt4v1. -# (https://github.com/enthought/pyface/blob/master/pyface/qt/__init__.py) -_ETS = {"pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2, - "pyqt": QT_API_PYQTv2, "pyside": QT_API_PYSIDE, - None: None} +_ETS = { # Mapping of QT_API_ENV to requested binding. + "pyqt6": QT_API_PYQT6, "pyside6": QT_API_PYSIDE6, + "pyqt5": QT_API_PYQT5, "pyside2": QT_API_PYSIDE2, +} # First, check if anything is already imported. -if "PyQt5.QtCore" in sys.modules: +if sys.modules.get("PyQt6.QtCore"): + QT_API = QT_API_PYQT6 +elif sys.modules.get("PySide6.QtCore"): + QT_API = QT_API_PYSIDE6 +elif sys.modules.get("PyQt5.QtCore"): QT_API = QT_API_PYQT5 -elif "PySide2.QtCore" in sys.modules: +elif sys.modules.get("PySide2.QtCore"): QT_API = QT_API_PYSIDE2 -elif "PyQt4.QtCore" in sys.modules: - QT_API = QT_API_PYQTv2 -elif "PySide.QtCore" in sys.modules: - QT_API = QT_API_PYSIDE # Otherwise, check the QT_API environment variable (from Enthought). This can # only override the binding, not the backend (in other words, we check that the -# requested backend actually matches). Use dict.__getitem__ to avoid +# requested backend actually matches). Use _get_backend_or_none to avoid # triggering backend resolution (which can result in a partially but # incompletely imported backend_qt5). -elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt5Agg", "Qt5Cairo"]: +elif (mpl.rcParams._get_backend_or_none() or "").lower().startswith("qt5"): if QT_API_ENV in ["pyqt5", "pyside2"]: QT_API = _ETS[QT_API_ENV] else: - QT_API = None -elif dict.__getitem__(mpl.rcParams, "backend") in ["Qt4Agg", "Qt4Cairo"]: - if QT_API_ENV in ["pyqt4", "pyside"]: - QT_API = _ETS[QT_API_ENV] - else: + _QT_FORCE_QT5_BINDING = True # noqa QT_API = None # A non-Qt backend was selected but we still got there (possible, e.g., when # fully manually embedding Matplotlib in a Qt app without using pyplot). +elif QT_API_ENV is None: + QT_API = None +elif QT_API_ENV in _ETS: + QT_API = _ETS[QT_API_ENV] else: - try: - QT_API = _ETS[QT_API_ENV] - except KeyError as err: - raise RuntimeError( - "The environment variable QT_API has the unrecognized value {!r};" - "valid values are 'pyqt5', 'pyside2', 'pyqt', and " - "'pyside'") from err + raise RuntimeError( + "The environment variable QT_API has the unrecognized value {!r}; " + "valid values are {}".format(QT_API_ENV, ", ".join(_ETS))) -def _setup_pyqt5(): - global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \ - _isdeleted, _getSaveFileName +def _setup_pyqt5plus(): + global QtCore, QtGui, QtWidgets, __version__ + global _getSaveFileName, _isdeleted, _to_int - if QT_API == QT_API_PYQT5: - from PyQt5 import QtCore, QtGui, QtWidgets - import sip + if QT_API == QT_API_PYQT6: + from PyQt6 import QtCore, QtGui, QtWidgets, sip __version__ = QtCore.PYQT_VERSION_STR QtCore.Signal = QtCore.pyqtSignal QtCore.Slot = QtCore.pyqtSlot QtCore.Property = QtCore.pyqtProperty _isdeleted = sip.isdeleted - elif QT_API == QT_API_PYSIDE2: - from PySide2 import QtCore, QtGui, QtWidgets, __version__ - import shiboken2 - def _isdeleted(obj): return not shiboken2.isValid(obj) - else: - raise ValueError("Unexpected value for the 'backend.qt5' rcparam") - _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName - - @_api.deprecated("3.3", alternative="QtCore.qVersion()") - def is_pyqt5(): - return True - - -def _setup_pyqt4(): - global QtCore, QtGui, QtWidgets, __version__, is_pyqt5, \ - _isdeleted, _getSaveFileName - - def _setup_pyqt4_internal(api): - global QtCore, QtGui, QtWidgets, \ - __version__, is_pyqt5, _isdeleted, _getSaveFileName - # List of incompatible APIs: - # http://pyqt.sourceforge.net/Docs/PyQt4/incompatible_apis.html - _sip_apis = ["QDate", "QDateTime", "QString", "QTextStream", "QTime", - "QUrl", "QVariant"] - try: - import sip - except ImportError: - pass + _to_int = operator.attrgetter('value') + elif QT_API == QT_API_PYSIDE6: + from PySide6 import QtCore, QtGui, QtWidgets, __version__ + import shiboken6 + def _isdeleted(obj): return not shiboken6.isValid(obj) + if parse_version(__version__) >= parse_version('6.4'): + _to_int = operator.attrgetter('value') else: - for _sip_api in _sip_apis: - try: - sip.setapi(_sip_api, api) - except (AttributeError, ValueError): - pass - from PyQt4 import QtCore, QtGui - import sip # Always succeeds *after* importing PyQt4. + _to_int = int + elif QT_API == QT_API_PYQT5: + from PyQt5 import QtCore, QtGui, QtWidgets + import sip __version__ = QtCore.PYQT_VERSION_STR - # PyQt 4.6 introduced getSaveFileNameAndFilter: - # https://riverbankcomputing.com/news/pyqt-46 - if __version__ < LooseVersion("4.6"): - raise ImportError("PyQt<4.6 is not supported") QtCore.Signal = QtCore.pyqtSignal QtCore.Slot = QtCore.pyqtSlot QtCore.Property = QtCore.pyqtProperty _isdeleted = sip.isdeleted - _getSaveFileName = QtGui.QFileDialog.getSaveFileNameAndFilter - - if QT_API == QT_API_PYQTv2: - _setup_pyqt4_internal(api=2) - elif QT_API == QT_API_PYSIDE: - from PySide import QtCore, QtGui, __version__, __version_info__ - import shiboken - # PySide 1.0.3 fixed the following: - # https://srinikom.github.io/pyside-bz-archive/809.html - if __version_info__ < (1, 0, 3): - raise ImportError("PySide<1.0.3 is not supported") - def _isdeleted(obj): return not shiboken.isValid(obj) - _getSaveFileName = QtGui.QFileDialog.getSaveFileName - elif QT_API == QT_API_PYQT: - _setup_pyqt4_internal(api=1) + _to_int = int + elif QT_API == QT_API_PYSIDE2: + from PySide2 import QtCore, QtGui, QtWidgets, __version__ + try: + from PySide2 import shiboken2 + except ImportError: + import shiboken2 + def _isdeleted(obj): + return not shiboken2.isValid(obj) + _to_int = int else: - raise ValueError("Unexpected value for the 'backend.qt4' rcparam") - QtWidgets = QtGui - - @_api.deprecated("3.3", alternative="QtCore.qVersion()") - def is_pyqt5(): - return False + raise AssertionError(f"Unexpected QT_API: {QT_API}") + _getSaveFileName = QtWidgets.QFileDialog.getSaveFileName -if QT_API in [QT_API_PYQT5, QT_API_PYSIDE2]: - _setup_pyqt5() -elif QT_API in [QT_API_PYQTv2, QT_API_PYSIDE, QT_API_PYQT]: - _setup_pyqt4() +if QT_API in [QT_API_PYQT6, QT_API_PYQT5, QT_API_PYSIDE6, QT_API_PYSIDE2]: + _setup_pyqt5plus() elif QT_API is None: # See above re: dict.__getitem__. - if dict.__getitem__(mpl.rcParams, "backend") == "Qt4Agg": - _candidates = [(_setup_pyqt4, QT_API_PYQTv2), - (_setup_pyqt4, QT_API_PYSIDE), - (_setup_pyqt4, QT_API_PYQT), - (_setup_pyqt5, QT_API_PYQT5), - (_setup_pyqt5, QT_API_PYSIDE2)] + if _QT_FORCE_QT5_BINDING: + _candidates = [ + (_setup_pyqt5plus, QT_API_PYQT5), + (_setup_pyqt5plus, QT_API_PYSIDE2), + ] else: - _candidates = [(_setup_pyqt5, QT_API_PYQT5), - (_setup_pyqt5, QT_API_PYSIDE2), - (_setup_pyqt4, QT_API_PYQTv2), - (_setup_pyqt4, QT_API_PYSIDE), - (_setup_pyqt4, QT_API_PYQT)] + _candidates = [ + (_setup_pyqt5plus, QT_API_PYQT6), + (_setup_pyqt5plus, QT_API_PYSIDE6), + (_setup_pyqt5plus, QT_API_PYQT5), + (_setup_pyqt5plus, QT_API_PYSIDE2), + ] for _setup, QT_API in _candidates: try: _setup() @@ -176,55 +132,114 @@ def is_pyqt5(): continue break else: - raise ImportError("Failed to import any qt binding") + raise ImportError( + "Failed to import any of the following Qt binding modules: {}" + .format(", ".join(_ETS.values()))) else: # We should not get there. - raise AssertionError("Unexpected QT_API: {}".format(QT_API)) + raise AssertionError(f"Unexpected QT_API: {QT_API}") +_version_info = tuple(QtCore.QLibraryInfo.version().segments()) + + +if _version_info < (5, 10): + raise ImportError( + f"The Qt version imported is " + f"{QtCore.QLibraryInfo.version().toString()} but Matplotlib requires " + f"Qt>=5.10") # Fixes issues with Big Sur # https://bugreports.qt.io/browse/QTBUG-87014, fixed in qt 5.15.2 if (sys.platform == 'darwin' and - LooseVersion(platform.mac_ver()[0]) >= LooseVersion("10.16") and - LooseVersion(QtCore.qVersion()) < LooseVersion("5.15.2") and - "QT_MAC_WANTS_LAYER" not in os.environ): - os.environ["QT_MAC_WANTS_LAYER"] = "1" + parse_version(platform.mac_ver()[0]) >= parse_version("10.16") and + _version_info < (5, 15, 2)): + os.environ.setdefault("QT_MAC_WANTS_LAYER", "1") -# These globals are only defined for backcompatibility purposes. -ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4), - pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5)) +# PyQt6 enum compat helpers. -QT_RC_MAJOR_VERSION = int(QtCore.qVersion().split(".")[0]) -if QT_RC_MAJOR_VERSION == 4: - _api.warn_deprecated("3.3", name="support for Qt4") +@functools.lru_cache(None) +def _enum(name): + # foo.bar.Enum.Entry (PyQt6) <=> foo.bar.Entry (non-PyQt6). + return operator.attrgetter( + name if QT_API == 'PyQt6' else name.rpartition(".")[0] + )(sys.modules[QtCore.__package__]) -def _devicePixelRatioF(obj): - """ - Return obj.devicePixelRatioF() with graceful fallback for older Qt. +# Backports. - This can be replaced by the direct call when we require Qt>=5.6. - """ - try: - # Not available on Qt<5.6 - return obj.devicePixelRatioF() or 1 - except AttributeError: - pass - try: - # Not available on Qt4 or some older Qt5. - # self.devicePixelRatio() returns 0 in rare cases - return obj.devicePixelRatio() or 1 - except AttributeError: - return 1 +def _exec(obj): + # exec on PyQt6, exec_ elsewhere. + obj.exec() if hasattr(obj, "exec") else obj.exec_() -def _setDevicePixelRatio(obj, val): - """ - Call obj.setDevicePixelRatio(val) with graceful fallback for older Qt. - This can be replaced by the direct call when we require Qt>=5.6. +@contextlib.contextmanager +def _maybe_allow_interrupt(qapp): + """ + This manager allows to terminate a plot by sending a SIGINT. It is + necessary because the running Qt backend prevents Python interpreter to + run and process signals (i.e., to raise KeyboardInterrupt exception). To + solve this one needs to somehow wake up the interpreter and make it close + the plot window. We do this by using the signal.set_wakeup_fd() function + which organizes a write of the signal number into a socketpair connected + to the QSocketNotifier (since it is part of the Qt backend, it can react + to that write event). Afterwards, the Qt handler empties the socketpair + by a recv() command to re-arm it (we need this if a signal different from + SIGINT was caught by set_wakeup_fd() and we shall continue waiting). If + the SIGINT was caught indeed, after exiting the on_signal() function the + interpreter reacts to the SIGINT according to the handle() function which + had been set up by a signal.signal() call: it causes the qt_object to + exit by calling its quit() method. Finally, we call the old SIGINT + handler with the same arguments that were given to our custom handle() + handler. + + We do this only if the old handler for SIGINT was not None, which means + that a non-python handler was installed, i.e. in Julia, and not SIG_IGN + which means we should ignore the interrupts. """ - if hasattr(obj, 'setDevicePixelRatio'): - # Not available on Qt4 or some older Qt5. - obj.setDevicePixelRatio(val) + old_sigint_handler = signal.getsignal(signal.SIGINT) + handler_args = None + skip = False + if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): + skip = True + else: + wsock, rsock = socket.socketpair() + wsock.setblocking(False) + old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno()) + sn = QtCore.QSocketNotifier( + rsock.fileno(), _enum('QtCore.QSocketNotifier.Type').Read + ) + + # We do not actually care about this value other than running some + # Python code to ensure that the interpreter has a chance to handle the + # signal in Python land. We also need to drain the socket because it + # will be written to as part of the wakeup! There are some cases where + # this may fire too soon / more than once on Windows so we should be + # forgiving about reading an empty socket. + rsock.setblocking(False) + # Clear the socket to re-arm the notifier. + @sn.activated.connect + def _may_clear_sock(*args): + try: + rsock.recv(1) + except BlockingIOError: + pass + + def handle(*args): + nonlocal handler_args + handler_args = args + qapp.quit() + + signal.signal(signal.SIGINT, handle) + try: + yield + finally: + if not skip: + wsock.close() + rsock.close() + sn.setEnabled(False) + signal.set_wakeup_fd(old_wakeup_fd) + signal.signal(signal.SIGINT, old_sigint_handler) + if handler_args is not None: + old_sigint_handler(*handler_args) diff --git a/lib/matplotlib/backends/qt_editor/_formlayout.py b/lib/matplotlib/backends/qt_editor/_formlayout.py index e2b371f1652d..1306e0c02fa6 100644 --- a/lib/matplotlib/backends/qt_editor/_formlayout.py +++ b/lib/matplotlib/backends/qt_editor/_formlayout.py @@ -47,7 +47,8 @@ from numbers import Integral, Real from matplotlib import _api, colors as mcolors -from matplotlib.backends.qt_compat import QtGui, QtWidgets, QtCore +from matplotlib.backends.qt_compat import ( + QtGui, QtWidgets, QtCore, _enum, _to_int) _log = logging.getLogger(__name__) @@ -70,7 +71,7 @@ def __init__(self, parent=None): def choose_color(self): color = QtWidgets.QColorDialog.getColor( self._color, self.parentWidget(), "", - QtWidgets.QColorDialog.ShowAlphaChannel) + _enum("QtWidgets.QColorDialog.ColorDialogOption").ShowAlphaChannel) if color.isValid(): self.set_color(color) @@ -203,8 +204,7 @@ def get_font(self): def is_edit_valid(edit): text = edit.text() state = edit.validator().validate(text, 0)[0] - - return state == QtGui.QDoubleValidator.Acceptable + return state == _enum("QtGui.QDoubleValidator.State").Acceptable class FormWidget(QtWidgets.QWidget): @@ -291,10 +291,7 @@ def setup(self): field.setCurrentIndex(selindex) elif isinstance(value, bool): field = QtWidgets.QCheckBox(self) - if value: - field.setCheckState(QtCore.Qt.Checked) - else: - field.setCheckState(QtCore.Qt.Unchecked) + field.setChecked(value) elif isinstance(value, Integral): field = QtWidgets.QSpinBox(self) field.setRange(-10**9, 10**9) @@ -336,15 +333,23 @@ def get(self): else: value = value[index] elif isinstance(value, bool): - value = field.checkState() == QtCore.Qt.Checked + value = field.isChecked() elif isinstance(value, Integral): value = int(field.value()) elif isinstance(value, Real): value = float(str(field.text())) elif isinstance(value, datetime.datetime): - value = field.dateTime().toPyDateTime() + datetime_ = field.dateTime() + if hasattr(datetime_, "toPyDateTime"): + value = datetime_.toPyDateTime() + else: + value = datetime_.toPython() elif isinstance(value, datetime.date): - value = field.date().toPyDate() + date_ = field.date() + if hasattr(date_, "toPyDate"): + value = date_.toPyDate() + else: + value = date_.toPython() else: value = eval(str(field.text())) valuelist.append(value) @@ -436,10 +441,16 @@ def __init__(self, data, title="", comment="", # Button box self.bbox = bbox = QtWidgets.QDialogButtonBox( - QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel) + QtWidgets.QDialogButtonBox.StandardButton( + _to_int( + _enum("QtWidgets.QDialogButtonBox.StandardButton").Ok) | + _to_int( + _enum("QtWidgets.QDialogButtonBox.StandardButton").Cancel) + )) self.formwidget.update_buttons.connect(self.update_buttons) if self.apply_callback is not None: - apply_btn = bbox.addButton(QtWidgets.QDialogButtonBox.Apply) + apply_btn = bbox.addButton( + _enum("QtWidgets.QDialogButtonBox.StandardButton").Apply) apply_btn.clicked.connect(self.apply) bbox.accepted.connect(self.accept) @@ -462,14 +473,16 @@ def update_buttons(self): for field in self.float_fields: if not is_edit_valid(field): valid = False - for btn_type in (QtWidgets.QDialogButtonBox.Ok, - QtWidgets.QDialogButtonBox.Apply): - btn = self.bbox.button(btn_type) + for btn_type in ["Ok", "Apply"]: + btn = self.bbox.button( + getattr(_enum("QtWidgets.QDialogButtonBox.StandardButton"), + btn_type)) if btn is not None: btn.setEnabled(valid) def accept(self): self.data = self.formwidget.get() + self.apply_callback(self.data) super().accept() def reject(self): @@ -486,8 +499,7 @@ def get(self): def fedit(data, title="", comment="", icon=None, parent=None, apply=None): """ - Create form dialog and return result - (if Cancel button is pressed, return None) + Create form dialog data: datalist, datagroup title: str @@ -505,7 +517,7 @@ def fedit(data, title="", comment="", icon=None, parent=None, apply=None): box) for each member of a datagroup inside a datagroup Supported types for field_value: - - int, float, str, unicode, bool + - int, float, str, bool - colors: in Qt-compatible text form, i.e. in hex format or name (red, ...) (automatically detected from a string) - list/tuple: @@ -518,12 +530,19 @@ def fedit(data, title="", comment="", icon=None, parent=None, apply=None): if QtWidgets.QApplication.startingUp(): _app = QtWidgets.QApplication([]) dialog = FormDialog(data, title, comment, icon, parent, apply) - if dialog.exec_(): - return dialog.get() + + if parent is not None: + if hasattr(parent, "_fedit_dialog"): + parent._fedit_dialog.close() + parent._fedit_dialog = dialog + + dialog.show() if __name__ == "__main__": + _app = QtWidgets.QApplication([]) + def create_datalist_example(): return [('str', 'this is a string'), ('list', [0, '1', '3', '4']), @@ -546,23 +565,29 @@ def create_datagroup_example(): (datalist, "Category 2", "Category 2 comment"), (datalist, "Category 3", "Category 3 comment")) - #--------- datalist example + # --------- datalist example datalist = create_datalist_example() def apply_test(data): print("data:", data) - print("result:", fedit(datalist, title="Example", - comment="This is just an example.", - apply=apply_test)) + fedit(datalist, title="Example", + comment="This is just an example.", + apply=apply_test) + + _app.exec() - #--------- datagroup example + # --------- datagroup example datagroup = create_datagroup_example() - print("result:", fedit(datagroup, "Global title")) + fedit(datagroup, "Global title", + apply=apply_test) + _app.exec() - #--------- datagroup inside a datagroup example + # --------- datagroup inside a datagroup example datalist = create_datalist_example() datagroup = create_datagroup_example() - print("result:", fedit(((datagroup, "Title 1", "Tab 1 comment"), - (datalist, "Title 2", "Tab 2 comment"), - (datalist, "Title 3", "Tab 3 comment")), - "Global title")) + fedit(((datagroup, "Title 1", "Tab 1 comment"), + (datalist, "Title 2", "Tab 2 comment"), + (datalist, "Title 3", "Tab 3 comment")), + "Global title", + apply=apply_test) + _app.exec() diff --git a/lib/matplotlib/backends/qt_editor/_formsubplottool.py b/lib/matplotlib/backends/qt_editor/_formsubplottool.py deleted file mode 100644 index d8d0af0107b4..000000000000 --- a/lib/matplotlib/backends/qt_editor/_formsubplottool.py +++ /dev/null @@ -1,40 +0,0 @@ -from matplotlib.backends.qt_compat import QtWidgets - - -class UiSubplotTool(QtWidgets.QDialog): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setObjectName("SubplotTool") - self._widgets = {} - - main_layout = QtWidgets.QHBoxLayout() - self.setLayout(main_layout) - - for group, spinboxes, buttons in [ - ("Borders", - ["top", "bottom", "left", "right"], ["Export values"]), - ("Spacings", - ["hspace", "wspace"], ["Tight layout", "Reset", "Close"]), - ]: - layout = QtWidgets.QVBoxLayout() - main_layout.addLayout(layout) - box = QtWidgets.QGroupBox(group) - layout.addWidget(box) - inner = QtWidgets.QFormLayout(box) - for name in spinboxes: - self._widgets[name] = widget = QtWidgets.QDoubleSpinBox() - widget.setMinimum(0) - widget.setMaximum(1) - widget.setDecimals(3) - widget.setSingleStep(0.005) - widget.setKeyboardTracking(False) - inner.addRow(name, widget) - layout.addStretch(1) - for name in buttons: - self._widgets[name] = widget = QtWidgets.QPushButton(name) - # Don't trigger on , which is used to input values. - widget.setAutoDefault(False) - layout.addWidget(widget) - - self._widgets["Close"].setFocus() diff --git a/lib/matplotlib/backends/qt_editor/figureoptions.py b/lib/matplotlib/backends/qt_editor/figureoptions.py index 98cf3c610773..2a9510980106 100644 --- a/lib/matplotlib/backends/qt_editor/figureoptions.py +++ b/lib/matplotlib/backends/qt_editor/figureoptions.py @@ -5,12 +5,11 @@ """Module that provides a GUI-based editor for Matplotlib's figure options.""" -import re - +from itertools import chain from matplotlib import cbook, cm, colors as mcolors, markers, image as mimage from matplotlib.backends.qt_compat import QtGui from matplotlib.backends.qt_editor import _formlayout - +from matplotlib.dates import DateConverter, num2date LINESTYLES = {'-': 'Solid', '--': 'Dashed', @@ -33,45 +32,55 @@ def figure_edit(axes, parent=None): sep = (None, None) # separator # Get / General - # Cast to builtin floats as they have nicer reprs. - xmin, xmax = map(float, axes.get_xlim()) - ymin, ymax = map(float, axes.get_ylim()) - general = [('Title', axes.get_title()), - sep, - (None, "X-Axis"), - ('Left', xmin), ('Right', xmax), - ('Label', axes.get_xlabel()), - ('Scale', [axes.get_xscale(), 'linear', 'log', 'logit']), - sep, - (None, "Y-Axis"), - ('Bottom', ymin), ('Top', ymax), - ('Label', axes.get_ylabel()), - ('Scale', [axes.get_yscale(), 'linear', 'log', 'logit']), - sep, - ('(Re-)Generate automatic legend', False), - ] + def convert_limits(lim, converter): + """Convert axis limits for correct input editors.""" + if isinstance(converter, DateConverter): + return map(num2date, lim) + # Cast to builtin floats as they have nicer reprs. + return map(float, lim) - # Save the unit data - xconverter = axes.xaxis.converter - yconverter = axes.yaxis.converter - xunits = axes.xaxis.get_units() - yunits = axes.yaxis.get_units() + axis_map = axes._axis_map + axis_limits = { + name: tuple(convert_limits( + getattr(axes, f'get_{name}lim')(), axis.converter + )) + for name, axis in axis_map.items() + } + general = [ + ('Title', axes.get_title()), + sep, + *chain.from_iterable([ + ( + (None, f"{name.title()}-Axis"), + ('Min', axis_limits[name][0]), + ('Max', axis_limits[name][1]), + ('Label', axis.get_label().get_text()), + ('Scale', [axis.get_scale(), + 'linear', 'log', 'symlog', 'logit']), + sep, + ) + for name, axis in axis_map.items() + ]), + ('(Re-)Generate automatic legend', False), + ] - # Sorting for default labels (_lineXXX, _imageXXX). - def cmp_key(label): - match = re.match(r"(_line|_image)(\d+)", label) - if match: - return match.group(1), int(match.group(2)) - else: - return label, 0 + # Save the converter and unit data + axis_converter = { + name: axis.converter + for name, axis in axis_map.items() + } + axis_units = { + name: axis.get_units() + for name, axis in axis_map.items() + } # Get / Curves - linedict = {} + labeled_lines = [] for line in axes.get_lines(): label = line.get_label() if label == '_nolegend_': continue - linedict[label] = line + labeled_lines.append((label, line)) curves = [] def prepare_data(d, init): @@ -101,9 +110,7 @@ def prepare_data(d, init): sorted(short2name.items(), key=lambda short_and_name: short_and_name[1])) - curvelabels = sorted(linedict, key=cmp_key) - for label in curvelabels: - line = linedict[label] + for label, line in labeled_lines: color = mcolors.to_hex( mcolors.to_rgba(line.get_color(), line.get_alpha()), keep_alpha=True) @@ -132,19 +139,17 @@ def prepare_data(d, init): has_curve = bool(curves) # Get ScalarMappables. - mappabledict = {} + labeled_mappables = [] for mappable in [*axes.images, *axes.collections]: label = mappable.get_label() if label == '_nolegend_' or mappable.get_array() is None: continue - mappabledict[label] = mappable - mappablelabels = sorted(mappabledict, key=cmp_key) + labeled_mappables.append((label, mappable)) mappables = [] - cmaps = [(cmap, name) for name, cmap in sorted(cm._cmap_registry.items())] - for label in mappablelabels: - mappable = mappabledict[label] + cmaps = [(cmap, name) for name, cmap in sorted(cm._colormaps.items())] + for label, mappable in labeled_mappables: cmap = mappable.get_cmap() - if cmap not in cm._cmap_registry.values(): + if cmap not in cm._colormaps.values(): cmaps = [(cmap, cmap.name), *cmaps] low, high = mappable.get_clim() mappabledata = [ @@ -171,8 +176,10 @@ def prepare_data(d, init): def apply_callback(data): """A callback to apply changes.""" - orig_xlim = axes.get_xlim() - orig_ylim = axes.get_ylim() + orig_limits = { + name: getattr(axes, f"get_{name}lim")() + for name in axis_map + } general = data.pop(0) curves = data.pop(0) if has_curve else [] @@ -180,32 +187,28 @@ def apply_callback(data): if data: raise ValueError("Unexpected field") - # Set / General - (title, xmin, xmax, xlabel, xscale, ymin, ymax, ylabel, yscale, - generate_legend) = general + title = general.pop(0) + axes.set_title(title) + generate_legend = general.pop() - if axes.get_xscale() != xscale: - axes.set_xscale(xscale) - if axes.get_yscale() != yscale: - axes.set_yscale(yscale) + for i, (name, axis) in enumerate(axis_map.items()): + axis_min = general[4*i] + axis_max = general[4*i + 1] + axis_label = general[4*i + 2] + axis_scale = general[4*i + 3] + if axis.get_scale() != axis_scale: + getattr(axes, f"set_{name}scale")(axis_scale) - axes.set_title(title) - axes.set_xlim(xmin, xmax) - axes.set_xlabel(xlabel) - axes.set_ylim(ymin, ymax) - axes.set_ylabel(ylabel) + axis._set_lim(axis_min, axis_max, auto=False) + axis.set_label_text(axis_label) - # Restore the unit data - axes.xaxis.converter = xconverter - axes.yaxis.converter = yconverter - axes.xaxis.set_units(xunits) - axes.yaxis.set_units(yunits) - axes.xaxis._update_axisinfo() - axes.yaxis._update_axisinfo() + # Restore the unit data + axis.converter = axis_converter[name] + axis.set_units(axis_units[name]) # Set / Curves for index, curve in enumerate(curves): - line = linedict[curvelabels[index]] + line = labeled_lines[index][1] (label, linestyle, drawstyle, linewidth, color, marker, markersize, markerfacecolor, markeredgecolor) = curve line.set_label(label) @@ -223,7 +226,7 @@ def apply_callback(data): # Set ScalarMappables. for index, mappable_settings in enumerate(mappables): - mappable = mappabledict[mappablelabels[index]] + mappable = labeled_mappables[index][1] if len(mappable_settings) == 5: label, cmap, low, high, interpolation = mappable_settings mappable.set_interpolation(interpolation) @@ -236,25 +239,25 @@ def apply_callback(data): # re-generate legend, if checkbox is checked if generate_legend: draggable = None - ncol = 1 + ncols = 1 if axes.legend_ is not None: old_legend = axes.get_legend() draggable = old_legend._draggable is not None - ncol = old_legend._ncol - new_legend = axes.legend(ncol=ncol) + ncols = old_legend._ncols + new_legend = axes.legend(ncols=ncols) if new_legend: new_legend.set_draggable(draggable) # Redraw figure = axes.get_figure() figure.canvas.draw() - if not (axes.get_xlim() == orig_xlim and axes.get_ylim() == orig_ylim): - figure.canvas.toolbar.push_current() + for name in axis_map: + if getattr(axes, f"get_{name}lim")() != orig_limits[name]: + figure.canvas.toolbar.push_current() + break - data = _formlayout.fedit( + _formlayout.fedit( datalist, title="Figure options", parent=parent, icon=QtGui.QIcon( str(cbook._get_data_path('images', 'qt4_editor_options.svg'))), apply=apply_callback) - if data is not None: - apply_callback(data) diff --git a/lib/matplotlib/backends/qt_editor/formsubplottool.py b/lib/matplotlib/backends/qt_editor/formsubplottool.py deleted file mode 100644 index db79a3a054e0..000000000000 --- a/lib/matplotlib/backends/qt_editor/formsubplottool.py +++ /dev/null @@ -1,8 +0,0 @@ -from matplotlib import _api -from ._formsubplottool import UiSubplotTool - - -_api.warn_deprecated( - "3.3", obj_type="module", name=__name__, - alternative="matplotlib.backends.backend_qt5.SubplotToolQt") -__all__ = ["UiSubplotTool"] diff --git a/lib/matplotlib/backends/web_backend/all_figures.html b/lib/matplotlib/backends/web_backend/all_figures.html index c29ee55f4efe..62f04b65c9bf 100644 --- a/lib/matplotlib/backends/web_backend/all_figures.html +++ b/lib/matplotlib/backends/web_backend/all_figures.html @@ -1,8 +1,9 @@ - + + - - + + @@ -23,7 +24,9 @@ figure_div.id = "figure-div"; main_div.appendChild(figure_div); var websocket_type = mpl.get_websocket_type(); - var websocket = new websocket_type("{{ ws_uri }}" + fig_id + "/ws"); + var uri = "{{ ws_uri }}" + fig_id + "/ws"; + if (window.location.protocol === "https:") uri = uri.replace('ws:', 'wss:') + var websocket = new websocket_type(uri); var fig = new mpl.figure(fig_id, websocket, mpl_ondownload, figure_div); fig.focus_on_mouseover = true; diff --git a/lib/matplotlib/backends/web_backend/css/fbm.css b/lib/matplotlib/backends/web_backend/css/fbm.css index 0e21d19ae801..ce35d99a5e64 100644 --- a/lib/matplotlib/backends/web_backend/css/fbm.css +++ b/lib/matplotlib/backends/web_backend/css/fbm.css @@ -1,95 +1,95 @@ /* Flexible box model classes */ -/* Taken from Alex Russell http://infrequently.org/2009/08/css-3-progress/ */ - +/* Taken from Alex Russell https://infrequently.org/2009/08/css-3-progress/ */ + .hbox { display: -webkit-box; -webkit-box-orient: horizontal; -webkit-box-align: stretch; - + display: -moz-box; -moz-box-orient: horizontal; -moz-box-align: stretch; - + display: box; box-orient: horizontal; box-align: stretch; } - + .hbox > * { -webkit-box-flex: 0; -moz-box-flex: 0; box-flex: 0; } - + .vbox { display: -webkit-box; -webkit-box-orient: vertical; -webkit-box-align: stretch; - + display: -moz-box; -moz-box-orient: vertical; -moz-box-align: stretch; - + display: box; box-orient: vertical; box-align: stretch; } - + .vbox > * { -webkit-box-flex: 0; -moz-box-flex: 0; box-flex: 0; } - + .reverse { -webkit-box-direction: reverse; -moz-box-direction: reverse; box-direction: reverse; } - + .box-flex0 { -webkit-box-flex: 0; -moz-box-flex: 0; box-flex: 0; } - + .box-flex1, .box-flex { -webkit-box-flex: 1; -moz-box-flex: 1; box-flex: 1; } - + .box-flex2 { -webkit-box-flex: 2; -moz-box-flex: 2; box-flex: 2; } - + .box-group1 { -webkit-box-flex-group: 1; -moz-box-flex-group: 1; box-flex-group: 1; } - + .box-group2 { -webkit-box-flex-group: 2; -moz-box-flex-group: 2; box-flex-group: 2; } - + .start { -webkit-box-pack: start; -moz-box-pack: start; box-pack: start; } - + .end { -webkit-box-pack: end; -moz-box-pack: end; box-pack: end; } - + .center { -webkit-box-pack: center; -moz-box-pack: center; diff --git a/lib/matplotlib/backends/web_backend/css/page.css b/lib/matplotlib/backends/web_backend/css/page.css index c380ef0a3ffc..ded0d9220379 100644 --- a/lib/matplotlib/backends/web_backend/css/page.css +++ b/lib/matplotlib/backends/web_backend/css/page.css @@ -80,4 +80,3 @@ span#login_widget { margin: 10px; vertical-align: top; } - diff --git a/lib/matplotlib/backends/web_backend/ipython_inline_figure.html b/lib/matplotlib/backends/web_backend/ipython_inline_figure.html index 9cc6aa9020e2..b941d352a7d6 100644 --- a/lib/matplotlib/backends/web_backend/ipython_inline_figure.html +++ b/lib/matplotlib/backends/web_backend/ipython_inline_figure.html @@ -2,7 +2,7 @@ websocket server, so we have to get in client-side and fetch our resources that way. --> @@ -18,8 +19,9 @@ ready( function () { var websocket_type = mpl.get_websocket_type(); - var websocket = new websocket_type( - "{{ ws_uri }}" + {{ str(fig_id) }} + "/ws"); + var uri = "{{ ws_uri }}" + {{ str(fig_id) }} + "/ws"; + if (window.location.protocol === 'https:') uri = uri.replace('ws:', 'wss:') + var websocket = new websocket_type(uri); var fig = new mpl.figure( {{ str(fig_id) }}, websocket, mpl_ondownload, document.getElementById("figure")); diff --git a/lib/matplotlib/bezier.py b/lib/matplotlib/bezier.py index 32889edc8fbd..f310f287e2c0 100644 --- a/lib/matplotlib/bezier.py +++ b/lib/matplotlib/bezier.py @@ -1,5 +1,5 @@ """ -A module providing some utility functions regarding Bezier path manipulation. +A module providing some utility functions regarding Bézier path manipulation. """ from functools import lru_cache @@ -94,7 +94,7 @@ def _de_casteljau1(beta, t): def split_de_casteljau(beta, t): """ - Split a Bezier segment defined by its control points *beta* into two + Split a Bézier segment defined by its control points *beta* into two separate segments divided at *t* and return their control points. """ beta = np.asarray(beta) @@ -113,7 +113,7 @@ def split_de_casteljau(beta, t): def find_bezier_t_intersecting_with_closedpath( bezier_point_at_t, inside_closedpath, t0=0., t1=1., tolerance=0.01): """ - Find the intersection of the Bezier curve with a closed path. + Find the intersection of the Bézier curve with a closed path. The intersection point *t* is approximated by two parameters *t0*, *t1* such that *t0* <= *t* <= *t1*. @@ -126,7 +126,7 @@ def find_bezier_t_intersecting_with_closedpath( Parameters ---------- bezier_point_at_t : callable - A function returning x, y coordinates of the Bezier at parameter *t*. + A function returning x, y coordinates of the Bézier at parameter *t*. It must have the signature:: bezier_point_at_t(t: float) -> tuple[float, float] @@ -146,7 +146,7 @@ def find_bezier_t_intersecting_with_closedpath( Returns ------- t0, t1 : float - The Bezier path parameters. + The Bézier path parameters. """ start = bezier_point_at_t(t0) end = bezier_point_at_t(t1) @@ -172,7 +172,6 @@ def find_bezier_t_intersecting_with_closedpath( if start_inside ^ middle_inside: t1 = middle_t end = middle - end_inside = middle_inside else: t0 = middle_t start = middle @@ -181,7 +180,7 @@ def find_bezier_t_intersecting_with_closedpath( class BezierSegment: """ - A d-dimensional Bezier segment. + A d-dimensional Bézier segment. Parameters ---------- @@ -200,7 +199,7 @@ def __init__(self, control_points): def __call__(self, t): """ - Evaluate the Bezier curve at point(s) t in [0, 1]. + Evaluate the Bézier curve at point(s) *t* in [0, 1]. Parameters ---------- @@ -240,7 +239,7 @@ def degree(self): @property def polynomial_coefficients(self): r""" - The polynomial coefficients of the Bezier curve. + The polynomial coefficients of the Bézier curve. .. warning:: Follows opposite convention from `numpy.polyval`. @@ -248,7 +247,7 @@ def polynomial_coefficients(self): ------- (n+1, d) array Coefficients after expanding in polynomial basis, where :math:`n` - is the degree of the bezier curve and :math:`d` its dimension. + is the degree of the Bézier curve and :math:`d` its dimension. These are the numbers (:math:`C_j`) such that the curve can be written :math:`\sum_{j=0}^n C_j t^j`. @@ -309,12 +308,12 @@ def axis_aligned_extrema(self): def split_bezier_intersecting_with_closedpath( bezier, inside_closedpath, tolerance=0.01): """ - Split a Bezier curve into two at the intersection with a closed path. + Split a Bézier curve into two at the intersection with a closed path. Parameters ---------- bezier : (N, 2) array-like - Control points of the Bezier segment. See `.BezierSegment`. + Control points of the Bézier segment. See `.BezierSegment`. inside_closedpath : callable A function returning True if a given point (x, y) is inside the closed path. See also `.find_bezier_t_intersecting_with_closedpath`. @@ -325,7 +324,7 @@ def split_bezier_intersecting_with_closedpath( Returns ------- left, right - Lists of control points for the two Bezier segments. + Lists of control points for the two Bézier segments. """ bz = BezierSegment(bezier) @@ -462,13 +461,13 @@ def check_if_parallel(dx1, dy1, dx2, dy2, tolerance=1.e-5): def get_parallels(bezier2, width): """ - Given the quadratic Bezier control points *bezier2*, returns - control points of quadratic Bezier lines roughly parallel to given + Given the quadratic Bézier control points *bezier2*, returns + control points of quadratic Bézier lines roughly parallel to given one separated by *width*. """ # The parallel Bezier lines are constructed by following ways. - # c1 and c2 are control points representing the begin and end of the + # c1 and c2 are control points representing the start and end of the # Bezier line. # cm is the middle point @@ -486,7 +485,7 @@ def get_parallels(bezier2, width): cos_t2, sin_t2 = cos_t1, sin_t1 else: # t1 and t2 is the angle between c1 and cm, cm, c2. They are - # also a angle of the tangential line of the path at c1 and c2 + # also an angle of the tangential line of the path at c1 and c2 cos_t1, sin_t1 = get_cos_sin(c1x, c1y, cmx, cmy) cos_t2, sin_t2 = get_cos_sin(cmx, cmy, c2x, c2y) @@ -536,7 +535,7 @@ def get_parallels(bezier2, width): def find_control_points(c1x, c1y, mmx, mmy, c2x, c2y): """ - Find control points of the Bezier curve passing through (*c1x*, *c1y*), + Find control points of the Bézier curve passing through (*c1x*, *c1y*), (*mmx*, *mmy*), and (*c2x*, *c2y*), at parametric values 0, 0.5, and 1. """ cmx = .5 * (4 * mmx - (c1x + c2x)) @@ -546,8 +545,8 @@ def find_control_points(c1x, c1y, mmx, mmy, c2x, c2y): def make_wedged_bezier2(bezier2, width, w1=1., wm=0.5, w2=0.): """ - Being similar to get_parallels, returns control points of two quadratic - Bezier lines having a width roughly parallel to given one separated by + Being similar to `get_parallels`, returns control points of two quadratic + Bézier lines having a width roughly parallel to given one separated by *width*. """ @@ -557,7 +556,7 @@ def make_wedged_bezier2(bezier2, width, w1=1., wm=0.5, w2=0.): c3x, c3y = bezier2[2] # t1 and t2 is the angle between c1 and cm, cm, c3. - # They are also a angle of the tangential line of the path at c1 and c3 + # They are also an angle of the tangential line of the path at c1 and c3 cos_t1, sin_t1 = get_cos_sin(c1x, c1y, cmx, cmy) cos_t2, sin_t2 = get_cos_sin(cmx, cmy, c3x, c3y) @@ -593,28 +592,3 @@ def make_wedged_bezier2(bezier2, width, w1=1., wm=0.5, w2=0.): c3x_right, c3y_right) return path_left, path_right - - -@_api.deprecated( - "3.3", alternative="Path.cleaned() and remove the final STOP if needed") -def make_path_regular(p): - """ - If the ``codes`` attribute of `.Path` *p* is None, return a copy of *p* - with ``codes`` set to (MOVETO, LINETO, LINETO, ..., LINETO); otherwise - return *p* itself. - """ - from .path import Path - c = p.codes - if c is None: - c = np.full(len(p.vertices), Path.LINETO, dtype=Path.code_type) - c[0] = Path.MOVETO - return Path(p.vertices, c) - else: - return p - - -@_api.deprecated("3.3", alternative="Path.make_compound_path()") -def concatenate_paths(paths): - """Concatenate a list of paths into a single path.""" - from .path import Path - return Path.make_compound_path(*paths) diff --git a/lib/matplotlib/blocking_input.py b/lib/matplotlib/blocking_input.py deleted file mode 100644 index afbfdacfdbd7..000000000000 --- a/lib/matplotlib/blocking_input.py +++ /dev/null @@ -1,353 +0,0 @@ -""" -Classes used for blocking interaction with figure windows: - -`BlockingInput` - Creates a callable object to retrieve events in a blocking way for - interactive sessions. Base class of the other classes listed here. - -`BlockingKeyMouseInput` - Creates a callable object to retrieve key or mouse clicks in a blocking - way for interactive sessions. Used by `~.Figure.waitforbuttonpress`. - -`BlockingMouseInput` - Creates a callable object to retrieve mouse clicks in a blocking way for - interactive sessions. Used by `~.Figure.ginput`. - -`BlockingContourLabeler` - Creates a callable object to retrieve mouse clicks in a blocking way that - will then be used to place labels on a `.ContourSet`. Used by - `~.Axes.clabel`. -""" - -import logging -from numbers import Integral - -from matplotlib import _api -from matplotlib.backend_bases import MouseButton -import matplotlib.lines as mlines - -_log = logging.getLogger(__name__) - - -class BlockingInput: - """Callable for retrieving events in a blocking way.""" - - def __init__(self, fig, eventslist=()): - self.fig = fig - self.eventslist = eventslist - - def on_event(self, event): - """ - Event handler; will be passed to the current figure to retrieve events. - """ - # Add a new event to list - using a separate function is overkill for - # the base class, but this is consistent with subclasses. - self.add_event(event) - _log.info("Event %i", len(self.events)) - - # This will extract info from events. - self.post_event() - - # Check if we have enough events already. - if len(self.events) >= self.n > 0: - self.fig.canvas.stop_event_loop() - - def post_event(self): - """For baseclass, do nothing but collect events.""" - - def cleanup(self): - """Disconnect all callbacks.""" - for cb in self.callbacks: - self.fig.canvas.mpl_disconnect(cb) - self.callbacks = [] - - def add_event(self, event): - """For base class, this just appends an event to events.""" - self.events.append(event) - - def pop_event(self, index=-1): - """ - Remove an event from the event list -- by default, the last. - - Note that this does not check that there are events, much like the - normal pop method. If no events exist, this will throw an exception. - """ - self.events.pop(index) - - pop = pop_event - - def __call__(self, n=1, timeout=30): - """Blocking call to retrieve *n* events.""" - _api.check_isinstance(Integral, n=n) - self.n = n - self.events = [] - - if hasattr(self.fig.canvas, "manager"): - # Ensure that the figure is shown, if we are managing it. - self.fig.show() - # Connect the events to the on_event function call. - self.callbacks = [self.fig.canvas.mpl_connect(name, self.on_event) - for name in self.eventslist] - try: - # Start event loop. - self.fig.canvas.start_event_loop(timeout=timeout) - finally: # Run even on exception like ctrl-c. - # Disconnect the callbacks. - self.cleanup() - # Return the events in this case. - return self.events - - -class BlockingMouseInput(BlockingInput): - """ - Callable for retrieving mouse clicks in a blocking way. - - This class will also retrieve keypresses and map them to mouse clicks: - delete and backspace are a right click, enter is like a middle click, - and all others are like a left click. - """ - - button_add = MouseButton.LEFT - button_pop = MouseButton.RIGHT - button_stop = MouseButton.MIDDLE - - def __init__(self, fig, - mouse_add=MouseButton.LEFT, - mouse_pop=MouseButton.RIGHT, - mouse_stop=MouseButton.MIDDLE): - super().__init__(fig=fig, - eventslist=('button_press_event', 'key_press_event')) - self.button_add = mouse_add - self.button_pop = mouse_pop - self.button_stop = mouse_stop - - def post_event(self): - """Process an event.""" - if len(self.events) == 0: - _log.warning("No events yet") - elif self.events[-1].name == 'key_press_event': - self.key_event() - else: - self.mouse_event() - - def mouse_event(self): - """Process a mouse click event.""" - event = self.events[-1] - button = event.button - if button == self.button_pop: - self.mouse_event_pop(event) - elif button == self.button_stop: - self.mouse_event_stop(event) - elif button == self.button_add: - self.mouse_event_add(event) - - def key_event(self): - """ - Process a key press event, mapping keys to appropriate mouse clicks. - """ - event = self.events[-1] - if event.key is None: - # At least in OSX gtk backend some keys return None. - return - if event.key in ['backspace', 'delete']: - self.mouse_event_pop(event) - elif event.key in ['escape', 'enter']: - self.mouse_event_stop(event) - else: - self.mouse_event_add(event) - - def mouse_event_add(self, event): - """ - Process an button-1 event (add a click if inside axes). - - Parameters - ---------- - event : `~.backend_bases.MouseEvent` - """ - if event.inaxes: - self.add_click(event) - else: # If not a valid click, remove from event list. - BlockingInput.pop(self) - - def mouse_event_stop(self, event): - """ - Process an button-2 event (end blocking input). - - Parameters - ---------- - event : `~.backend_bases.MouseEvent` - """ - # Remove last event just for cleanliness. - BlockingInput.pop(self) - # This will exit even if not in infinite mode. This is consistent with - # MATLAB and sometimes quite useful, but will require the user to test - # how many points were actually returned before using data. - self.fig.canvas.stop_event_loop() - - def mouse_event_pop(self, event): - """ - Process an button-3 event (remove the last click). - - Parameters - ---------- - event : `~.backend_bases.MouseEvent` - """ - # Remove this last event. - BlockingInput.pop(self) - # Now remove any existing clicks if possible. - if self.events: - self.pop(event) - - def add_click(self, event): - """ - Add the coordinates of an event to the list of clicks. - - Parameters - ---------- - event : `~.backend_bases.MouseEvent` - """ - self.clicks.append((event.xdata, event.ydata)) - _log.info("input %i: %f, %f", - len(self.clicks), event.xdata, event.ydata) - # If desired, plot up click. - if self.show_clicks: - line = mlines.Line2D([event.xdata], [event.ydata], - marker='+', color='r') - event.inaxes.add_line(line) - self.marks.append(line) - self.fig.canvas.draw() - - def pop_click(self, event, index=-1): - """ - Remove a click (by default, the last) from the list of clicks. - - Parameters - ---------- - event : `~.backend_bases.MouseEvent` - """ - self.clicks.pop(index) - if self.show_clicks: - self.marks.pop(index).remove() - self.fig.canvas.draw() - - def pop(self, event, index=-1): - """ - Remove a click and the associated event from the list of clicks. - - Defaults to the last click. - """ - self.pop_click(event, index) - super().pop(index) - - def cleanup(self, event=None): - """ - Parameters - ---------- - event : `~.backend_bases.MouseEvent`, optional - Not used - """ - # Clean the figure. - if self.show_clicks: - for mark in self.marks: - mark.remove() - self.marks = [] - self.fig.canvas.draw() - # Call base class to remove callbacks. - super().cleanup() - - def __call__(self, n=1, timeout=30, show_clicks=True): - """ - Blocking call to retrieve *n* coordinate pairs through mouse clicks. - """ - self.show_clicks = show_clicks - self.clicks = [] - self.marks = [] - super().__call__(n=n, timeout=timeout) - return self.clicks - - -class BlockingContourLabeler(BlockingMouseInput): - """ - Callable for retrieving mouse clicks and key presses in a blocking way. - - Used to place contour labels. - """ - - def __init__(self, cs): - self.cs = cs - super().__init__(fig=cs.axes.figure) - - def add_click(self, event): - self.button1(event) - - def pop_click(self, event, index=-1): - self.button3(event) - - def button1(self, event): - """ - Process an button-1 event (add a label to a contour). - - Parameters - ---------- - event : `~.backend_bases.MouseEvent` - """ - # Shorthand - if event.inaxes == self.cs.ax: - self.cs.add_label_near(event.x, event.y, self.inline, - inline_spacing=self.inline_spacing, - transform=False) - self.fig.canvas.draw() - else: # Remove event if not valid - BlockingInput.pop(self) - - def button3(self, event): - """ - Process an button-3 event (remove a label if not in inline mode). - - Unfortunately, if one is doing inline labels, then there is currently - no way to fix the broken contour - once humpty-dumpty is broken, he - can't be put back together. In inline mode, this does nothing. - - Parameters - ---------- - event : `~.backend_bases.MouseEvent` - """ - if self.inline: - pass - else: - self.cs.pop_label() - self.cs.ax.figure.canvas.draw() - - def __call__(self, inline, inline_spacing=5, n=-1, timeout=-1): - self.inline = inline - self.inline_spacing = inline_spacing - super().__call__(n=n, timeout=timeout, show_clicks=False) - - -class BlockingKeyMouseInput(BlockingInput): - """ - Callable for retrieving mouse clicks and key presses in a blocking way. - """ - - def __init__(self, fig): - super().__init__(fig=fig, - eventslist=('button_press_event', 'key_press_event')) - - def post_event(self): - """Determine if it is a key event.""" - if self.events: - self.keyormouse = self.events[-1].name == 'key_press_event' - else: - _log.warning("No events yet.") - - def __call__(self, timeout=30): - """ - Blocking call to retrieve a single mouse click or key press. - - Returns ``True`` if key press, ``False`` if mouse click, or ``None`` if - timed out. - """ - self.keyormouse = None - super().__call__(n=1, timeout=timeout) - - return self.keyormouse diff --git a/lib/matplotlib/category.py b/lib/matplotlib/category.py index 2e7f19cd7a64..4ac2379ea5f5 100644 --- a/lib/matplotlib/category.py +++ b/lib/matplotlib/category.py @@ -43,7 +43,7 @@ def convert(value, unit, axis): Returns ------- - float or ndarray[float] + float or `~numpy.ndarray` of float """ if unit is None: raise ValueError( @@ -53,11 +53,6 @@ def convert(value, unit, axis): StrCategoryConverter._validate_unit(unit) # dtype = object preserves numerical pass throughs values = np.atleast_1d(np.array(value, dtype=object)) - # pass through sequence of non binary numbers - if all(units.ConversionInterface.is_numlike(v) - and not isinstance(v, (str, bytes)) - for v in values): - return np.asarray(values, dtype=float) # force an update so it also does type checking unit.update(values) return np.vectorize(unit._mapping.__getitem__, otypes=[float])(values) @@ -74,12 +69,13 @@ def axisinfo(unit, axis): axis : `~matplotlib.axis.Axis` axis for which information is being set + .. note:: *axis* is not used + Returns ------- `~matplotlib.units.AxisInfo` Information to support default tick labeling - .. note: axis is not used """ StrCategoryConverter._validate_unit(unit) # locator and formatter take mapping dict because @@ -124,7 +120,7 @@ class StrCategoryLocator(ticker.Locator): def __init__(self, units_mapping): """ Parameters - ----------- + ---------- units_mapping : dict Mapping of category names (str) to indices (int). """ @@ -223,7 +219,7 @@ def update(self, data): convertible = self._str_is_convertible(val) if val not in self._mapping: self._mapping[val] = next(self._counter) - if convertible: + if data.size and convertible: _log.info('Using categorical units to plot a list of strings ' 'that are all parsable as floats or dates. If these ' 'strings should be plotted as numbers, cast to the ' diff --git a/lib/matplotlib/cbook/__init__.py b/lib/matplotlib/cbook/__init__.py index 7fae32ce8a4d..1e51f6a834cc 100644 --- a/lib/matplotlib/cbook/__init__.py +++ b/lib/matplotlib/cbook/__init__.py @@ -12,35 +12,35 @@ import functools import gzip import itertools +import math import operator import os from pathlib import Path -import re import shlex import subprocess import sys import time import traceback import types -import warnings import weakref import numpy as np import matplotlib from matplotlib import _api, _c_internal_utils -from matplotlib._api.deprecation import ( - MatplotlibDeprecationWarning, mplDeprecation) -@_api.deprecated("3.4") -def deprecated(*args, **kwargs): - return _api.deprecated(*args, **kwargs) - - -@_api.deprecated("3.4") -def warn_deprecated(*args, **kwargs): - _api.warn_deprecated(*args, **kwargs) +@_api.caching_module_getattr +class __getattr__: + # module-level deprecations + MatplotlibDeprecationWarning = _api.deprecated( + "3.6", obj_type="", + alternative="matplotlib.MatplotlibDeprecationWarning")( + property(lambda self: _api.deprecation.MatplotlibDeprecationWarning)) + mplDeprecation = _api.deprecated( + "3.6", obj_type="", + alternative="matplotlib.MatplotlibDeprecationWarning")( + property(lambda self: _api.deprecation.MatplotlibDeprecationWarning)) def _get_running_interactive_framework(): @@ -51,20 +51,27 @@ def _get_running_interactive_framework(): Returns ------- Optional[str] - One of the following values: "qt5", "qt4", "gtk3", "wx", "tk", + One of the following values: "qt", "gtk3", "gtk4", "wx", "tk", "macosx", "headless", ``None``. """ - QtWidgets = (sys.modules.get("PyQt5.QtWidgets") - or sys.modules.get("PySide2.QtWidgets")) + # Use ``sys.modules.get(name)`` rather than ``name in sys.modules`` as + # entries can also have been explicitly set to None. + QtWidgets = ( + sys.modules.get("PyQt6.QtWidgets") + or sys.modules.get("PySide6.QtWidgets") + or sys.modules.get("PyQt5.QtWidgets") + or sys.modules.get("PySide2.QtWidgets") + ) if QtWidgets and QtWidgets.QApplication.instance(): - return "qt5" - QtGui = (sys.modules.get("PyQt4.QtGui") - or sys.modules.get("PySide.QtGui")) - if QtGui and QtGui.QApplication.instance(): - return "qt4" + return "qt" Gtk = sys.modules.get("gi.repository.Gtk") - if Gtk and Gtk.main_level(): - return "gtk3" + if Gtk: + if Gtk.MAJOR_VERSION == 4: + from gi.repository import GLib + if GLib.main_depth(): + return "gtk4" + if Gtk.MAJOR_VERSION == 3 and Gtk.main_level(): + return "gtk3" wx = sys.modules.get("wx") if wx and wx.GetApp(): return "wx" @@ -76,9 +83,9 @@ def _get_running_interactive_framework(): if frame.f_code in codes: return "tk" frame = frame.f_back - if 'matplotlib.backends._macosx' in sys.modules: - if sys.modules["matplotlib.backends._macosx"].event_loop_is_running(): - return "macosx" + macosx = sys.modules.get("matplotlib.backends._macosx") + if macosx and macosx.event_loop_is_running(): + return "macosx" if not _c_internal_utils.display_is_valid(): return "headless" return None @@ -121,12 +128,13 @@ def _weak_or_strong_ref(func, callback): class CallbackRegistry: """ - Handle registering and disconnecting for a set of signals and callbacks: + Handle registering, processing, blocking, and disconnecting + for a set of signals and callbacks: >>> def oneat(x): - ... print('eat', x) + ... print('eat', x) >>> def ondrink(x): - ... print('drink', x) + ... print('drink', x) >>> from matplotlib.cbook import CallbackRegistry >>> callbacks = CallbackRegistry() @@ -138,9 +146,15 @@ class CallbackRegistry: drink 123 >>> callbacks.process('eat', 456) eat 456 - >>> callbacks.process('be merry', 456) # nothing will be called + >>> callbacks.process('be merry', 456) # nothing will be called + >>> callbacks.disconnect(id_eat) - >>> callbacks.process('eat', 456) # nothing will be called + >>> callbacks.process('eat', 456) # nothing will be called + + >>> with callbacks.blocked(signal='drink'): + ... callbacks.process('drink', 123) # nothing will be called + >>> callbacks.process('drink', 123) + drink 123 In practice, one should always disconnect all callbacks when they are no longer needed to avoid dangling references (and thus memory leaks). @@ -161,13 +175,20 @@ class CallbackRegistry: The default handler prints the exception (with `traceback.print_exc`) if an interactive event loop is running; it re-raises the exception if no interactive event loop is running. + + signals : list, optional + If not None, *signals* is a list of signals that this registry handles: + attempting to `process` or to `connect` to a signal not in the list + throws a `ValueError`. The default, None, does not restrict the + handled signals. """ # We maintain two mappings: # callbacks: signal -> {cid -> weakref-to-callback} # _func_cid_map: signal -> {weakref-to-callback -> cid} - def __init__(self, exception_handler=_exception_printer): + def __init__(self, exception_handler=_exception_printer, *, signals=None): + self._signals = None if signals is None else list(signals) # Copy it. self.exception_handler = exception_handler self.callbacks = {} self._cid_gen = itertools.count() @@ -197,9 +218,10 @@ def __setstate__(self, state): s: {proxy: cid for cid, proxy in d.items()} for s, d in self.callbacks.items()} - @_api.rename_parameter("3.4", "s", "signal") def connect(self, signal, func): """Register *func* to be called when signal *signal* is generated.""" + if self._signals is not None: + _api.check_in_list(self._signals, signal=signal) self._func_cid_map.setdefault(signal, {}) proxy = _weak_or_strong_ref(func, self._remove_proxy) if proxy in self._func_cid_map[signal]: @@ -210,6 +232,16 @@ def connect(self, signal, func): self.callbacks[signal][cid] = proxy return cid + def _connect_picklable(self, signal, func): + """ + Like `.connect`, but the callback is kept when pickling/unpickling. + + Currently internal-use only. + """ + cid = self.connect(signal, func) + self._pickled_cids.add(cid) + return cid + # Keep a reference to sys.is_finalizing, as sys may have been cleared out # at that point. def _remove_proxy(self, proxy, *, _is_finalizing=sys.is_finalizing): @@ -263,6 +295,8 @@ def process(self, s, *args, **kwargs): All of the functions registered to receive callbacks on *s* will be called with ``*args`` and ``**kwargs``. """ + if self._signals is not None: + _api.check_in_list(self._signals, signal=s) for cid, ref in list(self.callbacks.get(s, {}).items()): func = ref() if func is not None: @@ -276,6 +310,31 @@ def process(self, s, *args, **kwargs): else: raise + @contextlib.contextmanager + def blocked(self, *, signal=None): + """ + Block callback signals from being processed. + + A context manager to temporarily block/disable callback signals + from being processed by the registered listeners. + + Parameters + ---------- + signal : str, optional + The callback signal to block. The default is to block all signals. + """ + orig = self.callbacks + try: + if signal is None: + # Empty out the callbacks + self.callbacks = {} + else: + # Only remove the specific signal + self.callbacks = {k: orig[k] for k in orig if k != signal} + yield + finally: + self.callbacks = orig + class silent_list(list): """ @@ -311,56 +370,9 @@ def __repr__(self): return "" -@_api.deprecated("3.3") -class IgnoredKeywordWarning(UserWarning): - """ - A class for issuing warnings about keyword arguments that will be ignored - by Matplotlib. - """ - pass - - -@_api.deprecated("3.3", alternative="normalize_kwargs") -def local_over_kwdict(local_var, kwargs, *keys): - """ - Enforces the priority of a local variable over potentially conflicting - argument(s) from a kwargs dict. The following possible output values are - considered in order of priority:: - - local_var > kwargs[keys[0]] > ... > kwargs[keys[-1]] - - The first of these whose value is not None will be returned. If all are - None then None will be returned. Each key in keys will be removed from the - kwargs dict in place. - - Parameters - ---------- - local_var : any object - The local variable (highest priority). - - kwargs : dict - Dictionary of keyword arguments; modified in place. - - keys : str(s) - Name(s) of keyword arguments to process, in descending order of - priority. - - Returns - ------- - any object - Either local_var or one of kwargs[key] for key in keys. - - Raises - ------ - IgnoredKeywordWarning - For each key in keys that is removed from kwargs but not used as - the output value. - """ - return _local_over_kwdict(local_var, kwargs, *keys, IgnoredKeywordWarning) - - def _local_over_kwdict( - local_var, kwargs, *keys, warning_cls=MatplotlibDeprecationWarning): + local_var, kwargs, *keys, + warning_cls=_api.MatplotlibDeprecationWarning): out = local_var for key in keys: kwarg_val = kwargs.pop(key, None) @@ -396,6 +408,26 @@ def strip_math(s): return s +def _strip_comment(s): + """Strip everything from the first unquoted #.""" + pos = 0 + while True: + quote_pos = s.find('"', pos) + hash_pos = s.find('#', pos) + if quote_pos < 0: + without_comment = s if hash_pos < 0 else s[:hash_pos] + return without_comment.strip() + elif 0 <= hash_pos < quote_pos: + return s[:hash_pos].strip() + else: + closing_quote_pos = s.find('"', quote_pos + 1) + if closing_quote_pos < 0: + raise ValueError( + f"Missing closing quote in: {s!r}. If you need a double-" + 'quote inside a string, use escaping: e.g. "the \" char"') + pos = closing_quote_pos + 1 # behind closing quote + + def is_writable_file_like(obj): """Return whether *obj* looks like a file object with a *write* method.""" return callable(getattr(obj, 'write', None)) @@ -445,16 +477,11 @@ def to_filehandle(fname, flag='r', return_opened=False, encoding=None): """ if isinstance(fname, os.PathLike): fname = os.fspath(fname) - if "U" in flag: - _api.warn_deprecated( - "3.3", message="Passing a flag containing 'U' to to_filehandle() " - "is deprecated since %(since)s and will be removed %(removal)s.") - flag = flag.replace("U", "") if isinstance(fname, str): if fname.endswith('.gz'): fh = gzip.open(fname, flag) elif fname.endswith('.bz2'): - # python may not be complied with bz2 support, + # python may not be compiled with bz2 support, # bury import until we need it import bz2 fh = bz2.BZ2File(fname, flag) @@ -471,15 +498,10 @@ def to_filehandle(fname, flag='r', return_opened=False, encoding=None): return fh -@contextlib.contextmanager def open_file_cm(path_or_file, mode="r", encoding=None): r"""Pass through file objects and context-manage path-likes.""" fh, opened = to_filehandle(path_or_file, mode, True, encoding) - if opened: - with fh: - yield fh - else: - yield fh + return fh if opened else contextlib.nullcontext(fh) def is_scalar_or_string(val): @@ -556,23 +578,7 @@ def flatten(seq, scalarp=is_scalar_or_string): yield from flatten(item, scalarp) -@_api.deprecated("3.3", alternative="os.path.realpath and os.stat") -@functools.lru_cache() -def get_realpath_and_stat(path): - realpath = os.path.realpath(path) - stat = os.stat(realpath) - stat_key = (stat.st_ino, stat.st_dev) - return realpath, stat_key - - -# A regular expression used to determine the amount of space to -# remove. It looks for the first sequence of spaces immediately -# following the first newline, or at the beginning of the string. -_find_dedent_regex = re.compile(r"(?:(?:\n\r?)|^)( *)\S") -# A cache to hold the regexs that actually remove the indent. -_dedent_regex = {} - - +@_api.deprecated("3.6", alternative="functools.lru_cache") class maxdict(dict): """ A dictionary with a maximum size. @@ -582,18 +588,15 @@ class maxdict(dict): This doesn't override all the relevant methods to constrain the size, just ``__setitem__``, so use with caution. """ + def __init__(self, maxsize): - dict.__init__(self) + super().__init__() self.maxsize = maxsize - self._killkeys = [] def __setitem__(self, k, v): - if k not in self: - if len(self) >= self.maxsize: - del self[self._killkeys[0]] - del self._killkeys[0] - self._killkeys.append(k) - dict.__setitem__(self, k, v) + super().__setitem__(k, v) + while len(self) >= self.maxsize: + del self[next(iter(self))] class Stack: @@ -702,36 +705,6 @@ def remove(self, o): self.push(elem) -def report_memory(i=0): # argument may go away - """Return the memory consumed by the process.""" - def call(command, os_name): - try: - return subprocess.check_output(command) - except subprocess.CalledProcessError as err: - raise NotImplementedError( - "report_memory works on %s only if " - "the '%s' program is found" % (os_name, command[0]) - ) from err - - pid = os.getpid() - if sys.platform == 'sunos5': - lines = call(['ps', '-p', '%d' % pid, '-o', 'osz'], 'Sun OS') - mem = int(lines[-1].strip()) - elif sys.platform == 'linux': - lines = call(['ps', '-p', '%d' % pid, '-o', 'rss,sz'], 'Linux') - mem = int(lines[1].split()[1]) - elif sys.platform == 'darwin': - lines = call(['ps', '-p', '%d' % pid, '-o', 'rss,vsz'], 'Mac OS') - mem = int(lines[1].split()[0]) - elif sys.platform == 'win32': - lines = call(["tasklist", "/nh", "/fi", "pid eq %d" % pid], 'Windows') - mem = int(lines.strip().split()[-2].replace(',', '')) - else: - raise NotImplementedError( - "We don't have a memory monitor for %s" % sys.platform) - return mem - - def safe_masked_invalid(x, copy=False): x = np.array(x, subok=True, copy=copy) if not x.dtype.isnative: @@ -911,6 +884,34 @@ def get_siblings(self, a): return [x() for x in siblings] +class GrouperView: + """Immutable view over a `.Grouper`.""" + + def __init__(self, grouper): + self._grouper = grouper + + class _GrouperMethodForwarder: + def __init__(self, deprecated_kw=None): + self._deprecated_kw = deprecated_kw + + def __set_name__(self, owner, name): + wrapped = getattr(Grouper, name) + forwarder = functools.wraps(wrapped)( + lambda self, *args, **kwargs: wrapped( + self._grouper, *args, **kwargs)) + if self._deprecated_kw: + forwarder = _api.deprecated(**self._deprecated_kw)(forwarder) + setattr(owner, name, forwarder) + + __contains__ = _GrouperMethodForwarder() + __iter__ = _GrouperMethodForwarder() + joined = _GrouperMethodForwarder() + get_siblings = _GrouperMethodForwarder() + clean = _GrouperMethodForwarder(deprecated_kw=dict(since="3.6")) + join = _GrouperMethodForwarder(deprecated_kw=dict(since="3.6")) + remove = _GrouperMethodForwarder(deprecated_kw=dict(since="3.6")) + + def simple_linear_interpolation(a, steps): """ Resample an array with ``steps - 1`` points between original point pairs. @@ -983,7 +984,7 @@ def delete_masked_points(*args): else: x = np.asarray(x) margs.append(x) - masks = [] # list of masks that are True where good + masks = [] # List of masks that are True where good. for i, x in enumerate(margs): if seqlist[i]: if x.ndim > 1: @@ -1135,6 +1136,7 @@ def boxplot_stats(X, whis=1.5, bootstrap=None, labels=None, med 50th percentile q1 first quartile (25th percentile) q3 third quartile (75th percentile) + iqr interquartile range cilo lower notch around the median cihi upper notch around the median whislo end of the lower whisker @@ -1216,11 +1218,11 @@ def _compute_conf_interval(data, med, iqr, bootstrap): stats['med'] = np.nan stats['q1'] = np.nan stats['q3'] = np.nan + stats['iqr'] = np.nan stats['cilo'] = np.nan stats['cihi'] = np.nan stats['whislo'] = np.nan stats['whishi'] = np.nan - stats['med'] = np.nan continue # up-convert to an array, just to be safe @@ -1335,47 +1337,17 @@ def _to_unmasked_float_array(x): def _check_1d(x): """Convert scalars to 1D arrays; pass-through arrays as is.""" - if not hasattr(x, 'shape') or len(x.shape) < 1: + # Unpack in case of e.g. Pandas or xarray object + x = _unpack_to_numpy(x) + # plot requires `shape` and `ndim`. If passed an + # object that doesn't provide them, then force to numpy array. + # Note this will strip unit information. + if (not hasattr(x, 'shape') or + not hasattr(x, 'ndim') or + len(x.shape) < 1): return np.atleast_1d(x) else: - try: - # work around - # https://github.com/pandas-dev/pandas/issues/27775 which - # means the shape of multi-dimensional slicing is not as - # expected. That this ever worked was an unintentional - # quirk of pandas and will raise an exception in the - # future. This slicing warns in pandas >= 1.0rc0 via - # https://github.com/pandas-dev/pandas/pull/30588 - # - # < 1.0rc0 : x[:, None].ndim == 1, no warning, custom type - # >= 1.0rc1 : x[:, None].ndim == 2, warns, numpy array - # future : x[:, None] -> raises - # - # This code should correctly identify and coerce to a - # numpy array all pandas versions. - with warnings.catch_warnings(record=True) as w: - warnings.filterwarnings( - "always", - category=Warning, - message='Support for multi-dimensional indexing') - - ndim = x[:, None].ndim - # we have definitely hit a pandas index or series object - # cast to a numpy array. - if len(w) > 0: - return np.asanyarray(x) - # We have likely hit a pandas object, or at least - # something where 2D slicing does not result in a 2D - # object. - if ndim < 2: - return np.atleast_1d(x) - return x - # In pandas 1.1.0, multidimensional indexing leads to an - # AssertionError for some Series objects, but should be - # IndexError as described in - # https://github.com/pandas-dev/pandas/issues/35527 - except (AssertionError, IndexError, TypeError): - return np.atleast_1d(x) + return x def _reshape_2D(X, name): @@ -1390,15 +1362,8 @@ def _reshape_2D(X, name): *name* is used to generate the error message for invalid inputs. """ - # unpack if we have a values or to_numpy method. - try: - X = X.to_numpy() - except AttributeError: - try: - if isinstance(X.values, np.ndarray): - X = X.values - except AttributeError: - pass + # Unpack in case of e.g. Pandas or xarray object + X = _unpack_to_numpy(X) # Iterate over columns for ndarrays. if isinstance(X, np.ndarray): @@ -1424,9 +1389,13 @@ def _reshape_2D(X, name): for xi in X: # check if this is iterable, except for strings which we # treat as singletons. - if (isinstance(xi, collections.abc.Iterable) and - not isinstance(xi, str)): - is_1d = False + if not isinstance(xi, str): + try: + iter(xi) + except TypeError: + pass + else: + is_1d = False xi = np.asanyarray(xi) nd = np.ndim(xi) if nd > 1: @@ -1502,11 +1471,11 @@ def violin_stats(X, method, points=100, quantiles=None): # Want quantiles to be as the same shape as data sequences if quantiles is not None and len(quantiles) != 0: quantiles = _reshape_2D(quantiles, "quantiles") - # Else, mock quantiles if is none or empty + # Else, mock quantiles if it's none or empty else: quantiles = [[]] * len(X) - # quantiles should has the same size as dataset + # quantiles should have the same size as dataset if len(X) != len(quantiles): raise ValueError("List of violinplot statistics and quantiles values" " must have the same length") @@ -1680,7 +1649,7 @@ def index_of(y): The x and y values to plot. """ try: - return y.index.values, y.values + return y.index.to_numpy(), y.to_numpy() except AttributeError: pass try: @@ -1697,22 +1666,53 @@ def safe_first_element(obj): """ Return the first element in *obj*. - This is an type-independent way of obtaining the first element, supporting - both index access and the iterator protocol. + This is a type-independent way of obtaining the first element, + supporting both index access and the iterator protocol. + """ + return _safe_first_finite(obj, skip_nonfinite=False) + + +def _safe_first_finite(obj, *, skip_nonfinite=True): + """ + Return the first non-None (and optionally finite) element in *obj*. + + This is a method for internal use. + + This is a type-independent way of obtaining the first non-None element, + supporting both index access and the iterator protocol. + The first non-None element will be obtained when skip_none is True. """ - if isinstance(obj, collections.abc.Iterator): - # needed to accept `array.flat` as input. - # np.flatiter reports as an instance of collections.Iterator - # but can still be indexed via []. - # This has the side effect of re-setting the iterator, but - # that is acceptable. + def safe_isfinite(val): + if val is None: + return False try: - return obj[0] + return np.isfinite(val) if np.isscalar(val) else True except TypeError: - pass - raise RuntimeError("matplotlib does not support generators " - "as input") - return next(iter(obj)) + # This is something that numpy can not make heads or tails + # of, assume "finite" + return True + if skip_nonfinite is False: + if isinstance(obj, collections.abc.Iterator): + # needed to accept `array.flat` as input. + # np.flatiter reports as an instance of collections.Iterator + # but can still be indexed via []. + # This has the side effect of re-setting the iterator, but + # that is acceptable. + try: + return obj[0] + except TypeError: + pass + raise RuntimeError("matplotlib does not support generators " + "as input") + return next(iter(obj)) + elif isinstance(obj, np.flatiter): + # TODO do the finite filtering on this + return obj[0] + elif isinstance(obj, collections.abc.Iterator): + raise RuntimeError("matplotlib does not " + "support generators as input") + else: + return next(val for val in obj if safe_isfinite(val)) def sanitize_sequence(data): @@ -1723,24 +1723,10 @@ def sanitize_sequence(data): else data) -@_api.delete_parameter("3.3", "required") -@_api.delete_parameter("3.3", "forbidden") -@_api.delete_parameter("3.3", "allowed") -def normalize_kwargs(kw, alias_mapping=None, required=(), forbidden=(), - allowed=None): +def normalize_kwargs(kw, alias_mapping=None): """ Helper function to normalize kwarg inputs. - The order they are resolved are: - - 1. aliasing - 2. required - 3. forbidden - 4. allowed - - This order means that only the canonical names need appear in - *allowed*, *forbidden*, *required*. - Parameters ---------- kw : dict or None @@ -1749,32 +1735,20 @@ def normalize_kwargs(kw, alias_mapping=None, required=(), forbidden=(), the form ``props=None``. alias_mapping : dict or Artist subclass or Artist instance, optional - A mapping between a canonical name to a list of - aliases, in order of precedence from lowest to highest. + A mapping between a canonical name to a list of aliases, in order of + precedence from lowest to highest. - If the canonical value is not in the list it is assumed to have - the highest priority. + If the canonical value is not in the list it is assumed to have the + highest priority. If an Artist subclass or instance is passed, use its properties alias mapping. - required : list of str, optional - A list of keys that must be in *kws*. This parameter is deprecated. - - forbidden : list of str, optional - A list of keys which may not be in *kw*. This parameter is deprecated. - - allowed : list of str, optional - A list of allowed fields. If this not None, then raise if - *kw* contains any keys not in the union of *required* - and *allowed*. To allow only the required fields pass in - an empty tuple ``allowed=()``. This parameter is deprecated. - Raises ------ TypeError - To match what python raises if invalid args/kwargs are passed to - a callable. + To match what Python raises if invalid arguments/keyword arguments are + passed to a callable. """ from matplotlib.artist import Artist @@ -1802,25 +1776,6 @@ def normalize_kwargs(kw, alias_mapping=None, required=(), forbidden=(), canonical_to_seen[canonical] = k ret[canonical] = v - fail_keys = [k for k in required if k not in ret] - if fail_keys: - raise TypeError("The required keys {keys!r} " - "are not in kwargs".format(keys=fail_keys)) - - fail_keys = [k for k in forbidden if k in ret] - if fail_keys: - raise TypeError("The forbidden keys {keys!r} " - "are in kwargs".format(keys=fail_keys)) - - if allowed is not None: - allowed_set = {*required, *allowed} - fail_keys = [k for k in ret if k not in allowed_set] - if fail_keys: - raise TypeError( - "kwargs contains {keys!r} which are not in the required " - "{req!r} or allowed {allow!r} keys".format( - keys=fail_keys, req=required, allow=allowed)) - return ret @@ -1898,61 +1853,6 @@ def _str_lower_equal(obj, s): return isinstance(obj, str) and obj.lower() == s -def _define_aliases(alias_d, cls=None): - """ - Class decorator for defining property aliases. - - Use as :: - - @cbook._define_aliases({"property": ["alias", ...], ...}) - class C: ... - - For each property, if the corresponding ``get_property`` is defined in the - class so far, an alias named ``get_alias`` will be defined; the same will - be done for setters. If neither the getter nor the setter exists, an - exception will be raised. - - The alias map is stored as the ``_alias_map`` attribute on the class and - can be used by `~.normalize_kwargs` (which assumes that higher priority - aliases come last). - """ - if cls is None: # Return the actual class decorator. - return functools.partial(_define_aliases, alias_d) - - def make_alias(name): # Enforce a closure over *name*. - @functools.wraps(getattr(cls, name)) - def method(self, *args, **kwargs): - return getattr(self, name)(*args, **kwargs) - return method - - for prop, aliases in alias_d.items(): - exists = False - for prefix in ["get_", "set_"]: - if prefix + prop in vars(cls): - exists = True - for alias in aliases: - method = make_alias(prefix + prop) - method.__name__ = prefix + alias - method.__doc__ = "Alias for `{}`.".format(prefix + prop) - setattr(cls, prefix + alias, method) - if not exists: - raise ValueError( - "Neither getter nor setter exists for {!r}".format(prop)) - - def get_aliased_and_aliases(d): - return {*d, *(alias for aliases in d.values() for alias in aliases)} - - preexisting_aliases = getattr(cls, "_alias_map", {}) - conflicting = (get_aliased_and_aliases(preexisting_aliases) - & get_aliased_and_aliases(alias_d)) - if conflicting: - # Need to decide on conflict resolution policy. - raise NotImplementedError( - f"Parent class already defines conflicting aliases: {conflicting}") - cls._alias_map = {**preexisting_aliases, **alias_d} - return cls - - def _array_perimeter(arr): """ Get the elements on the perimeter of *arr*. @@ -1971,7 +1871,7 @@ def _array_perimeter(arr): Examples -------- - >>> i, j = np.ogrid[:3,:4] + >>> i, j = np.ogrid[:3, :4] >>> a = i*10 + j >>> a array([[ 0, 1, 2, 3], @@ -2015,7 +1915,7 @@ def _unfold(arr, axis, size, step): Examples -------- - >>> i, j = np.ogrid[:3,:7] + >>> i, j = np.ogrid[:3, :7] >>> a = i*10 + j >>> a array([[ 0, 1, 2, 3, 4, 5, 6], @@ -2156,7 +2056,7 @@ def discard(self, key): self._od.pop(key, None) -# Agg's buffers are unmultiplied RGBA8888, which neither PyQt4 nor cairo +# Agg's buffers are unmultiplied RGBA8888, which neither PyQt<=5.1 nor cairo # support; however, both do support premultiplied ARGB32. @@ -2287,6 +2187,27 @@ def _format_approx(number, precision): return f'{number:.{precision}f}'.rstrip('0').rstrip('.') or '0' +def _g_sig_digits(value, delta): + """ + Return the number of significant digits to %g-format *value*, assuming that + it is known with an error of *delta*. + """ + if delta == 0: + # delta = 0 may occur when trying to format values over a tiny range; + # in that case, replace it by the distance to the closest float. + delta = abs(np.spacing(value)) + # 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. + # For inf or nan, the precision doesn't matter. + return max( + 0, + (math.floor(math.log10(abs(value))) + 1 if value else 1) + - math.floor(math.log10(delta))) if math.isfinite(value) else 0 + + def _unikey_or_keysym_to_mplkey(unikey, keysym): """ Convert a Unicode key or X keysym to a Matplotlib key name. @@ -2310,3 +2231,101 @@ def _unikey_or_keysym_to_mplkey(unikey, keysym): "next": "pagedown", # Used by tk. }.get(key, key) return key + + +@functools.lru_cache(None) +def _make_class_factory(mixin_class, fmt, attr_name=None): + """ + Return a function that creates picklable classes inheriting from a mixin. + + After :: + + factory = _make_class_factory(FooMixin, fmt, attr_name) + FooAxes = factory(Axes) + + ``Foo`` is a class that inherits from ``FooMixin`` and ``Axes`` and **is + picklable** (picklability is what differentiates this from a plain call to + `type`). Its ``__name__`` is set to ``fmt.format(Axes.__name__)`` and the + base class is stored in the ``attr_name`` attribute, if not None. + + Moreover, the return value of ``factory`` is memoized: calls with the same + ``Axes`` class always return the same subclass. + """ + + @functools.lru_cache(None) + def class_factory(axes_class): + # if we have already wrapped this class, declare victory! + if issubclass(axes_class, mixin_class): + return axes_class + + # The parameter is named "axes_class" for backcompat but is really just + # a base class; no axes semantics are used. + base_class = axes_class + + class subcls(mixin_class, base_class): + # Better approximation than __module__ = "matplotlib.cbook". + __module__ = mixin_class.__module__ + + def __reduce__(self): + return (_picklable_class_constructor, + (mixin_class, fmt, attr_name, base_class), + self.__getstate__()) + + subcls.__name__ = subcls.__qualname__ = fmt.format(base_class.__name__) + if attr_name is not None: + setattr(subcls, attr_name, base_class) + return subcls + + class_factory.__module__ = mixin_class.__module__ + return class_factory + + +def _picklable_class_constructor(mixin_class, fmt, attr_name, base_class): + """Internal helper for _make_class_factory.""" + factory = _make_class_factory(mixin_class, fmt, attr_name) + cls = factory(base_class) + return cls.__new__(cls) + + +def _unpack_to_numpy(x): + """Internal helper to extract data from e.g. pandas and xarray objects.""" + if isinstance(x, np.ndarray): + # If numpy, return directly + return x + if hasattr(x, 'to_numpy'): + # Assume that any function to_numpy() do actually return a numpy array + return x.to_numpy() + if hasattr(x, 'values'): + xtmp = x.values + # For example a dict has a 'values' attribute, but it is not a property + # so in this case we do not want to return a function + if isinstance(xtmp, np.ndarray): + return xtmp + return x + + +def _auto_format_str(fmt, value): + """ + Apply *value* to the format string *fmt*. + + This works both with unnamed %-style formatting and + unnamed {}-style formatting. %-style formatting has priority. + If *fmt* is %-style formattable that will be used. Otherwise, + {}-formatting is applied. Strings without formatting placeholders + are passed through as is. + + Examples + -------- + >>> _auto_format_str('%.2f m', 0.2) + '0.20 m' + >>> _auto_format_str('{} m', 0.2) + '0.2 m' + >>> _auto_format_str('const', 0.2) + 'const' + >>> _auto_format_str('%d or {}', 0.2) + '0 or {}' + """ + try: + return fmt % (value,) + except (TypeError, ValueError): + return fmt.format(value) diff --git a/lib/matplotlib/cbook/deprecation.py b/lib/matplotlib/cbook/deprecation.py deleted file mode 100644 index 5a0f02f7decf..000000000000 --- a/lib/matplotlib/cbook/deprecation.py +++ /dev/null @@ -1,8 +0,0 @@ -# imports are for backward compatibility -from matplotlib._api.deprecation import ( - MatplotlibDeprecationWarning, mplDeprecation, warn_deprecated, deprecated) - -warn_deprecated("3.4", - message="The module matplotlib.cbook.deprecation is " - "considered internal and it will be made private in the " - "future.") diff --git a/lib/matplotlib/cm.py b/lib/matplotlib/cm.py index 376d703da13f..d170b7d6b37f 100644 --- a/lib/matplotlib/cm.py +++ b/lib/matplotlib/cm.py @@ -15,18 +15,19 @@ normalization. """ -from collections.abc import MutableMapping +from collections.abc import Mapping +import functools import numpy as np from numpy import ma import matplotlib as mpl -from matplotlib import _api, colors, cbook +from matplotlib import _api, colors, cbook, scale from matplotlib._cm import datad from matplotlib._cm_listed import cmaps as cmaps_listed -LUTSIZE = mpl.rcParams['image.lut'] +_LUTSIZE = mpl.rcParams['image.lut'] def _gen_cmap_registry(): @@ -37,69 +38,189 @@ def _gen_cmap_registry(): cmap_d = {**cmaps_listed} for name, spec in datad.items(): cmap_d[name] = ( # Precache the cmaps at a fixed lutsize.. - colors.LinearSegmentedColormap(name, spec, LUTSIZE) + colors.LinearSegmentedColormap(name, spec, _LUTSIZE) if 'red' in spec else colors.ListedColormap(spec['listed'], name) if 'listed' in spec else - colors.LinearSegmentedColormap.from_list(name, spec, LUTSIZE)) + colors.LinearSegmentedColormap.from_list(name, spec, _LUTSIZE)) # Generate reversed cmaps. for cmap in list(cmap_d.values()): rmap = cmap.reversed() - cmap._global = True - rmap._global = True cmap_d[rmap.name] = rmap return cmap_d -class _DeprecatedCmapDictWrapper(MutableMapping): - """Dictionary mapping for deprecated _cmap_d access.""" +class ColormapRegistry(Mapping): + r""" + Container for colormaps that are known to Matplotlib by name. - def __init__(self, cmap_registry): - self._cmap_registry = cmap_registry + The universal registry instance is `matplotlib.colormaps`. There should be + no need for users to instantiate `.ColormapRegistry` themselves. - def __delitem__(self, key): - self._warn_deprecated() - self._cmap_registry.__delitem__(key) + Read access uses a dict-like interface mapping names to `.Colormap`\s:: - def __getitem__(self, key): - self._warn_deprecated() - return self._cmap_registry.__getitem__(key) + import matplotlib as mpl + cmap = mpl.colormaps['viridis'] + + Returned `.Colormap`\s are copies, so that their modification does not + change the global definition of the colormap. + + Additional colormaps can be added via `.ColormapRegistry.register`:: + + mpl.colormaps.register(my_colormap) + """ + def __init__(self, cmaps): + self._cmaps = cmaps + self._builtin_cmaps = tuple(cmaps) + # A shim to allow register_cmap() to force an override + self._allow_override_builtin = False + + def __getitem__(self, item): + try: + return self._cmaps[item].copy() + except KeyError: + raise KeyError(f"{item!r} is not a known colormap name") from None def __iter__(self): - self._warn_deprecated() - return self._cmap_registry.__iter__() + return iter(self._cmaps) def __len__(self): - self._warn_deprecated() - return self._cmap_registry.__len__() - - def __setitem__(self, key, val): - self._warn_deprecated() - self._cmap_registry.__setitem__(key, val) - - def get(self, key, default=None): - self._warn_deprecated() - return self._cmap_registry.get(key, default) - - def _warn_deprecated(self): - _api.warn_deprecated( - "3.3", - message="The global colormaps dictionary is no longer " - "considered public API.", - alternative="Please use register_cmap() and get_cmap() to " - "access the contents of the dictionary." - ) + return len(self._cmaps) + + def __str__(self): + return ('ColormapRegistry; available colormaps:\n' + + ', '.join(f"'{name}'" for name in self)) + + def __call__(self): + """ + Return a list of the registered colormap names. + + This exists only for backward-compatibility in `.pyplot` which had a + ``plt.colormaps()`` method. The recommended way to get this list is + now ``list(colormaps)``. + """ + return list(self) + + def register(self, cmap, *, name=None, force=False): + """ + Register a new colormap. + + The colormap name can then be used as a string argument to any ``cmap`` + parameter in Matplotlib. It is also available in ``pyplot.get_cmap``. + + The colormap registry stores a copy of the given colormap, so that + future changes to the original colormap instance do not affect the + registered colormap. Think of this as the registry taking a snapshot + of the colormap at registration. + + Parameters + ---------- + cmap : matplotlib.colors.Colormap + The colormap to register. + + name : str, optional + The name for the colormap. If not given, ``cmap.name`` is used. + + force : bool, default: False + If False, a ValueError is raised if trying to overwrite an already + registered name. True supports overwriting registered colormaps + other than the builtin colormaps. + """ + _api.check_isinstance(colors.Colormap, cmap=cmap) + + name = name or cmap.name + if name in self: + if not force: + # don't allow registering an already existing cmap + # unless explicitly asked to + raise ValueError( + f'A colormap named "{name}" is already registered.') + elif (name in self._builtin_cmaps + and not self._allow_override_builtin): + # We don't allow overriding a builtin unless privately + # coming from register_cmap() + raise ValueError("Re-registering the builtin cmap " + f"{name!r} is not allowed.") + + # Warn that we are updating an already existing colormap + _api.warn_external(f"Overwriting the cmap {name!r} " + "that was already in the registry.") + + self._cmaps[name] = cmap.copy() + + def unregister(self, name): + """ + Remove a colormap from the registry. + + You cannot remove built-in colormaps. + If the named colormap is not registered, returns with no error, raises + if you try to de-register a default colormap. -_cmap_registry = _gen_cmap_registry() -globals().update(_cmap_registry) -# This is no longer considered public API -cmap_d = _DeprecatedCmapDictWrapper(_cmap_registry) -__builtin_cmaps = tuple(_cmap_registry) + .. warning:: -# Continue with definitions ... + Colormap names are currently a shared namespace that may be used + by multiple packages. Use `unregister` only if you know you + have registered that name before. In particular, do not + unregister just in case to clean the name before registering a + new colormap. + + Parameters + ---------- + name : str + The name of the colormap to be removed. + + Raises + ------ + ValueError + If you try to remove a default built-in colormap. + """ + if name in self._builtin_cmaps: + raise ValueError(f"cannot unregister {name!r} which is a builtin " + "colormap.") + self._cmaps.pop(name, None) + + def get_cmap(self, cmap): + """ + Return a color map specified through *cmap*. + + Parameters + ---------- + cmap : str or `~matplotlib.colors.Colormap` or None + + - if a `.Colormap`, return it + - if a string, look it up in ``mpl.colormaps`` + - if None, return the Colormap defined in :rc:`image.cmap` + + Returns + ------- + Colormap + """ + # get the default color map + if cmap is None: + return self[mpl.rcParams["image.cmap"]] + + # if the user passed in a Colormap, simply return it + if isinstance(cmap, colors.Colormap): + return cmap + if isinstance(cmap, str): + _api.check_in_list(sorted(_colormaps), cmap=cmap) + # otherwise, it must be a string so look it up + return self[cmap] + raise TypeError( + 'get_cmap expects None or an instance of a str or Colormap . ' + + f'you passed {cmap!r} of type {type(cmap)}' + ) + + +# public access to the colormaps should be via `matplotlib.colormaps`. For now, +# we still create the registry here, but that should stay an implementation +# detail. +_colormaps = ColormapRegistry(_gen_cmap_registry()) +globals().update(_colormaps) +@_api.deprecated("3.7", alternative="``matplotlib.colormaps.register(name)``") def register_cmap(name=None, cmap=None, *, override_builtin=False): """ Add a colormap to the set recognized by :func:`get_cmap`. @@ -127,14 +248,6 @@ def register_cmap(name=None, cmap=None, *, override_builtin=False): colormap. Please do not use this unless you are sure you need it. - - Notes - ----- - Registering a colormap stores a reference to the colormap object - which can currently be modified and inadvertently change the global - colormap state. This behavior is deprecated and in Matplotlib 3.5 - the registered colormap will be immutable. - """ _api.check_isinstance((str, None), name=name) if name is None: @@ -143,36 +256,18 @@ def register_cmap(name=None, cmap=None, *, override_builtin=False): except AttributeError as err: raise ValueError("Arguments must include a name or a " "Colormap") from err - if name in _cmap_registry: - if not override_builtin and name in __builtin_cmaps: - msg = f"Trying to re-register the builtin cmap {name!r}." - raise ValueError(msg) - else: - msg = f"Trying to register the cmap {name!r} which already exists." - _api.warn_external(msg) - - if not isinstance(cmap, colors.Colormap): - raise ValueError("You must pass a Colormap instance. " - f"You passed {cmap} a {type(cmap)} object.") + # override_builtin is allowed here for backward compatibility + # this is just a shim to enable that to work privately in + # the global ColormapRegistry + _colormaps._allow_override_builtin = override_builtin + _colormaps.register(cmap, name=name, force=override_builtin) + _colormaps._allow_override_builtin = False - cmap._global = True - _cmap_registry[name] = cmap - return - -def get_cmap(name=None, lut=None): +def _get_cmap(name=None, lut=None): """ Get a colormap instance, defaulting to rc values if *name* is None. - Colormaps added with :func:`register_cmap` take precedence over - built-in colormaps. - - Notes - ----- - Currently, this returns the global colormap object, which is deprecated. - In Matplotlib 3.5, you will no longer be able to modify the global - colormaps in-place. - Parameters ---------- name : `matplotlib.colors.Colormap` or str or None, default: None @@ -182,18 +277,35 @@ def get_cmap(name=None, lut=None): lut : int or None, default: None If *name* is not already a Colormap instance and *lut* is not None, the colormap will be resampled to have *lut* entries in the lookup table. + + Returns + ------- + Colormap """ if name is None: name = mpl.rcParams['image.cmap'] if isinstance(name, colors.Colormap): return name - _api.check_in_list(sorted(_cmap_registry), name=name) + _api.check_in_list(sorted(_colormaps), name=name) if lut is None: - return _cmap_registry[name] + return _colormaps[name] else: - return _cmap_registry[name]._resample(lut) - - + return _colormaps[name].resampled(lut) + +# do it in two steps like this so we can have an un-deprecated version in +# pyplot. +get_cmap = _api.deprecated( + '3.7', + name='get_cmap', + alternative=( + "``matplotlib.colormaps[name]`` " + + "or ``matplotlib.colormaps.get_cmap(obj)``" + ) +)(_get_cmap) + + +@_api.deprecated("3.7", + alternative="``matplotlib.colormaps.unregister(name)``") def unregister_cmap(name): """ Remove a colormap recognized by :func:`get_cmap`. @@ -203,7 +315,7 @@ def unregister_cmap(name): If the named colormap is not registered, returns with no error, raises if you try to de-register a default colormap. - .. warning :: + .. warning:: Colormap names are currently a shared namespace that may be used by multiple packages. Use `unregister_cmap` only if you know you @@ -225,14 +337,38 @@ def unregister_cmap(name): ------ ValueError If you try to de-register a default built-in colormap. + """ + cmap = _colormaps.get(name, None) + _colormaps.unregister(name) + return cmap + +def _auto_norm_from_scale(scale_cls): + """ + Automatically generate a norm class from *scale_cls*. + + This differs from `.colors.make_norm_from_scale` in the following points: + + - This function is not a class decorator, but directly returns a norm class + (as if decorating `.Normalize`). + - The scale is automatically constructed with ``nonpositive="mask"``, if it + supports such a parameter, to work around the difference in defaults + between standard scales (which use "clip") and norms (which use "mask"). + + Note that ``make_norm_from_scale`` caches the generated norm classes + (not the instances) and reuses them for later calls. For example, + ``type(_auto_norm_from_scale("log")) == LogNorm``. """ - if name not in _cmap_registry: - return - if name in __builtin_cmaps: - raise ValueError(f"cannot unregister {name!r} which is a builtin " - "colormap.") - return _cmap_registry.pop(name) + # Actually try to construct an instance, to verify whether + # ``nonpositive="mask"`` is supported. + try: + norm = colors.make_norm_from_scale( + functools.partial(scale_cls, nonpositive="mask"))( + colors.Normalize)() + except TypeError: + norm = colors.make_norm_from_scale(scale_cls)( + colors.Normalize)() + return type(norm) class ScalarMappable: @@ -245,26 +381,26 @@ class ScalarMappable: def __init__(self, norm=None, cmap=None): """ - Parameters ---------- - norm : `matplotlib.colors.Normalize` (or subclass thereof) + norm : `.Normalize` (or subclass thereof) or str or None The normalizing object which scales data, typically into the interval ``[0, 1]``. + If a `str`, a `.Normalize` subclass is dynamically generated based + on the scale with the corresponding name. If *None*, *norm* defaults to a *colors.Normalize* object which initializes its scaling based on the first data processed. cmap : str or `~matplotlib.colors.Colormap` The colormap used to map normalized data values to RGBA colors. """ self._A = None - self.norm = None # So that the setter knows we're initializing. + self._norm = None # So that the setter knows we're initializing. self.set_norm(norm) # The Normalize instance of this ScalarMappable. self.cmap = None # So that the setter knows we're initializing. self.set_cmap(cmap) # The Colormap instance of this ScalarMappable. #: The last colorbar associated with this ScalarMappable. May be None. self.colorbar = None - self.callbacksSM = cbook.CallbackRegistry() - self._update_dict = {'array': False} + self.callbacks = cbook.CallbackRegistry(signals=["changed"]) def _scale_norm(self, norm, vmin, vmax): """ @@ -278,13 +414,11 @@ def _scale_norm(self, norm, vmin, vmax): """ if vmin is not None or vmax is not None: self.set_clim(vmin, vmax) - if norm is not None: - _api.warn_deprecated( - "3.3", - message="Passing parameters norm and vmin/vmax " - "simultaneously is deprecated since %(since)s and " - "will become an error %(removal)s. Please pass " - "vmin/vmax directly to the norm when creating it.") + if isinstance(norm, colors.Normalize): + raise ValueError( + "Passing a Normalize instance simultaneously with " + "vmin/vmax is not supported. Please pass vmin/vmax " + "directly to the norm when creating it.") # always resolve the autoscaling so we have concrete limits # rather than deferring to draw time. @@ -295,12 +429,12 @@ def to_rgba(self, x, alpha=None, bytes=False, norm=True): Return a normalized rgba array corresponding to *x*. In the normal case, *x* is a 1D or 2D sequence of scalars, and - the corresponding ndarray of rgba values will be returned, + the corresponding `~numpy.ndarray` of rgba values will be returned, based on the norm and colormap set for this ScalarMappable. There is one special case, for handling images that are already rgb or rgba, such as might have been read from an image file. - If *x* is an ndarray with 3 dimensions, + If *x* is an `~numpy.ndarray` with 3 dimensions, and the last dimension is either 3 or 4, then it will be treated as an rgb or rgba array, and no mapping will be done. The array can be uint8, or it can be floating point with @@ -309,7 +443,7 @@ def to_rgba(self, x, alpha=None, bytes=False, norm=True): If the last dimension is 3, the *alpha* kwarg (defaulting to 1) will be used to fill in the transparency. If the last dimension is 4, the *alpha* kwarg is ignored; it does not - replace the pre-existing alpha. A ValueError will be raised + replace the preexisting alpha. A ValueError will be raised if the third dimension is other than 3 or 4. In either case, if *bytes* is *False* (default), the rgba @@ -362,17 +496,34 @@ def to_rgba(self, x, alpha=None, bytes=False, norm=True): def set_array(self, A): """ - Set the image array from numpy array *A*. + Set the value array from array-like *A*. Parameters ---------- - A : ndarray or None + A : array-like or None + The values that are mapped to colors. + + The base class `.ScalarMappable` does not make any assumptions on + the dimensionality and shape of the value array *A*. """ + if A is None: + self._A = None + return + + A = cbook.safe_masked_invalid(A, copy=True) + if not np.can_cast(A.dtype, float, "same_kind"): + raise TypeError(f"Image data of dtype {A.dtype} cannot be " + "converted to float") + self._A = A - self._update_dict['array'] = True def get_array(self): - """Return the data array.""" + """ + Return the array of values, that are mapped to colors. + + The base class `.ScalarMappable` does not make any assumptions on + the dimensionality and shape of the array. + """ return self._A def get_cmap(self): @@ -399,6 +550,8 @@ def set_clim(self, vmin=None, vmax=None): .. ACCEPTS: (vmin: float, vmax: float) """ + # If the norm's limits are updated self.changed() will be called + # through the callbacks attached to the norm if vmax is None: try: vmin, vmax = vmin @@ -408,7 +561,6 @@ def set_clim(self, vmin=None, vmax=None): self.norm.vmin = colors._sanitize_extrema(vmin) if vmax is not None: self.norm.vmax = colors._sanitize_extrema(vmax) - self.changed() def get_alpha(self): """ @@ -429,18 +581,51 @@ def set_cmap(self, cmap): cmap : `.Colormap` or str or None """ in_init = self.cmap is None - cmap = get_cmap(cmap) - self.cmap = cmap + + self.cmap = _ensure_cmap(cmap) if not in_init: self.changed() # Things are not set up properly yet. + @property + def norm(self): + return self._norm + + @norm.setter + def norm(self, norm): + _api.check_isinstance((colors.Normalize, str, None), norm=norm) + if norm is None: + norm = colors.Normalize() + elif isinstance(norm, str): + try: + scale_cls = scale._scale_mapping[norm] + except KeyError: + raise ValueError( + "Invalid norm str name; the following values are " + "supported: {}".format(", ".join(scale._scale_mapping)) + ) from None + norm = _auto_norm_from_scale(scale_cls)() + + if norm is self.norm: + # We aren't updating anything + return + + in_init = self.norm is None + # Remove the current callback and connect to the new one + if not in_init: + self.norm.callbacks.disconnect(self._id_norm) + self._norm = norm + self._id_norm = self.norm.callbacks.connect('changed', + self.changed) + if not in_init: + self.changed() + def set_norm(self, norm): """ Set the normalization instance. Parameters ---------- - norm : `.Normalize` or None + norm : `.Normalize` or str or None Notes ----- @@ -448,13 +633,7 @@ def set_norm(self, norm): the norm of the mappable will reset the norm, locator, and formatters on the colorbar to default. """ - _api.check_isinstance((colors.Normalize, None), norm=norm) - in_init = self.norm is None - if norm is None: - norm = colors.Normalize() self.norm = norm - if not in_init: - self.changed() # Things are not set up properly yet. def autoscale(self): """ @@ -463,8 +642,9 @@ def autoscale(self): """ if self._A is None: raise TypeError('You must first set_array for mappable') + # If the norm's limits are updated self.changed() will be called + # through the callbacks attached to the norm self.norm.autoscale(self._A) - self.changed() def autoscale_None(self): """ @@ -473,39 +653,72 @@ def autoscale_None(self): """ if self._A is None: raise TypeError('You must first set_array for mappable') + # If the norm's limits are updated self.changed() will be called + # through the callbacks attached to the norm self.norm.autoscale_None(self._A) - self.changed() - - def _add_checker(self, checker): - """ - Add an entry to a dictionary of boolean flags - that are set to True when the mappable is changed. - """ - self._update_dict[checker] = False - - def _check_update(self, checker): - """Return whether mappable has changed since the last check.""" - if self._update_dict[checker]: - self._update_dict[checker] = False - return True - return False def changed(self): """ Call this whenever the mappable is changed to notify all the callbackSM listeners to the 'changed' signal. """ - self.callbacksSM.process('changed', self) - for key in self._update_dict: - self._update_dict[key] = True + self.callbacks.process('changed', self) self.stale = True - update_dict = _api.deprecate_privatize_attribute("3.3") - @_api.deprecated("3.3") - def add_checker(self, checker): - return self._add_checker(checker) +# The docstrings here must be generic enough to apply to all relevant methods. +mpl._docstring.interpd.update( + cmap_doc="""\ +cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` + The Colormap instance or registered colormap name used to map scalar data + to colors.""", + norm_doc="""\ +norm : str or `~matplotlib.colors.Normalize`, optional + The normalization method used to scale scalar data to the [0, 1] range + before mapping to colors using *cmap*. By default, a linear scaling is + used, mapping the lowest value to 0 and the highest to 1. + + If given, this can be one of the following: + + - An instance of `.Normalize` or one of its subclasses + (see :doc:`/tutorials/colors/colormapnorms`). + - A scale name, i.e. one of "linear", "log", "symlog", "logit", etc. For a + list of available scales, call `matplotlib.scale.get_scale_names()`. + In that case, a suitable `.Normalize` subclass is dynamically generated + and instantiated.""", + vmin_vmax_doc="""\ +vmin, vmax : float, optional + When using scalar data and no explicit *norm*, *vmin* and *vmax* define + the data range that the colormap covers. By default, the colormap covers + the complete value range of the supplied data. It is an error to use + *vmin*/*vmax* when a *norm* instance is given (but using a `str` *norm* + name together with *vmin*/*vmax* is acceptable).""", +) + + +def _ensure_cmap(cmap): + """ + Ensure that we have a `.Colormap` object. + + For internal use to preserve type stability of errors. + + Parameters + ---------- + cmap : None, str, Colormap - @_api.deprecated("3.3") - def check_update(self, checker): - return self._check_update(checker) + - if a `Colormap`, return it + - if a string, look it up in mpl.colormaps + - if None, look up the default color map in mpl.colormaps + + Returns + ------- + Colormap + + """ + if isinstance(cmap, colors.Colormap): + return cmap + cmap_name = cmap if cmap is not None else mpl.rcParams["image.cmap"] + # use check_in_list to ensure type stability of the exception raised by + # the internal usage of this (ValueError vs KeyError) + _api.check_in_list(sorted(_colormaps), cmap=cmap_name) + return mpl.colormaps[cmap_name] diff --git a/lib/matplotlib/collections.py b/lib/matplotlib/collections.py index 0213f3cf1633..bf88dd2b68a3 100644 --- a/lib/matplotlib/collections.py +++ b/lib/matplotlib/collections.py @@ -11,23 +11,25 @@ import math from numbers import Number +import warnings + import numpy as np import matplotlib as mpl -from . import (_api, _path, artist, cbook, cm, colors as mcolors, docstring, +from . import (_api, _path, artist, cbook, cm, colors as mcolors, _docstring, hatch as mhatch, lines as mlines, path as mpath, transforms) from ._enums import JoinStyle, CapStyle -import warnings # "color" is excluded; it is a compound setter, and its docstring differs # in LineCollection. -@cbook._define_aliases({ +@_api.define_aliases({ "antialiased": ["antialiaseds", "aa"], "edgecolor": ["edgecolors", "ec"], "facecolor": ["facecolors", "fc"], "linestyle": ["linestyles", "dashes", "ls"], "linewidth": ["linewidths", "lw"], + "offset_transform": ["transOffset"], }) class Collection(artist.Artist, cm.ScalarMappable): r""" @@ -59,8 +61,6 @@ class Collection(artist.Artist, cm.ScalarMappable): mappable will be used to set the ``facecolors`` and ``edgecolors``, ignoring those that were manually passed in. """ - _offsets = np.zeros((0, 2)) - _transOffset = transforms.IdentityTransform() #: Either a list of 3x3 arrays or an Nx3x3 array (representing N #: transforms), suitable for the `all_transforms` argument to #: `~matplotlib.backend_bases.RendererBase.draw_path_collection`; @@ -73,8 +73,8 @@ class Collection(artist.Artist, cm.ScalarMappable): # subclass-by-subclass basis. _edge_default = False - @_api.delete_parameter("3.3", "offset_position") - @docstring.interpd + @_docstring.interpd + @_api.make_keyword_only("3.6", name="edgecolors") def __init__(self, edgecolors=None, facecolors=None, @@ -84,13 +84,13 @@ def __init__(self, joinstyle=None, antialiaseds=None, offsets=None, - transOffset=None, + offset_transform=None, norm=None, # optional for ScalarMappable cmap=None, # ditto pickradius=5.0, hatch=None, urls=None, - offset_position='screen', + *, zorder=1, **kwargs ): @@ -127,19 +127,12 @@ def __init__(self, A vector by which to translate each patch after rendering (default is no translation). The translation is performed in screen (pixel) coordinates (i.e. after the Artist's transform is applied). - transOffset : `~.transforms.Transform`, default: `.IdentityTransform` + offset_transform : `~.Transform`, default: `.IdentityTransform` A single transform which will be applied to each *offsets* vector before it is used. - offset_position : {{'screen' (default), 'data' (deprecated)}} - If set to 'data' (deprecated), *offsets* will be treated as if it - is in data coordinates instead of in screen coordinates. - norm : `~.colors.Normalize`, optional - Forwarded to `.ScalarMappable`. The default of - ``None`` means that the first draw call will set ``vmin`` and - ``vmax`` using the minimum and maximum values of the data. - cmap : `~.colors.Colormap`, optional - Forwarded to `.ScalarMappable`. The default of - ``None`` will result in :rc:`image.cmap` being used. + cmap, norm + Data normalization and colormapping parameters. See + `.ScalarMappable` for a detailed description. hatch : str, optional Hatching pattern to use in filled paths, if any. Valid strings are ['/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*']. See @@ -183,9 +176,6 @@ def __init__(self, self.set_pickradius(pickradius) self.set_urls(urls) self.set_hatch(hatch) - self._offset_position = "screen" - if offset_position != "screen": - self.set_offset_position(offset_position) # emit deprecation. self.set_zorder(zorder) if capstyle: @@ -198,40 +188,48 @@ def __init__(self, else: self._joinstyle = None - self._offsets = np.zeros((1, 2)) - # save if offsets passed in were none... - self._offsetsNone = offsets is None - self._uniform_offsets = None if offsets is not None: offsets = np.asanyarray(offsets, float) # Broadcast (2,) -> (1, 2) but nothing else. if offsets.shape == (2,): offsets = offsets[None, :] - if transOffset is not None: - self._offsets = offsets - self._transOffset = transOffset - else: - self._uniform_offsets = offsets + + self._offsets = offsets + self._offset_transform = offset_transform self._path_effects = None - self.update(kwargs) + self._internal_update(kwargs) self._paths = None def get_paths(self): return self._paths - def set_paths(self): + def set_paths(self, paths): raise NotImplementedError def get_transforms(self): return self._transforms def get_offset_transform(self): - t = self._transOffset - if (not isinstance(t, transforms.Transform) - and hasattr(t, '_as_mpl_transform')): - t = t._as_mpl_transform(self.axes) - return t + """Return the `.Transform` instance used by this artist offset.""" + if self._offset_transform is None: + self._offset_transform = transforms.IdentityTransform() + elif (not isinstance(self._offset_transform, transforms.Transform) + and hasattr(self._offset_transform, '_as_mpl_transform')): + self._offset_transform = \ + self._offset_transform._as_mpl_transform(self.axes) + return self._offset_transform + + @_api.rename_parameter("3.6", "transOffset", "offset_transform") + def set_offset_transform(self, offset_transform): + """ + Set the artist offset transform. + + Parameters + ---------- + offset_transform : `.Transform` + """ + self._offset_transform = offset_transform def get_datalim(self, transData): # Calculate the data limits and return them as a `.Bbox`. @@ -251,15 +249,18 @@ def get_datalim(self, transData): # 3. otherwise return a null Bbox. transform = self.get_transform() - transOffset = self.get_offset_transform() - if (not self._offsetsNone and - not transOffset.contains_branch(transData)): - # if there are offsets but in some coords other than data, + offset_trf = self.get_offset_transform() + if not (isinstance(offset_trf, transforms.IdentityTransform) + or offset_trf.contains_branch(transData)): + # if the offsets are in some coords other than data, # then don't use them for autoscaling. return transforms.Bbox.null() - offsets = self._offsets + offsets = self.get_offsets() paths = self.get_paths() + if not len(paths): + # No paths to transform + return transforms.Bbox.null() if not transform.is_affine: paths = [transform.transform_path_non_affine(p) for p in paths] @@ -268,38 +269,37 @@ def get_datalim(self, transData): # transforms.get_affine().contains_branch(transData). But later, # be careful to only apply the affine part that remains. - if isinstance(offsets, np.ma.MaskedArray): - offsets = offsets.filled(np.nan) - # get_path_collection_extents handles nan but not masked arrays - - if len(paths) and len(offsets): - if any(transform.contains_branch_seperately(transData)): - # collections that are just in data units (like quiver) - # can properly have the axes limits set by their shape + - # offset. LineCollections that have no offsets can - # also use this algorithm (like streamplot). - return mpath.get_path_collection_extents( - transform.get_affine() - transData, paths, - self.get_transforms(), - transOffset.transform_non_affine(offsets), - transOffset.get_affine().frozen()) - if not self._offsetsNone: - # this is for collections that have their paths (shapes) - # in physical, axes-relative, or figure-relative units - # (i.e. like scatter). We can't uniquely set limits based on - # those shapes, so we just set the limits based on their - # location. - - offsets = (transOffset - transData).transform(offsets) - # note A-B means A B^{-1} - offsets = np.ma.masked_invalid(offsets) - if not offsets.mask.all(): - bbox = transforms.Bbox.null() - bbox.update_from_data_xy(offsets) - return bbox + if any(transform.contains_branch_seperately(transData)): + # collections that are just in data units (like quiver) + # can properly have the axes limits set by their shape + + # offset. LineCollections that have no offsets can + # also use this algorithm (like streamplot). + if isinstance(offsets, np.ma.MaskedArray): + offsets = offsets.filled(np.nan) + # get_path_collection_extents handles nan but not masked arrays + return mpath.get_path_collection_extents( + transform.get_affine() - transData, paths, + self.get_transforms(), + offset_trf.transform_non_affine(offsets), + offset_trf.get_affine().frozen()) + + # NOTE: None is the default case where no offsets were passed in + if self._offsets is not None: + # this is for collections that have their paths (shapes) + # in physical, axes-relative, or figure-relative units + # (i.e. like scatter). We can't uniquely set limits based on + # those shapes, so we just set the limits based on their + # location. + offsets = (offset_trf - transData).transform(offsets) + # note A-B means A B^{-1} + offsets = np.ma.masked_invalid(offsets) + if not offsets.mask.all(): + bbox = transforms.Bbox.null() + bbox.update_from_data_xy(offsets) + return bbox return transforms.Bbox.null() - def get_window_extent(self, renderer): + def get_window_extent(self, renderer=None): # TODO: check to ensure that this does not fail for # cases other than scatter plot legend return self.get_datalim(transforms.IdentityTransform()) @@ -308,8 +308,8 @@ def _prepare_points(self): # Helper for drawing and hit testing. transform = self.get_transform() - transOffset = self.get_offset_transform() - offsets = self._offsets + offset_trf = self.get_offset_transform() + offsets = self.get_offsets() paths = self.get_paths() if self.have_units(): @@ -320,26 +320,25 @@ def _prepare_points(self): xs = self.convert_xunits(xs) ys = self.convert_yunits(ys) paths.append(mpath.Path(np.column_stack([xs, ys]), path.codes)) - if offsets.size: - xs = self.convert_xunits(offsets[:, 0]) - ys = self.convert_yunits(offsets[:, 1]) - offsets = np.column_stack([xs, ys]) + xs = self.convert_xunits(offsets[:, 0]) + ys = self.convert_yunits(offsets[:, 1]) + offsets = np.ma.column_stack([xs, ys]) if not transform.is_affine: paths = [transform.transform_path_non_affine(path) for path in paths] transform = transform.get_affine() - if not transOffset.is_affine: - offsets = transOffset.transform_non_affine(offsets) + if not offset_trf.is_affine: + offsets = offset_trf.transform_non_affine(offsets) # This might have changed an ndarray into a masked array. - transOffset = transOffset.get_affine() + offset_trf = offset_trf.get_affine() if isinstance(offsets, np.ma.MaskedArray): offsets = offsets.filled(np.nan) # Changing from a masked array to nan-filled ndarray # is probably most efficient at this point. - return transform, transOffset, offsets, paths + return transform, offset_trf, offsets, paths @artist.allow_rasterization def draw(self, renderer): @@ -349,7 +348,7 @@ def draw(self, renderer): self.update_scalarmappable() - transform, transOffset, offsets, paths = self._prepare_points() + transform, offset_trf, offsets, paths = self._prepare_points() gc = renderer.new_gc() self._set_gc_clip(gc) @@ -405,30 +404,31 @@ def draw(self, renderer): gc.set_url(self._urls[0]) renderer.draw_markers( gc, paths[0], combined_transform.frozen(), - mpath.Path(offsets), transOffset, tuple(facecolors[0])) + mpath.Path(offsets), offset_trf, tuple(facecolors[0])) else: renderer.draw_path_collection( gc, transform.frozen(), paths, - self.get_transforms(), offsets, transOffset, + self.get_transforms(), offsets, offset_trf, self.get_facecolor(), self.get_edgecolor(), self._linewidths, self._linestyles, self._antialiaseds, self._urls, - self._offset_position) + "screen") # offset_position, kept for backcompat. gc.restore() renderer.close_group(self.__class__.__name__) self.stale = False - def set_pickradius(self, pr): + @_api.rename_parameter("3.6", "pr", "pickradius") + def set_pickradius(self, pickradius): """ Set the pick radius used for containment tests. Parameters ---------- - pr : float + pickradius : float Pick radius, in points. """ - self._pickradius = pr + self._pickradius = pickradius def get_pickradius(self): return self._pickradius @@ -456,7 +456,7 @@ def contains(self, mouseevent): if self.axes: self.axes._unstale_viewLim() - transform, transOffset, offsets, paths = self._prepare_points() + transform, offset_trf, offsets, paths = self._prepare_points() # Tests if the point is contained on one of the polygons formed # by the control points of each of the paths. A point is considered @@ -466,8 +466,7 @@ def contains(self, mouseevent): ind = _path.point_in_path_collection( mouseevent.x, mouseevent.y, pickradius, transform.frozen(), paths, self.get_transforms(), - offsets, transOffset, pickradius <= 0, - self._offset_position) + offsets, offset_trf, pickradius <= 0) return len(ind) > 0, dict(ind=ind) @@ -543,52 +542,20 @@ def set_offsets(self, offsets): ---------- offsets : (N, 2) or (2,) array-like """ - offsets = np.asanyarray(offsets, float) + offsets = np.asanyarray(offsets) if offsets.shape == (2,): # Broadcast (2,) -> (1, 2) but nothing else. offsets = offsets[None, :] - # This decision is based on how they are initialized above in __init__. - if self._uniform_offsets is None: - self._offsets = offsets - else: - self._uniform_offsets = offsets + cstack = (np.ma.column_stack if isinstance(offsets, np.ma.MaskedArray) + else np.column_stack) + self._offsets = cstack( + (np.asanyarray(self.convert_xunits(offsets[:, 0]), float), + np.asanyarray(self.convert_yunits(offsets[:, 1]), float))) self.stale = True def get_offsets(self): """Return the offsets for the collection.""" - # This decision is based on how they are initialized above in __init__. - if self._uniform_offsets is None: - return self._offsets - else: - return self._uniform_offsets - - @_api.deprecated("3.3") - def set_offset_position(self, offset_position): - """ - Set how offsets are applied. If *offset_position* is 'screen' - (default) the offset is applied after the master transform has - been applied, that is, the offsets are in screen coordinates. - If offset_position is 'data', the offset is applied before the - master transform, i.e., the offsets are in data coordinates. - - Parameters - ---------- - offset_position : {'screen', 'data'} - """ - _api.check_in_list(['screen', 'data'], offset_position=offset_position) - self._offset_position = offset_position - self.stale = True - - @_api.deprecated("3.3") - def get_offset_position(self): - """ - Return how offsets are applied for the collection. If - *offset_position* is 'screen', the offset is applied after the - master transform has been applied, that is, the offsets are in - screen coordinates. If offset_position is 'data', the offset - is applied before the master transform, i.e., the offsets are - in data coordinates. - """ - return self._offset_position + # Default to zeros in the no-offset (None) case + return np.zeros((1, 2)) if self._offsets is None else self._offsets def _get_default_linewidth(self): # This may be overridden in a subclass. @@ -607,7 +574,7 @@ def set_linewidth(self, lw): if lw is None: lw = self._get_default_linewidth() # get the un-scaled/broadcast lw - self._us_lw = np.atleast_1d(np.asarray(lw)) + self._us_lw = np.atleast_1d(lw) # scale all of the dash patterns. self._linewidths, self._linestyles = self._bcast_lwls( @@ -641,18 +608,13 @@ def set_linestyle(self, ls): complete description. """ try: - if isinstance(ls, str): - ls = cbook.ls_mapper.get(ls, ls) - dashes = [mlines._get_dash_pattern(ls)] - else: - try: - dashes = [mlines._get_dash_pattern(ls)] - except ValueError: - dashes = [mlines._get_dash_pattern(x) for x in ls] - - except ValueError as err: - raise ValueError('Do not know how to convert {!r} to ' - 'dashes'.format(ls)) from err + dashes = [mlines._get_dash_pattern(ls)] + except ValueError: + try: + dashes = [mlines._get_dash_pattern(x) for x in ls] + except ValueError as err: + emsg = f'Do not know how to convert {ls!r} to dashes' + raise ValueError(emsg) from err # get the list of raw 'unscaled' dash patterns self._us_linestyles = dashes @@ -661,7 +623,7 @@ def set_linestyle(self, ls): self._linewidths, self._linestyles = self._bcast_lwls( self._us_lw, self._us_linestyles) - @docstring.interpd + @_docstring.interpd def set_capstyle(self, cs): """ Set the `.CapStyle` for the collection (for all its elements). @@ -675,7 +637,7 @@ def set_capstyle(self, cs): def get_capstyle(self): return self._capstyle.name - @docstring.interpd + @_docstring.interpd def set_joinstyle(self, js): """ Set the `.JoinStyle` for the collection (for all its elements). @@ -721,7 +683,7 @@ def _bcast_lwls(linewidths, dashes): dashes = list(dashes) * (l_lw // gcd) linewidths = list(linewidths) * (l_dashes // gcd) - # scale the dash patters + # scale the dash patterns dashes = [mlines._scale_dashes(o, d, lw) for (o, d), lw in zip(dashes, linewidths)] @@ -750,7 +712,7 @@ def set_color(self, c): Parameters ---------- - c : color or list of rgba tuples + c : color or list of RGBA tuples See Also -------- @@ -851,7 +813,6 @@ def set_alpha(self, alpha): supported. """ artist.Artist._set_alpha_for_array(self, alpha) - self._update_dict['array'] = True self._set_facecolor(self._original_facecolor) self._set_edgecolor(self._original_edgecolor) @@ -907,7 +868,7 @@ def update_scalarmappable(self): if not self._set_mappable_flags(): return # Allow possibility to call 'self.set_array(None)'. - if self._check_update("array") and self._A is not None: + if self._A is not None: # QuadMesh can map 2d arrays (but pcolormesh supplies 1d array) if self._A.ndim > 1 and not isinstance(self, QuadMesh): raise ValueError('Collections can only map rank 1 arrays') @@ -961,7 +922,6 @@ def update_from(self, other): self._A = other._A self.norm = other.norm self.cmap = other.cmap - self._update_dict = other._update_dict.copy() self.stale = True @@ -988,7 +948,7 @@ def set_sizes(self, sizes, dpi=72.0): Parameters ---------- - sizes : ndarray or None + sizes : `numpy.ndarray` or None The size to set for each element of the collection. The value is the 'area' of the element. dpi : float, default: 72 @@ -1081,8 +1041,8 @@ def legend_elements(self, prop="colors", num="auto", Finally, a `~.ticker.Locator` can be provided. fmt : str, `~matplotlib.ticker.Formatter`, or None (default) The format or formatter to use for the labels. If a string must be - a valid input for a `~.StrMethodFormatter`. If None (the default), - use a `~.ScalarFormatter`. + a valid input for a `.StrMethodFormatter`. If None (the default), + use a `.ScalarFormatter`. func : function, default: ``lambda x: x`` Function to calculate the labels. Often the size (or color) argument to `~.Axes.scatter` will have been pre-processed by the @@ -1130,7 +1090,9 @@ def legend_elements(self, prop="colors", num="auto", raise ValueError("Valid values for `prop` are 'colors' or " f"'sizes'. You supplied '{prop}' instead.") - fmt.set_bounds(func(u).min(), func(u).max()) + fu = func(u) + fmt.axis.set_view_interval(fu.min(), fu.max()) + fmt.axis.set_data_interval(fu.min(), fu.max()) if num == "auto": num = 9 if len(u) <= num: @@ -1160,9 +1122,9 @@ def legend_elements(self, prop="colors", num="auto", ix = np.argsort(xarr) values = np.interp(label_values, xarr[ix], yarr[ix]) - kw = dict(markeredgewidth=self.get_linewidths()[0], - alpha=self.get_alpha()) - kw.update(kwargs) + kw = {"markeredgewidth": self.get_linewidths()[0], + "alpha": self.get_alpha(), + **kwargs} for val, lab in zip(values, label_values): if prop == "colors": @@ -1183,6 +1145,8 @@ def legend_elements(self, prop="colors", num="auto", class PolyCollection(_CollectionWithSizes): + + @_api.make_keyword_only("3.6", name="closed") def __init__(self, verts, sizes=None, closed=True, **kwargs): """ Parameters @@ -1246,11 +1210,7 @@ def set_verts(self, verts, closed=True): self._paths = [] for xy in verts: if len(xy): - if isinstance(xy, np.ma.MaskedArray): - xy = np.ma.concatenate([xy, xy[:1]]) - else: - xy = np.concatenate([xy, xy[:1]]) - self._paths.append(mpath.Path(xy, closed=True)) + self._paths.append(mpath.Path._create_closed(xy)) else: self._paths.append(mpath.Path(xy)) @@ -1261,15 +1221,30 @@ def set_verts_and_codes(self, verts, codes): if len(verts) != len(codes): raise ValueError("'codes' must be a 1D list or array " "with the same length of 'verts'") - self._paths = [] - for xy, cds in zip(verts, codes): - if len(xy): - self._paths.append(mpath.Path(xy, cds)) - else: - self._paths.append(mpath.Path(xy)) + self._paths = [mpath.Path(xy, cds) if len(xy) else mpath.Path(xy) + for xy, cds in zip(verts, codes)] self.stale = True + @classmethod + @_api.deprecated("3.7", alternative="fill_between") + def span_where(cls, x, ymin, ymax, where, **kwargs): + """ + Return a `.BrokenBarHCollection` that plots horizontal bars from + over the regions in *x* where *where* is True. The bars range + on the y-axis from *ymin* to *ymax* + + *kwargs* are passed on to the collection. + """ + xranges = [] + for ind0, ind1 in cbook.contiguous_regions(where): + xslice = x[ind0:ind1] + if not len(xslice): + continue + xranges.append((xslice[0], xslice[-1] - xslice[0])) + return BrokenBarHCollection(xranges, [ymin, ymax - ymin], **kwargs) + +@_api.deprecated("3.7") class BrokenBarHCollection(PolyCollection): """ A collection of horizontal bars spanning *yrange* with a sequence of @@ -1295,23 +1270,6 @@ def __init__(self, xranges, yrange, **kwargs): (xmin, ymin)] for xmin, xwidth in xranges] super().__init__(verts, **kwargs) - @classmethod - def span_where(cls, x, ymin, ymax, where, **kwargs): - """ - Return a `.BrokenBarHCollection` that plots horizontal bars from - over the regions in *x* where *where* is True. The bars range - on the y-axis from *ymin* to *ymax* - - *kwargs* are passed on to the collection. - """ - xranges = [] - for ind0, ind1 in cbook.contiguous_regions(where): - xslice = x[ind0:ind1] - if not len(xslice): - continue - xranges.append((xslice[0], xslice[-1] - xslice[0])) - return cls(xranges, [ymin, ymax - ymin], **kwargs) - class RegularPolyCollection(_CollectionWithSizes): """A collection of n-sided regular polygons.""" @@ -1319,6 +1277,7 @@ class RegularPolyCollection(_CollectionWithSizes): _path_generator = mpath.Path.unit_regular_polygon _factor = np.pi ** (-1/2) + @_api.make_keyword_only("3.6", name="rotation") def __init__(self, numsides, rotation=0, @@ -1350,7 +1309,7 @@ def __init__(self, edgecolors=("black",), linewidths=(1,), offsets=offsets, - transOffset=ax.transData, + offset_transform=ax.transData, ) """ super().__init__(**kwargs) @@ -1393,7 +1352,7 @@ class LineCollection(Collection): Represents a sequence of `.Line2D`\s that should be drawn together. This class extends `.Collection` to represent a sequence of - `~.Line2D`\s instead of just a sequence of `~.Patch`\s. + `.Line2D`\s instead of just a sequence of `.Patch`\s. Just as in `.Collection`, each property of a *LineCollection* may be either a single value or a list of values. This list is then used cyclically for each element of the LineCollection, so the property of the ``i``\th element @@ -1409,7 +1368,7 @@ class LineCollection(Collection): _edge_default = True def __init__(self, segments, # Can be None. - *args, # Deprecated. + *, zorder=2, # Collection.zorder is 1 **kwargs ): @@ -1430,7 +1389,7 @@ def __init__(self, segments, # Can be None. allowed). antialiaseds : bool or list of bool, default: :rc:`lines.antialiased` Whether to use antialiasing for each line. - zorder : int, default: 2 + zorder : float, default: 2 zorder of the lines once drawn. facecolors : color or list of color, default: 'none' @@ -1445,18 +1404,6 @@ def __init__(self, segments, # Can be None. **kwargs Forwarded to `.Collection`. """ - argnames = ["linewidths", "colors", "antialiaseds", "linestyles", - "offsets", "transOffset", "norm", "cmap", "pickradius", - "zorder", "facecolors"] - if args: - argkw = {name: val for name, val in zip(argnames, args)} - kwargs.update(argkw) - cbook.warn_deprecated( - "3.4", message="Since %(since)s, passing LineCollection " - "arguments other than the first, 'segments', as positional " - "arguments is deprecated, and they will become keyword-only " - "arguments %(removal)s." - ) # Unfortunately, mplot3d needs this explicit setting of 'facecolors'. kwargs.setdefault('facecolors', 'none') super().__init__( @@ -1467,17 +1414,10 @@ def __init__(self, segments, # Can be None. def set_segments(self, segments): if segments is None: return - _segments = [] - - for seg in segments: - if not isinstance(seg, np.ma.MaskedArray): - seg = np.asarray(seg, float) - _segments.append(seg) - if self._uniform_offsets is not None: - _segments = self._add_offsets(_segments) - - self._paths = [mpath.Path(_seg) for _seg in _segments] + self._paths = [mpath.Path(seg) if isinstance(seg, np.ma.MaskedArray) + else mpath.Path(np.asarray(seg, float)) + for seg in segments] self.stale = True set_verts = set_segments # for compatibility with PolyCollection @@ -1507,19 +1447,6 @@ def get_segments(self): return segments - def _add_offsets(self, segs): - offsets = self._uniform_offsets - Nsegs = len(segs) - Noffs = offsets.shape[0] - if Noffs == 1: - for i in range(Nsegs): - segs[i] = segs[i] + i * offsets - else: - for i in range(Nsegs): - io = i % Noffs - segs[i] = segs[i] + offsets[io:io + 1] - return segs - def _get_default_linewidth(self): return mpl.rcParams['lines.linewidth'] @@ -1540,7 +1467,7 @@ def set_color(self, c): ---------- c : color or list of colors Single color (all lines have same color), or a - sequence of rgba tuples; if it is a sequence the lines will + sequence of RGBA tuples; if it is a sequence the lines will cycle through the sequence. """ self.set_edgecolor(c) @@ -1563,6 +1490,7 @@ class EventCollection(LineCollection): _edge_default = True + @_api.make_keyword_only("3.6", name="lineoffset") def __init__(self, positions, # Cannot be None. orientation='horizontal', @@ -1674,7 +1602,7 @@ def switch_orientation(self): self._is_horizontal = not self.is_horizontal() self.stale = True - def set_orientation(self, orientation=None): + def set_orientation(self, orientation): """ Set the orientation of the event line. @@ -1682,24 +1610,9 @@ def set_orientation(self, orientation=None): ---------- orientation : {'horizontal', 'vertical'} """ - try: - is_horizontal = _api.check_getitem( - {"horizontal": True, "vertical": False}, - orientation=orientation) - except ValueError: - if (orientation is None or orientation.lower() == "none" - or orientation.lower() == "horizontal"): - is_horizontal = True - elif orientation.lower() == "vertical": - is_horizontal = False - else: - raise - normalized = "horizontal" if is_horizontal else "vertical" - _api.warn_deprecated( - "3.3", message="Support for setting the orientation of " - f"EventCollection to {orientation!r} is deprecated since " - f"%(since)s and will be removed %(removal)s; please set it to " - f"{normalized!r} instead.") + is_horizontal = _api.check_getitem( + {"horizontal": True, "vertical": False}, + orientation=orientation) if is_horizontal == self.is_horizontal(): return self.switch_orientation() @@ -1773,6 +1686,7 @@ def __init__(self, sizes, **kwargs): class EllipseCollection(Collection): """A collection of ellipses, drawn using splines.""" + @_api.make_keyword_only("3.6", name="units") def __init__(self, widths, heights, angles, units='points', **kwargs): """ Parameters @@ -1826,7 +1740,7 @@ def _set_transforms(self): elif self._units == 'dots': sc = 1.0 else: - raise ValueError('unrecognized units: %s' % self._units) + raise ValueError(f'Unrecognized units: {self._units!r}') self._transforms = np.zeros((len(self._widths), 3, 3)) widths = self._widths * sc @@ -1855,29 +1769,35 @@ class PatchCollection(Collection): """ A generic collection of patches. - This makes it easier to assign a colormap to a heterogeneous + PatchCollection draws faster than a large number of equivalent individual + Patches. It also makes it easier to assign a colormap to a heterogeneous collection of patches. - - This also may improve plotting speed, since PatchCollection will - draw faster than a large number of patches. """ + @_api.make_keyword_only("3.6", name="match_original") def __init__(self, patches, match_original=False, **kwargs): """ - *patches* - a sequence of Patch objects. This list may include + Parameters + ---------- + patches : list of `.Patch` + A sequence of Patch objects. This list may include a heterogeneous assortment of different patch types. - *match_original* + match_original : bool, default: False If True, use the colors and linewidths of the original patches. If False, new colors may be assigned by providing the standard collection arguments, facecolor, edgecolor, linewidths, norm or cmap. - If any of *edgecolors*, *facecolors*, *linewidths*, *antialiaseds* are - None, they default to their `.rcParams` patch setting, in sequence - form. + **kwargs + All other parameters are forwarded to `.Collection`. + + If any of *edgecolors*, *facecolors*, *linewidths*, *antialiaseds* + are None, they default to their `.rcParams` patch setting, in + sequence form. + Notes + ----- The use of `~matplotlib.cm.ScalarMappable` functionality is optional. If the `~matplotlib.cm.ScalarMappable` matrix ``_A`` has been set (via a call to `~.ScalarMappable.set_array`), at draw time a call to scalar @@ -1936,7 +1856,7 @@ def set_paths(self): @staticmethod def convert_mesh_to_paths(tri): """ - Convert 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. @@ -1970,48 +1890,54 @@ def draw(self, renderer): class QuadMesh(Collection): - """ + r""" Class for the efficient drawing of a quadrilateral mesh. - A quadrilateral mesh consists of a grid of vertices. - The dimensions of this array are (*meshWidth* + 1, *meshHeight* + 1). - Each vertex in the mesh has a different set of "mesh coordinates" - representing its position in the topology of the mesh. - For any values (*m*, *n*) such that 0 <= *m* <= *meshWidth* - and 0 <= *n* <= *meshHeight*, the vertices at mesh coordinates - (*m*, *n*), (*m*, *n* + 1), (*m* + 1, *n* + 1), and (*m* + 1, *n*) - form one of the quadrilaterals in the mesh. There are thus - (*meshWidth* * *meshHeight*) quadrilaterals in the mesh. The mesh - need not be regular and the polygons need not be convex. - - A quadrilateral mesh is represented by a (2 x ((*meshWidth* + 1) * - (*meshHeight* + 1))) numpy array *coordinates*, where each row is - the *x* and *y* coordinates of one of the vertices. To define the - function that maps from a data point to its corresponding color, - use the :meth:`set_cmap` method. Each of these arrays is indexed in - row-major order by the mesh coordinates of the vertex (or the mesh - coordinates of the lower left vertex, in the case of the colors). - - For example, the first entry in *coordinates* is the coordinates of the - vertex at mesh coordinates (0, 0), then the one at (0, 1), then at (0, 2) - .. (0, meshWidth), (1, 0), (1, 1), and so on. - - *shading* may be 'flat', or 'gouraud' + A quadrilateral mesh is a grid of M by N adjacent quadrilaterals that are + defined via a (M+1, N+1) grid of vertices. The quadrilateral (m, n) is + defined by the vertices :: + + (m+1, n) ----------- (m+1, n+1) + / / + / / + / / + (m, n) -------- (m, n+1) + + The mesh need not be regular and the polygons need not be convex. + + Parameters + ---------- + coordinates : (M+1, N+1, 2) array-like + The vertices. ``coordinates[m, n]`` specifies the (x, y) coordinates + of vertex (m, n). + + antialiased : bool, default: True + + shading : {'flat', 'gouraud'}, default: 'flat' + + Notes + ----- + Unlike other `.Collection`\s, the default *pickradius* of `.QuadMesh` is 0, + i.e. `~.Artist.contains` checks whether the test point is within any of the + mesh quadrilaterals. + """ - def __init__(self, meshWidth, meshHeight, coordinates, - antialiased=True, shading='flat', **kwargs): - super().__init__(**kwargs) - self._meshWidth = meshWidth - self._meshHeight = meshHeight - # By converting to floats now, we can avoid that on every draw. - self._coordinates = np.asarray(coordinates, float).reshape( - (meshHeight + 1, meshWidth + 1, 2)) + + def __init__(self, coordinates, *, antialiased=True, shading='flat', + **kwargs): + kwargs.setdefault("pickradius", 0) + # end of signature deprecation code + + _api.check_shape((None, None, 2), coordinates=coordinates) + self._coordinates = coordinates self._antialiased = antialiased self._shading = shading - self._bbox = transforms.Bbox.unit() - self._bbox.update_from_data_xy(coordinates.reshape( - ((meshWidth + 1) * (meshHeight + 1), 2))) + self._bbox.update_from_data_xy(self._coordinates.reshape(-1, 2)) + # super init delayed after own init because array kwarg requires + # self._coordinates and self._shading + super().__init__(**kwargs) + self.set_mouseover(False) def get_paths(self): if self._paths is None: @@ -2019,17 +1945,63 @@ def get_paths(self): return self._paths def set_paths(self): - self._paths = self.convert_mesh_to_paths( - self._meshWidth, self._meshHeight, self._coordinates) + self._paths = self._convert_mesh_to_paths(self._coordinates) self.stale = True + def set_array(self, A): + """ + Set the data values. + + Parameters + ---------- + A : array-like + The mesh data. Supported array shapes are: + + - (M, N) or (M*N,): a mesh with scalar data. The values are mapped + to colors using normalization and a colormap. See parameters + *norm*, *cmap*, *vmin*, *vmax*. + - (M, N, 3): an image with RGB values (0-1 float or 0-255 int). + - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int), + i.e. including transparency. + + If the values are provided as a 2D grid, the shape must match the + coordinates grid. If the values are 1D, they are reshaped to 2D. + M, N follow from the coordinates grid, where the coordinates grid + shape is (M, N) for 'gouraud' *shading* and (M+1, N+1) for 'flat' + shading. + """ + height, width = self._coordinates.shape[0:-1] + if self._shading == 'flat': + h, w = height - 1, width - 1 + else: + h, w = height, width + ok_shapes = [(h, w, 3), (h, w, 4), (h, w), (h * w,)] + if A is not None: + shape = np.shape(A) + if shape not in ok_shapes: + raise ValueError( + f"For X ({width}) and Y ({height}) with {self._shading} " + f"shading, A should have shape " + f"{' or '.join(map(str, ok_shapes))}, not {A.shape}") + return super().set_array(A) + def get_datalim(self, transData): return (self.get_transform() - transData).transform_bbox(self._bbox) + def get_coordinates(self): + """ + Return the vertices of the mesh as an (M+1, N+1, 2) array. + + M, N are the number of quadrilaterals in the rows / columns of the + mesh, corresponding to (M+1, N+1) vertices. + The last dimension specifies the components (x, y). + """ + return self._coordinates + @staticmethod - def convert_mesh_to_paths(meshWidth, meshHeight, coordinates): + def _convert_mesh_to_paths(coordinates): """ - Convert 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. @@ -2038,21 +2010,20 @@ def convert_mesh_to_paths(meshWidth, meshHeight, coordinates): c = coordinates.data else: c = coordinates - points = np.concatenate(( - c[:-1, :-1], - c[:-1, 1:], - c[1:, 1:], - c[1:, :-1], - c[:-1, :-1] - ), axis=2) - points = points.reshape((meshWidth * meshHeight, 5, 2)) + points = np.concatenate([ + c[:-1, :-1], + c[:-1, 1:], + c[1:, 1:], + c[1:, :-1], + c[:-1, :-1] + ], axis=2).reshape((-1, 5, 2)) return [mpath.Path(x) for x in points] - def convert_mesh_to_triangles(self, meshWidth, meshHeight, coordinates): + def _convert_mesh_to_triangles(self, coordinates): """ 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`. + with its own color. The result can be used to construct a call to + `~.RendererBase.draw_gouraud_triangles`. """ if isinstance(coordinates, np.ma.MaskedArray): p = coordinates.data @@ -2064,29 +2035,25 @@ def convert_mesh_to_triangles(self, meshWidth, meshHeight, coordinates): p_c = p[1:, 1:] p_d = p[1:, :-1] p_center = (p_a + p_b + p_c + p_d) / 4.0 - - triangles = np.concatenate(( - p_a, p_b, p_center, - p_b, p_c, p_center, - p_c, p_d, p_center, - p_d, p_a, p_center, - ), axis=2) - triangles = triangles.reshape((meshWidth * meshHeight * 4, 3, 2)) - - c = self.get_facecolor().reshape((meshHeight + 1, meshWidth + 1, 4)) + triangles = np.concatenate([ + p_a, p_b, p_center, + p_b, p_c, p_center, + p_c, p_d, p_center, + p_d, p_a, p_center, + ], axis=2).reshape((-1, 3, 2)) + + c = self.get_facecolor().reshape((*coordinates.shape[:2], 4)) c_a = c[:-1, :-1] c_b = c[:-1, 1:] c_c = c[1:, 1:] c_d = c[1:, :-1] c_center = (c_a + c_b + c_c + c_d) / 4.0 - - colors = np.concatenate(( - c_a, c_b, c_center, - c_b, c_c, c_center, - c_c, c_d, c_center, - c_d, c_a, c_center, - ), axis=2) - colors = colors.reshape((meshWidth * meshHeight * 4, 3, 4)) + colors = np.concatenate([ + c_a, c_b, c_center, + c_b, c_c, c_center, + c_c, c_d, c_center, + c_d, c_a, c_center, + ], axis=2).reshape((-1, 3, 4)) return triangles, colors @@ -2096,14 +2063,13 @@ def draw(self, renderer): return renderer.open_group(self.__class__.__name__, self.get_gid()) transform = self.get_transform() - transOffset = self.get_offset_transform() - offsets = self._offsets + offset_trf = self.get_offset_transform() + offsets = self.get_offsets() if self.have_units(): - if len(self._offsets): - xs = self.convert_xunits(self._offsets[:, 0]) - ys = self.convert_yunits(self._offsets[:, 1]) - offsets = np.column_stack([xs, ys]) + xs = self.convert_xunits(offsets[:, 0]) + ys = self.convert_yunits(offsets[:, 1]) + offsets = np.column_stack([xs, ys]) self.update_scalarmappable() @@ -2115,9 +2081,9 @@ def draw(self, renderer): else: coordinates = self._coordinates - if not transOffset.is_affine: - offsets = transOffset.transform_non_affine(offsets) - transOffset = transOffset.get_affine() + if not offset_trf.is_affine: + offsets = offset_trf.transform_non_affine(offsets) + offset_trf = offset_trf.get_affine() gc = renderer.new_gc() gc.set_snap(self.get_snap()) @@ -2125,14 +2091,14 @@ def draw(self, renderer): gc.set_linewidth(self.get_linewidth()[0]) if self._shading == 'gouraud': - triangles, colors = self.convert_mesh_to_triangles( - self._meshWidth, self._meshHeight, coordinates) + triangles, colors = self._convert_mesh_to_triangles(coordinates) renderer.draw_gouraud_triangles( gc, triangles, colors, transform.frozen()) else: renderer.draw_quad_mesh( - gc, transform.frozen(), self._meshWidth, self._meshHeight, - coordinates, offsets, transOffset, + gc, transform.frozen(), + coordinates.shape[1] - 1, coordinates.shape[0] - 1, + coordinates, offsets, offset_trf, # Backends expect flattened rgba arrays (n*m, 4) for fc and ec self.get_facecolor().reshape((-1, 4)), self._antialiased, self.get_edgecolors().reshape((-1, 4))) @@ -2140,11 +2106,8 @@ def draw(self, renderer): renderer.close_group(self.__class__.__name__) self.stale = False - -_artist_kwdoc = artist.kwdoc(Collection) -for k in ('QuadMesh', 'TriMesh', 'PolyCollection', 'BrokenBarHCollection', - 'RegularPolyCollection', 'PathCollection', - 'StarPolygonCollection', 'PatchCollection', - 'CircleCollection', 'Collection',): - docstring.interpd.update({f'{k}_kwdoc': _artist_kwdoc}) -docstring.interpd.update(LineCollection_kwdoc=artist.kwdoc(LineCollection)) + def get_cursor_data(self, event): + contained, info = self.contains(event) + if contained and self.get_array() is not None: + return self.get_array().ravel()[info["ind"]] + return None diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 31d9b6176821..14c7c1e58b9a 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -4,48 +4,30 @@ .. note:: Colorbars are typically created through `.Figure.colorbar` or its pyplot - wrapper `.pyplot.colorbar`, which use `.make_axes` and `.Colorbar` - internally. + wrapper `.pyplot.colorbar`, which internally use `.Colorbar` together with + `.make_axes_gridspec` (for `.GridSpec`-positioned axes) or `.make_axes` (for + non-`.GridSpec`-positioned axes). - As an end-user, you most likely won't have to call the methods or - instantiate the classes in this module explicitly. - -:class:`ColorbarBase` - The base class with full colorbar drawing functionality. - It can be used as-is to make a colorbar for a given colormap; - a mappable object (e.g., image) is not needed. - -:class:`Colorbar` - On top of `.ColorbarBase` this connects the colorbar with a - `.ScalarMappable` such as an image or contour plot. - -:func:`make_axes` - Create an `~.axes.Axes` suitable for a colorbar. This functions can be - used with figures containing a single axes or with freely placed axes. - -:func:`make_axes_gridspec` - Create a `~.SubplotBase` suitable for a colorbar. This function should - be used for adding a colorbar to a `.GridSpec`. + End-users most likely won't need to directly use this module's API. """ -import copy import logging -import textwrap import numpy as np import matplotlib as mpl -from matplotlib import _api, collections, cm, colors, contour, ticker +from matplotlib import _api, cbook, collections, cm, colors, contour, ticker import matplotlib.artist as martist import matplotlib.patches as mpatches import matplotlib.path as mpath import matplotlib.spines as mspines import matplotlib.transforms as mtransforms -from matplotlib import docstring +from matplotlib import _docstring _log = logging.getLogger(__name__) -_make_axes_param_doc = """ +_docstring.interpd.update( + _make_axes_kw_doc=""" location : None or {'left', 'right', 'top', 'bottom'} The location, relative to the parent axes, where the colorbar axes is created. It also determines the *orientation* of the colorbar @@ -53,271 +35,96 @@ and bottom are horizontal). If None, the location will come from the *orientation* if it is set (vertical colorbars on the right, horizontal ones at the bottom), or default to 'right' if *orientation* is unset. + orientation : None or {'vertical', 'horizontal'} The orientation of the colorbar. It is preferable to set the *location* of the colorbar, as that also determines the *orientation*; passing incompatible values for *location* and *orientation* raises an exception. + fraction : float, default: 0.15 Fraction of original axes to use for colorbar. + shrink : float, default: 1.0 Fraction by which to multiply the size of the colorbar. + aspect : float, default: 20 Ratio of long to short dimensions. -""" -_make_axes_other_param_doc = """ + pad : float, default: 0.05 if vertical, 0.15 if horizontal Fraction of original axes between colorbar and new image axes. + anchor : (float, float), optional The anchor point of the colorbar axes. Defaults to (0.0, 0.5) if vertical; (0.5, 1.0) if horizontal. + panchor : (float, float), or *False*, optional The anchor point of the colorbar parent axes. If *False*, the parent axes' anchor will be unchanged. - Defaults to (1.0, 0.5) if vertical; (0.5, 0.0) if horizontal. -""" - -_colormap_kw_doc = """ - - ============ ==================================================== - Property Description - ============ ==================================================== - *extend* {'neither', 'both', 'min', 'max'} - If not 'neither', make pointed end(s) for out-of- - range values. These are set for a given colormap - using the colormap set_under and set_over methods. - *extendfrac* {*None*, 'auto', length, lengths} - If set to *None*, both the minimum and maximum - triangular colorbar extensions with have a length of - 5% of the interior colorbar length (this is the - default setting). If set to 'auto', makes the - triangular colorbar extensions the same lengths as - the interior boxes (when *spacing* is set to - 'uniform') or the same lengths as the respective - adjacent interior boxes (when *spacing* is set to - 'proportional'). If a scalar, indicates the length - of both the minimum and maximum triangular colorbar - extensions as a fraction of the interior colorbar - length. A two-element sequence of fractions may also - be given, indicating the lengths of the minimum and - maximum colorbar extensions respectively as a - fraction of the interior colorbar length. - *extendrect* bool - If *False* the minimum and maximum colorbar extensions - will be triangular (the default). If *True* the - extensions will be rectangular. - *spacing* {'uniform', 'proportional'} - Uniform spacing gives each discrete color the same - space; proportional makes the space proportional to - the data interval. - *ticks* *None* or list of ticks or Locator - If None, ticks are determined automatically from the - input. - *format* None or str or Formatter - If None, `~.ticker.ScalarFormatter` is used. - If a format string is given, e.g., '%.3f', that is used. - An alternative `~.ticker.Formatter` may be given instead. - *drawedges* bool - Whether to draw lines at color boundaries. - *label* str - The label on the colorbar's long axis. - ============ ==================================================== - - The following will probably be useful only in the context of - indexed colors (that is, when the mappable has norm=NoNorm()), - or other unusual circumstances. - - ============ =================================================== - Property Description - ============ =================================================== - *boundaries* None or a sequence - *values* None or a sequence which must be of length 1 less - than the sequence of *boundaries*. For each region - delimited by adjacent entries in *boundaries*, the - colormapped to the corresponding value in values - will be used. - ============ =================================================== - -""" - -docstring.interpd.update(colorbar_doc=""" -Add a colorbar to a plot. - -Parameters ----------- -mappable - The `matplotlib.cm.ScalarMappable` (i.e., `~matplotlib.image.AxesImage`, - `~matplotlib.contour.ContourSet`, etc.) described by this colorbar. - This argument is mandatory for the `.Figure.colorbar` method but optional - for the `.pyplot.colorbar` function, which sets the default to the current - image. - - Note that one can create a `.ScalarMappable` "on-the-fly" to generate - colorbars not attached to a previously drawn artist, e.g. :: - - fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax) - -cax : `~matplotlib.axes.Axes`, optional - Axes into which the colorbar will be drawn. - -ax : `~matplotlib.axes.Axes`, list of Axes, optional - One or more parent axes from which space for a new colorbar axes will be - stolen, if *cax* is None. This has no effect if *cax* is set. - -use_gridspec : bool, optional - If *cax* is ``None``, a new *cax* is created as an instance of Axes. If - *ax* is an instance of Subplot and *use_gridspec* is ``True``, *cax* is - created as an instance of Subplot using the :mod:`~.gridspec` module. - -Returns -------- -colorbar : `~matplotlib.colorbar.Colorbar` - See also its base class, `~matplotlib.colorbar.ColorbarBase`. - -Notes ------ -Additional keyword arguments are of two kinds: - - axes properties: -%s -%s - colorbar properties: -%s - -If *mappable* is a `~.contour.ContourSet`, its *extend* kwarg is included -automatically. - -The *shrink* kwarg provides a simple way to scale the colorbar with respect -to the axes. Note that if *cax* is specified, it determines the size of the -colorbar and *shrink* and *aspect* kwargs are ignored. - -For more precise control, you can manually specify the positions of -the axes objects in which the mappable and the colorbar are drawn. In -this case, do not use any of the axes properties kwargs. - -It is known that some vector graphics viewers (svg and pdf) renders white gaps -between segments of the colorbar. This is due to bugs in the viewers, not -Matplotlib. As a workaround, the colorbar can be rendered with overlapping -segments:: - - cbar = colorbar() - cbar.solids.set_edgecolor("face") - draw() - -However this has negative consequences in other circumstances, e.g. with -semi-transparent images (alpha < 1) and colorbar extensions; therefore, this -workaround is not used by default (see issue #1188). -""" % (textwrap.indent(_make_axes_param_doc, " "), - textwrap.indent(_make_axes_other_param_doc, " "), - _colormap_kw_doc)) - -# Deprecated since 3.4. -colorbar_doc = docstring.interpd.params["colorbar_doc"] -colormap_kw_doc = _colormap_kw_doc -make_axes_kw_doc = _make_axes_param_doc + _make_axes_other_param_doc - - -def _set_ticks_on_axis_warn(*args, **kw): + Defaults to (1.0, 0.5) if vertical; (0.5, 0.0) if horizontal.""", + _colormap_kw_doc=""" +extend : {'neither', 'both', 'min', 'max'} + Make pointed end(s) for out-of-range values (unless 'neither'). These are + set for a given colormap using the colormap set_under and set_over methods. + +extendfrac : {*None*, 'auto', length, lengths} + If set to *None*, both the minimum and maximum triangular colorbar + extensions will have a length of 5% of the interior colorbar length (this + is the default setting). + + If set to 'auto', makes the triangular colorbar extensions the same lengths + as the interior boxes (when *spacing* is set to 'uniform') or the same + lengths as the respective adjacent interior boxes (when *spacing* is set to + 'proportional'). + + If a scalar, indicates the length of both the minimum and maximum + triangular colorbar extensions as a fraction of the interior colorbar + length. A two-element sequence of fractions may also be given, indicating + the lengths of the minimum and maximum colorbar extensions respectively as + a fraction of the interior colorbar length. + +extendrect : bool + If *False* the minimum and maximum colorbar extensions will be triangular + (the default). If *True* the extensions will be rectangular. + +spacing : {'uniform', 'proportional'} + For discrete colorbars (`.BoundaryNorm` or contours), 'uniform' gives each + color the same space; 'proportional' makes the space proportional to the + data interval. + +ticks : None or list of ticks or Locator + If None, ticks are determined automatically from the input. + +format : None or str or Formatter + If None, `~.ticker.ScalarFormatter` is used. + Format strings, e.g., ``"%4.2e"`` or ``"{x:.2e}"``, are supported. + An alternative `~.ticker.Formatter` may be given instead. + +drawedges : bool + Whether to draw lines at color boundaries. + +label : str + The label on the colorbar's long axis. + +boundaries, values : None or a sequence + If unset, the colormap will be displayed on a 0-1 scale. + If sequences, *values* must have a length 1 less than *boundaries*. For + each region delimited by adjacent entries in *boundaries*, the color mapped + to the corresponding value in values will be used. + Normally only useful for indexed colors (i.e. ``norm=NoNorm()``) or other + unusual circumstances.""") + + +def _set_ticks_on_axis_warn(*args, **kwargs): # a top level function which gets put in at the axes' - # set_xticks and set_yticks by ColorbarBase.__init__. + # set_xticks and set_yticks by Colorbar.__init__. _api.warn_external("Use the colorbar set_ticks() method instead.") -class _ColorbarAutoLocator(ticker.MaxNLocator): - """ - AutoLocator for Colorbar - - This locator is just a `.MaxNLocator` except the min and max are - clipped by the norm's min and max (i.e. vmin/vmax from the - image/pcolor/contour object). This is necessary so ticks don't - extrude into the "extend regions". - """ - - def __init__(self, colorbar): - """ - This ticker needs to know the *colorbar* so that it can access - its *vmin* and *vmax*. Otherwise it is the same as - `~.ticker.AutoLocator`. - """ - - self._colorbar = colorbar - nbins = 'auto' - steps = [1, 2, 2.5, 5, 10] - super().__init__(nbins=nbins, steps=steps) - - def tick_values(self, vmin, vmax): - # flip if needed: - if vmin > vmax: - vmin, vmax = vmax, vmin - vmin = max(vmin, self._colorbar.norm.vmin) - vmax = min(vmax, self._colorbar.norm.vmax) - ticks = super().tick_values(vmin, vmax) - rtol = (vmax - vmin) * 1e-10 - return ticks[(ticks >= vmin - rtol) & (ticks <= vmax + rtol)] - - -class _ColorbarAutoMinorLocator(ticker.AutoMinorLocator): - """ - AutoMinorLocator for Colorbar - - This locator is just a `.AutoMinorLocator` except the min and max are - clipped by the norm's min and max (i.e. vmin/vmax from the - image/pcolor/contour object). This is necessary so that the minorticks - don't extrude into the "extend regions". - """ - - def __init__(self, colorbar, n=None): - """ - This ticker needs to know the *colorbar* so that it can access - its *vmin* and *vmax*. - """ - self._colorbar = colorbar - self.ndivs = n - super().__init__(n=None) - - def __call__(self): - vmin = self._colorbar.norm.vmin - vmax = self._colorbar.norm.vmax - ticks = super().__call__() - rtol = (vmax - vmin) * 1e-10 - return ticks[(ticks >= vmin - rtol) & (ticks <= vmax + rtol)] - - -class _ColorbarLogLocator(ticker.LogLocator): - """ - LogLocator for Colorbarbar - - This locator is just a `.LogLocator` except the min and max are - clipped by the norm's min and max (i.e. vmin/vmax from the - image/pcolor/contour object). This is necessary so ticks don't - extrude into the "extend regions". - - """ - def __init__(self, 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 - same as `~.ticker.LogLocator`. - """ - self._colorbar = colorbar - super().__init__(*args, **kwargs) - - def tick_values(self, vmin, vmax): - if vmin > vmax: - vmin, vmax = vmax, vmin - vmin = max(vmin, self._colorbar.norm.vmin) - vmax = min(vmax, self._colorbar.norm.vmax) - ticks = super().tick_values(vmin, vmax) - rtol = (np.log10(vmax) - np.log10(vmin)) * 1e-10 - ticks = ticks[(np.log10(ticks) >= np.log10(vmin) - rtol) & - (np.log10(ticks) <= np.log10(vmax) + rtol)] - return ticks - - class _ColorbarSpine(mspines.Spine): def __init__(self, axes): - super().__init__(axes, 'colorbar', - mpath.Path(np.empty((0, 2)), closed=True)) + self._ax = axes + super().__init__(axes, 'colorbar', mpath.Path(np.empty((0, 2)))) + mpatches.Patch.set_transform(self, axes.transAxes) def get_window_extent(self, renderer=None): # This Spine has no Axis associated with it, and doesn't need to adjust @@ -327,6 +134,7 @@ def get_window_extent(self, renderer=None): def set_xy(self, xy): self._path = mpath.Path(xy, closed=True) + self._xy = xy self.stale = True def draw(self, renderer): @@ -335,30 +143,68 @@ def draw(self, renderer): return ret -class ColorbarBase: - r""" - Draw a colorbar in an existing axes. +class _ColorbarAxesLocator: + """ + Shrink the axes if there are triangular or rectangular extends. + """ + def __init__(self, cbar): + self._cbar = cbar + self._orig_locator = cbar.ax._axes_locator - There are only some rare cases in which you would work directly with a - `.ColorbarBase` as an end-user. Typically, colorbars are used - with `.ScalarMappable`\s such as an `.AxesImage` generated via - `~.axes.Axes.imshow`. For these cases you will use `.Colorbar` and - likely create it via `.pyplot.colorbar` or `.Figure.colorbar`. + def __call__(self, ax, renderer): + if self._orig_locator is not None: + pos = self._orig_locator(ax, renderer) + else: + pos = ax.get_position(original=True) + if self._cbar.extend == 'neither': + return pos + + y, extendlen = self._cbar._proportional_y() + if not self._cbar._extend_lower(): + extendlen[0] = 0 + if not self._cbar._extend_upper(): + extendlen[1] = 0 + len = sum(extendlen) + 1 + shrink = 1 / len + offset = extendlen[0] / len + # we need to reset the aspect ratio of the axes to account + # of the extends... + if hasattr(ax, '_colorbar_info'): + aspect = ax._colorbar_info['aspect'] + else: + aspect = False + # now shrink and/or offset to take into account the + # extend tri/rectangles. + if self._cbar.orientation == 'vertical': + if aspect: + self._cbar.ax.set_box_aspect(aspect*shrink) + pos = pos.shrunk(1, shrink).translated(0, offset * pos.height) + else: + if aspect: + self._cbar.ax.set_box_aspect(1/(aspect * shrink)) + pos = pos.shrunk(shrink, 1).translated(offset * pos.width, 0) + return pos - The main application of using a `.ColorbarBase` explicitly is drawing - colorbars that are not associated with other elements in the figure, e.g. - when showing a colormap by itself. + def get_subplotspec(self): + # make tight_layout happy.. + return ( + self._cbar.ax.get_subplotspec() + or getattr(self._orig_locator, "get_subplotspec", lambda: None)()) - If the *cmap* kwarg is given but *boundaries* and *values* are left as - None, then the colormap will be displayed on a 0-1 scale. To show the - under- and over-value colors, specify the *norm* as:: - norm=colors.Normalize(clip=False) +@_docstring.interpd +class Colorbar: + r""" + Draw a colorbar in an existing axes. - To show the colors versus index instead of on the 0-1 scale, - use:: + Typically, colorbars are created using `.Figure.colorbar` or + `.pyplot.colorbar` and associated with `.ScalarMappable`\s (such as an + `.AxesImage` generated via `~.axes.Axes.imshow`). - norm=colors.NoNorm() + In order to draw a colorbar not associated with other elements in the + figure, e.g. when showing a colormap by itself, one can create an empty + `.ScalarMappable`, or directly pass *cmap* and *norm* instead of *mappable* + to `Colorbar`. Useful public methods are :meth:`set_label` and :meth:`add_lines`. @@ -375,49 +221,70 @@ class ColorbarBase: ---------- ax : `~matplotlib.axes.Axes` The `~.axes.Axes` instance in which the colorbar is drawn. - cmap : `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` - The colormap to use. - norm : `~matplotlib.colors.Normalize` - alpha : float - The colorbar transparency between 0 (transparent) and 1 (opaque). + mappable : `.ScalarMappable` + The mappable whose colormap and norm will be used. - values + To show the under- and over- value colors, the mappable's norm should + be specified as :: - boundaries + norm = colors.Normalize(clip=False) - orientation : {'vertical', 'horizontal'} + To show the colors versus index instead of on a 0-1 scale, use:: - ticklocation : {'auto', 'left', 'right', 'top', 'bottom'} + norm=colors.NoNorm() + + cmap : `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` + The colormap to use. This parameter is ignored, unless *mappable* is + None. - extend : {'neither', 'both', 'min', 'max'} + norm : `~matplotlib.colors.Normalize` + The normalization to use. This parameter is ignored, unless *mappable* + is None. - spacing : {'uniform', 'proportional'} + alpha : float + The colorbar transparency between 0 (transparent) and 1 (opaque). - ticks : `~matplotlib.ticker.Locator` or array-like of float + orientation : None or {'vertical', 'horizontal'} + If None, use the value determined by *location*. If both + *orientation* and *location* are None then defaults to 'vertical'. - format : str or `~matplotlib.ticker.Formatter` + ticklocation : {'auto', 'left', 'right', 'top', 'bottom'} + The location of the colorbar ticks. The *ticklocation* must match + *orientation*. For example, a horizontal colorbar can only have ticks + at the top or the bottom. If 'auto', the ticks will be the same as + *location*, so a colorbar to the left will have ticks to the left. If + *location* is None, the ticks will be at the bottom for a horizontal + colorbar and at the right for a vertical. drawedges : bool + Whether to draw lines at color boundaries. filled : bool - extendfrac + %(_colormap_kw_doc)s - extendrec + location : None or {'left', 'right', 'top', 'bottom'} + Set the *orientation* and *ticklocation* of the colorbar using a + single argument. Colorbars on the left and right are vertical, + colorbars at the top and bottom are horizontal. The *ticklocation* is + the same as *location*, so if *location* is 'top', the ticks are on + the top. *orientation* and/or *ticklocation* can be provided as well + and overrides the value set by *location*, but there will be an error + for incompatible combinations. - label : str + .. versionadded:: 3.7 """ n_rasterize = 50 # rasterize solids if number of colors >= n_rasterize - @_api.make_keyword_only("3.3", "cmap") - def __init__(self, ax, cmap=None, + @_api.delete_parameter("3.6", "filled") + def __init__(self, ax, mappable=None, *, cmap=None, norm=None, alpha=None, values=None, boundaries=None, - orientation='vertical', + orientation=None, ticklocation='auto', extend=None, spacing='uniform', # uniform or proportional @@ -428,31 +295,68 @@ def __init__(self, ax, cmap=None, extendfrac=None, extendrect=False, label='', + location=None, ): - _api.check_isinstance([colors.Colormap, None], cmap=cmap) + + if mappable is None: + mappable = cm.ScalarMappable(norm=norm, cmap=cmap) + + # Ensure the given mappable's norm has appropriate vmin and vmax + # set even if mappable.draw has not yet been called. + if mappable.get_array() is not None: + mappable.autoscale_None() + + self.mappable = mappable + cmap = mappable.cmap + norm = mappable.norm + + if isinstance(mappable, contour.ContourSet): + cs = mappable + alpha = cs.get_alpha() + boundaries = cs._levels + values = cs.cvalues + extend = cs.extend + filled = cs.filled + if ticks is None: + ticks = ticker.FixedLocator(cs.levels, nbins=10) + elif isinstance(mappable, martist.Artist): + alpha = mappable.get_alpha() + + mappable.colorbar = self + mappable.colorbar_cid = mappable.callbacks.connect( + 'changed', self.update_normal) + + location_orientation = _get_orientation_from_location(location) + _api.check_in_list( - ['vertical', 'horizontal'], orientation=orientation) + [None, 'vertical', 'horizontal'], orientation=orientation) _api.check_in_list( ['auto', 'left', 'right', 'top', 'bottom'], ticklocation=ticklocation) _api.check_in_list( ['uniform', 'proportional'], spacing=spacing) + if location_orientation is not None and orientation is not None: + if location_orientation != orientation: + raise TypeError( + "location and orientation are mutually exclusive") + else: + orientation = orientation or location_orientation or "vertical" + self.ax = ax - # Bind some methods to the axes to warn users against using them. - ax.set_xticks = ax.set_yticks = _set_ticks_on_axis_warn - ax.set(navigate=False) - - if cmap is None: - cmap = cm.get_cmap() - if norm is None: - norm = colors.Normalize() + self.ax._axes_locator = _ColorbarAxesLocator(self) + if extend is None: - if hasattr(norm, 'extend'): + if (not isinstance(mappable, contour.ContourSet) + and getattr(cmap, 'colorbar_extend', False) is not False): + extend = cmap.colorbar_extend + elif hasattr(norm, 'extend'): extend = norm.extend else: extend = 'neither' - self.alpha = alpha + self.alpha = None + # Call set_alpha to handle array-like alphas properly + self.set_alpha(alpha) self.cmap = cmap self.norm = norm self.values = values @@ -465,312 +369,214 @@ def __init__(self, ax, cmap=None, self.spacing = spacing self.orientation = orientation self.drawedges = drawedges - self.filled = filled + self._filled = filled self.extendfrac = extendfrac self.extendrect = extendrect + self._extend_patches = [] self.solids = None self.solids_patches = [] self.lines = [] - for spine in ax.spines.values(): + for spine in self.ax.spines.values(): spine.set_visible(False) - self.outline = ax.spines['outline'] = _ColorbarSpine(ax) - - self.patch = mpatches.Polygon( - np.empty((0, 2)), - color=mpl.rcParams['axes.facecolor'], linewidth=0.01, zorder=-1) - ax.add_artist(self.patch) + self.outline = self.ax.spines['outline'] = _ColorbarSpine(self.ax) self.dividers = collections.LineCollection( [], colors=[mpl.rcParams['axes.edgecolor']], - linewidths=[0.5 * mpl.rcParams['axes.linewidth']]) + linewidths=[0.5 * mpl.rcParams['axes.linewidth']], + clip_on=False) self.ax.add_collection(self.dividers) - self.locator = None - self.formatter = None - self._manual_tick_data_values = None - self.__scale = None # linear, log10 for now. Hopefully more? + self._locator = None + self._minorlocator = None + self._formatter = None + self._minorformatter = None if ticklocation == 'auto': - ticklocation = 'bottom' if orientation == 'horizontal' else 'right' + ticklocation = _get_ticklocation_from_orientation( + orientation) if location is None else location self.ticklocation = ticklocation self.set_label(label) self._reset_locator_formatter_scale() if np.iterable(ticks): - self.locator = ticker.FixedLocator(ticks, nbins=len(ticks)) + self._locator = ticker.FixedLocator(ticks, nbins=len(ticks)) else: - self.locator = ticks # Handle default in _ticker() + self._locator = ticks if isinstance(format, str): - self.formatter = ticker.FormatStrFormatter(format) + # Check format between FormatStrFormatter and StrMethodFormatter + try: + self._formatter = ticker.FormatStrFormatter(format) + _ = self._formatter(0) + except TypeError: + self._formatter = ticker.StrMethodFormatter(format) else: - self.formatter = format # Assume it is a Formatter or None - self.draw_all() + self._formatter = format # Assume it is a Formatter or None + self._draw_all() + + if isinstance(mappable, contour.ContourSet) and not mappable.filled: + self.add_lines(mappable) + + # Link the Axes and Colorbar for interactive use + self.ax._colorbar = self + # Don't navigate on any of these types of mappables + if (isinstance(self.norm, (colors.BoundaryNorm, colors.NoNorm)) or + isinstance(self.mappable, contour.ContourSet)): + self.ax.set_navigate(False) + + # These are the functions that set up interactivity on this colorbar + self._interactive_funcs = ["_get_view", "_set_view", + "_set_view_from_bbox", "drag_pan"] + for x in self._interactive_funcs: + setattr(self.ax, x, getattr(self, x)) + # Set the cla function to the cbar's method to override it + self.ax.cla = self._cbar_cla + # Callbacks for the extend calculations to handle inverting the axis + self._extend_cid1 = self.ax.callbacks.connect( + "xlim_changed", self._do_extends) + self._extend_cid2 = self.ax.callbacks.connect( + "ylim_changed", self._do_extends) + + @property + def locator(self): + """Major tick `.Locator` for the colorbar.""" + return self._long_axis().get_major_locator() + + @locator.setter + def locator(self, loc): + self._long_axis().set_major_locator(loc) + self._locator = loc + + @property + def minorlocator(self): + """Minor tick `.Locator` for the colorbar.""" + return self._long_axis().get_minor_locator() + + @minorlocator.setter + def minorlocator(self, loc): + self._long_axis().set_minor_locator(loc) + self._minorlocator = loc + + @property + def formatter(self): + """Major tick label `.Formatter` for the colorbar.""" + return self._long_axis().get_major_formatter() + + @formatter.setter + def formatter(self, fmt): + self._long_axis().set_major_formatter(fmt) + self._formatter = fmt + + @property + def minorformatter(self): + """Minor tick `.Formatter` for the colorbar.""" + return self._long_axis().get_minor_formatter() + + @minorformatter.setter + def minorformatter(self, fmt): + self._long_axis().set_minor_formatter(fmt) + self._minorformatter = fmt + + def _cbar_cla(self): + """Function to clear the interactive colorbar state.""" + for x in self._interactive_funcs: + delattr(self.ax, x) + # We now restore the old cla() back and can call it directly + del self.ax.cla + self.ax.cla() - def _extend_lower(self): - """Return whether the lower limit is open ended.""" - return self.extend in ('both', 'min') + filled = _api.deprecate_privatize_attribute("3.6") - def _extend_upper(self): - """Return whether the upper limit is open ended.""" - return self.extend in ('both', 'max') + def update_normal(self, mappable): + """ + Update solid patches, lines, etc. + + This is meant to be called when the norm of the image or contour plot + to which this colorbar belongs changes. + + If the norm on the mappable is different than before, this resets the + locator and formatter for the axis, so if these have been customized, + they will need to be customized again. However, if the norm only + changes values of *vmin*, *vmax* or *cmap* then the old formatter + and locator will be preserved. + """ + _log.debug('colorbar update normal %r %r', mappable.norm, self.norm) + self.mappable = mappable + self.set_alpha(mappable.get_alpha()) + self.cmap = mappable.cmap + if mappable.norm != self.norm: + self.norm = mappable.norm + self._reset_locator_formatter_scale() + + self._draw_all() + if isinstance(self.mappable, contour.ContourSet): + CS = self.mappable + if not CS.filled: + self.add_lines(CS) + self.stale = True + @_api.deprecated("3.6", alternative="fig.draw_without_rendering()") def draw_all(self): """ Calculate any free parameters based on the current cmap and norm, and do all the drawing. """ - self._config_axis() # Inline it after deprecation elapses. - # Set self._boundaries and self._values, including extensions. - self._process_values() - # Set self.vmin and self.vmax to first and last boundary, excluding - # extensions. - self.vmin, self.vmax = self._boundaries[self._inside][[0, -1]] - # Compute the X/Y mesh. - X, Y = self._mesh() - # Extract bounding polygon (the last entry's value (X[0, 1]) doesn't - # matter, it just matches the CLOSEPOLY code). - x = np.concatenate([X[[0, 1, -2, -1], 0], X[[-1, -2, 1, 0, 0], 1]]) - y = np.concatenate([Y[[0, 1, -2, -1], 0], Y[[-1, -2, 1, 0, 0], 1]]) - xy = np.column_stack([x, y]) - # Configure axes limits, patch, and outline. - xmin, ymin = xy.min(axis=0) - xmax, ymax = xy.max(axis=0) - self.ax.set(xlim=(xmin, xmax), ylim=(ymin, ymax)) - self.outline.set_xy(xy) - self.patch.set_xy(xy) - self.update_ticks() - if self.filled: - self._add_solids(X, Y, self._values[:, np.newaxis]) - - @_api.deprecated("3.3") - def config_axis(self): - self._config_axis() + self._draw_all() - def _config_axis(self): - """Set up long and short axis.""" - ax = self.ax + def _draw_all(self): + """ + Calculate any free parameters based on the current cmap and norm, + and do all the drawing. + """ if self.orientation == 'vertical': - long_axis, short_axis = ax.yaxis, ax.xaxis if mpl.rcParams['ytick.minor.visible']: self.minorticks_on() else: - long_axis, short_axis = ax.xaxis, ax.yaxis if mpl.rcParams['xtick.minor.visible']: self.minorticks_on() - long_axis.set(label_position=self.ticklocation, - ticks_position=self.ticklocation) - short_axis.set_ticks([]) - short_axis.set_ticks([], minor=True) - self.stale = True - - def _get_ticker_locator_formatter(self): - """ - Return the ``locator`` and ``formatter`` of the colorbar. - - If they have not been defined (i.e. are *None*), suitable formatter - and locator instances will be created, attached to the respective - attributes and returned. - """ - locator = self.locator - formatter = self.formatter - if locator is None: - if self.boundaries is None: - if isinstance(self.norm, colors.NoNorm): - nv = len(self._values) - base = 1 + int(nv / 10) - locator = ticker.IndexLocator(base=base, offset=0) - elif isinstance(self.norm, colors.BoundaryNorm): - b = self.norm.boundaries - locator = ticker.FixedLocator(b, nbins=10) - elif isinstance(self.norm, colors.LogNorm): - locator = _ColorbarLogLocator(self) - elif isinstance(self.norm, colors.SymLogNorm): - # The subs setting here should be replaced - # by logic in the locator. - locator = ticker.SymmetricalLogLocator( - subs=np.arange(1, 10), - linthresh=self.norm.linthresh, - base=10) - else: - if mpl.rcParams['_internal.classic_mode']: - locator = ticker.MaxNLocator() - else: - locator = _ColorbarAutoLocator(self) - else: - b = self._boundaries[self._inside] - locator = ticker.FixedLocator(b, nbins=10) - - if formatter is None: - if isinstance(self.norm, colors.LogNorm): - formatter = ticker.LogFormatterSciNotation() - elif isinstance(self.norm, colors.SymLogNorm): - formatter = ticker.LogFormatterSciNotation( - linthresh=self.norm.linthresh) - else: - formatter = ticker.ScalarFormatter() - else: - formatter = self.formatter - - self.locator = locator - self.formatter = formatter - _log.debug('locator: %r', locator) - return locator, formatter - - def _use_auto_colorbar_locator(self): - """ - Return if we should use an adjustable tick locator or a fixed - one. (check is used twice so factored out here...) - """ - contouring = self.boundaries is not None and self.spacing == 'uniform' - return (type(self.norm) in [colors.Normalize, colors.LogNorm] and - not contouring) - - def _reset_locator_formatter_scale(self): - """ - Reset the locator et al to defaults. Any user-hardcoded changes - need to be re-entered if this gets called (either at init, or when - the mappable normal gets changed: Colorbar.update_normal) - """ - self.locator = None - self.formatter = None - if isinstance(self.norm, colors.LogNorm): - # *both* axes are made log so that determining the - # mid point is easier. - self.ax.set_xscale('log') - self.ax.set_yscale('log') - self.minorticks_on() - self.__scale = 'log' - else: - self.ax.set_xscale('linear') - self.ax.set_yscale('linear') - if type(self.norm) is colors.Normalize: - self.__scale = 'linear' - else: - self.__scale = 'manual' - - def update_ticks(self): - """ - Force the update of the ticks and ticklabels. This must be - called whenever the tick locator and/or tick formatter changes. - """ - ax = self.ax - # Get the locator and formatter; defaults to self.locator if not None. - locator, formatter = self._get_ticker_locator_formatter() - long_axis = ax.yaxis if self.orientation == 'vertical' else ax.xaxis - if self._use_auto_colorbar_locator(): - _log.debug('Using auto colorbar locator %r on colorbar', locator) - long_axis.set_major_locator(locator) - long_axis.set_major_formatter(formatter) - else: - _log.debug('Using fixed locator on colorbar') - ticks, ticklabels, offset_string = self._ticker(locator, formatter) - long_axis.set_ticks(ticks) - long_axis.set_ticklabels(ticklabels) - long_axis.get_major_formatter().set_offset_string(offset_string) - - def set_ticks(self, ticks, update_ticks=True): - """ - Set tick locations. - - Parameters - ---------- - ticks : array-like or `~matplotlib.ticker.Locator` or None - The tick positions can be hard-coded by an array of values; or - they can be defined by a `.Locator`. Setting to *None* reverts - to using a default locator. - - update_ticks : bool, default: True - If True, tick locations are updated immediately. If False, the - user has to call `update_ticks` later to update the ticks. - - """ - if np.iterable(ticks): - self.locator = ticker.FixedLocator(ticks, nbins=len(ticks)) - else: - self.locator = ticks - - if update_ticks: - self.update_ticks() - self.stale = True - - def get_ticks(self, minor=False): - """Return the x ticks as a list of locations.""" - if self._manual_tick_data_values is None: - ax = self.ax - long_axis = ( - ax.yaxis if self.orientation == 'vertical' else ax.xaxis) - return long_axis.get_majorticklocs() - else: - # We made the axes manually, the old way, and the ylim is 0-1, - # so the majorticklocs are in those units, not data units. - return self._manual_tick_data_values - - def set_ticklabels(self, ticklabels, update_ticks=True): - """ - Set tick labels. - - Tick labels are updated immediately unless *update_ticks* is *False*, - in which case one should call `.update_ticks` explicitly. - """ - if isinstance(self.locator, ticker.FixedLocator): - self.formatter = ticker.FixedFormatter(ticklabels) - if update_ticks: - self.update_ticks() - else: - _api.warn_external("set_ticks() must have been called.") - self.stale = True - - def minorticks_on(self): - """ - Turn the minor ticks of the colorbar on without extruding - into the "extend regions". - """ - ax = self.ax - long_axis = ax.yaxis if self.orientation == 'vertical' else ax.xaxis + self._long_axis().set(label_position=self.ticklocation, + ticks_position=self.ticklocation) + self._short_axis().set_ticks([]) + self._short_axis().set_ticks([], minor=True) - if long_axis.get_scale() == 'log': - long_axis.set_minor_locator(_ColorbarLogLocator(self, base=10., - subs='auto')) - long_axis.set_minor_formatter(ticker.LogFormatterSciNotation()) + # Set self._boundaries and self._values, including extensions. + # self._boundaries are the edges of each square of color, and + # self._values are the value to map into the norm to get the + # color: + self._process_values() + # Set self.vmin and self.vmax to first and last boundary, excluding + # extensions: + self.vmin, self.vmax = self._boundaries[self._inside][[0, -1]] + # Compute the X/Y mesh. + X, Y = self._mesh() + # draw the extend triangles, and shrink the inner axes to accommodate. + # also adds the outline path to self.outline spine: + self._do_extends() + lower, upper = self.vmin, self.vmax + if self._long_axis().get_inverted(): + # If the axis is inverted, we need to swap the vmin/vmax + lower, upper = upper, lower + if self.orientation == 'vertical': + self.ax.set_xlim(0, 1) + self.ax.set_ylim(lower, upper) else: - long_axis.set_minor_locator(_ColorbarAutoMinorLocator(self)) - - def minorticks_off(self): - """Turn the minor ticks of the colorbar off.""" - ax = self.ax - long_axis = ax.yaxis if self.orientation == 'vertical' else ax.xaxis - long_axis.set_minor_locator(ticker.NullLocator()) - - def set_label(self, label, *, loc=None, **kwargs): - """ - Add a label to the long axis of the colorbar. - - Parameters - ---------- - label : str - The label text. - loc : str, optional - The location of the label. + self.ax.set_ylim(0, 1) + self.ax.set_xlim(lower, upper) - - For horizontal orientation one of {'left', 'center', 'right'} - - For vertical orientation one of {'bottom', 'center', 'top'} + # set up the tick locators and formatters. A bit complicated because + # boundary norms + uniform spacing requires a manual locator. + self.update_ticks() - Defaults to :rc:`xaxis.labellocation` or :rc:`yaxis.labellocation` - depending on the orientation. - **kwargs - Keyword arguments are passed to `~.Axes.set_xlabel` / - `~.Axes.set_ylabel`. - Supported keywords are *labelpad* and `.Text` properties. - """ - if self.orientation == "vertical": - self.ax.set_ylabel(label, loc=loc, **kwargs) - else: - self.ax.set_xlabel(label, loc=loc, **kwargs) - self.stale = True + if self._filled: + ind = np.arange(len(self._values)) + if self._extend_lower(): + ind = ind[1:] + if self._extend_upper(): + ind = ind[:-1] + self._add_solids(X, Y, self._values[ind, np.newaxis]) def _add_solids(self, X, Y, C): """Draw the colors; optionally add separators.""" @@ -786,27 +592,46 @@ def _add_solids(self, X, Y, C): and any(hatch is not None for hatch in mappable.hatches)): self._add_solids_patches(X, Y, C, mappable) else: - self._add_solids_pcolormesh(X, Y, C) - self.dividers.set_segments( - np.dstack([X, Y])[1:-1] if self.drawedges else []) - - def _add_solids_pcolormesh(self, X, Y, C): - _log.debug('Setting pcolormesh') - if C.shape[0] == Y.shape[0]: - # trim the last one to be compatible with old behavior. - C = C[:-1] - self.solids = self.ax.pcolormesh( - X, Y, C, cmap=self.cmap, norm=self.norm, alpha=self.alpha, - edgecolors='none', shading='flat') + self.solids = self.ax.pcolormesh( + X, Y, C, cmap=self.cmap, norm=self.norm, alpha=self.alpha, + edgecolors='none', shading='flat') + if not self.drawedges: + if len(self._y) >= self.n_rasterize: + self.solids.set_rasterized(True) + self._update_dividers() + + def _update_dividers(self): if not self.drawedges: - if len(self._y) >= self.n_rasterize: - self.solids.set_rasterized(True) + self.dividers.set_segments([]) + return + # Place all *internal* dividers. + if self.orientation == 'vertical': + lims = self.ax.get_ylim() + bounds = (lims[0] < self._y) & (self._y < lims[1]) + else: + lims = self.ax.get_xlim() + bounds = (lims[0] < self._y) & (self._y < lims[1]) + y = self._y[bounds] + # And then add outer dividers if extensions are on. + if self._extend_lower(): + y = np.insert(y, 0, lims[0]) + if self._extend_upper(): + y = np.append(y, lims[1]) + X, Y = np.meshgrid([0, 1], y) + if self.orientation == 'vertical': + segments = np.dstack([X, Y]) + else: + segments = np.dstack([Y, X]) + self.dividers.set_segments(segments) def _add_solids_patches(self, X, Y, C, mappable): - hatches = mappable.hatches * len(C) # Have enough hatches. + hatches = mappable.hatches * (len(C) + 1) # Have enough hatches. + if self._extend_lower(): + # remove first hatch that goes into the extend patch + hatches = hatches[1:] patches = [] for i in range(len(X) - 1): - xy = np.array([[X[i, 0], Y[i, 0]], + xy = np.array([[X[i, 0], Y[i, 1]], [X[i, 1], Y[i, 0]], [X[i + 1, 1], Y[i + 1, 0]], [X[i + 1, 0], Y[i + 1, 1]]]) @@ -818,13 +643,107 @@ def _add_solids_patches(self, X, Y, C, mappable): patches.append(patch) self.solids_patches = patches - def add_lines(self, levels, colors, linewidths, erase=True): + def _do_extends(self, ax=None): """ - Draw lines on the colorbar. + Add the extend tri/rectangles on the outside of the axes. - The lines are appended to the list :attr:`lines`. + ax is unused, but required due to the callbacks on xlim/ylim changed + """ + # Clean up any previous extend patches + for patch in self._extend_patches: + patch.remove() + self._extend_patches = [] + # extend lengths are fraction of the *inner* part of colorbar, + # not the total colorbar: + _, extendlen = self._proportional_y() + bot = 0 - (extendlen[0] if self._extend_lower() else 0) + top = 1 + (extendlen[1] if self._extend_upper() else 0) + + # xyout is the outline of the colorbar including the extend patches: + if not self.extendrect: + # triangle: + xyout = np.array([[0, 0], [0.5, bot], [1, 0], + [1, 1], [0.5, top], [0, 1], [0, 0]]) + else: + # rectangle: + xyout = np.array([[0, 0], [0, bot], [1, bot], [1, 0], + [1, 1], [1, top], [0, top], [0, 1], + [0, 0]]) - Parameters + if self.orientation == 'horizontal': + xyout = xyout[:, ::-1] + + # xyout is the path for the spine: + self.outline.set_xy(xyout) + if not self._filled: + return + + # Make extend triangles or rectangles filled patches. These are + # defined in the outer parent axes' coordinates: + mappable = getattr(self, 'mappable', None) + if (isinstance(mappable, contour.ContourSet) + and any(hatch is not None for hatch in mappable.hatches)): + hatches = mappable.hatches * (len(self._y) + 1) + else: + hatches = [None] * (len(self._y) + 1) + + if self._extend_lower(): + if not self.extendrect: + # triangle + xy = np.array([[0, 0], [0.5, bot], [1, 0]]) + else: + # rectangle + xy = np.array([[0, 0], [0, bot], [1., bot], [1, 0]]) + if self.orientation == 'horizontal': + xy = xy[:, ::-1] + # add the patch + val = -1 if self._long_axis().get_inverted() else 0 + color = self.cmap(self.norm(self._values[val])) + patch = mpatches.PathPatch( + mpath.Path(xy), facecolor=color, alpha=self.alpha, + linewidth=0, antialiased=False, + transform=self.ax.transAxes, + hatch=hatches[0], clip_on=False, + # Place it right behind the standard patches, which is + # needed if we updated the extends + zorder=np.nextafter(self.ax.patch.zorder, -np.inf)) + self.ax.add_patch(patch) + self._extend_patches.append(patch) + # remove first hatch that goes into the extend patch + hatches = hatches[1:] + if self._extend_upper(): + if not self.extendrect: + # triangle + xy = np.array([[0, 1], [0.5, top], [1, 1]]) + else: + # rectangle + xy = np.array([[0, 1], [0, top], [1, top], [1, 1]]) + if self.orientation == 'horizontal': + xy = xy[:, ::-1] + # add the patch + val = 0 if self._long_axis().get_inverted() else -1 + color = self.cmap(self.norm(self._values[val])) + hatch_idx = len(self._y) - 1 + patch = mpatches.PathPatch( + mpath.Path(xy), facecolor=color, alpha=self.alpha, + linewidth=0, antialiased=False, + transform=self.ax.transAxes, hatch=hatches[hatch_idx], + clip_on=False, + # Place it right behind the standard patches, which is + # needed if we updated the extends + zorder=np.nextafter(self.ax.patch.zorder, -np.inf)) + self.ax.add_patch(patch) + self._extend_patches.append(patch) + + self._update_dividers() + + def add_lines(self, *args, **kwargs): + """ + Draw lines on the colorbar. + + The lines are appended to the list :attr:`lines`. + + Parameters ---------- levels : array-like The positions of the lines. @@ -836,7 +755,31 @@ def add_lines(self, levels, colors, linewidths, erase=True): for each line. erase : bool, default: True Whether to remove any previously added lines. + + Notes + ----- + Alternatively, this method can also be called with the signature + ``colorbar.add_lines(contour_set, erase=True)``, in which case + *levels*, *colors*, and *linewidths* are taken from *contour_set*. """ + params = _api.select_matching_signature( + [lambda self, CS, erase=True: locals(), + lambda self, levels, colors, linewidths, erase=True: locals()], + self, *args, **kwargs) + if "CS" in params: + self, CS, erase = params.values() + if not isinstance(CS, contour.ContourSet) or CS.filled: + raise ValueError("If a single artist is passed to add_lines, " + "it must be a ContourSet of lines") + # TODO: Make colorbar lines auto-follow changes in contour lines. + return self.add_lines( + CS.levels, + [c[0] for c in CS.tcolors], + [t[0] for t in CS.tlinewidths], + erase=erase) + else: + self, levels, colors, linewidths, erase = params.values() + y = self._locate(levels) rtol = (self._y[-1] - self._y[0]) * 1e-10 igood = (y < self._y[-1] + rtol) & (y > self._y[0] - rtol) @@ -845,459 +788,242 @@ def add_lines(self, levels, colors, linewidths, erase=True): colors = np.asarray(colors)[igood] if np.iterable(linewidths): linewidths = np.asarray(linewidths)[igood] - X, Y = np.meshgrid([self._y[0], self._y[-1]], y) + X, Y = np.meshgrid([0, 1], y) if self.orientation == 'vertical': xy = np.stack([X, Y], axis=-1) else: xy = np.stack([Y, X], axis=-1) - col = collections.LineCollection(xy, linewidths=linewidths) + col = collections.LineCollection(xy, linewidths=linewidths, + colors=colors) if erase and self.lines: for lc in self.lines: lc.remove() self.lines = [] self.lines.append(col) - col.set_color(colors) + + # make a clip path that is just a linewidth bigger than the axes... + fac = np.max(linewidths) / 72 + xy = np.array([[0, 0], [1, 0], [1, 1], [0, 1], [0, 0]]) + inches = self.ax.get_figure().dpi_scale_trans + # do in inches: + xy = inches.inverted().transform(self.ax.transAxes.transform(xy)) + xy[[0, 1, 4], 1] -= fac + xy[[2, 3], 1] += fac + # back to axes units... + xy = self.ax.transAxes.inverted().transform(inches.transform(xy)) + col.set_clip_path(mpath.Path(xy, closed=True), + self.ax.transAxes) self.ax.add_collection(col) self.stale = True - def _ticker(self, locator, formatter): - """ - Return the sequence of ticks (colorbar data locations), - ticklabels (strings), and the corresponding offset string. - """ - if isinstance(self.norm, colors.NoNorm) and self.boundaries is None: - intv = self._values[0], self._values[-1] - else: - intv = self.vmin, self.vmax - locator.create_dummy_axis(minpos=intv[0]) - formatter.create_dummy_axis(minpos=intv[0]) - locator.set_view_interval(*intv) - locator.set_data_interval(*intv) - formatter.set_view_interval(*intv) - formatter.set_data_interval(*intv) - - b = np.array(locator()) - if isinstance(locator, ticker.LogLocator): - eps = 1e-10 - b = b[(b <= intv[1] * (1 + eps)) & (b >= intv[0] * (1 - eps))] - else: - eps = (intv[1] - intv[0]) * 1e-10 - b = b[(b <= intv[1] + eps) & (b >= intv[0] - eps)] - self._manual_tick_data_values = b - ticks = self._locate(b) - ticklabels = formatter.format_ticks(b) - offset_string = formatter.get_offset() - return ticks, ticklabels, offset_string - - def _process_values(self, b=None): + def update_ticks(self): """ - Set the :attr:`_boundaries` and :attr:`_values` attributes - based on the input boundaries and values. Input boundaries - can be *self.boundaries* or the argument *b*. + Set up the ticks and ticklabels. This should not be needed by users. """ - if b is None: - b = self.boundaries - if b is not None: - self._boundaries = np.asarray(b, dtype=float) - if self.values is None: - self._values = 0.5 * (self._boundaries[:-1] - + self._boundaries[1:]) - if isinstance(self.norm, colors.NoNorm): - self._values = (self._values + 0.00001).astype(np.int16) - else: - self._values = np.array(self.values) - return - if self.values is not None: - self._values = np.array(self.values) - if self.boundaries is None: - b = np.zeros(len(self.values) + 1) - b[1:-1] = 0.5 * (self._values[:-1] + self._values[1:]) - b[0] = 2.0 * b[1] - b[2] - b[-1] = 2.0 * b[-2] - b[-3] - self._boundaries = b - return - self._boundaries = np.array(self.boundaries) - return - # Neither boundaries nor values are specified; - # make reasonable ones based on cmap and norm. - if isinstance(self.norm, colors.NoNorm): - b = self._uniform_y(self.cmap.N + 1) * self.cmap.N - 0.5 - v = np.zeros(len(b) - 1, dtype=np.int16) - v[self._inside] = np.arange(self.cmap.N, dtype=np.int16) - if self._extend_lower(): - v[0] = -1 - if self._extend_upper(): - v[-1] = self.cmap.N - self._boundaries = b - self._values = v - return - elif isinstance(self.norm, colors.BoundaryNorm): - b = list(self.norm.boundaries) - if self._extend_lower(): - b = [b[0] - 1] + b - if self._extend_upper(): - b = b + [b[-1] + 1] - b = np.array(b) - v = np.zeros(len(b) - 1) - bi = self.norm.boundaries - v[self._inside] = 0.5 * (bi[:-1] + bi[1:]) - if self._extend_lower(): - v[0] = b[0] - 1 - if self._extend_upper(): - v[-1] = b[-1] + 1 - self._boundaries = b - self._values = v - return - else: - if not self.norm.scaled(): - self.norm.vmin = 0 - self.norm.vmax = 1 - - self.norm.vmin, self.norm.vmax = mtransforms.nonsingular( - self.norm.vmin, - self.norm.vmax, - expander=0.1) - - b = self.norm.inverse(self._uniform_y(self.cmap.N + 1)) - - if isinstance(self.norm, (colors.PowerNorm, colors.LogNorm)): - # If using a lognorm or powernorm, ensure extensions don't - # go negative - if self._extend_lower(): - b[0] = 0.9 * b[0] - if self._extend_upper(): - b[-1] = 1.1 * b[-1] - else: - if self._extend_lower(): - b[0] = b[0] - 1 - if self._extend_upper(): - b[-1] = b[-1] + 1 - self._process_values(b) - - def _get_extension_lengths(self, frac, automin, automax, default=0.05): - """ - Return the lengths of colorbar extensions. + # Get the locator and formatter; defaults to self._locator if not None. + self._get_ticker_locator_formatter() + self._long_axis().set_major_locator(self._locator) + self._long_axis().set_minor_locator(self._minorlocator) + self._long_axis().set_major_formatter(self._formatter) - This is a helper method for _uniform_y and _proportional_y. + def _get_ticker_locator_formatter(self): """ - # Set the default value. - extendlength = np.array([default, default]) - if isinstance(frac, str): - _api.check_in_list(['auto'], extendfrac=frac.lower()) - # Use the provided values when 'auto' is required. - extendlength[:] = [automin, automax] - elif frac is not None: - try: - # Try to set min and max extension fractions directly. - extendlength[:] = frac - # If frac is a sequence containing None then NaN may - # be encountered. This is an error. - if np.isnan(extendlength).any(): - raise ValueError() - except (TypeError, ValueError) as err: - # Raise an error on encountering an invalid value for frac. - raise ValueError('invalid value for extendfrac') from err - return extendlength + Return the ``locator`` and ``formatter`` of the colorbar. - def _uniform_y(self, N): - """ - Return colorbar data coordinates for *N* uniformly - spaced boundaries, plus ends if required. - """ - if self.extend == 'neither': - y = np.linspace(0, 1, N) - else: - automin = automax = 1. / (N - 1.) - extendlength = self._get_extension_lengths(self.extendfrac, - automin, automax, - default=0.05) - if self.extend == 'both': - y = np.zeros(N + 2, 'd') - y[0] = 0. - extendlength[0] - y[-1] = 1. + extendlength[1] - elif self.extend == 'min': - y = np.zeros(N + 1, 'd') - y[0] = 0. - extendlength[0] - else: - y = np.zeros(N + 1, 'd') - y[-1] = 1. + extendlength[1] - y[self._inside] = np.linspace(0, 1, N) - return y + If they have not been defined (i.e. are *None*), the formatter and + locator are retrieved from the axis, or from the value of the + boundaries for a boundary norm. - def _proportional_y(self): - """ - Return colorbar data coordinates for the boundaries of - a proportional colorbar. + Called by update_ticks... """ + locator = self._locator + formatter = self._formatter + minorlocator = self._minorlocator if isinstance(self.norm, colors.BoundaryNorm): - y = (self._boundaries - self._boundaries[0]) - y = y / (self._boundaries[-1] - self._boundaries[0]) - else: - y = self.norm(self._boundaries.copy()) - y = np.ma.filled(y, np.nan) - if self.extend == 'min': - # Exclude leftmost interval of y. - clen = y[-1] - y[1] - automin = (y[2] - y[1]) / clen - automax = (y[-1] - y[-2]) / clen - elif self.extend == 'max': - # Exclude rightmost interval in y. - clen = y[-2] - y[0] - automin = (y[1] - y[0]) / clen - automax = (y[-2] - y[-3]) / clen - elif self.extend == 'both': - # Exclude leftmost and rightmost intervals in y. - clen = y[-2] - y[1] - automin = (y[2] - y[1]) / clen - automax = (y[-2] - y[-3]) / clen - if self.extend in ('both', 'min', 'max'): - extendlength = self._get_extension_lengths(self.extendfrac, - automin, automax, - default=0.05) - if self.extend in ('both', 'min'): - y[0] = 0. - extendlength[0] - if self.extend in ('both', 'max'): - y[-1] = 1. + extendlength[1] - yi = y[self._inside] - norm = colors.Normalize(yi[0], yi[-1]) - y[self._inside] = np.ma.filled(norm(yi), np.nan) - return y + b = self.norm.boundaries + if locator is None: + locator = ticker.FixedLocator(b, nbins=10) + if minorlocator is None: + minorlocator = ticker.FixedLocator(b) + elif isinstance(self.norm, colors.NoNorm): + if locator is None: + # put ticks on integers between the boundaries of NoNorm + nv = len(self._values) + base = 1 + int(nv / 10) + locator = ticker.IndexLocator(base=base, offset=.5) + elif self.boundaries is not None: + b = self._boundaries[self._inside] + if locator is None: + locator = ticker.FixedLocator(b, nbins=10) + else: # most cases: + if locator is None: + # we haven't set the locator explicitly, so use the default + # for this axis: + locator = self._long_axis().get_major_locator() + if minorlocator is None: + minorlocator = self._long_axis().get_minor_locator() - def _mesh(self): + if minorlocator is None: + minorlocator = ticker.NullLocator() + + if formatter is None: + formatter = self._long_axis().get_major_formatter() + + self._locator = locator + self._formatter = formatter + self._minorlocator = minorlocator + _log.debug('locator: %r', locator) + + def set_ticks(self, ticks, *, labels=None, minor=False, **kwargs): """ - Return the coordinate arrays for the colorbar pcolormesh/patches. + Set tick locations. - These are scaled between vmin and vmax, and already handle colorbar - orientation. + Parameters + ---------- + ticks : list of floats + List of tick locations. + labels : list of str, optional + List of tick labels. If not set, the labels show the data value. + minor : bool, default: False + If ``False``, set the major ticks; if ``True``, the minor ticks. + **kwargs + `.Text` properties for the labels. These take effect only if you + pass *labels*. In other cases, please use `~.Axes.tick_params`. """ - # copy the norm and change the vmin and vmax to the vmin and - # vmax of the colorbar, not the norm. This allows the situation - # where the colormap has a narrower range than the colorbar, to - # accommodate extra contours: - norm = copy.copy(self.norm) - norm.vmin = self.vmin - norm.vmax = self.vmax - x = np.array([0.0, 1.0]) - if self.spacing == 'uniform': - n_boundaries_no_extensions = len(self._boundaries[self._inside]) - y = self._uniform_y(n_boundaries_no_extensions) - else: - y = self._proportional_y() - xmid = np.array([0.5]) - if self.__scale != 'manual': - y = norm.inverse(y) - x = norm.inverse(x) - xmid = norm.inverse(xmid) + if np.iterable(ticks): + self._long_axis().set_ticks(ticks, labels=labels, minor=minor, + **kwargs) + self._locator = self._long_axis().get_major_locator() else: - # if a norm doesn't have a named scale, or - # we are not using a norm - dv = self.vmax - self.vmin - x = x * dv + self.vmin - y = y * dv + self.vmin - xmid = xmid * dv + self.vmin - self._y = y - X, Y = np.meshgrid(x, y) - if self._extend_lower() and not self.extendrect: - X[0, :] = xmid - if self._extend_upper() and not self.extendrect: - X[-1, :] = xmid - return (X, Y) if self.orientation == 'vertical' else (Y, X) + self._locator = ticks + self._long_axis().set_major_locator(self._locator) + self.stale = True - def _locate(self, x): + def get_ticks(self, minor=False): """ - Given a set of color data values, return their - corresponding colorbar data coordinates. + Return the ticks as a list of locations. + + Parameters + ---------- + minor : boolean, default: False + if True return the minor ticks. """ - if isinstance(self.norm, (colors.NoNorm, colors.BoundaryNorm)): - b = self._boundaries - xn = x + if minor: + return self._long_axis().get_minorticklocs() else: - # Do calculations using normalized coordinates so - # as to make the interpolation more accurate. - b = self.norm(self._boundaries, clip=False).filled() - xn = self.norm(x, clip=False).filled() - - bunique = b - yunique = self._y - # trim extra b values at beginning and end if they are - # not unique. These are here for extended colorbars, and are not - # wanted for the interpolation. - if b[0] == b[1]: - bunique = bunique[1:] - yunique = yunique[1:] - if b[-1] == b[-2]: - bunique = bunique[:-1] - yunique = yunique[:-1] - - z = np.interp(xn, bunique, yunique) - return z - - def set_alpha(self, alpha): - """Set the transparency between 0 (transparent) and 1 (opaque).""" - self.alpha = alpha - - def remove(self): - """Remove this colorbar from the figure.""" - self.ax.remove() - - -def _add_disjoint_kwargs(d, **kwargs): - """ - Update dict *d* with entries in *kwargs*, which must be absent from *d*. - """ - for k, v in kwargs.items(): - if k in d: - _api.warn_deprecated( - "3.3", message=f"The {k!r} parameter to Colorbar has no " - "effect because it is overridden by the mappable; it is " - "deprecated since %(since)s and will be removed %(removal)s.") - d[k] = v + return self._long_axis().get_majorticklocs() + def set_ticklabels(self, ticklabels, *, minor=False, **kwargs): + """ + [*Discouraged*] Set tick labels. -class Colorbar(ColorbarBase): - """ - This class connects a `ColorbarBase` to a `~.cm.ScalarMappable` - such as an `~.image.AxesImage` generated via `~.axes.Axes.imshow`. + .. admonition:: Discouraged - .. note:: - This class is not intended to be instantiated directly; instead, use - `.Figure.colorbar` or `.pyplot.colorbar` to create a colorbar. - """ + The use of this method is discouraged, because of the dependency + on tick positions. In most cases, you'll want to use + ``set_ticks(positions, labels=labels)`` instead. - def __init__(self, ax, mappable, **kwargs): - # Ensure the given mappable's norm has appropriate vmin and vmax set - # even if mappable.draw has not yet been called. - if mappable.get_array() is not None: - mappable.autoscale_None() + If you are using this method, you should always fix the tick + positions before, e.g. by using `.Colorbar.set_ticks` or by + explicitly setting a `~.ticker.FixedLocator` on the long axis + of the colorbar. Otherwise, ticks are free to move and the + labels may end up in unexpected positions. - self.mappable = mappable - _add_disjoint_kwargs(kwargs, cmap=mappable.cmap, norm=mappable.norm) + Parameters + ---------- + ticklabels : sequence of str or of `.Text` + Texts for labeling each tick location in the sequence set by + `.Colorbar.set_ticks`; the number of labels must match the number + of locations. - if isinstance(mappable, contour.ContourSet): - cs = mappable - _add_disjoint_kwargs( - kwargs, - alpha=cs.get_alpha(), - boundaries=cs._levels, - values=cs.cvalues, - extend=cs.extend, - filled=cs.filled, - ) - kwargs.setdefault( - 'ticks', ticker.FixedLocator(cs.levels, nbins=10)) - super().__init__(ax, **kwargs) - if not cs.filled: - self.add_lines(cs) - else: - if getattr(mappable.cmap, 'colorbar_extend', False) is not False: - kwargs.setdefault('extend', mappable.cmap.colorbar_extend) - if isinstance(mappable, martist.Artist): - _add_disjoint_kwargs(kwargs, alpha=mappable.get_alpha()) - super().__init__(ax, **kwargs) + update_ticks : bool, default: True + This keyword argument is ignored and will be removed. + Deprecated - mappable.colorbar = self - mappable.colorbar_cid = mappable.callbacksSM.connect( - 'changed', self.update_normal) + minor : bool + If True, set minor ticks instead of major ticks. - @_api.deprecated("3.3", alternative="update_normal") - def on_mappable_changed(self, mappable): + **kwargs + `.Text` properties for the labels. """ - Update this colorbar to match the mappable's properties. + self._long_axis().set_ticklabels(ticklabels, minor=minor, **kwargs) - Typically this is automatically registered as an event handler - by :func:`colorbar_factory` and should not be called manually. + def minorticks_on(self): + """ + Turn on colorbar minor ticks. """ - _log.debug('colorbar mappable changed') - self.update_normal(mappable) + self.ax.minorticks_on() + self._short_axis().set_minor_locator(ticker.NullLocator()) - def add_lines(self, CS, erase=True): + def minorticks_off(self): + """Turn the minor ticks of the colorbar off.""" + self._minorlocator = ticker.NullLocator() + self._long_axis().set_minor_locator(self._minorlocator) + + def set_label(self, label, *, loc=None, **kwargs): """ - Add the lines from a non-filled `~.contour.ContourSet` to the colorbar. + Add a label to the long axis of the colorbar. Parameters ---------- - CS : `~.contour.ContourSet` - The line positions are taken from the ContourSet levels. The - ContourSet must not be filled. - erase : bool, default: True - Whether to remove any previously added lines. + label : str + The label text. + loc : str, optional + The location of the label. + + - For horizontal orientation one of {'left', 'center', 'right'} + - For vertical orientation one of {'bottom', 'center', 'top'} + + Defaults to :rc:`xaxis.labellocation` or :rc:`yaxis.labellocation` + depending on the orientation. + **kwargs + Keyword arguments are passed to `~.Axes.set_xlabel` / + `~.Axes.set_ylabel`. + Supported keywords are *labelpad* and `.Text` properties. """ - if not isinstance(CS, contour.ContourSet) or CS.filled: - raise ValueError('add_lines is only for a ContourSet of lines') - tcolors = [c[0] for c in CS.tcolors] - tlinewidths = [t[0] for t in CS.tlinewidths] - # Wishlist: Make colorbar lines auto-follow changes in contour lines. - super().add_lines(CS.levels, tcolors, tlinewidths, erase=erase) + if self.orientation == "vertical": + self.ax.set_ylabel(label, loc=loc, **kwargs) + else: + self.ax.set_xlabel(label, loc=loc, **kwargs) + self.stale = True - def update_normal(self, mappable): + def set_alpha(self, alpha): """ - Update solid patches, lines, etc. + Set the transparency between 0 (transparent) and 1 (opaque). - This is meant to be called when the norm of the image or contour plot - to which this colorbar belongs changes. + If an array is provided, *alpha* will be set to None to use the + transparency values associated with the colormap. + """ + self.alpha = None if isinstance(alpha, np.ndarray) else alpha - If the norm on the mappable is different than before, this resets the - locator and formatter for the axis, so if these have been customized, - they will need to be customized again. However, if the norm only - changes values of *vmin*, *vmax* or *cmap* then the old formatter - and locator will be preserved. + def _set_scale(self, scale, **kwargs): """ - _log.debug('colorbar update normal %r %r', mappable.norm, self.norm) - self.mappable = mappable - self.set_alpha(mappable.get_alpha()) - self.cmap = mappable.cmap - if mappable.norm != self.norm: - self.norm = mappable.norm - self._reset_locator_formatter_scale() + Set the colorbar long axis scale. - self.draw_all() - if isinstance(self.mappable, contour.ContourSet): - CS = self.mappable - if not CS.filled: - self.add_lines(CS) - self.stale = True + Parameters + ---------- + scale : {"linear", "log", "symlog", "logit", ...} or `.ScaleBase` + The axis scale type to apply. - @_api.deprecated("3.3", alternative="update_normal") - def update_bruteforce(self, mappable): - """ - Destroy and rebuild the colorbar. This is - intended to become obsolete, and will probably be - deprecated and then removed. It is not called when - the pyplot.colorbar function or the Figure.colorbar - method are used to create the colorbar. + **kwargs + Different keyword arguments are accepted, depending on the scale. + See the respective class keyword arguments: + + - `matplotlib.scale.LinearScale` + - `matplotlib.scale.LogScale` + - `matplotlib.scale.SymmetricalLogScale` + - `matplotlib.scale.LogitScale` + - `matplotlib.scale.FuncScale` + + Notes + ----- + By default, Matplotlib supports the above-mentioned scales. + Additionally, custom scales may be registered using + `matplotlib.scale.register_scale`. These scales can then also + be used here. """ - # We are using an ugly brute-force method: clearing and - # redrawing the whole thing. The problem is that if any - # properties have been changed by methods other than the - # colorbar methods, those changes will be lost. - self.ax.cla() - self.locator = None - self.formatter = None - - # clearing the axes will delete outline, patch, solids, and lines: - for spine in self.ax.spines.values(): - spine.set_visible(False) - self.outline = self.ax.spines['outline'] = _ColorbarSpine(self.ax) - self.patch = mpatches.Polygon( - np.empty((0, 2)), - color=mpl.rcParams['axes.facecolor'], linewidth=0.01, zorder=-1) - self.ax.add_artist(self.patch) - self.solids = None - self.lines = [] - self.update_normal(mappable) - self.draw_all() - if isinstance(self.mappable, contour.ContourSet): - CS = self.mappable - if not CS.filled: - self.add_lines(CS) - #if self.lines is not None: - # tcolors = [c[0] for c in CS.tcolors] - # self.lines.set_color(tcolors) - #Fixme? Recalculate boundaries, ticks if vmin, vmax have changed. - #Fixme: Some refactoring may be needed; we should not - # be recalculating everything if there was a simple alpha - # change. + self._long_axis()._set_axes_scale(scale, **kwargs) def remove(self): """ @@ -1306,16 +1032,25 @@ def remove(self): If the colorbar was created with ``use_gridspec=True`` the previous gridspec is restored. """ - super().remove() - self.mappable.callbacksSM.disconnect(self.mappable.colorbar_cid) + if hasattr(self.ax, '_colorbar_info'): + parents = self.ax._colorbar_info['parents'] + for a in parents: + if self.ax in a._colorbars: + a._colorbars.remove(self.ax) + + self.ax.remove() + + self.mappable.callbacks.disconnect(self.mappable.colorbar_cid) self.mappable.colorbar = None self.mappable.colorbar_cid = None + # Remove the extension callbacks + self.ax.callbacks.disconnect(self._extend_cid1) + self.ax.callbacks.disconnect(self._extend_cid2) try: ax = self.mappable.axes except AttributeError: return - try: gs = ax.get_subplotspec().get_gridspec() subplotspec = gs.get_topmost_subplotspec() @@ -1327,31 +1062,330 @@ def remove(self): # use_gridspec was True ax.set_subplotspec(subplotspec) + def _process_values(self): + """ + Set `_boundaries` and `_values` based on the self.boundaries and + self.values if not None, or based on the size of the colormap and + the vmin/vmax of the norm. + """ + if self.values is not None: + # set self._boundaries from the values... + self._values = np.array(self.values) + if self.boundaries is None: + # bracket values by 1/2 dv: + b = np.zeros(len(self.values) + 1) + b[1:-1] = 0.5 * (self._values[:-1] + self._values[1:]) + b[0] = 2.0 * b[1] - b[2] + b[-1] = 2.0 * b[-2] - b[-3] + self._boundaries = b + return + self._boundaries = np.array(self.boundaries) + return + + # otherwise values are set from the boundaries + if isinstance(self.norm, colors.BoundaryNorm): + b = self.norm.boundaries + elif isinstance(self.norm, colors.NoNorm): + # NoNorm has N blocks, so N+1 boundaries, centered on integers: + b = np.arange(self.cmap.N + 1) - .5 + elif self.boundaries is not None: + b = self.boundaries + else: + # otherwise make the boundaries from the size of the cmap: + N = self.cmap.N + 1 + b, _ = self._uniform_y(N) + # add extra boundaries if needed: + if self._extend_lower(): + b = np.hstack((b[0] - 1, b)) + if self._extend_upper(): + b = np.hstack((b, b[-1] + 1)) + + # transform from 0-1 to vmin-vmax: + if not self.norm.scaled(): + self.norm.vmin = 0 + self.norm.vmax = 1 + self.norm.vmin, self.norm.vmax = mtransforms.nonsingular( + self.norm.vmin, self.norm.vmax, expander=0.1) + if (not isinstance(self.norm, colors.BoundaryNorm) and + (self.boundaries is None)): + b = self.norm.inverse(b) + + self._boundaries = np.asarray(b, dtype=float) + self._values = 0.5 * (self._boundaries[:-1] + self._boundaries[1:]) + if isinstance(self.norm, colors.NoNorm): + self._values = (self._values + 0.00001).astype(np.int16) + + def _mesh(self): + """ + Return the coordinate arrays for the colorbar pcolormesh/patches. + + These are scaled between vmin and vmax, and already handle colorbar + orientation. + """ + y, _ = self._proportional_y() + # Use the vmin and vmax of the colorbar, which may not be the same + # as the norm. There are situations where the colormap has a + # narrower range than the colorbar and we want to accommodate the + # extra contours. + if (isinstance(self.norm, (colors.BoundaryNorm, colors.NoNorm)) + or self.boundaries is not None): + # not using a norm. + y = y * (self.vmax - self.vmin) + self.vmin + else: + # Update the norm values in a context manager as it is only + # a temporary change and we don't want to propagate any signals + # attached to the norm (callbacks.blocked). + with self.norm.callbacks.blocked(), \ + cbook._setattr_cm(self.norm, + vmin=self.vmin, + vmax=self.vmax): + y = self.norm.inverse(y) + self._y = y + X, Y = np.meshgrid([0., 1.], y) + if self.orientation == 'vertical': + return (X, Y) + else: + return (Y, X) + + def _forward_boundaries(self, x): + # map boundaries equally between 0 and 1... + b = self._boundaries + y = np.interp(x, b, np.linspace(0, 1, len(b))) + # the following avoids ticks in the extends: + eps = (b[-1] - b[0]) * 1e-6 + # map these _well_ out of bounds to keep any ticks out + # of the extends region... + y[x < b[0]-eps] = -1 + y[x > b[-1]+eps] = 2 + return y + + def _inverse_boundaries(self, x): + # invert the above... + b = self._boundaries + return np.interp(x, np.linspace(0, 1, len(b)), b) + + def _reset_locator_formatter_scale(self): + """ + Reset the locator et al to defaults. Any user-hardcoded changes + need to be re-entered if this gets called (either at init, or when + the mappable normal gets changed: Colorbar.update_normal) + """ + self._process_values() + self._locator = None + self._minorlocator = None + self._formatter = None + self._minorformatter = None + if (isinstance(self.mappable, contour.ContourSet) and + isinstance(self.norm, colors.LogNorm)): + # if contours have lognorm, give them a log scale... + self._set_scale('log') + elif (self.boundaries is not None or + isinstance(self.norm, colors.BoundaryNorm)): + if self.spacing == 'uniform': + funcs = (self._forward_boundaries, self._inverse_boundaries) + self._set_scale('function', functions=funcs) + elif self.spacing == 'proportional': + self._set_scale('linear') + elif getattr(self.norm, '_scale', None): + # use the norm's scale (if it exists and is not None): + self._set_scale(self.norm._scale) + elif type(self.norm) is colors.Normalize: + # plain Normalize: + self._set_scale('linear') + else: + # norm._scale is None or not an attr: derive the scale from + # the Norm: + funcs = (self.norm, self.norm.inverse) + self._set_scale('function', functions=funcs) + + def _locate(self, x): + """ + Given a set of color data values, return their + corresponding colorbar data coordinates. + """ + if isinstance(self.norm, (colors.NoNorm, colors.BoundaryNorm)): + b = self._boundaries + xn = x + else: + # Do calculations using normalized coordinates so + # as to make the interpolation more accurate. + b = self.norm(self._boundaries, clip=False).filled() + xn = self.norm(x, clip=False).filled() + + bunique = b[self._inside] + yunique = self._y + + z = np.interp(xn, bunique, yunique) + return z + + # trivial helpers + + def _uniform_y(self, N): + """ + Return colorbar data coordinates for *N* uniformly + spaced boundaries, plus extension lengths if required. + """ + automin = automax = 1. / (N - 1.) + extendlength = self._get_extension_lengths(self.extendfrac, + automin, automax, + default=0.05) + y = np.linspace(0, 1, N) + return y, extendlength + + def _proportional_y(self): + """ + Return colorbar data coordinates for the boundaries of + a proportional colorbar, plus extension lengths if required: + """ + if (isinstance(self.norm, colors.BoundaryNorm) or + self.boundaries is not None): + y = (self._boundaries - self._boundaries[self._inside][0]) + y = y / (self._boundaries[self._inside][-1] - + self._boundaries[self._inside][0]) + # need yscaled the same as the axes scale to get + # the extend lengths. + if self.spacing == 'uniform': + yscaled = self._forward_boundaries(self._boundaries) + else: + yscaled = y + else: + y = self.norm(self._boundaries.copy()) + y = np.ma.filled(y, np.nan) + # the norm and the scale should be the same... + yscaled = y + y = y[self._inside] + yscaled = yscaled[self._inside] + # normalize from 0..1: + norm = colors.Normalize(y[0], y[-1]) + y = np.ma.filled(norm(y), np.nan) + norm = colors.Normalize(yscaled[0], yscaled[-1]) + yscaled = np.ma.filled(norm(yscaled), np.nan) + # make the lower and upper extend lengths proportional to the lengths + # of the first and last boundary spacing (if extendfrac='auto'): + automin = yscaled[1] - yscaled[0] + automax = yscaled[-1] - yscaled[-2] + extendlength = [0, 0] + if self._extend_lower() or self._extend_upper(): + extendlength = self._get_extension_lengths( + self.extendfrac, automin, automax, default=0.05) + return y, extendlength + + def _get_extension_lengths(self, frac, automin, automax, default=0.05): + """ + Return the lengths of colorbar extensions. + + This is a helper method for _uniform_y and _proportional_y. + """ + # Set the default value. + extendlength = np.array([default, default]) + if isinstance(frac, str): + _api.check_in_list(['auto'], extendfrac=frac.lower()) + # Use the provided values when 'auto' is required. + extendlength[:] = [automin, automax] + elif frac is not None: + try: + # Try to set min and max extension fractions directly. + extendlength[:] = frac + # If frac is a sequence containing None then NaN may + # be encountered. This is an error. + if np.isnan(extendlength).any(): + raise ValueError() + except (TypeError, ValueError) as err: + # Raise an error on encountering an invalid value for frac. + raise ValueError('invalid value for extendfrac') from err + return extendlength + + def _extend_lower(self): + """Return whether the lower limit is open ended.""" + minmax = "max" if self._long_axis().get_inverted() else "min" + return self.extend in ('both', minmax) + + def _extend_upper(self): + """Return whether the upper limit is open ended.""" + minmax = "min" if self._long_axis().get_inverted() else "max" + return self.extend in ('both', minmax) + + def _long_axis(self): + """Return the long axis""" + if self.orientation == 'vertical': + return self.ax.yaxis + return self.ax.xaxis + + def _short_axis(self): + """Return the short axis""" + if self.orientation == 'vertical': + return self.ax.xaxis + return self.ax.yaxis + + def _get_view(self): + # docstring inherited + # An interactive view for a colorbar is the norm's vmin/vmax + return self.norm.vmin, self.norm.vmax + + def _set_view(self, view): + # docstring inherited + # An interactive view for a colorbar is the norm's vmin/vmax + self.norm.vmin, self.norm.vmax = view + + def _set_view_from_bbox(self, bbox, direction='in', + mode=None, twinx=False, twiny=False): + # docstring inherited + # For colorbars, we use the zoom bbox to scale the norm's vmin/vmax + new_xbound, new_ybound = self.ax._prepare_view_from_bbox( + bbox, direction=direction, mode=mode, twinx=twinx, twiny=twiny) + if self.orientation == 'horizontal': + self.norm.vmin, self.norm.vmax = new_xbound + elif self.orientation == 'vertical': + self.norm.vmin, self.norm.vmax = new_ybound + + def drag_pan(self, button, key, x, y): + # docstring inherited + points = self.ax._get_pan_points(button, key, x, y) + if points is not None: + if self.orientation == 'horizontal': + self.norm.vmin, self.norm.vmax = points[:, 0] + elif self.orientation == 'vertical': + self.norm.vmin, self.norm.vmax = points[:, 1] + + +ColorbarBase = Colorbar # Backcompat API + def _normalize_location_orientation(location, orientation): if location is None: - location = _api.check_getitem( - {None: "right", "vertical": "right", "horizontal": "bottom"}, - orientation=orientation) + location = _get_ticklocation_from_orientation(orientation) loc_settings = _api.check_getitem({ - "left": {"location": "left", "orientation": "vertical", - "anchor": (1.0, 0.5), "panchor": (0.0, 0.5), "pad": 0.10}, - "right": {"location": "right", "orientation": "vertical", - "anchor": (0.0, 0.5), "panchor": (1.0, 0.5), "pad": 0.05}, - "top": {"location": "top", "orientation": "horizontal", - "anchor": (0.5, 0.0), "panchor": (0.5, 1.0), "pad": 0.05}, - "bottom": {"location": "bottom", "orientation": "horizontal", - "anchor": (0.5, 1.0), "panchor": (0.5, 0.0), "pad": 0.15}, + "left": {"location": "left", "anchor": (1.0, 0.5), + "panchor": (0.0, 0.5), "pad": 0.10}, + "right": {"location": "right", "anchor": (0.0, 0.5), + "panchor": (1.0, 0.5), "pad": 0.05}, + "top": {"location": "top", "anchor": (0.5, 0.0), + "panchor": (0.5, 1.0), "pad": 0.05}, + "bottom": {"location": "bottom", "anchor": (0.5, 1.0), + "panchor": (0.5, 0.0), "pad": 0.15}, }, location=location) + loc_settings["orientation"] = _get_orientation_from_location(location) if orientation is not None and orientation != loc_settings["orientation"]: # Allow the user to pass both if they are consistent. raise TypeError("location and orientation are mutually exclusive") return loc_settings -@docstring.Substitution(_make_axes_param_doc, _make_axes_other_param_doc) +def _get_orientation_from_location(location): + return _api.check_getitem( + {None: None, "left": "vertical", "right": "vertical", + "top": "horizontal", "bottom": "horizontal"}, location=location) + + +def _get_ticklocation_from_orientation(orientation): + return _api.check_getitem( + {None: "right", "vertical": "right", "horizontal": "bottom"}, + orientation=orientation) + + +@_docstring.interpd def make_axes(parents, location=None, orientation=None, fraction=0.15, - shrink=1.0, aspect=20, **kw): + shrink=1.0, aspect=20, **kwargs): """ Create an `~.axes.Axes` suitable for a colorbar. @@ -1360,40 +1394,41 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, Parameters ---------- - parents : `~.axes.Axes` or list of `~.axes.Axes` + parents : `~.axes.Axes` or iterable or `numpy.ndarray` of `~.axes.Axes` The Axes to use as parents for placing the colorbar. - %s + %(_make_axes_kw_doc)s Returns ------- cax : `~.axes.Axes` The child axes. - kw : dict + kwargs : dict The reduced keyword dictionary to be passed when creating the colorbar instance. - - Other Parameters - ---------------- - %s """ loc_settings = _normalize_location_orientation(location, orientation) - # put appropriate values into the kw dict for passing back to + # put appropriate values into the kwargs dict for passing back to # the Colorbar class - kw['orientation'] = loc_settings['orientation'] - location = kw['ticklocation'] = loc_settings['location'] - - anchor = kw.pop('anchor', loc_settings['anchor']) - parent_anchor = kw.pop('panchor', loc_settings['panchor']) + kwargs['orientation'] = loc_settings['orientation'] + location = kwargs['ticklocation'] = loc_settings['location'] + + anchor = kwargs.pop('anchor', loc_settings['anchor']) + panchor = kwargs.pop('panchor', loc_settings['panchor']) + aspect0 = aspect + # turn parents into a list if it is not already. Note we cannot + # use .flatten or .ravel as these copy the references rather than + # reuse them, leading to a memory leak + if isinstance(parents, np.ndarray): + parents = list(parents.flat) + elif np.iterable(parents): + parents = list(parents) + else: + parents = [parents] - parents_iterable = np.iterable(parents) - # turn parents into a list if it is not already. We do this w/ np - # because `plt.subplots` can return an ndarray and is natural to - # pass to `colorbar`. - parents = np.atleast_1d(parents).ravel() fig = parents[0].get_figure() pad0 = 0.05 if fig.get_constrained_layout() else loc_settings['pad'] - pad = kw.pop('pad', pad0) + pad = kwargs.pop('pad', pad0) if not all(fig is ax.get_figure() for ax in parents): raise ValueError('Unable to create a colorbar axes as not all ' @@ -1429,76 +1464,71 @@ def make_axes(parents, location=None, orientation=None, fraction=0.15, new_posn = shrinking_trans.transform(ax.get_position(original=True)) new_posn = mtransforms.Bbox(new_posn) ax._set_position(new_posn) - if parent_anchor is not False: - ax.set_anchor(parent_anchor) + if panchor is not False: + ax.set_anchor(panchor) cax = fig.add_axes(pbcb, label="") for a in parents: # tell the parent it has a colorbar a._colorbars += [cax] cax._colorbar_info = dict( - location=location, parents=parents, + location=location, shrink=shrink, anchor=anchor, - panchor=parent_anchor, + panchor=panchor, fraction=fraction, - aspect=aspect, + aspect=aspect0, pad=pad) # and we need to set the aspect ratio by hand... - cax.set_aspect(aspect, anchor=anchor, adjustable='box') + cax.set_anchor(anchor) + cax.set_box_aspect(aspect) + cax.set_aspect('auto') - return cax, kw + return cax, kwargs -@docstring.Substitution(_make_axes_param_doc, _make_axes_other_param_doc) +@_docstring.interpd def make_axes_gridspec(parent, *, location=None, orientation=None, - fraction=0.15, shrink=1.0, aspect=20, **kw): + fraction=0.15, shrink=1.0, aspect=20, **kwargs): """ - Create a `~.SubplotBase` suitable for a colorbar. + Create an `~.axes.Axes` suitable for a colorbar. The axes is placed in the figure of the *parent* axes, by resizing and repositioning *parent*. - This function is similar to `.make_axes`. Primary differences are - - - `.make_axes_gridspec` should only be used with a `.SubplotBase` parent. - - - `.make_axes` creates an `~.axes.Axes`; `.make_axes_gridspec` creates a - `.SubplotBase`. + This function is similar to `.make_axes` and mostly compatible with it. + Primary differences are + - `.make_axes_gridspec` requires the *parent* to have a subplotspec. + - `.make_axes` positions the axes in figure coordinates; + `.make_axes_gridspec` positions it using a subplotspec. - `.make_axes` updates the position of the parent. `.make_axes_gridspec` - replaces the ``grid_spec`` attribute of the parent with a new one. - - While this function is meant to be compatible with `.make_axes`, - there could be some minor differences. + replaces the parent gridspec with a new one. Parameters ---------- parent : `~.axes.Axes` The Axes to use as parent for placing the colorbar. - %s + %(_make_axes_kw_doc)s Returns ------- - cax : `~.axes.SubplotBase` + cax : `~.axes.Axes` The child axes. - kw : dict + kwargs : dict The reduced keyword dictionary to be passed when creating the colorbar instance. - - Other Parameters - ---------------- - %s """ loc_settings = _normalize_location_orientation(location, orientation) - kw['orientation'] = loc_settings['orientation'] - location = kw['ticklocation'] = loc_settings['location'] + kwargs['orientation'] = loc_settings['orientation'] + location = kwargs['ticklocation'] = loc_settings['location'] - anchor = kw.pop('anchor', loc_settings['anchor']) - panchor = kw.pop('panchor', loc_settings['panchor']) - pad = kw.pop('pad', loc_settings["pad"]) + aspect0 = aspect + anchor = kwargs.pop('anchor', loc_settings['anchor']) + panchor = kwargs.pop('panchor', loc_settings['panchor']) + pad = kwargs.pop('pad', loc_settings["pad"]) wh_space = 2 * pad / (1 - pad) if location in ('left', 'right'): @@ -1543,42 +1573,22 @@ def make_axes_gridspec(parent, *, location=None, orientation=None, aspect = 1 / aspect parent.set_subplotspec(ss_main) - parent.set_anchor(loc_settings["panchor"]) + if panchor is not False: + parent.set_anchor(panchor) fig = parent.get_figure() cax = fig.add_subplot(ss_cb, label="") - cax.set_aspect(aspect, anchor=loc_settings["anchor"], adjustable='box') - return cax, kw - - -@_api.deprecated("3.4", alternative="Colorbar") -class ColorbarPatch(Colorbar): - pass - - -@_api.deprecated("3.4", alternative="Colorbar") -def colorbar_factory(cax, mappable, **kwargs): - """ - Create a colorbar on the given axes for the given mappable. - - .. note:: - This is a low-level function to turn an existing axes into a colorbar - axes. Typically, you'll want to use `~.Figure.colorbar` instead, which - automatically handles creation and placement of a suitable axes as - well. - - Parameters - ---------- - cax : `~matplotlib.axes.Axes` - The `~.axes.Axes` to turn into a colorbar. - mappable : `~matplotlib.cm.ScalarMappable` - The mappable to be described by the colorbar. - **kwargs - Keyword arguments are passed to the respective colorbar class. + cax.set_anchor(anchor) + cax.set_box_aspect(aspect) + cax.set_aspect('auto') + cax._colorbar_info = dict( + location=location, + parents=[parent], + shrink=shrink, + anchor=anchor, + panchor=panchor, + fraction=fraction, + aspect=aspect0, + pad=pad) - Returns - ------- - `.Colorbar` - The created colorbar instance. - """ - return Colorbar(cax, mappable, **kwargs) + return cax, kwargs diff --git a/lib/matplotlib/colors.py b/lib/matplotlib/colors.py index 2ce6610fc0c4..a74650d5a1f3 100644 --- a/lib/matplotlib/colors.py +++ b/lib/matplotlib/colors.py @@ -32,43 +32,17 @@ "#rrggbb" format (`to_hex`), and a sequence of colors to an (n, 4) RGBA array (`to_rgba_array`). Caching is used for efficiency. -Matplotlib recognizes the following formats to specify a color: - -* an RGB or RGBA (red, green, blue, alpha) tuple of float values in closed - interval ``[0, 1]`` (e.g., ``(0.1, 0.2, 0.5)`` or ``(0.1, 0.2, 0.5, 0.3)``); -* a hex RGB or RGBA string (e.g., ``'#0f0f0f'`` or ``'#0f0f0f80'``; - case-insensitive); -* a shorthand hex RGB or RGBA string, equivalent to the hex RGB or RGBA - string obtained by duplicating each character, (e.g., ``'#abc'``, equivalent - to ``'#aabbcc'``, or ``'#abcd'``, equivalent to ``'#aabbccdd'``; - case-insensitive); -* a string representation of a float value in ``[0, 1]`` inclusive for gray - level (e.g., ``'0.5'``); -* one of the characters ``{'b', 'g', 'r', 'c', 'm', 'y', 'k', 'w'}``, which - are short-hand notations for shades of blue, green, red, cyan, magenta, - yellow, black, and white. Note that the colors ``'g', 'c', 'm', 'y'`` do not - coincide with the X11/CSS4 colors. Their particular shades were chosen for - better visibility of colored lines against typical backgrounds. -* a X11/CSS4 color name (case-insensitive); -* a name from the `xkcd color survey`_, prefixed with ``'xkcd:'`` (e.g., - ``'xkcd:sky blue'``; case insensitive); -* one of the Tableau Colors from the 'T10' categorical palette (the default - color cycle): ``{'tab:blue', 'tab:orange', 'tab:green', 'tab:red', - 'tab:purple', 'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan'}`` - (case-insensitive); -* a "CN" color spec, i.e. 'C' followed by a number, which is an index into the - default property cycle (:rc:`axes.prop_cycle`); the indexing is intended to - occur at rendering time, and defaults to black if the cycle does not include - color. +Colors that Matplotlib recognizes are listed at +:doc:`/tutorials/colors/colors`. .. _palettable: https://jiffyclub.github.io/palettable/ .. _xkcd color survey: https://xkcd.com/color/rgb/ """ import base64 -from collections.abc import Sized, Sequence -import copy +from collections.abc import Sized, Sequence, Mapping import functools +import importlib import inspect import io import itertools @@ -79,7 +53,7 @@ import matplotlib as mpl import numpy as np -from matplotlib import _api, cbook, scale +from matplotlib import _api, _cm, cbook, scale from ._color_data import BASE_COLORS, TABLEAU_COLORS, CSS4_COLORS, XKCD_COLORS @@ -119,6 +93,113 @@ def get_named_colors_mapping(): return _colors_full_map +class ColorSequenceRegistry(Mapping): + r""" + Container for sequences of colors that are known to Matplotlib by name. + + The universal registry instance is `matplotlib.color_sequences`. There + should be no need for users to instantiate `.ColorSequenceRegistry` + themselves. + + Read access uses a dict-like interface mapping names to lists of colors:: + + import matplotlib as mpl + cmap = mpl.color_sequences['tab10'] + + The returned lists are copies, so that their modification does not change + the global definition of the color sequence. + + Additional color sequences can be added via + `.ColorSequenceRegistry.register`:: + + mpl.color_sequences.register('rgb', ['r', 'g', 'b']) + """ + + _BUILTIN_COLOR_SEQUENCES = { + 'tab10': _cm._tab10_data, + 'tab20': _cm._tab20_data, + 'tab20b': _cm._tab20b_data, + 'tab20c': _cm._tab20c_data, + 'Pastel1': _cm._Pastel1_data, + 'Pastel2': _cm._Pastel2_data, + 'Paired': _cm._Paired_data, + 'Accent': _cm._Accent_data, + 'Dark2': _cm._Dark2_data, + 'Set1': _cm._Set1_data, + 'Set2': _cm._Set1_data, + 'Set3': _cm._Set1_data, + } + + def __init__(self): + self._color_sequences = {**self._BUILTIN_COLOR_SEQUENCES} + + def __getitem__(self, item): + try: + return list(self._color_sequences[item]) + except KeyError: + raise KeyError(f"{item!r} is not a known color sequence name") + + def __iter__(self): + return iter(self._color_sequences) + + def __len__(self): + return len(self._color_sequences) + + def __str__(self): + return ('ColorSequenceRegistry; available colormaps:\n' + + ', '.join(f"'{name}'" for name in self)) + + def register(self, name, color_list): + """ + Register a new color sequence. + + The color sequence registry stores a copy of the given *color_list*, so + that future changes to the original list do not affect the registered + color sequence. Think of this as the registry taking a snapshot + of *color_list* at registration. + + Parameters + ---------- + name : str + The name for the color sequence. + + color_list : list of colors + An iterable returning valid Matplotlib colors when iterating over. + Note however that the returned color sequence will always be a + list regardless of the input type. + + """ + if name in self._BUILTIN_COLOR_SEQUENCES: + raise ValueError(f"{name!r} is a reserved name for a builtin " + "color sequence") + + color_list = list(color_list) # force copy and coerce type to list + for color in color_list: + try: + to_rgba(color) + except ValueError: + raise ValueError( + f"{color!r} is not a valid color specification") + + self._color_sequences[name] = color_list + + def unregister(self, name): + """ + Remove a sequence from the registry. + + You cannot remove built-in color sequences. + + If the name is not registered, returns with no error. + """ + if name in self._BUILTIN_COLOR_SEQUENCES: + raise ValueError( + f"Cannot unregister builtin color sequence {name!r}") + self._color_sequences.pop(name, None) + + +_color_sequences = ColorSequenceRegistry() + + def _sanitize_extrema(ex): if ex is None: return ex @@ -147,6 +228,12 @@ def is_color_like(c): return True +def _has_alpha_channel(c): + """Return whether *c* is a color with an alpha channel.""" + # 4-element sequences are interpreted as r, g, b, a + return not isinstance(c, str) and len(c) == 4 + + def _check_color_like(**kwargs): """ For each *key, value* pair in *kwargs*, check that *value* is color-like. @@ -184,18 +271,24 @@ def to_rgba(c, alpha=None): c : Matplotlib color or ``np.ma.masked`` alpha : float, optional - If *alpha* is not ``None``, it forces the alpha value, except if *c* is - ``"none"`` (case-insensitive), which always maps to ``(0, 0, 0, 0)``. + If *alpha* is given, force the alpha value of the returned RGBA tuple + to *alpha*. + + If None, the alpha value from *c* is used. If *c* does not have an + alpha channel, then alpha defaults to 1. + + *alpha* is ignored for the color value ``"none"`` (case-insensitive), + which always maps to ``(0, 0, 0, 0)``. Returns ------- tuple - Tuple of ``(r, g, b, a)`` scalars. + Tuple of floats ``(r, g, b, a)``, where each channel (red, green, blue, + alpha) can assume values between 0 and 1. """ # Special-case nth color syntax because it should not be cached. if _is_nth_color(c): - from matplotlib import rcParams - prop_cycler = rcParams['axes.prop_cycle'] + prop_cycler = mpl.rcParams['axes.prop_cycle'] colors = prop_cycler.by_key().get('color', ['k']) c = colors[int(c[1:]) % len(colors)] try: @@ -215,8 +308,12 @@ def _to_rgba_no_colorcycle(c, alpha=None): """ Convert *c* to an RGBA color, with no support for color-cycle syntax. - If *alpha* is not ``None``, it forces the alpha value, except if *c* is - ``"none"`` (case-insensitive), which always maps to ``(0, 0, 0, 0)``. + If *alpha* is given, force the alpha value of the returned RGBA tuple + to *alpha*. Otherwise, the alpha value from *c* is used, if it has alpha + information, or defaults to 1. + + *alpha* is ignored for the color value ``"none"`` (case-insensitive), + which always maps to ``(0, 0, 0, 0)``. """ orig_c = c if c is np.ma.masked: @@ -306,20 +403,27 @@ def to_rgba_array(c, alpha=None): Parameters ---------- c : Matplotlib color or array of colors - If *c* is a masked array, an ndarray is returned with a (0, 0, 0, 0) - row for each masked value or row in *c*. + If *c* is a masked array, an `~numpy.ndarray` is returned with a + (0, 0, 0, 0) row for each masked value or row in *c*. alpha : float or sequence of floats, optional - If *alpha* is not ``None``, it forces the alpha value, except if *c* is - ``"none"`` (case-insensitive), which always maps to ``(0, 0, 0, 0)``. + If *alpha* is given, force the alpha value of the returned RGBA tuple + to *alpha*. + + If None, the alpha value from *c* is used. If *c* does not have an + alpha channel, then alpha defaults to 1. + + *alpha* is ignored for the color value ``"none"`` (case-insensitive), + which always maps to ``(0, 0, 0, 0)``. + If *alpha* is a sequence and *c* is a single color, *c* will be repeated to match the length of *alpha*. Returns ------- array - (n, 4) array of RGBA colors. - + (n, 4) array of RGBA colors, where each channel (red, green, blue, + alpha) can assume values between 0 and 1. """ # Special-case inputs that are already arrays, for performance. (If the # array has the wrong kind or shape, raise the error during one-at-a-time @@ -364,9 +468,7 @@ def to_rgba_array(c, alpha=None): pass if isinstance(c, str): - raise ValueError("Using a string of single character colors as " - "a color sequence is not supported. The colors can " - "be passed as an explicit list instead.") + raise ValueError(f"{c!r} is not a valid color value.") if len(c) == 0: return np.zeros((0, 4), float) @@ -398,13 +500,22 @@ def to_hex(c, keep_alpha=False): """ Convert *c* to a hex color. - Uses the ``#rrggbb`` format if *keep_alpha* is False (the default), - ``#rrggbbaa`` otherwise. + Parameters + ---------- + c : :doc:`color ` or `numpy.ma.masked` + + keep_alpha : bool, default: False + If False, use the ``#rrggbb`` format, otherwise use ``#rrggbbaa``. + + Returns + ------- + str + ``#rrggbb`` or ``#rrggbbaa`` hex color string """ c = to_rgba(c) if not keep_alpha: c = c[:3] - return "#" + "".join(format(int(round(val * 255)), "02x") for val in c) + return "#" + "".join(format(round(val * 255), "02x") for val in c) ### Backwards-compatible color-conversion API @@ -503,9 +614,7 @@ def _create_lookup_table(N, data, gamma=1.0): adata = np.array(data) except Exception as err: raise TypeError("data must be convertible to an array") from err - shape = adata.shape - if len(shape) != 2 or shape[1] != 3: - raise ValueError("data must be nx3 format") + _api.check_shape((None, 3), data=adata) x = adata[:, 0] y0 = adata[:, 1] @@ -535,20 +644,6 @@ def _create_lookup_table(N, data, gamma=1.0): return np.clip(lut, 0.0, 1.0) -def _warn_if_global_cmap_modified(cmap): - if getattr(cmap, '_global', False): - _api.warn_deprecated( - "3.3", - removal="3.6", - message="You are modifying the state of a globally registered " - "colormap. This has been deprecated since %(since)s and " - "%(removal)s, you will not be able to modify a " - "registered colormap in-place. To remove this warning, " - "you can make a copy of the colormap first. " - f'cmap = mpl.cm.get_cmap("{cmap.name}").copy()' - ) - - class Colormap: """ Baseclass for all scalar to RGBA mappings. @@ -568,7 +663,7 @@ def __init__(self, name, N=256): name : str The name of the colormap. N : int - The number of rgb quantization levels. + The number of RGB quantization levels. """ self.name = name self.N = int(N) # ensure that N is always int @@ -589,11 +684,11 @@ def __call__(self, X, alpha=None, bytes=False): """ Parameters ---------- - X : float or int, ndarray or scalar + X : float or int, `~numpy.ndarray` or scalar The data value(s) to convert to RGBA. - For floats, X should be in the interval ``[0.0, 1.0]`` to + For floats, *X* should be in the interval ``[0.0, 1.0]`` to return the RGBA values ``X*100`` percent along the Colormap line. - For integers, X should be in the interval ``[0, Colormap.N)`` to + For integers, *X* should be in the interval ``[0, Colormap.N)`` to return RGBA values *indexed* from the Colormap with index ``X``. alpha : float or array-like or None Alpha must be a scalar between 0 and 1, a sequence of such @@ -611,45 +706,46 @@ def __call__(self, X, alpha=None, bytes=False): if not self._isinit: self._init() - mask_bad = X.mask if np.ma.is_masked(X) else np.isnan(X) # Mask nan's. + # Take the bad mask from a masked array, or in all other cases defer + # np.isnan() to after we have converted to an array. + mask_bad = X.mask if np.ma.is_masked(X) else None xa = np.array(X, copy=True) + if mask_bad is None: + mask_bad = np.isnan(xa) if not xa.dtype.isnative: xa = xa.byteswap().newbyteorder() # Native byteorder is faster. if xa.dtype.kind == "f": - with np.errstate(invalid="ignore"): - xa *= self.N - # Negative values are out of range, but astype(int) would - # truncate them towards zero. - xa[xa < 0] = -1 - # xa == 1 (== N after multiplication) is not out of range. - xa[xa == self.N] = self.N - 1 - # Avoid converting large positive values to negative integers. - np.clip(xa, -1, self.N, out=xa) - xa = xa.astype(int) + xa *= self.N + # Negative values are out of range, but astype(int) would + # truncate them towards zero. + xa[xa < 0] = -1 + # xa == 1 (== N after multiplication) is not out of range. + xa[xa == self.N] = self.N - 1 + # Avoid converting large positive values to negative integers. + np.clip(xa, -1, self.N, out=xa) + with np.errstate(invalid="ignore"): + # We need this cast for unsigned ints as well as floats + xa = xa.astype(int) # Set the over-range indices before the under-range; # otherwise the under-range values get converted to over-range. xa[xa > self.N - 1] = self._i_over xa[xa < 0] = self._i_under xa[mask_bad] = self._i_bad + lut = self._lut if bytes: - lut = (self._lut * 255).astype(np.uint8) - else: - lut = self._lut.copy() # Don't let alpha modify original _lut. + lut = (lut * 255).astype(np.uint8) - rgba = np.empty(shape=xa.shape + (4,), dtype=lut.dtype) - lut.take(xa, axis=0, mode='clip', out=rgba) + rgba = lut.take(xa, axis=0, mode='clip') if alpha is not None: - if np.iterable(alpha): - alpha = np.asarray(alpha) - if alpha.shape != xa.shape: - raise ValueError("alpha is array-like but its shape" - " %s doesn't match that of X %s" % - (alpha.shape, xa.shape)) alpha = np.clip(alpha, 0, 1) if bytes: - alpha = (alpha * 255).astype(np.uint8) + alpha *= 255 # Will be cast to uint8 upon assignment. + if alpha.shape not in [(), xa.shape]: + raise ValueError( + f"alpha is array-like but its shape {alpha.shape} does " + f"not match that of X {xa.shape}") rgba[..., -1] = alpha # If the "bad" color is all zeros, then ignore alpha input. @@ -669,9 +765,19 @@ def __copy__(self): cmapobject.__dict__.update(self.__dict__) if self._isinit: cmapobject._lut = np.copy(self._lut) - cmapobject._global = False return cmapobject + def __eq__(self, other): + if (not isinstance(other, Colormap) or self.name != other.name or + self.colorbar_extend != other.colorbar_extend): + return False + # To compare lookup tables the Colormaps have to be initialized + if not self._isinit: + self._init() + if not other._isinit: + other._init() + return np.array_equal(self._lut, other._lut) + def get_bad(self): """Get the color for masked values.""" if not self._isinit: @@ -680,7 +786,6 @@ def get_bad(self): def set_bad(self, color='k', alpha=None): """Set the color for masked values.""" - _warn_if_global_cmap_modified(self) self._rgba_bad = to_rgba(color, alpha) if self._isinit: self._set_extremes() @@ -693,7 +798,6 @@ def get_under(self): def set_under(self, color='k', alpha=None): """Set the color for low out-of-range values.""" - _warn_if_global_cmap_modified(self) self._rgba_under = to_rgba(color, alpha) if self._isinit: self._set_extremes() @@ -706,7 +810,6 @@ def get_over(self): def set_over(self, color='k', alpha=None): """Set the color for high out-of-range values.""" - _warn_if_global_cmap_modified(self) self._rgba_over = to_rgba(color, alpha) if self._isinit: self._set_extremes() @@ -729,7 +832,7 @@ def with_extremes(self, *, bad=None, under=None, over=None): values and, when ``norm.clip = False``, low (*under*) and high (*over*) out-of-range values, have been set accordingly. """ - new_cm = copy.copy(self) + new_cm = self.copy() new_cm.set_extremes(bad=bad, under=under, over=over) return new_cm @@ -755,21 +858,30 @@ def is_gray(self): return (np.all(self._lut[:, 0] == self._lut[:, 1]) and np.all(self._lut[:, 0] == self._lut[:, 2])) - def _resample(self, lutsize): + def resampled(self, lutsize): """Return a new colormap with *lutsize* entries.""" + if hasattr(self, '_resample'): + _api.warn_external( + "The ability to resample a color map is now public API " + f"However the class {type(self)} still only implements " + "the previous private _resample method. Please update " + "your class." + ) + return self._resample(lutsize) + raise NotImplementedError() def reversed(self, name=None): """ Return a reversed instance of the Colormap. - .. note:: This function is not implemented for base class. + .. note:: This function is not implemented for the base class. Parameters ---------- name : str, optional - The name for the reversed colormap. If it's None the - name will be the name of the parent colormap + "_r". + The name for the reversed colormap. If None, the + name is set to ``self.name + "_r"``. See Also -------- @@ -928,7 +1040,7 @@ def from_list(name, colors, N=256, gamma=1.0): If (value, color) pairs are given, the mapping is from *value* to *color*. This can be used to divide the range unevenly. N : int - The number of rgb quantization levels. + The number of RGB quantization levels. gamma : float """ if not np.iterable(colors): @@ -951,7 +1063,7 @@ def from_list(name, colors, N=256, gamma=1.0): return LinearSegmentedColormap(name, cdict, N, gamma) - def _resample(self, lutsize): + def resampled(self, lutsize): """Return a new colormap with *lutsize* entries.""" new_cmap = LinearSegmentedColormap(self.name, self._segmentdata, lutsize) @@ -972,8 +1084,8 @@ def reversed(self, name=None): Parameters ---------- name : str, optional - The name for the reversed colormap. If it's None the - name will be the name of the parent colormap + "_r". + The name for the reversed colormap. If None, the + name is set to ``self.name + "_r"``. Returns ------- @@ -1009,7 +1121,7 @@ class ListedColormap(Colormap): ---------- colors : list, array List of Matplotlib color specifications, or an equivalent Nx3 or Nx4 - floating point array (*N* rgb or rgba values). + floating point array (*N* RGB or RGBA values). name : str, optional String to identify the colormap. N : int, optional @@ -1055,7 +1167,7 @@ def _init(self): self._isinit = True self._set_extremes() - def _resample(self, lutsize): + def resampled(self, lutsize): """Return a new colormap with *lutsize* entries.""" colors = self(np.linspace(0, 1, lutsize)) new_cmap = ListedColormap(colors, name=self.name) @@ -1072,8 +1184,8 @@ def reversed(self, name=None): Parameters ---------- name : str, optional - The name for the reversed colormap. If it's None the - name will be the name of the parent colormap + "_r". + The name for the reversed colormap. If None, the + name is set to ``self.name + "_r"``. Returns ------- @@ -1120,10 +1232,50 @@ def __init__(self, vmin=None, vmax=None, clip=False): ----- Returns 0 if ``vmin == vmax``. """ - self.vmin = _sanitize_extrema(vmin) - self.vmax = _sanitize_extrema(vmax) - self.clip = clip - self._scale = scale.LinearScale(axis=None) + self._vmin = _sanitize_extrema(vmin) + self._vmax = _sanitize_extrema(vmax) + self._clip = clip + self._scale = None + self.callbacks = cbook.CallbackRegistry(signals=["changed"]) + + @property + def vmin(self): + return self._vmin + + @vmin.setter + def vmin(self, value): + value = _sanitize_extrema(value) + if value != self._vmin: + self._vmin = value + self._changed() + + @property + def vmax(self): + return self._vmax + + @vmax.setter + def vmax(self, value): + value = _sanitize_extrema(value) + if value != self._vmax: + self._vmax = value + self._changed() + + @property + def clip(self): + return self._clip + + @clip.setter + def clip(self, value): + if value != self._clip: + self._clip = value + self._changed() + + def _changed(self): + """ + Call this whenever the norm is changed to notify all the + callback listeners to the 'changed' signal. + """ + self.callbacks.process('changed') @staticmethod def process_value(value): @@ -1169,7 +1321,7 @@ def __call__(self, value, clip=None): ---------- value Data to normalize. - clip : bool + clip : bool, optional If ``None``, defaults to ``self.clip`` (which defaults to ``False``). @@ -1183,12 +1335,13 @@ def __call__(self, value, clip=None): result, is_scalar = self.process_value(value) - self.autoscale_None(result) + if self.vmin is None or self.vmax is None: + self.autoscale_None(result) # Convert at least to float, without losing precision. (vmin,), _ = self.process_value(self.vmin) (vmax,), _ = self.process_value(self.vmax) if vmin == vmax: - result.fill(0) # Or should it be all masked? Or 0.5? + result.fill(0) # Or should it be all masked? Or 0.5? elif vmin > vmax: raise ValueError("minvalue must be less than or equal to maxvalue") else: @@ -1219,9 +1372,12 @@ def inverse(self, value): def autoscale(self, A): """Set *vmin*, *vmax* to min, max of *A*.""" - A = np.asanyarray(A) - self.vmin = A.min() - self.vmax = A.max() + with self.callbacks.blocked(): + # Pause callbacks while we are updating so we only get + # a single update signal at the end + self.vmin = self.vmax = None + self.autoscale_None(A) + self._changed() def autoscale_None(self, A): """If vmin or vmax are not set, use the min/max of *A* to set them.""" @@ -1254,7 +1410,7 @@ def __init__(self, vcenter, vmin=None, vmax=None): Defaults to the min value of the dataset. vmax : float, optional The data value that defines ``1.0`` in the normalization. - Defaults to the the max value of the dataset. + Defaults to the max value of the dataset. Examples -------- @@ -1269,9 +1425,8 @@ def __init__(self, vcenter, vmin=None, vmax=None): array([0., 0.25, 0.5, 0.625, 0.75, 0.875, 1.0]) """ - self.vcenter = vcenter - self.vmin = vmin - self.vmax = vmax + super().__init__(vmin=vmin, vmax=vmax) + self._vcenter = vcenter if vcenter is not None and vmax is not None and vcenter >= vmax: raise ValueError('vmin, vcenter, and vmax must be in ' 'ascending order') @@ -1279,6 +1434,16 @@ def __init__(self, vcenter, vmin=None, vmax=None): raise ValueError('vmin, vcenter, and vmax must be in ' 'ascending order') + @property + def vcenter(self): + return self._vcenter + + @vcenter.setter + def vcenter(self, value): + if value != self._vcenter: + self._vcenter = value + self._changed() + def autoscale_None(self, A): """ Get vmin and vmax, and then clip at vcenter @@ -1291,20 +1456,32 @@ def autoscale_None(self, A): def __call__(self, value, clip=None): """ - Map value to the interval [0, 1]. The clip argument is unused. + Map value to the interval [0, 1]. The *clip* argument is unused. """ result, is_scalar = self.process_value(value) self.autoscale_None(result) # sets self.vmin, self.vmax if None if not self.vmin <= self.vcenter <= self.vmax: raise ValueError("vmin, vcenter, vmax must increase monotonically") + # note that we must extrapolate for tick locators: result = np.ma.masked_array( np.interp(result, [self.vmin, self.vcenter, self.vmax], - [0, 0.5, 1.]), mask=np.ma.getmask(result)) + [0, 0.5, 1], left=-np.inf, right=np.inf), + mask=np.ma.getmask(result)) if is_scalar: result = np.atleast_1d(result)[0] return result + def inverse(self, value): + if not self.scaled(): + raise ValueError("Not invertible until both vmin and vmax are set") + (vmin,), _ = self.process_value(self.vmin) + (vmax,), _ = self.process_value(self.vmax) + (vcenter,), _ = self.process_value(self.vcenter) + result = np.interp(value, [0, 0.5, 1], [vmin, vcenter, vmax], + left=-np.inf, right=np.inf) + return result + class CenteredNorm(Normalize): def __init__(self, vcenter=0, halfrange=None, clip=False): @@ -1328,6 +1505,10 @@ def __init__(self, vcenter=0, halfrange=None, clip=False): *vcenter* + *halfrange* is ``1.0`` in the normalization. Defaults to the largest absolute difference to *vcenter* for the values in the dataset. + clip : bool, default: False + If ``True`` values falling outside the range ``[vmin, vmax]``, + are mapped to 0 or 1, whichever is closer, and masked values are + set to 1. If ``False`` masked values remain masked. Examples -------- @@ -1340,76 +1521,85 @@ def __init__(self, vcenter=0, halfrange=None, clip=False): >>> norm(data) array([0.25, 0.5 , 1. ]) """ + super().__init__(vmin=None, vmax=None, clip=clip) self._vcenter = vcenter - self.vmin = None - self.vmax = None # calling the halfrange setter to set vmin and vmax self.halfrange = halfrange - self.clip = clip - - def _set_vmin_vmax(self): - """ - Set *vmin* and *vmax* based on *vcenter* and *halfrange*. - """ - self.vmax = self._vcenter + self._halfrange - self.vmin = self._vcenter - self._halfrange def autoscale(self, A): """ Set *halfrange* to ``max(abs(A-vcenter))``, then set *vmin* and *vmax*. """ A = np.asanyarray(A) - self._halfrange = max(self._vcenter-A.min(), - A.max()-self._vcenter) - self._set_vmin_vmax() + self.halfrange = max(self._vcenter-A.min(), + A.max()-self._vcenter) def autoscale_None(self, A): """Set *vmin* and *vmax*.""" A = np.asanyarray(A) - if self._halfrange is None and A.size: + if self.halfrange is None and A.size: self.autoscale(A) + @property + def vmin(self): + return self._vmin + + @vmin.setter + def vmin(self, value): + value = _sanitize_extrema(value) + if value != self._vmin: + self._vmin = value + self._vmax = 2*self.vcenter - value + self._changed() + + @property + def vmax(self): + return self._vmax + + @vmax.setter + def vmax(self, value): + value = _sanitize_extrema(value) + if value != self._vmax: + self._vmax = value + self._vmin = 2*self.vcenter - value + self._changed() + @property def vcenter(self): return self._vcenter @vcenter.setter def vcenter(self, vcenter): - self._vcenter = vcenter - if self.vmax is not None: - # recompute halfrange assuming vmin and vmax represent - # min and max of data - self._halfrange = max(self._vcenter-self.vmin, - self.vmax-self._vcenter) - self._set_vmin_vmax() + if vcenter != self._vcenter: + self._vcenter = vcenter + # Trigger an update of the vmin/vmax values through the setter + self.halfrange = self.halfrange + self._changed() @property def halfrange(self): - return self._halfrange + if self.vmin is None or self.vmax is None: + return None + return (self.vmax - self.vmin) / 2 @halfrange.setter def halfrange(self, halfrange): if halfrange is None: - self._halfrange = None self.vmin = None self.vmax = None else: - self._halfrange = abs(halfrange) - - def __call__(self, value, clip=None): - if self._halfrange is not None: - # enforce symmetry, reset vmin and vmax - self._set_vmin_vmax() - return super().__call__(value, clip=clip) + self.vmin = self.vcenter - abs(halfrange) + self.vmax = self.vcenter + abs(halfrange) -def _make_norm_from_scale(scale_cls, base_norm_cls=None, *, init=None): +def make_norm_from_scale(scale_cls, base_norm_cls=None, *, init=None): """ - Decorator for building a `.Normalize` subclass from a `.Scale` subclass. + Decorator for building a `.Normalize` subclass from a `~.scale.ScaleBase` + subclass. After :: - @_make_norm_from_scale(scale_cls) + @make_norm_from_scale(scale_cls) class norm_cls(Normalize): ... @@ -1424,7 +1614,7 @@ class norm_cls(Normalize): a dummy axis). If the *scale_cls* constructor takes additional parameters, then *init* - should be passed to `_make_norm_from_scale`. It is a callable which is + should be passed to `make_norm_from_scale`. It is a callable which is *only* used for its signature. First, this signature will become the signature of *norm_cls*. Second, the *norm_cls* constructor will bind the parameters passed to it using this signature, extract the bound *vmin*, @@ -1434,25 +1624,79 @@ class norm_cls(Normalize): """ if base_norm_cls is None: - return functools.partial(_make_norm_from_scale, scale_cls, init=init) + return functools.partial(make_norm_from_scale, scale_cls, init=init) + + if isinstance(scale_cls, functools.partial): + scale_args = scale_cls.args + scale_kwargs_items = tuple(scale_cls.keywords.items()) + scale_cls = scale_cls.func + else: + scale_args = scale_kwargs_items = () if init is None: def init(vmin=None, vmax=None, clip=False): pass - bound_init_signature = inspect.signature(init) + + return _make_norm_from_scale( + scale_cls, scale_args, scale_kwargs_items, + base_norm_cls, inspect.signature(init)) + + +@functools.lru_cache(None) +def _make_norm_from_scale( + scale_cls, scale_args, scale_kwargs_items, + base_norm_cls, bound_init_signature, +): + """ + Helper for `make_norm_from_scale`. + + This function is split out to enable caching (in particular so that + different unpickles reuse the same class). In order to do so, + + - ``functools.partial`` *scale_cls* is expanded into ``func, args, kwargs`` + to allow memoizing returned norms (partial instances always compare + unequal, but we can check identity based on ``func, args, kwargs``; + - *init* is replaced by *init_signature*, as signatures are picklable, + unlike to arbitrary lambdas. + """ class Norm(base_norm_cls): + def __reduce__(self): + cls = type(self) + # If the class is toplevel-accessible, it is possible to directly + # pickle it "by name". This is required to support norm classes + # defined at a module's toplevel, as the inner base_norm_cls is + # otherwise unpicklable (as it gets shadowed by the generated norm + # class). If either import or attribute access fails, fall back to + # the general path. + try: + if cls is getattr(importlib.import_module(cls.__module__), + cls.__qualname__): + return (_create_empty_object_of_class, (cls,), vars(self)) + except (ImportError, AttributeError): + pass + return (_picklable_norm_constructor, + (scale_cls, scale_args, scale_kwargs_items, + base_norm_cls, bound_init_signature), + vars(self)) def __init__(self, *args, **kwargs): ba = bound_init_signature.bind(*args, **kwargs) ba.apply_defaults() super().__init__( **{k: ba.arguments.pop(k) for k in ["vmin", "vmax", "clip"]}) - self._scale = scale_cls(axis=None, **ba.arguments) + self._scale = functools.partial( + scale_cls, *scale_args, **dict(scale_kwargs_items))( + axis=None, **ba.arguments) self._trf = self._scale.get_transform() + __init__.__signature__ = bound_init_signature.replace(parameters=[ + inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD), + *bound_init_signature.parameters.values()]) + def __call__(self, value, clip=None): value, is_scalar = self.process_value(value) - self.autoscale_None(value) + if self.vmin is None or self.vmax is None: + self.autoscale_None(value) if self.vmin > self.vmax: raise ValueError("vmin must be less or equal to vmax") if self.vmin == self.vmax: @@ -1487,17 +1731,34 @@ def inverse(self, value): .reshape(np.shape(value))) return value[0] if is_scalar else value - Norm.__name__ = base_norm_cls.__name__ - Norm.__qualname__ = base_norm_cls.__qualname__ + def autoscale_None(self, A): + # i.e. A[np.isfinite(...)], but also for non-array A's + in_trf_domain = np.extract(np.isfinite(self._trf.transform(A)), A) + if in_trf_domain.size == 0: + in_trf_domain = np.ma.masked + return super().autoscale_None(in_trf_domain) + + if base_norm_cls is Normalize: + Norm.__name__ = f"{scale_cls.__name__}Norm" + Norm.__qualname__ = f"{scale_cls.__qualname__}Norm" + else: + Norm.__name__ = base_norm_cls.__name__ + Norm.__qualname__ = base_norm_cls.__qualname__ Norm.__module__ = base_norm_cls.__module__ Norm.__doc__ = base_norm_cls.__doc__ - Norm.__init__.__signature__ = bound_init_signature.replace(parameters=[ - inspect.Parameter("self", inspect.Parameter.POSITIONAL_OR_KEYWORD), - *bound_init_signature.parameters.values()]) + return Norm -@_make_norm_from_scale( +def _create_empty_object_of_class(cls): + return cls.__new__(cls) + + +def _picklable_norm_constructor(*args): + return _create_empty_object_of_class(_make_norm_from_scale(*args)) + + +@make_norm_from_scale( scale.FuncScale, init=lambda functions, vmin=None, vmax=None, clip=False: None) class FuncNorm(Normalize): @@ -1530,20 +1791,13 @@ def forward(values: array-like) -> array-like """ -@_make_norm_from_scale(functools.partial(scale.LogScale, nonpositive="mask")) -class LogNorm(Normalize): - """Normalize a given value to the 0-1 range on a log scale.""" - - def autoscale(self, A): - # docstring inherited. - super().autoscale(np.ma.array(A, mask=(A <= 0))) - - def autoscale_None(self, A): - # docstring inherited. - super().autoscale_None(np.ma.array(A, mask=(A <= 0))) +LogNorm = make_norm_from_scale( + functools.partial(scale.LogScale, nonpositive="mask"))(Normalize) +LogNorm.__name__ = LogNorm.__qualname__ = "LogNorm" +LogNorm.__doc__ = "Normalize a given value to the 0-1 range on a log scale." -@_make_norm_from_scale( +@make_norm_from_scale( scale.SymmetricalLogScale, init=lambda linthresh, linscale=1., vmin=None, vmax=None, clip=False, *, base=10: None) @@ -1581,6 +1835,38 @@ def linthresh(self, value): self._scale.linthresh = value +@make_norm_from_scale( + scale.AsinhScale, + init=lambda linear_width=1, vmin=None, vmax=None, clip=False: None) +class AsinhNorm(Normalize): + """ + The inverse hyperbolic sine scale is approximately linear near + the origin, but becomes logarithmic for larger positive + or negative values. Unlike the `SymLogNorm`, the transition between + these linear and logarithmic regions is smooth, which may reduce + the risk of visual artifacts. + + .. note:: + + This API is provisional and may be revised in the future + based on early user feedback. + + Parameters + ---------- + linear_width : float, default: 1 + The effective width of the linear region, beyond which + the transformation becomes asymptotically logarithmic + """ + + @property + def linear_width(self): + return self._scale.linear_width + + @linear_width.setter + def linear_width(self, value): + self._scale.linear_width = value + + class PowerNorm(Normalize): """ Linearly map a given value to the 0-1 range and then apply @@ -1638,19 +1924,23 @@ class BoundaryNorm(Normalize): Unlike `Normalize` or `LogNorm`, `BoundaryNorm` maps values to integers instead of to the interval 0-1. - - Mapping to the 0-1 interval could have been done via piece-wise linear - interpolation, but using integers seems simpler, and reduces the number of - conversions back and forth between integer and floating point. """ + + # Mapping to the 0-1 interval could have been done via piece-wise linear + # interpolation, but using integers seems simpler, and reduces the number + # of conversions back and forth between int and float. + def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): """ Parameters ---------- boundaries : array-like - Monotonically increasing sequence of at least 2 boundaries. + Monotonically increasing sequence of at least 2 bin edges: data + falling in the n-th bin will be mapped to the n-th color. + ncolors : int Number of colors in the colormap to be used. + clip : bool, optional If clip is ``True``, out of range values are mapped to 0 if they are below ``boundaries[0]`` or mapped to ``ncolors - 1`` if they @@ -1660,6 +1950,7 @@ def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): they are below ``boundaries[0]`` or mapped to *ncolors* if they are above ``boundaries[-1]``. These are then converted to valid indices by `Colormap.__call__`. + extend : {'neither', 'both', 'min', 'max'}, default: 'neither' Extend the number of bins to include one or both of the regions beyond the boundaries. For example, if ``extend`` @@ -1669,24 +1960,16 @@ def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): `~matplotlib.colorbar.Colorbar` will be drawn with the triangle extension on the left or lower end. - Returns - ------- - int16 scalar or array - Notes ----- - *boundaries* defines the edges of bins, and data falling within a bin - is mapped to the color with the same index. - - If the number of bins, including any extensions, is less than - *ncolors*, the color index is chosen by linear interpolation, mapping - the ``[0, nbins - 1]`` range onto the ``[0, ncolors - 1]`` range. + If there are fewer bins (including extensions) than colors, then the + color index is chosen by linearly interpolating the ``[0, nbins - 1]`` + range onto the ``[0, ncolors - 1]`` range, effectively skipping some + colors in the middle of the colormap. """ if clip and extend != 'neither': raise ValueError("'clip=True' is not compatible with 'extend'") - self.clip = clip - self.vmin = boundaries[0] - self.vmax = boundaries[-1] + super().__init__(vmin=boundaries[0], vmax=boundaries[-1], clip=clip) self.boundaries = np.asarray(boundaries) self.N = len(self.boundaries) if self.N < 2: @@ -1711,6 +1994,10 @@ def __init__(self, boundaries, ncolors, clip=False, *, extend='neither'): "number of bins") def __call__(self, value, clip=None): + """ + This method behaves similarly to `.Normalize.__call__`, except that it + returns integers or arrays of int16. + """ if clip is None: clip = self.clip @@ -1724,7 +2011,7 @@ def __call__(self, value, clip=None): else: max_col = self.Ncmap # this gives us the bins in the lookup table in the range - # [0, _n_regions - 1] (the offset is baked in in the init) + # [0, _n_regions - 1] (the offset is set in the init) iret = np.digitize(xx, self.boundaries) - 1 + self._offset # if we have more colors than regions, stretch the region # index computed above to full range of the color bins. This @@ -1773,7 +2060,7 @@ def inverse(self, value): def rgb_to_hsv(arr): """ - Convert float rgb values (in the range [0, 1]), in a numpy array to hsv + Convert float RGB values (in the range [0, 1]), in a numpy array to HSV values. Parameters @@ -1783,8 +2070,8 @@ def rgb_to_hsv(arr): Returns ------- - (..., 3) ndarray - Colors converted to hsv values in range [0, 1] + (..., 3) `~numpy.ndarray` + Colors converted to HSV values in range [0, 1] """ arr = np.asarray(arr) @@ -1825,7 +2112,7 @@ def rgb_to_hsv(arr): def hsv_to_rgb(hsv): """ - Convert hsv values to rgb. + Convert HSV values to RGB. Parameters ---------- @@ -1834,7 +2121,7 @@ def hsv_to_rgb(hsv): Returns ------- - (..., 3) ndarray + (..., 3) `~numpy.ndarray` Colors converted to RGB values in range [0, 1] """ hsv = np.asarray(hsv) @@ -1921,8 +2208,8 @@ class LightSource: Angles are in degrees, with the azimuth measured clockwise from north and elevation up from the zero plane of the surface. - `shade` is used to produce "shaded" rgb values for a data array. - `shade_rgb` can be used to combine an rgb image with an elevation map. + `shade` is used to produce "shaded" RGB values for a data array. + `shade_rgb` can be used to combine an RGB image with an elevation map. `hillshade` produces an illumination map of a surface. """ @@ -2001,7 +2288,7 @@ def hillshade(self, elevation, vert_exag=1, dx=1, dy=1, fraction=1.): Returns ------- - ndarray + `~numpy.ndarray` A 2D array of illumination values between 0-1, where 0 is completely in shadow and 1 is completely illuminated. """ @@ -2044,7 +2331,7 @@ def shade_normals(self, normals, fraction=1.): Returns ------- - ndarray + `~numpy.ndarray` A 2D array of illumination values between 0-1, where 0 is completely in shadow and 1 is completely illuminated. """ @@ -2122,11 +2409,12 @@ def shade(self, data, cmap, norm=None, blend_mode='overlay', vmin=None, full illumination or shadow (and clipping any values that move beyond 0 or 1). Note that this is not visually or mathematically the same as vertical exaggeration. - Additional kwargs are passed on to the *blend_mode* function. + **kwargs + Additional kwargs are passed on to the *blend_mode* function. Returns ------- - ndarray + `~numpy.ndarray` An MxNx4 array of floats ranging between 0-1. """ if vmin is None: @@ -2183,11 +2471,12 @@ def shade_rgb(self, rgb, elevation, fraction=1., blend_mode='hsv', The x-spacing (columns) of the input *elevation* grid. dy : number, optional The y-spacing (rows) of the input *elevation* grid. - Additional kwargs are passed on to the *blend_mode* function. + **kwargs + Additional kwargs are passed on to the *blend_mode* function. Returns ------- - ndarray + `~numpy.ndarray` An (m, n, 3) array of floats ranging between 0-1. """ # Calculate the "hillshade" intensity. @@ -2226,7 +2515,7 @@ def blend_hsv(self, rgb, intensity, hsv_max_sat=None, hsv_max_val=None, which can then be used to plot the shaded image with imshow. The color of the resulting image will be darkened by moving the (s, v) - values (in hsv colorspace) toward (hsv_min_sat, hsv_min_val) in the + values (in HSV colorspace) toward (hsv_min_sat, hsv_min_val) in the shaded regions, or lightened by sliding (s, v) toward (hsv_max_sat, hsv_max_val) in regions that are illuminated. The default extremes are chose so that completely shaded points are nearly black (s = 1, v = 0) @@ -2234,9 +2523,9 @@ def blend_hsv(self, rgb, intensity, hsv_max_sat=None, hsv_max_val=None, Parameters ---------- - rgb : ndarray + rgb : `~numpy.ndarray` An MxNx3 RGB array of floats ranging from 0 to 1 (color image). - intensity : ndarray + intensity : `~numpy.ndarray` An MxNx1 array of floats ranging from 0 to 1 (grayscale image). hsv_max_sat : number, default: 1 The maximum saturation value that the *intensity* map can shift the @@ -2253,7 +2542,7 @@ def blend_hsv(self, rgb, intensity, hsv_max_sat=None, hsv_max_val=None, Returns ------- - ndarray + `~numpy.ndarray` An MxNx3 RGB array representing the combined images. """ # Backward compatibility... @@ -2291,32 +2580,32 @@ def blend_hsv(self, rgb, intensity, hsv_max_sat=None, hsv_max_val=None, def blend_soft_light(self, rgb, intensity): """ - Combine an rgb image with an intensity map using "soft light" blending, + Combine an RGB image with an intensity map using "soft light" blending, using the "pegtop" formula. Parameters ---------- - rgb : ndarray + rgb : `~numpy.ndarray` An MxNx3 RGB array of floats ranging from 0 to 1 (color image). - intensity : ndarray + intensity : `~numpy.ndarray` An MxNx1 array of floats ranging from 0 to 1 (grayscale image). Returns ------- - ndarray + `~numpy.ndarray` An MxNx3 RGB array representing the combined images. """ return 2 * intensity * rgb + (1 - 2 * intensity) * rgb**2 def blend_overlay(self, rgb, intensity): """ - Combines an rgb image with an intensity map using "overlay" blending. + Combine an RGB image with an intensity map using "overlay" blending. Parameters ---------- - rgb : ndarray + rgb : `~numpy.ndarray` An MxNx3 RGB array of floats ranging from 0 to 1 (color image). - intensity : ndarray + intensity : `~numpy.ndarray` An MxNx1 array of floats ranging from 0 to 1 (grayscale image). Returns diff --git a/lib/matplotlib/compat/__init__.py b/lib/matplotlib/compat/__init__.py deleted file mode 100644 index 1d382019d5a8..000000000000 --- a/lib/matplotlib/compat/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from matplotlib import _api - - -_api.warn_deprecated("3.3", name=__name__, obj_type="module") diff --git a/lib/matplotlib/container.py b/lib/matplotlib/container.py index c53cf9fccff9..a58e55ca196c 100644 --- a/lib/matplotlib/container.py +++ b/lib/matplotlib/container.py @@ -1,5 +1,5 @@ +from matplotlib import cbook from matplotlib.artist import Artist -import matplotlib.cbook as cbook class Container(tuple): @@ -18,7 +18,7 @@ def __new__(cls, *args, **kwargs): return tuple.__new__(cls, args[0]) def __init__(self, kl, label=None): - self._callbacks = cbook.CallbackRegistry() + self._callbacks = cbook.CallbackRegistry(signals=["pchanged"]) self._remove_method = None self.set_label(label) diff --git a/lib/matplotlib/contour.py b/lib/matplotlib/contour.py index 00db07ab5bb8..42096958bb93 100644 --- a/lib/matplotlib/contour.py +++ b/lib/matplotlib/contour.py @@ -2,27 +2,26 @@ Classes to support contour plotting and labelling for the Axes class. """ +import functools from numbers import Integral import numpy as np from numpy import ma import matplotlib as mpl -from matplotlib import _api +from matplotlib import _api, _docstring +from matplotlib.backend_bases import MouseButton +from matplotlib.text import Text import matplotlib.path as mpath import matplotlib.ticker as ticker import matplotlib.cm as cm import matplotlib.colors as mcolors import matplotlib.collections as mcoll import matplotlib.font_manager as font_manager -import matplotlib.text as text import matplotlib.cbook as cbook import matplotlib.patches as mpatches import matplotlib.transforms as mtransforms -# Import needed for adding manual selection capability to clabel -from matplotlib.blocking_input import BlockingContourLabeler -from matplotlib import docstring # We can't use a single line collection for contour because a line # collection can have only a single line style, and we want to be able to have @@ -32,7 +31,8 @@ # per level. -class ClabelText(text.Text): +@_api.deprecated("3.7", alternative="Text.set_transform_rotates_text") +class ClabelText(Text): """ Unlike the ordinary text, the get_rotation returns an updated angle in the pixel coordinate assuming that the input rotation is @@ -45,6 +45,35 @@ def get_rotation(self): return new_angle +def _contour_labeler_event_handler(cs, inline, inline_spacing, event): + canvas = cs.axes.figure.canvas + is_button = event.name == "button_press_event" + is_key = event.name == "key_press_event" + # Quit (even if not in infinite mode; this is consistent with + # MATLAB and sometimes quite useful, but will require the user to + # test how many points were actually returned before using data). + if (is_button and event.button == MouseButton.MIDDLE + or is_key and event.key in ["escape", "enter"]): + canvas.stop_event_loop() + # Pop last click. + elif (is_button and event.button == MouseButton.RIGHT + or is_key and event.key in ["backspace", "delete"]): + # Unfortunately, if one is doing inline labels, then there is currently + # no way to fix the broken contour - once humpty-dumpty is broken, he + # can't be put back together. In inline mode, this does nothing. + if not inline: + cs.pop_label() + canvas.draw() + # Add new click. + elif (is_button and event.button == MouseButton.LEFT + # On macOS/gtk, some keys return None. + or is_key and event.key is not None): + if event.inaxes == cs.axes: + cs.add_label_near(event.x, event.y, transform=False, + inline=inline, inline_spacing=inline_spacing) + canvas.draw() + + class ContourLabeler: """Mixin to provide labelling capability to `.ContourSet`.""" @@ -122,10 +151,8 @@ def clabel(self, levels=None, *, or minus 90 degrees from level. use_clabeltext : bool, default: False - If ``True``, `.ClabelText` class (instead of `.Text`) is used to - create labels. `ClabelText` recalculates rotation angles - of texts during the drawing time, therefore this can be used if - aspect of the axes changes. + If ``True``, use `.Text.set_transform_rotates_text` to ensure that + label rotation is updated whenever the axes aspect changes. zorder : float or None, default: ``(2 + contour.get_zorder())`` zorder of the contour labels. @@ -174,10 +201,7 @@ def clabel(self, levels=None, *, self.labelLevelList = levels self.labelIndiceList = indices - self.labelFontProps = font_manager.FontProperties() - self.labelFontProps.set_size(fontsize) - font_size_pts = self.labelFontProps.get_size_in_points() - self.labelFontSizeList = [font_size_pts] * len(levels) + self._label_font_props = font_manager.FontProperties(size=fontsize) if colors is None: self.labelMappable = self @@ -190,21 +214,39 @@ def clabel(self, levels=None, *, self.labelXYs = [] - if np.iterable(self.labelManual): - for x, y in self.labelManual: + if np.iterable(manual): + for x, y in manual: self.add_label_near(x, y, inline, inline_spacing) - elif self.labelManual: + elif manual: print('Select label locations manually using first mouse button.') print('End manual selection with second mouse button.') if not inline: print('Remove last label by clicking third mouse button.') - blocking_contour_labeler = BlockingContourLabeler(self) - blocking_contour_labeler(inline, inline_spacing) + mpl._blocking_input.blocking_input_loop( + self.axes.figure, ["button_press_event", "key_press_event"], + timeout=-1, handler=functools.partial( + _contour_labeler_event_handler, + self, inline, inline_spacing)) else: self.labels(inline, inline_spacing) - self.labelTextsList = cbook.silent_list('text.Text', self.labelTexts) - return self.labelTextsList + return cbook.silent_list('text.Text', self.labelTexts) + + @_api.deprecated("3.7", alternative="cs.labelTexts[0].get_font()") + @property + def labelFontProps(self): + return self._label_font_props + + @_api.deprecated("3.7", alternative=( + "[cs.labelTexts[0].get_font().get_size()] * len(cs.labelLevelList)")) + @property + def labelFontSizeList(self): + return [self._label_font_props.get_size()] * len(self.labelLevelList) + + @_api.deprecated("3.7", alternative="cs.labelTexts") + @property + def labelTextsList(self): + return cbook.silent_list('text.Text', self.labelTexts) def print_label(self, linecontour, labelwidth): """Return whether a contour is long enough to hold a label.""" @@ -217,49 +259,21 @@ def too_close(self, x, y, lw): return any((x - loc[0]) ** 2 + (y - loc[1]) ** 2 < thresh for loc in self.labelXYs) - @_api.deprecated("3.4") - def get_label_coords(self, distances, XX, YY, ysize, lw): - """ - Return x, y, and the index of a label location. - - Labels are plotted at a location with the smallest - deviation of the contour from a straight line - unless there is another label nearby, in which case - the next best place on the contour is picked up. - If all such candidates are rejected, the beginning - of the contour is chosen. - """ - hysize = int(ysize / 2) - adist = np.argsort(distances) - - for ind in adist: - x, y = XX[ind][hysize], YY[ind][hysize] - if self.too_close(x, y, lw): - continue - return x, y, ind - - ind = adist[0] - x, y = XX[ind][hysize], YY[ind][hysize] - return x, y, ind - - def get_label_width(self, lev, fmt, fsize): - """ - Return the width of the label in points. - """ - if not isinstance(lev, str): - lev = self.get_text(lev, fmt) + def _get_nth_label_width(self, nth): + """Return the width of the *nth* label, in pixels.""" fig = self.axes.figure - width = (text.Text(0, 0, lev, figure=fig, - size=fsize, fontproperties=self.labelFontProps) - .get_window_extent(mpl.tight_layout.get_renderer(fig)).width) - width *= 72 / fig.dpi - return width + renderer = fig._get_renderer() + return (Text(0, 0, + self.get_text(self.labelLevelList[nth], self.labelFmt), + figure=fig, fontproperties=self._label_font_props) + .get_window_extent(renderer).width) + @_api.deprecated("3.7", alternative="Artist.set") def set_label_props(self, label, text, color): """Set the label properties - color, fontsize, text.""" label.set_text(text) label.set_color(color) - label.set_fontproperties(self.labelFontProps) + label.set_fontproperties(self._label_font_props) label.set_clip_box(self.axes.bbox) def get_text(self, lev, fmt): @@ -399,57 +413,32 @@ 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.axes.transData.inverted().transform((x, y)) - t = text.Text(dx, dy, rotation=rotation, - horizontalalignment='center', - verticalalignment='center', zorder=self._clabel_zorder) - return t - - def _get_label_clabeltext(self, x, y, rotation): - # x, y, rotation is given in pixel coordinate. Convert them to - # 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.axes.transData.inverted() - dx, dy = transDataInv.transform((x, y)) - drotation = transDataInv.transform_angles(np.array([rotation]), - np.array([[x, y]])) - t = ClabelText(dx, dy, rotation=drotation[0], - horizontalalignment='center', - verticalalignment='center', zorder=self._clabel_zorder) - - return t - - def _add_label(self, t, x, y, lev, cvalue): - color = self.labelMappable.to_rgba(cvalue, alpha=self.alpha) - - _text = self.get_text(lev, self.labelFmt) - self.set_label_props(t, _text, color) + def add_label(self, x, y, rotation, lev, cvalue): + """Add contour label without `.Text.set_transform_rotates_text`.""" + data_x, data_y = self.axes.transData.inverted().transform((x, y)) + t = Text( + data_x, data_y, + text=self.get_text(lev, self.labelFmt), + rotation=rotation, + horizontalalignment='center', verticalalignment='center', + zorder=self._clabel_zorder, + color=self.labelMappable.to_rgba(cvalue, alpha=self.alpha), + fontproperties=self._label_font_props, + clip_box=self.axes.bbox) self.labelTexts.append(t) self.labelCValues.append(cvalue) self.labelXYs.append((x, y)) - # Add label to plot here - useful for manual mode label selection self.axes.add_artist(t) - def add_label(self, x, y, rotation, lev, cvalue): - """ - Add contour label using :class:`~matplotlib.text.Text` class. - """ - t = self._get_label_text(x, y, rotation) - self._add_label(t, x, y, lev, cvalue) - def add_label_clabeltext(self, x, y, rotation, lev, cvalue): - """ - Add contour label using :class:`ClabelText` class. - """ - # x, y, rotation is given in pixel coordinate. Convert them to - # the data coordinate and create a label using ClabelText - # class. This way, the rotation of the clabel is along the - # contour line always. - t = self._get_label_clabeltext(x, y, rotation) - self._add_label(t, x, y, lev, cvalue) + """Add contour label with `.Text.set_transform_rotates_text`.""" + self.add_label(x, y, rotation, lev, cvalue) + # Grab the last added text, and reconfigure its rotation. + t = self.labelTexts[-1] + data_rotation, = self.axes.transData.inverted().transform_angles( + [rotation], [[x, y]]) + t.set(rotation=data_rotation, transform_rotates_text=True) def add_label_near(self, x, y, inline=True, inline_spacing=5, transform=None): @@ -498,11 +487,7 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5, lmin = self.labelIndiceList.index(conmin) # 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.axes.figure.dpi / 72 # scale to screen coordinates - # now lw in pixels + lw = self._get_nth_label_width(lmin) # Figure out label rotation. rotation, nlc = self.calc_label_rot_and_inline( @@ -517,9 +502,7 @@ def add_label_near(self, x, y, inline=True, inline_spacing=5, paths.pop(segmin) # Add paths if not empty or single point - for n in nlc: - if len(n) > 1: - paths.append(mpath.Path(n)) + paths.extend([mpath.Path(n) for n in nlc if len(n) > 1]) def pop_label(self, index=-1): """Defaults to removing last label, but any index can be supplied""" @@ -534,14 +517,15 @@ def labels(self, inline, inline_spacing): else: add_label = self.add_label - for icon, lev, fsize, cvalue in zip( - self.labelIndiceList, self.labelLevelList, - self.labelFontSizeList, self.labelCValueList): + for idx, (icon, lev, cvalue) in enumerate(zip( + self.labelIndiceList, + self.labelLevelList, + self.labelCValueList, + )): con = self.collections[icon] trans = con.get_transform() - lw = self.get_label_width(lev, self.labelFmt, fsize) - lw *= self.axes.figure.dpi / 72 # scale to screen coordinates + lw = self._get_nth_label_width(idx) additions = [] paths = con.get_paths() for segNum, linepath in enumerate(paths): @@ -572,6 +556,10 @@ def labels(self, inline, inline_spacing): if inline: paths[:] = additions + def remove(self): + for text in self.labelTexts: + text.remove() + def _is_closed_polygon(X): """ @@ -599,7 +587,7 @@ def _find_closest_point_on_path(xys, p): Projection of *p* onto *xys*. imin : (int, int) Consecutive indices of vertices of segment in *xys* where *proj* is. - Segments are considered as including their end-points; i.e if the + Segments are considered as including their end-points; i.e. if the closest point on the path is a node in *xys* with index *i*, this returns ``(i-1, i)``. For the special case where *xys* is a single point, this returns ``(0, 0)``. @@ -618,16 +606,15 @@ def _find_closest_point_on_path(xys, p): return (d2s[imin], projs[imin], (imin, imin+1)) -docstring.interpd.update(contour_set_attributes=r""" +_docstring.interpd.update(contour_set_attributes=r""" Attributes ---------- ax : `~matplotlib.axes.Axes` The Axes object in which the contours are drawn. -collections : `.silent_list` of `.LineCollection`\s or `.PathCollection`\s +collections : `.silent_list` of `.PathCollection`\s The `.Artist`\s representing the contour. This is a list of - `.LineCollection`\s for line contours and a list of `.PathCollection`\s - for filled contours. + `.PathCollection`\s for both line and filled contours. levels : array The values of the contour levels. @@ -638,7 +625,7 @@ def _find_closest_point_on_path(xys, p): """) -@docstring.dedent_interpd +@_docstring.dedent_interpd class ContourSet(cm.ScalarMappable, ContourLabeler): """ Store a set of contour lines or filled regions. @@ -682,14 +669,12 @@ class ContourSet(cm.ScalarMappable, ContourLabeler): %(contour_set_attributes)s """ - ax = _api.deprecated("3.3")(property(lambda self: self.axes)) - def __init__(self, ax, *args, levels=None, filled=False, linewidths=None, linestyles=None, hatches=(None,), alpha=None, origin=None, extent=None, cmap=None, colors=None, norm=None, vmin=None, vmax=None, extend='neither', antialiased=None, nchunk=0, locator=None, - transform=None, + transform=None, negative_linestyles=None, **kwargs): """ Draw contour lines or filled regions, depending on @@ -774,9 +759,18 @@ def __init__(self, ax, *args, self._transform = transform + self.negative_linestyles = negative_linestyles + # If negative_linestyles was not defined as a keyword argument, define + # negative_linestyles with rcParams + if self.negative_linestyles is None: + self.negative_linestyles = \ + mpl.rcParams['contour.negative_linestyle'] + kwargs = self._process_args(*args, **kwargs) self._process_levels() + self._extend_min = self.extend in ['min', 'both'] + self._extend_max = self.extend in ['max', 'both'] if self.colors is not None: ncolors = len(self.levels) if self.filled: @@ -785,25 +779,27 @@ def __init__(self, ax, *args, # Handle the case where colors are given for the extended # parts of the contour. - extend_min = self.extend in ['min', 'both'] - extend_max = self.extend in ['max', 'both'] + use_set_under_over = False # if we are extending the lower end, and we've been given enough # colors then skip the first color in the resulting cmap. For the # extend_max case we don't need to worry about passing more colors # than ncolors as ListedColormap will clip. - total_levels = ncolors + int(extend_min) + int(extend_max) - if len(self.colors) == total_levels and (extend_min or extend_max): + total_levels = (ncolors + + int(self._extend_min) + + int(self._extend_max)) + if (len(self.colors) == total_levels and + (self._extend_min or self._extend_max)): use_set_under_over = True - if extend_min: + if self._extend_min: i0 = 1 cmap = mcolors.ListedColormap(self.colors[i0:None], N=ncolors) if use_set_under_over: - if extend_min: + if self._extend_min: cmap.set_under(self.colors[0]) - if extend_max: + if self._extend_max: cmap.set_over(self.colors[-1]) self.collections = cbook.silent_list(None) @@ -823,16 +819,18 @@ def __init__(self, ax, *args, self.norm.vmax = vmax self._process_colors() - self.allsegs, self.allkinds = self._get_allsegs_and_allkinds() + if getattr(self, 'allsegs', None) is None: + self.allsegs, self.allkinds = self._get_allsegs_and_allkinds() + elif self.allkinds is None: + # allsegs specified in constructor may or may not have allkinds as + # well. Must ensure allkinds can be zipped below. + self.allkinds = [None] * len(self.allsegs) if self.filled: if self.linewidths is not None: _api.warn_external('linewidths is ignored by contourf') # Lower and upper contour levels. lowers, uppers = self._get_lowers_and_uppers() - # Ensure allkinds can be zipped below. - if self.allkinds is None: - self.allkinds = [None] * len(self.allsegs) # Default zorder taken from Collection self._contour_zorder = kwargs.pop('zorder', 1) @@ -852,12 +850,14 @@ def __init__(self, ax, *args, aa = self.antialiased if aa is not None: aa = (self.antialiased,) - # Default zorder taken from LineCollection + # Default zorder taken from LineCollection, which is higher than + # for filled contours so that lines are displayed on top. self._contour_zorder = kwargs.pop('zorder', 2) self.collections[:] = [ - mcoll.LineCollection( - segs, + mcoll.PathCollection( + self._make_paths(segs, kinds), + facecolors="none", antialiaseds=aa, linewidths=width, linestyles=[lstyle], @@ -865,8 +865,9 @@ def __init__(self, ax, *args, transform=self.get_transform(), zorder=self._contour_zorder, label='_nolegend_') - for level, width, lstyle, segs - in zip(self.levels, tlinewidths, tlinestyles, self.allsegs)] + for level, width, lstyle, segs, kinds + in zip(self.levels, tlinewidths, tlinestyles, self.allsegs, + self.allkinds)] for col in self.collections: self.axes.add_collection(col, autolim=False) @@ -884,10 +885,7 @@ def __init__(self, ax, *args, ) def get_transform(self): - """ - Return the :class:`~matplotlib.transforms.Transform` - instance used by this ContourSet. - """ + """Return the `.Transform` instance used by this ContourSet.""" if self._transform is None: self._transform = self.axes.transData elif (not isinstance(self._transform, mtransforms.Transform) @@ -998,11 +996,23 @@ def _process_args(self, *args, **kwargs): return kwargs def _get_allsegs_and_allkinds(self): - """ - Override in derived classes to create and return allsegs and allkinds. - allkinds can be None. - """ - return self.allsegs, self.allkinds + """Compute ``allsegs`` and ``allkinds`` using C extension.""" + allsegs = [] + allkinds = [] + if self.filled: + lowers, uppers = self._get_lowers_and_uppers() + for level, level_upper in zip(lowers, uppers): + vertices, kinds = \ + self._contour_generator.create_filled_contour( + level, level_upper) + allsegs.append(vertices) + allkinds.append(kinds) + else: + for level in self.levels: + vertices, kinds = self._contour_generator.create_contour(level) + allsegs.append(vertices) + allkinds.append(kinds) + return allsegs, allkinds def _get_lowers_and_uppers(self): """ @@ -1020,13 +1030,32 @@ def _get_lowers_and_uppers(self): return (lowers, uppers) def _make_paths(self, segs, kinds): - if kinds is not None: - return [mpath.Path(seg, codes=kind) - for seg, kind in zip(segs, kinds)] - else: + """ + Create and return Path objects for the specified segments and optional + kind codes. *segs* is a list of numpy arrays, each array is either a + closed line loop or open line strip of 2D points with a shape of + (npoints, 2). *kinds* is either None or a list (with the same length + as *segs*) of numpy arrays, each array is of shape (npoints,) and + contains the kind codes for the corresponding line in *segs*. If + *kinds* is None then the Path constructor creates the kind codes + assuming that the line is an open strip. + """ + if kinds is None: return [mpath.Path(seg) for seg in segs] + else: + return [mpath.Path(seg, codes=kind) for seg, kind + in zip(segs, kinds)] def changed(self): + if not hasattr(self, "cvalues"): + # Just return after calling the super() changed function + cm.ScalarMappable.changed(self) + return + # Force an autoscale immediately because self.to_rgba() calls + # autoscale_None() internally with the data passed to it, + # so if vmin/vmax are not set yet, this would override them with + # content from *cvalues* rather than levels like we want + self.norm.autoscale_None(self.levels) tcolors = [(tuple(rgba),) for rgba in self.to_rgba(self.cvalues, alpha=self.alpha)] self.tcolors = tcolors @@ -1038,7 +1067,7 @@ def changed(self): # update the collection's hatch (may be None) collection.set_hatch(hatch) else: - collection.set_color(color) + collection.set_edgecolor(color) for label, cv in zip(self.labelTexts, self.labelCValues): label.set_alpha(self.alpha) label.set_color(self.labelMappable.to_rgba(cv)) @@ -1088,33 +1117,28 @@ def _autolev(self, N): return lev[i0:i1] - def _process_contour_level_args(self, args): + def _process_contour_level_args(self, args, z_dtype): """ Determine the contour levels and store in self.levels. """ if self.levels is None: - if len(args) == 0: - levels_arg = 7 # Default, hard-wired. - else: + if args: levels_arg = args[0] + elif np.issubdtype(z_dtype, bool): + if self.filled: + levels_arg = [0, .5, 1] + else: + levels_arg = [.5] + else: + levels_arg = 7 # Default, hard-wired. else: levels_arg = self.levels if isinstance(levels_arg, Integral): self.levels = self._autolev(levels_arg) else: - self.levels = np.asarray(levels_arg).astype(np.float64) - - if not self.filled: - inside = (self.levels > self.zmin) & (self.levels < self.zmax) - levels_in = self.levels[inside] - if len(levels_in) == 0: - self.levels = [self.zmin] - _api.warn_external( - "No contour levels were found within the data range.") - + self.levels = np.asarray(levels_arg, np.float64) if self.filled and len(self.levels) < 2: raise ValueError("Filled contours require at least 2 levels.") - if len(self.levels) > 1 and np.min(np.diff(self.levels)) <= 0.0: raise ValueError("Contour levels must be increasing") @@ -1227,11 +1251,10 @@ def _process_linestyles(self): if linestyles is None: tlinestyles = ['solid'] * Nlev if self.monochrome: - neg_ls = mpl.rcParams['contour.negative_linestyle'] eps = - (self.zmax - self.zmin) * 1e-15 for i, lev in enumerate(self.levels): if lev < eps: - tlinestyles[i] = neg_ls + tlinestyles[i] = self.negative_linestyles else: if isinstance(linestyles, str): tlinestyles = [linestyles] * Nlev @@ -1262,9 +1285,11 @@ def find_nearest_contour(self, x, y, indices=None, pixel=True): """ Find the point in the contour plot that is closest to ``(x, y)``. + This method does not support filled contours. + Parameters ---------- - x, y: float + x, y : float The reference point. indices : list of int or None, default: None Indices of contour levels to consider. If None (the default), all @@ -1298,12 +1323,16 @@ def find_nearest_contour(self, x, y, indices=None, pixel=True): # sufficiently well that the time is not noticeable. # Nonetheless, improvements could probably be made. + if self.filled: + raise ValueError("Method does not support filled contours.") + if indices is None: - indices = range(len(self.levels)) + indices = range(len(self.collections)) d2min = np.inf conmin = None segmin = None + imin = None xmin = None ymin = None @@ -1331,8 +1360,13 @@ def find_nearest_contour(self, x, y, indices=None, pixel=True): return (conmin, segmin, imin, xmin, ymin, d2min) + def remove(self): + super().remove() + for coll in self.collections: + coll.remove() -@docstring.dedent_interpd + +@_docstring.dedent_interpd class QuadContourSet(ContourSet): """ Create and store a set of contour lines or filled regions. @@ -1343,7 +1377,7 @@ class QuadContourSet(ContourSet): %(contour_set_attributes)s """ - def _process_args(self, *args, corner_mask=None, **kwargs): + def _process_args(self, *args, corner_mask=None, algorithm=None, **kwargs): """ Process args and kwargs. """ @@ -1356,21 +1390,31 @@ def _process_args(self, *args, corner_mask=None, **kwargs): contour_generator = args[0]._contour_generator self._mins = args[0]._mins self._maxs = args[0]._maxs + self._algorithm = args[0]._algorithm else: - import matplotlib._contour as _contour + import contourpy + + if algorithm is None: + algorithm = mpl.rcParams['contour.algorithm'] + mpl.rcParams.validate["contour.algorithm"](algorithm) + self._algorithm = algorithm if corner_mask is None: - corner_mask = mpl.rcParams['contour.corner_mask'] + if self._algorithm == "mpl2005": + # mpl2005 does not support corner_mask=True so if not + # specifically requested then disable it. + corner_mask = False + else: + corner_mask = mpl.rcParams['contour.corner_mask'] self._corner_mask = corner_mask x, y, z = self._contour_args(args, kwargs) - _mask = ma.getmask(z) - if _mask is ma.nomask or not _mask.any(): - _mask = None - - contour_generator = _contour.QuadContourGenerator( - x, y, z.filled(), _mask, self._corner_mask, self.nchunk) + contour_generator = contourpy.contour_generator( + x, y, z, name=self._algorithm, corner_mask=self._corner_mask, + line_type=contourpy.LineType.SeparateCode, + fill_type=contourpy.FillType.OuterCode, + chunk_size=self.nchunk) t = self.get_transform() @@ -1391,41 +1435,21 @@ def _process_args(self, *args, corner_mask=None, **kwargs): return kwargs - def _get_allsegs_and_allkinds(self): - """Compute ``allsegs`` and ``allkinds`` using C extension.""" - allsegs = [] - if self.filled: - lowers, uppers = self._get_lowers_and_uppers() - allkinds = [] - for level, level_upper in zip(lowers, uppers): - vertices, kinds = \ - self._contour_generator.create_filled_contour( - level, level_upper) - allsegs.append(vertices) - allkinds.append(kinds) - else: - allkinds = None - for level in self.levels: - vertices = self._contour_generator.create_contour(level) - allsegs.append(vertices) - return allsegs, allkinds - def _contour_args(self, args, kwargs): if self.filled: fn = 'contourf' else: fn = 'contour' - Nargs = len(args) - if Nargs <= 2: - z = ma.asarray(args[0], dtype=np.float64) + nargs = len(args) + if nargs <= 2: + z, *args = args + z = ma.asarray(z) x, y = self._initialize_x_y(z) - args = args[1:] - elif Nargs <= 4: - x, y, z = self._check_xyz(args[:3], kwargs) - args = args[3:] + elif nargs <= 4: + x, y, z_orig, *args = args + x, y, z = self._check_xyz(x, y, z_orig, kwargs) else: - raise TypeError("Too many arguments to %s; see help(%s)" % - (fn, fn)) + raise _api.nargs_error(fn, takes="from 1 to 4", given=nargs) z = ma.masked_invalid(z, copy=False) self.zmax = float(z.max()) self.zmin = float(z.min()) @@ -1433,20 +1457,19 @@ def _contour_args(self, args, kwargs): z = ma.masked_where(z <= 0, z) _api.warn_external('Log scale: values of z <= 0 have been masked') self.zmin = float(z.min()) - self._process_contour_level_args(args) + self._process_contour_level_args(args, z.dtype) return (x, y, z) - def _check_xyz(self, args, kwargs): + def _check_xyz(self, x, y, z, kwargs): """ Check that the shapes of the input arrays match; if x and y are 1D, convert them to 2D using meshgrid. """ - x, y = args[:2] x, y = self.axes._process_unit_info([("x", x), ("y", y)], kwargs) x = np.asarray(x, dtype=np.float64) y = np.asarray(y, dtype=np.float64) - z = ma.asarray(args[2], dtype=np.float64) + z = ma.asarray(z) if z.ndim != 2: raise TypeError(f"Input z must be 2D, not {z.ndim}D") @@ -1521,207 +1544,240 @@ def _initialize_x_y(self, z): y = y[::-1] return np.meshgrid(x, y) - _contour_doc = """ - `.contour` and `.contourf` draw contour lines and filled contours, - respectively. Except as noted, function signatures and return values - are the same for both versions. - Parameters - ---------- - X, Y : array-like, optional - The coordinates of the values in *Z*. +_docstring.interpd.update(contour_doc=""" +`.contour` and `.contourf` draw contour lines and filled contours, +respectively. Except as noted, function signatures and return values +are the same for both versions. - *X* and *Y* must both be 2D with the same shape as *Z* (e.g. - created via `numpy.meshgrid`), or they must both be 1-D such - that ``len(X) == M`` is the number of columns in *Z* and - ``len(Y) == N`` is the number of rows in *Z*. +Parameters +---------- +X, Y : array-like, optional + The coordinates of the values in *Z*. - If not given, they are assumed to be integer indices, i.e. - ``X = range(M)``, ``Y = range(N)``. + *X* and *Y* must both be 2D with the same shape as *Z* (e.g. + created via `numpy.meshgrid`), or they must both be 1-D such + that ``len(X) == N`` is the number of columns in *Z* and + ``len(Y) == M`` is the number of rows in *Z*. - Z : (M, N) array-like - The height values over which the contour is drawn. + *X* and *Y* must both be ordered monotonically. - levels : int or array-like, optional - Determines the number and positions of the contour lines / regions. + If not given, they are assumed to be integer indices, i.e. + ``X = range(N)``, ``Y = range(M)``. - If an int *n*, use `~matplotlib.ticker.MaxNLocator`, which tries - to automatically choose no more than *n+1* "nice" contour levels - between *vmin* and *vmax*. +Z : (M, N) array-like + The height values over which the contour is drawn. Color-mapping is + controlled by *cmap*, *norm*, *vmin*, and *vmax*. - If array-like, draw contour lines at the specified levels. - The values must be in increasing order. +levels : int or array-like, optional + Determines the number and positions of the contour lines / regions. - Returns - ------- - `~.contour.QuadContourSet` - - Other Parameters - ---------------- - corner_mask : bool, default: :rc:`contour.corner_mask` - Enable/disable corner masking, which only has an effect if *Z* is - a masked array. If ``False``, any quad touching a masked point is - masked out. If ``True``, only the triangular corners of quads - nearest those points are always masked out, other triangular - corners comprising three unmasked points are contoured as usual. - - colors : color string or sequence of colors, optional - The colors of the levels, i.e. the lines for `.contour` and the - areas for `.contourf`. - - The sequence is cycled for the levels in ascending order. If the - sequence is shorter than the number of levels, it's repeated. - - As a shortcut, single color strings may be used in place of - one-element lists, i.e. ``'red'`` instead of ``['red']`` to color - all levels with the same color. This shortcut does only work for - color strings, not for other ways of specifying colors. - - By default (value *None*), the colormap specified by *cmap* - will be used. - - alpha : float, default: 1 - The alpha blending value, between 0 (transparent) and 1 (opaque). - - cmap : str or `.Colormap`, default: :rc:`image.cmap` - A `.Colormap` instance or registered colormap name. The colormap - maps the level values to colors. - - If both *colors* and *cmap* are given, an error is raised. - - norm : `~matplotlib.colors.Normalize`, optional - If a colormap is used, the `.Normalize` instance scales the level - values to the canonical colormap range [0, 1] for mapping to - colors. If not given, the default linear scaling is used. - - vmin, vmax : float, optional - If not *None*, either or both of these values will be supplied to - the `.Normalize` instance, overriding the default color scaling - based on *levels*. - - origin : {*None*, 'upper', 'lower', 'image'}, default: None - Determines the orientation and exact position of *Z* by specifying - the position of ``Z[0, 0]``. This is only relevant, if *X*, *Y* - are not given. - - - *None*: ``Z[0, 0]`` is at X=0, Y=0 in the lower left corner. - - 'lower': ``Z[0, 0]`` is at X=0.5, Y=0.5 in the lower left corner. - - 'upper': ``Z[0, 0]`` is at X=N+0.5, Y=0.5 in the upper left - corner. - - 'image': Use the value from :rc:`image.origin`. - - extent : (x0, x1, y0, y1), optional - If *origin* is not *None*, then *extent* is interpreted as in - `.imshow`: it gives the outer pixel boundaries. In this case, the - position of Z[0, 0] is the center of the pixel, not a corner. If - *origin* is *None*, then (*x0*, *y0*) is the position of Z[0, 0], - and (*x1*, *y1*) is the position of Z[-1, -1]. - - This argument is ignored if *X* and *Y* are specified in the call - to contour. - - locator : ticker.Locator subclass, optional - The locator is used to determine the contour levels if they - are not given explicitly via *levels*. - Defaults to `~.ticker.MaxNLocator`. - - extend : {'neither', 'both', 'min', 'max'}, default: 'neither' - Determines the ``contourf``-coloring of values that are outside the - *levels* range. - - If 'neither', values outside the *levels* range are not colored. - If 'min', 'max' or 'both', color the values below, above or below - and above the *levels* range. - - Values below ``min(levels)`` and above ``max(levels)`` are mapped - to the under/over values of the `.Colormap`. Note that most - colormaps do not have dedicated colors for these by default, so - that the over and under values are the edge values of the colormap. - You may want to set these values explicitly using - `.Colormap.set_under` and `.Colormap.set_over`. - - .. note:: - - An existing `.QuadContourSet` does not get notified if - properties of its colormap are changed. Therefore, an explicit - call `.QuadContourSet.changed()` is needed after modifying the - colormap. The explicit call can be left out, if a colorbar is - assigned to the `.QuadContourSet` because it internally calls - `.QuadContourSet.changed()`. - - Example:: - - x = np.arange(1, 10) - y = x.reshape(-1, 1) - h = x * y - - cs = plt.contourf(h, levels=[10, 30, 50], - colors=['#808080', '#A0A0A0', '#C0C0C0'], extend='both') - cs.cmap.set_over('red') - cs.cmap.set_under('blue') - cs.changed() - - xunits, yunits : registered units, optional - Override axis units by specifying an instance of a - :class:`matplotlib.units.ConversionInterface`. - - antialiased : bool, optional - Enable antialiasing, overriding the defaults. For - filled contours, the default is *True*. For line contours, - it is taken from :rc:`lines.antialiased`. - - nchunk : int >= 0, optional - If 0, no subdivision of the domain. Specify a positive integer to - divide the domain into subdomains of *nchunk* by *nchunk* quads. - Chunking reduces the maximum length of polygons generated by the - contouring algorithm which reduces the rendering workload passed - on to the backend and also requires slightly less RAM. It can - however introduce rendering artifacts at chunk boundaries depending - on the backend, the *antialiased* flag and value of *alpha*. - - linewidths : float or array-like, default: :rc:`contour.linewidth` - *Only applies to* `.contour`. - - The line width of the contour lines. - - If a number, all levels will be plotted with this linewidth. - - If a sequence, the levels in ascending order will be plotted with - the linewidths in the order specified. - - If None, this falls back to :rc:`lines.linewidth`. - - linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, optional - *Only applies to* `.contour`. - - If *linestyles* is *None*, the default is 'solid' unless the lines - are monochrome. In that case, negative contours will take their - linestyle from :rc:`contour.negative_linestyle` setting. - - *linestyles* can also be an iterable of the above strings - specifying a set of linestyles to be used. If this - iterable is shorter than the number of contour levels - it will be repeated as necessary. - - hatches : list[str], optional - *Only applies to* `.contourf`. - - A list of cross hatch patterns to use on the filled areas. - If None, no hatching will be added to the contour. - Hatching is supported in the PostScript, PDF, SVG and Agg - backends only. - - Notes - ----- - 1. `.contourf` differs from the MATLAB version in that it does not draw - the polygon edges. To draw edges, add line contours with calls to - `.contour`. + If an int *n*, use `~matplotlib.ticker.MaxNLocator`, which tries + to automatically choose no more than *n+1* "nice" contour levels + between minimum and maximum numeric values of *Z*. - 2. `.contourf` fills intervals that are closed at the top; that is, for - boundaries *z1* and *z2*, the filled region is:: + If array-like, draw contour lines at the specified levels. + The values must be in increasing order. - z1 < Z <= z2 +Returns +------- +`~.contour.QuadContourSet` - except for the lowest interval, which is closed on both sides (i.e. - it includes the lowest value). - """ +Other Parameters +---------------- +corner_mask : bool, default: :rc:`contour.corner_mask` + Enable/disable corner masking, which only has an effect if *Z* is + a masked array. If ``False``, any quad touching a masked point is + masked out. If ``True``, only the triangular corners of quads + nearest those points are always masked out, other triangular + corners comprising three unmasked points are contoured as usual. + +colors : color string or sequence of colors, optional + The colors of the levels, i.e. the lines for `.contour` and the + areas for `.contourf`. + + The sequence is cycled for the levels in ascending order. If the + sequence is shorter than the number of levels, it's repeated. + + As a shortcut, single color strings may be used in place of + one-element lists, i.e. ``'red'`` instead of ``['red']`` to color + all levels with the same color. This shortcut does only work for + color strings, not for other ways of specifying colors. + + By default (value *None*), the colormap specified by *cmap* + will be used. + +alpha : float, default: 1 + The alpha blending value, between 0 (transparent) and 1 (opaque). + +%(cmap_doc)s + + This parameter is ignored if *colors* is set. + +%(norm_doc)s + + This parameter is ignored if *colors* is set. + +%(vmin_vmax_doc)s + + If *vmin* or *vmax* are not given, the default color scaling is based on + *levels*. + + This parameter is ignored if *colors* is set. + +origin : {*None*, 'upper', 'lower', 'image'}, default: None + Determines the orientation and exact position of *Z* by specifying + the position of ``Z[0, 0]``. This is only relevant, if *X*, *Y* + are not given. + + - *None*: ``Z[0, 0]`` is at X=0, Y=0 in the lower left corner. + - 'lower': ``Z[0, 0]`` is at X=0.5, Y=0.5 in the lower left corner. + - 'upper': ``Z[0, 0]`` is at X=N+0.5, Y=0.5 in the upper left + corner. + - 'image': Use the value from :rc:`image.origin`. + +extent : (x0, x1, y0, y1), optional + If *origin* is not *None*, then *extent* is interpreted as in + `.imshow`: it gives the outer pixel boundaries. In this case, the + position of Z[0, 0] is the center of the pixel, not a corner. If + *origin* is *None*, then (*x0*, *y0*) is the position of Z[0, 0], + and (*x1*, *y1*) is the position of Z[-1, -1]. + + This argument is ignored if *X* and *Y* are specified in the call + to contour. + +locator : ticker.Locator subclass, optional + The locator is used to determine the contour levels if they + are not given explicitly via *levels*. + Defaults to `~.ticker.MaxNLocator`. + +extend : {'neither', 'both', 'min', 'max'}, default: 'neither' + Determines the ``contourf``-coloring of values that are outside the + *levels* range. + + If 'neither', values outside the *levels* range are not colored. + If 'min', 'max' or 'both', color the values below, above or below + and above the *levels* range. + + Values below ``min(levels)`` and above ``max(levels)`` are mapped + to the under/over values of the `.Colormap`. Note that most + colormaps do not have dedicated colors for these by default, so + that the over and under values are the edge values of the colormap. + You may want to set these values explicitly using + `.Colormap.set_under` and `.Colormap.set_over`. + + .. note:: + + An existing `.QuadContourSet` does not get notified if + properties of its colormap are changed. Therefore, an explicit + call `.QuadContourSet.changed()` is needed after modifying the + colormap. The explicit call can be left out, if a colorbar is + assigned to the `.QuadContourSet` because it internally calls + `.QuadContourSet.changed()`. + + Example:: + + x = np.arange(1, 10) + y = x.reshape(-1, 1) + h = x * y + + cs = plt.contourf(h, levels=[10, 30, 50], + colors=['#808080', '#A0A0A0', '#C0C0C0'], extend='both') + cs.cmap.set_over('red') + cs.cmap.set_under('blue') + cs.changed() + +xunits, yunits : registered units, optional + Override axis units by specifying an instance of a + :class:`matplotlib.units.ConversionInterface`. + +antialiased : bool, optional + Enable antialiasing, overriding the defaults. For + filled contours, the default is *True*. For line contours, + it is taken from :rc:`lines.antialiased`. + +nchunk : int >= 0, optional + If 0, no subdivision of the domain. Specify a positive integer to + divide the domain into subdomains of *nchunk* by *nchunk* quads. + Chunking reduces the maximum length of polygons generated by the + contouring algorithm which reduces the rendering workload passed + on to the backend and also requires slightly less RAM. It can + however introduce rendering artifacts at chunk boundaries depending + on the backend, the *antialiased* flag and value of *alpha*. + +linewidths : float or array-like, default: :rc:`contour.linewidth` + *Only applies to* `.contour`. + + The line width of the contour lines. + + If a number, all levels will be plotted with this linewidth. + + If a sequence, the levels in ascending order will be plotted with + the linewidths in the order specified. + + If None, this falls back to :rc:`lines.linewidth`. + +linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, optional + *Only applies to* `.contour`. + + If *linestyles* is *None*, the default is 'solid' unless the lines are + monochrome. In that case, negative contours will instead take their + linestyle from the *negative_linestyles* argument. + + *linestyles* can also be an iterable of the above strings specifying a set + of linestyles to be used. If this iterable is shorter than the number of + contour levels it will be repeated as necessary. + +negative_linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, \ + optional + *Only applies to* `.contour`. + + If *linestyles* is *None* and the lines are monochrome, this argument + specifies the line style for negative contours. + + If *negative_linestyles* is *None*, the default is taken from + :rc:`contour.negative_linestyles`. + + *negative_linestyles* can also be an iterable of the above strings + specifying a set of linestyles to be used. If this iterable is shorter than + the number of contour levels it will be repeated as necessary. + +hatches : list[str], optional + *Only applies to* `.contourf`. + + A list of cross hatch patterns to use on the filled areas. + If None, no hatching will be added to the contour. + Hatching is supported in the PostScript, PDF, SVG and Agg + backends only. + +algorithm : {'mpl2005', 'mpl2014', 'serial', 'threaded'}, optional + Which contouring algorithm to use to calculate the contour lines and + polygons. The algorithms are implemented in + `ContourPy `_, consult the + `ContourPy documentation `_ for + further information. + + The default is taken from :rc:`contour.algorithm`. + +data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + +Notes +----- +1. `.contourf` differs from the MATLAB version in that it does not draw + the polygon edges. To draw edges, add line contours with calls to + `.contour`. + +2. `.contourf` fills intervals that are closed at the top; that is, for + boundaries *z1* and *z2*, the filled region is:: + + z1 < Z <= z2 + + except for the lowest interval, which is closed on both sides (i.e. + it includes the lowest value). + +3. `.contour` and `.contourf` use a `marching squares + `_ algorithm to + compute contour locations. More information can be found in + `ContourPy documentation `_. +""" % _docstring.interpd.params) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index c462758e4850..2c2293e03986 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -1,6 +1,28 @@ """ Matplotlib provides sophisticated date plotting capabilities, standing on the -shoulders of python :mod:`datetime` and the add-on module :mod:`dateutil`. +shoulders of python :mod:`datetime` and the add-on module dateutil_. + +By default, Matplotlib uses the units machinery described in +`~matplotlib.units` to convert `datetime.datetime`, and `numpy.datetime64` +objects when plotted on an x- or y-axis. The user does not +need to do anything for dates to be formatted, but dates often have strict +formatting needs, so this module provides many axis locators and formatters. +A basic example using `numpy.datetime64` is:: + + import numpy as np + + times = np.arange(np.datetime64('2001-01-02'), + np.datetime64('2002-02-03'), np.timedelta64(75, 'm')) + y = np.random.randn(len(times)) + + fig, ax = plt.subplots() + ax.plot(times, y) + +.. seealso:: + + - :doc:`/gallery/text_labels_and_annotations/date` + - :doc:`/gallery/ticks/date_concise_formatter` + - :doc:`/gallery/ticks/date_demo_convert` .. _date-format: @@ -16,7 +38,7 @@ 20 microseconds for the rest of the allowable range of dates (year 0001 to 9999). The epoch can be changed at import time via `.dates.set_epoch` or :rc:`dates.epoch` to other dates if necessary; see -:doc:`/gallery/ticks_and_spines/date_precision_and_epochs` for a discussion. +:doc:`/gallery/ticks/date_precision_and_epochs` for a discussion. .. note:: @@ -61,10 +83,11 @@ Out[1]: 732401 All the Matplotlib date converters, tickers and formatters are timezone aware. -If no explicit timezone is provided, :rc:`timezone` is assumed. If you want to -use a custom time zone, pass a `datetime.tzinfo` instance with the tz keyword -argument to `num2date`, `~.Axes.plot_date`, and any custom date tickers or -locators you create. +If no explicit timezone is provided, :rc:`timezone` is assumed, provided as a +string. If you want to use a different timezone, pass the *tz* keyword +argument of `num2date` to any date tickers or locators you create. This can +be either a `datetime.tzinfo` instance or a string with the timezone name that +can be parsed by `~dateutil.tz.gettz`. A wide range of specific and general purpose date tick locators and formatters are provided in this module. See @@ -84,15 +107,15 @@ # import constants for the days of the week from matplotlib.dates import MO, TU, WE, TH, FR, SA, SU - # tick on mondays every week + # tick on Mondays every week loc = WeekdayLocator(byweekday=MO, tz=tz) - # tick on mondays and saturdays + # tick on Mondays and Saturdays loc = WeekdayLocator(byweekday=(MO, SA)) In addition, most of the constructors take an interval argument:: - # tick on mondays every second week + # tick on Mondays every second week loc = WeekdayLocator(byweekday=MO, interval=2) The rrule locator allows completely general date ticking:: @@ -119,17 +142,17 @@ * `YearLocator`: Locate years that are multiples of base. -* `RRuleLocator`: Locate using a `matplotlib.dates.rrulewrapper`. - `.rrulewrapper` is a simple wrapper around dateutil_'s `dateutil.rrule` which - allow almost arbitrary date tick specifications. See :doc:`rrule example - `. +* `RRuleLocator`: Locate using a `rrulewrapper`. + `rrulewrapper` is a simple wrapper around dateutil_'s `dateutil.rrule` + which allow almost arbitrary date tick specifications. + See :doc:`rrule example `. * `AutoDateLocator`: On autoscale, this class picks the best `DateLocator` (e.g., `RRuleLocator`) to set the view limits and the tick locations. If called with ``interval_multiples=True`` it will make ticks line up with - sensible multiples of the tick intervals. E.g. if the interval is 4 hours, - it will pick hours 0, 4, 8, etc as ticks. This behaviour is not guaranteed - by default. + sensible multiples of the tick intervals. For example, if the interval is + 4 hours, it will pick hours 0, 4, 8, etc. as ticks. This behaviour is not + guaranteed by default. Date formatters --------------- @@ -144,8 +167,6 @@ date information. This is most useful when used with the `AutoDateLocator`. * `DateFormatter`: use `~datetime.datetime.strftime` format strings. - -* `IndexDateFormatter`: date plots with implicit *x* indexing. """ import datetime @@ -166,41 +187,56 @@ from matplotlib import _api, cbook, ticker, units __all__ = ('datestr2num', 'date2num', 'num2date', 'num2timedelta', 'drange', - 'epoch2num', 'num2epoch', 'set_epoch', 'get_epoch', 'DateFormatter', - 'ConciseDateFormatter', 'IndexDateFormatter', 'AutoDateFormatter', - 'DateLocator', 'RRuleLocator', 'AutoDateLocator', 'YearLocator', - 'MonthLocator', 'WeekdayLocator', + 'set_epoch', 'get_epoch', 'DateFormatter', 'ConciseDateFormatter', + 'AutoDateFormatter', 'DateLocator', 'RRuleLocator', + 'AutoDateLocator', 'YearLocator', 'MonthLocator', 'WeekdayLocator', 'DayLocator', 'HourLocator', 'MinuteLocator', 'SecondLocator', 'MicrosecondLocator', 'rrule', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA', 'SU', 'YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY', 'MICROSECONDLY', 'relativedelta', - 'DateConverter', 'ConciseDateConverter') + 'DateConverter', 'ConciseDateConverter', 'rrulewrapper') _log = logging.getLogger(__name__) UTC = datetime.timezone.utc -def _get_rc_timezone(): - """Retrieve the preferred timezone from the rcParams dictionary.""" - s = mpl.rcParams['timezone'] - if s == 'UTC': - return UTC - return dateutil.tz.gettz(s) +@_api.caching_module_getattr +class __getattr__: + JULIAN_OFFSET = _api.deprecated("3.7")(property(lambda self: 1721424.5)) + # Julian date at 0000-12-31 + # note that the Julian day epoch is achievable w/ + # np.datetime64('-4713-11-24T12:00:00'); datetime64 is proleptic + # Gregorian and BC has a one-year offset. So + # np.datetime64('0000-12-31') - np.datetime64('-4713-11-24T12:00') = + # 1721424.5 + # Ref: https://en.wikipedia.org/wiki/Julian_day -""" -Time-related constants. -""" +def _get_tzinfo(tz=None): + """ + Generate `~datetime.tzinfo` from a string or return `~datetime.tzinfo`. + If None, retrieve the preferred timezone from the rcParams dictionary. + """ + if tz is None: + tz = mpl.rcParams['timezone'] + if tz == 'UTC': + return UTC + if isinstance(tz, str): + tzinfo = dateutil.tz.gettz(tz) + if tzinfo is None: + raise ValueError(f"{tz} is not a valid timezone as parsed by" + " dateutil.tz.gettz.") + return tzinfo + if isinstance(tz, datetime.tzinfo): + return tz + raise TypeError("tz must be string or tzinfo subclass.") + + +# Time-related constants. EPOCH_OFFSET = float(datetime.datetime(1970, 1, 1).toordinal()) # EPOCH_OFFSET is not used by matplotlib -JULIAN_OFFSET = 1721424.5 # Julian date at 0000-12-31 -# note that the Julian day epoch is achievable w/ -# np.datetime64('-4713-11-24T12:00:00'); datetime64 is proleptic -# Gregorian and BC has a one-year offset. So -# np.datetime64('0000-12-31') - np.datetime64('-4713-11-24T12:00') = 1721424.5 -# Ref: https://en.wikipedia.org/wiki/Julian_day MICROSECONDLY = SECONDLY + 1 HOURS_PER_DAY = 24. MIN_PER_HOUR = 60. @@ -251,7 +287,7 @@ def set_epoch(epoch): `~.dates.set_epoch` must be called before any dates are converted (i.e. near the import section) or a RuntimeError will be raised. - See also :doc:`/gallery/ticks_and_spines/date_precision_and_epochs`. + See also :doc:`/gallery/ticks/date_precision_and_epochs`. Parameters ---------- @@ -284,10 +320,10 @@ def get_epoch(): def _dt64_to_ordinalf(d): """ - Convert `numpy.datetime64` or an ndarray of those types to Gregorian - date as UTC float relative to the epoch (see `.get_epoch`). Roundoff - is float64 precision. Practically: microseconds for dates between - 290301 BC, 294241 AD, milliseconds for larger dates + Convert `numpy.datetime64` or an `numpy.ndarray` of those types to + Gregorian date as UTC float relative to the epoch (see `.get_epoch`). + Roundoff is float64 precision. Practically: microseconds for dates + between 290301 BC, 294241 AD, milliseconds for larger dates (see `numpy.datetime64`). """ @@ -302,11 +338,7 @@ def _dt64_to_ordinalf(d): NaT_int = np.datetime64('NaT').astype(np.int64) d_int = d.astype(np.int64) - try: - dt[d_int == NaT_int] = np.nan - except TypeError: - if d_int == NaT_int: - dt = np.nan + dt[d_int == NaT_int] = np.nan return dt @@ -321,8 +353,7 @@ def _from_ordinalf(x, tz=None): :rc:`timezone`. """ - if tz is None: - tz = _get_rc_timezone() + tz = _get_tzinfo(tz) dt = (np.datetime64(get_epoch()) + np.timedelta64(int(np.round(x * MUSECONDS_PER_DAY)), 'us')) @@ -375,7 +406,9 @@ def datestr2num(d, default=None): return date2num(dt) else: if default is not None: - d = [dateutil.parser.parse(s, default=default) for s in d] + d = [date2num(dateutil.parser.parse(s, default=default)) + for s in d] + return np.asarray(d) d = np.asarray(d) if not d.size: return d @@ -403,16 +436,18 @@ def date2num(d): The Gregorian calendar is assumed; this is not universal practice. For details see the module docstring. """ - if hasattr(d, "values"): - # this unpacks pandas series or dataframes... - d = d.values + # Unpack in case of e.g. Pandas or xarray object + d = cbook._unpack_to_numpy(d) # make an iterable, but save state to unpack later: iterable = np.iterable(d) if not iterable: d = [d] + masked = np.ma.is_masked(d) + mask = np.ma.getmask(d) d = np.asarray(d) + # convert to datetime64 arrays, if not already: if not np.issubdtype(d.dtype, np.datetime64): # datetime arrays @@ -426,11 +461,13 @@ def date2num(d): d = np.asarray(d) d = d.astype('datetime64[us]') + d = np.ma.masked_array(d, mask=mask) if masked else d d = _dt64_to_ordinalf(d) return d if iterable else d[0] +@_api.deprecated("3.7") def julian2num(j): """ Convert a Julian date (or sequence) to a Matplotlib date (or sequence). @@ -450,10 +487,11 @@ def julian2num(j): ep0 = np.datetime64('0000-12-31T00:00:00', 'h').astype(float) / 24. # Julian offset defined above is relative to 0000-12-31, but we need # relative to our current epoch: - dt = JULIAN_OFFSET - ep0 + ep + dt = __getattr__("JULIAN_OFFSET") - ep0 + ep return np.subtract(j, dt) # Handles both scalar & nonscalar j. +@_api.deprecated("3.7") def num2julian(n): """ Convert a Matplotlib date (or sequence) to a Julian date (or sequence). @@ -472,7 +510,7 @@ def num2julian(n): ep0 = np.datetime64('0000-12-31T00:00:00', 'h').astype(float) / 24. # Julian offset defined above is relative to 0000-12-31, but we need # relative to our current epoch: - dt = JULIAN_OFFSET - ep0 + ep + dt = __getattr__("JULIAN_OFFSET") - ep0 + ep return np.add(n, dt) # Handles both scalar & nonscalar j. @@ -486,8 +524,8 @@ def num2date(x, tz=None): Number of days (fraction part represents hours, minutes, seconds) since the epoch. See `.get_epoch` for the epoch, which can be changed by :rc:`date.epoch` or `.set_epoch`. - tz : str, default: :rc:`timezone` - Timezone of *x*. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Timezone of *x*. If a string, *tz* is passed to `dateutil.tz`. Returns ------- @@ -499,12 +537,10 @@ def num2date(x, tz=None): Notes ----- - The addition of one here is a historical artifact. Also, note that the - Gregorian calendar is assumed; this is not universal practice. + The Gregorian calendar is assumed; this is not universal practice. For details, see the module docstring. """ - if tz is None: - tz = _get_rc_timezone() + tz = _get_tzinfo(tz) return _from_ordinalf_np_vectorized(x, tz).tolist() @@ -563,7 +599,8 @@ def drange(dstart, dend, delta): # ensure, that an half open interval will be generated [dstart, dend) if dinterval_end >= dend: - # if the endpoint is greater than dend, just subtract one delta + # if the endpoint is greater than or equal to dend, + # just subtract one delta dinterval_end -= delta num -= 1 @@ -575,8 +612,11 @@ def _wrap_in_tex(text): p = r'([a-zA-Z]+)' ret_text = re.sub(p, r'}$\1$\\mathdefault{', text) - # Braces ensure dashes are not spaced like binary operators. - ret_text = '$\\mathdefault{'+ret_text.replace('-', '{-}')+'}$' + # Braces ensure symbols are not spaced like binary operators. + ret_text = ret_text.replace('-', '{-}').replace(':', '{:}') + # To not concatenate space between numbers. + ret_text = ret_text.replace(' ', r'\;') + ret_text = '$\\mathdefault{' + ret_text + '}$' ret_text = ret_text.replace('$\\mathdefault{}$', '') return ret_text @@ -590,27 +630,20 @@ class DateFormatter(ticker.Formatter): `~datetime.datetime.strftime` format string. """ - @_api.deprecated("3.3") - @property - def illegal_s(self): - return re.compile(r"((^|[^%])(%%)*%s)") - def __init__(self, fmt, tz=None, *, usetex=None): """ Parameters ---------- fmt : str `~datetime.datetime.strftime` format string - tz : `datetime.tzinfo`, default: :rc:`timezone` - Ticks timezone. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. usetex : bool, default: :rc:`text.usetex` To enable/disable the use of TeX's math mode for rendering the results of the formatter. """ - if tz is None: - tz = _get_rc_timezone() + self.tz = _get_tzinfo(tz) self.fmt = fmt - self.tz = tz self._usetex = (usetex if usetex is not None else mpl.rcParams['text.usetex']) @@ -619,34 +652,7 @@ def __call__(self, x, pos=0): return _wrap_in_tex(result) if self._usetex else result def set_tzinfo(self, tz): - self.tz = tz - - -@_api.deprecated("3.3") -class IndexDateFormatter(ticker.Formatter): - """Use with `.IndexLocator` to cycle format strings by index.""" - - def __init__(self, t, fmt, tz=None): - """ - Parameters - ---------- - t : list of float - A sequence of dates (floating point days). - fmt : str - A `~datetime.datetime.strftime` format string. - """ - if tz is None: - tz = _get_rc_timezone() - self.t = t - self.fmt = fmt - self.tz = tz - - def __call__(self, x, pos=0): - """Return the label for time *x* at position *pos*.""" - ind = int(round(x)) - if ind >= len(self.t) or ind <= 0: - return '' - return num2date(self.t[ind], self.tz).strftime(self.fmt) + self.tz = _get_tzinfo(tz) class ConciseDateFormatter(ticker.Formatter): @@ -663,8 +669,8 @@ class ConciseDateFormatter(ticker.Formatter): locator : `.ticker.Locator` Locator that this axis is using. - tz : str, optional - Passed to `.dates.date2num`. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone, passed to `.dates.num2date`. formats : list of 6 strings, optional Format strings for 6 levels of tick labelling: mostly years, @@ -695,7 +701,7 @@ class ConciseDateFormatter(ticker.Formatter): Examples -------- - See :doc:`/gallery/ticks_and_spines/date_concise_formatter` + See :doc:`/gallery/ticks/date_concise_formatter` .. plot:: @@ -764,7 +770,7 @@ def __init__(self, locator, tz=None, formats=None, offset_formats=None, if offset_formats: if len(offset_formats) != 6: - raise ValueError('offsetfmts argument must be a list of ' + raise ValueError('offset_formats argument must be a list of ' '6 format strings (or None)') self.offset_formats = offset_formats else: @@ -795,20 +801,22 @@ def format_ticks(self, values): # year, month, day etc. # fmt for most ticks at this level fmts = self.formats - # format beginnings of days, months, years, etc... + # format beginnings of days, months, years, etc. zerofmts = self.zero_formats # offset fmt are for the offset in the upper left of the # or lower right of the axis. offsetfmts = self.offset_formats + show_offset = self.show_offset # determine the level we will label at: # mostly 0: years, 1: months, 2: days, # 3: hours, 4: minutes, 5: seconds, 6: microseconds for level in range(5, -1, -1): - if len(np.unique(tickdate[:, level])) > 1: - # level is less than 2 so a year is already present in the axis - if (level < 2): - self.show_offset = False + unique = np.unique(tickdate[:, level]) + if len(unique) > 1: + # if 1 is included in unique, the year is shown in ticks + if level < 2 and np.any(unique == 1): + show_offset = False break elif level == 0: # all tickdate are the same, so only micros might be different @@ -848,11 +856,13 @@ def format_ticks(self, values): if '.' in labels[nn]: labels[nn] = labels[nn][:-trailing_zeros].rstrip('.') - if self.show_offset: + if show_offset: # set the offset string: self.offset_string = tickdatetime[-1].strftime(offsetfmts[level]) if self._usetex: self.offset_string = _wrap_in_tex(self.offset_string) + else: + self.offset_string = '' if self._usetex: return [_wrap_in_tex(l) for l in labels] @@ -871,48 +881,48 @@ class AutoDateFormatter(ticker.Formatter): 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 - format string. The default looks like this:: + `.AutoDateFormatter` has a ``.scale`` dictionary that maps tick scales (the + interval in days between one major tick) to format strings; this dictionary + defaults to :: self.scaled = { - DAYS_PER_YEAR: rcParams['date.autoformat.year'], - DAYS_PER_MONTH: rcParams['date.autoformat.month'], - 1.0: rcParams['date.autoformat.day'], - 1. / HOURS_PER_DAY: rcParams['date.autoformat.hour'], - 1. / (MINUTES_PER_DAY): rcParams['date.autoformat.minute'], - 1. / (SEC_PER_DAY): rcParams['date.autoformat.second'], - 1. / (MUSECONDS_PER_DAY): rcParams['date.autoformat.microsecond'], + DAYS_PER_YEAR: rcParams['date.autoformatter.year'], + DAYS_PER_MONTH: rcParams['date.autoformatter.month'], + 1: rcParams['date.autoformatter.day'], + 1 / HOURS_PER_DAY: rcParams['date.autoformatter.hour'], + 1 / MINUTES_PER_DAY: rcParams['date.autoformatter.minute'], + 1 / SEC_PER_DAY: rcParams['date.autoformatter.second'], + 1 / MUSECONDS_PER_DAY: rcParams['date.autoformatter.microsecond'], } - The algorithm picks the key in the dictionary that is >= the - current scale and uses that format string. You can customize this - dictionary by doing:: + The formatter uses the format string corresponding to the lowest key in + the dictionary that is greater or equal to the current scale. Dictionary + entries can be customized:: - >>> locator = AutoDateLocator() - >>> formatter = AutoDateFormatter(locator) - >>> formatter.scaled[1/(24.*60.)] = '%M:%S' # only show min and sec - - A custom `.FuncFormatter` can also be used. The following example shows - how to use a custom format function to strip trailing zeros from decimal - seconds and adds the date to the first ticklabel:: - - >>> def my_format_function(x, pos=None): - ... x = matplotlib.dates.num2date(x) - ... if pos == 0: - ... fmt = '%D %H:%M:%S.%f' - ... else: - ... fmt = '%H:%M:%S.%f' - ... label = x.strftime(fmt) - ... label = label.rstrip("0") - ... label = label.rstrip(".") - ... return label - >>> from matplotlib.ticker import FuncFormatter - >>> formatter.scaled[1/(24.*60.)] = FuncFormatter(my_format_function) + locator = AutoDateLocator() + formatter = AutoDateFormatter(locator) + formatter.scaled[1/(24*60)] = '%M:%S' # only show min and sec + + Custom callables can also be used instead of format strings. The following + example shows how to use a custom format function to strip trailing zeros + from decimal seconds and adds the date to the first ticklabel:: + + def my_format_function(x, pos=None): + x = matplotlib.dates.num2date(x) + if pos == 0: + fmt = '%D %H:%M:%S.%f' + else: + fmt = '%H:%M:%S.%f' + label = x.strftime(fmt) + label = label.rstrip("0") + label = label.rstrip(".") + return label + + formatter.scaled[1/(24*60)] = my_format_function """ # This can be improved by providing some user-level direction on - # how to choose the best format (precedence, etc...) + # how to choose the best format (precedence, etc.). # Perhaps a 'struct' that has a field for each time-type where a # zero would indicate "don't show" and a number would indicate @@ -932,8 +942,8 @@ def __init__(self, locator, tz=None, defaultfmt='%Y-%m-%d', *, locator : `.ticker.Locator` Locator that this axis is using. - tz : str, optional - Passed to `.dates.date2num`. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. defaultfmt : str The default format to use if none of the values in ``self.scaled`` @@ -987,13 +997,29 @@ def __call__(self, x, pos=None): class rrulewrapper: + """ + A simple wrapper around a `dateutil.rrule` allowing flexible + date tick specifications. + """ def __init__(self, freq, tzinfo=None, **kwargs): + """ + Parameters + ---------- + freq : {YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY} + Tick frequency. These constants are defined in `dateutil.rrule`, + but they are accessible from `matplotlib.dates` as well. + tzinfo : `datetime.tzinfo`, optional + Time zone information. The default is None. + **kwargs + Additional keyword arguments are passed to the `dateutil.rrule`. + """ kwargs['freq'] = freq self._base_tzinfo = tzinfo self._update_rrule(**kwargs) def set(self, **kwargs): + """Set parameters for an existing wrapper.""" self._construct.update(kwargs) self._update_rrule(**self._construct) @@ -1001,7 +1027,7 @@ def set(self, **kwargs): def _update_rrule(self, **kwargs): tzinfo = self._base_tzinfo - # rrule does not play nicely with time zones - especially pytz time + # rrule does not play nicely with timezones - especially pytz time # zones, it's best to use naive zones and attach timezones once the # datetimes are returned if 'dtstart' in kwargs: @@ -1104,17 +1130,21 @@ def __init__(self, tz=None): """ Parameters ---------- - tz : `datetime.tzinfo` + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ - if tz is None: - tz = _get_rc_timezone() - self.tz = tz + self.tz = _get_tzinfo(tz) def set_tzinfo(self, tz): """ - Set time zone info. + Set timezone info. + + Parameters + ---------- + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ - self.tz = tz + self.tz = _get_tzinfo(tz) def datalim_to_dt(self): """Convert axis data interval to datetime objects.""" @@ -1150,9 +1180,9 @@ def nonsingular(self, vmin, vmax): if it is too close to being singular (i.e. a range of ~0). """ if not np.isfinite(vmin) or not np.isfinite(vmax): - # Except if there is no data, then use 2000-2010 as default. - return (date2num(datetime.date(2000, 1, 1)), - date2num(datetime.date(2010, 1, 1))) + # Except if there is no data, then use 1970 as default. + return (date2num(datetime.date(1970, 1, 1)), + date2num(datetime.date(1970, 1, 2))) if vmax < vmin: vmin, vmax = vmax, vmin unit = self._get_unit() @@ -1180,6 +1210,15 @@ def __call__(self): return self.tick_values(dmin, dmax) def tick_values(self, vmin, vmax): + start, stop = self._create_rrule(vmin, vmax) + dates = self.rule.between(start, stop, True) + if len(dates) == 0: + return date2num([vmin, vmax]) + return self.raise_if_exceeds(date2num(dates)) + + def _create_rrule(self, vmin, vmax): + # set appropriate rrule dtstart and until and return + # start and end delta = relativedelta(vmax, vmin) # We need to cap at the endpoints of valid datetime @@ -1199,10 +1238,7 @@ def tick_values(self, vmin, vmax): self.rule.set(dtstart=start, until=stop) - dates = self.rule.between(vmin, vmax, True) - if len(dates) == 0: - return date2num([vmin, vmax]) - return self.raise_if_exceeds(date2num(dates)) + return vmin, vmax def _get_unit(self): # docstring inherited @@ -1227,7 +1263,7 @@ def get_unit_generic(freq): return 1.0 / SEC_PER_DAY else: # error - return -1 # or should this just return '1'? + return -1 # or should this just return '1'? def _get_interval(self): return self.rule._rrule._interval @@ -1278,8 +1314,8 @@ def __init__(self, tz=None, minticks=5, maxticks=None, """ Parameters ---------- - tz : `datetime.tzinfo` - Ticks timezone. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. minticks : int The minimum number of ticks desired; controls whether ticks occur yearly, monthly, etc. @@ -1298,7 +1334,7 @@ def __init__(self, tz=None, minticks=5, maxticks=None, the ticks to be at hours 0, 6, 12, 18 when hourly ticking is done at 6 hour intervals. """ - super().__init__(tz) + super().__init__(tz=tz) self._freq = YEARLY self._freqs = [YEARLY, MONTHLY, DAILY, HOURLY, MINUTELY, SECONDLY, MICROSECONDLY] @@ -1349,9 +1385,9 @@ def nonsingular(self, vmin, vmax): # whatever is thrown at us, we can scale the unit. # But default nonsingular date plots at an ~4 year period. if not np.isfinite(vmin) or not np.isfinite(vmax): - # Except if there is no data, then use 2000-2010 as default. - return (date2num(datetime.date(2000, 1, 1)), - date2num(datetime.date(2010, 1, 1))) + # Except if there is no data, then use 1970 as default. + return (date2num(datetime.date(1970, 1, 1)), + date2num(datetime.date(1970, 1, 2))) if vmax < vmin: vmin, vmax = vmax, vmin if vmin == vmax: @@ -1380,7 +1416,7 @@ def get_locator(self, dmin, dmax): # whenever possible. numYears = float(delta.years) numMonths = numYears * MONTHS_PER_YEAR + delta.months - numDays = tdelta.days # Avoids estimates of days/month, days/year + numDays = tdelta.days # Avoids estimates of days/month, days/year. numHours = numDays * HOURS_PER_DAY + delta.hours numMinutes = numHours * MIN_PER_HOUR + delta.minutes numSeconds = np.floor(tdelta.total_seconds()) @@ -1398,7 +1434,7 @@ def get_locator(self, dmin, dmax): # Loop over all the frequencies and try to find one that gives at # least a minticks tick positions. Once this is found, look for - # an interval from an list specific to that frequency that gives no + # an interval from a list specific to that frequency that gives no # more than maxticks tick positions. Also, set up some ranges # (bymonth, etc.) as appropriate to be passed to rrulewrapper. for i, (freq, num) in enumerate(zip(self._freqs, nums)): @@ -1452,7 +1488,7 @@ def get_locator(self, dmin, dmax): byhour=byhour, byminute=byminute, bysecond=bysecond) - locator = RRuleLocator(rrule, self.tz) + locator = RRuleLocator(rrule, tz=self.tz) else: locator = MicrosecondLocator(interval, tz=self.tz) if date2num(dmin) > 70 * 365 and interval < 1000: @@ -1463,14 +1499,10 @@ def get_locator(self, dmin, dmax): 'epoch.') locator.set_axis(self.axis) - - if self.axis is not None: - locator.set_view_interval(*self.axis.get_view_interval()) - locator.set_data_interval(*self.axis.get_data_interval()) return locator -class YearLocator(DateLocator): +class YearLocator(RRuleLocator): """ Make ticks on a given day of each year that is a multiple of base. @@ -1484,55 +1516,40 @@ class YearLocator(DateLocator): """ def __init__(self, base=1, month=1, day=1, tz=None): """ - Mark years that are multiple of base on a given month and day - (default jan 1). + Parameters + ---------- + base : int, default: 1 + Mark ticks every *base* years. + month : int, default: 1 + The month on which to place the ticks, starting from 1. Default is + January. + day : int, default: 1 + The day on which to place the ticks. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ - super().__init__(tz) + rule = rrulewrapper(YEARLY, interval=base, bymonth=month, + bymonthday=day, **self.hms0d) + super().__init__(rule, tz=tz) self.base = ticker._Edge_integer(base, 0) - self.replaced = {'month': month, - 'day': day, - 'hour': 0, - 'minute': 0, - 'second': 0, - } - if not hasattr(tz, 'localize'): - # if tz is pytz, we need to do this w/ the localize fcn, - # otherwise datetime.replace works fine... - self.replaced['tzinfo'] = tz - def __call__(self): - # if no data have been set, this will tank with a ValueError - try: - dmin, dmax = self.viewlim_to_dt() - except ValueError: - return [] - - return self.tick_values(dmin, dmax) - - def tick_values(self, vmin, vmax): - ymin = self.base.le(vmin.year) * self.base.step - ymax = self.base.ge(vmax.year) * self.base.step + def _create_rrule(self, vmin, vmax): + # 'start' needs to be a multiple of the interval to create ticks on + # interval multiples when the tick frequency is YEARLY + ymin = max(self.base.le(vmin.year) * self.base.step, 1) + ymax = min(self.base.ge(vmax.year) * self.base.step, 9999) - vmin = vmin.replace(year=ymin, **self.replaced) - if hasattr(self.tz, 'localize'): - # look after pytz - if not vmin.tzinfo: - vmin = self.tz.localize(vmin, is_dst=True) + c = self.rule._construct + replace = {'year': ymin, + 'month': c.get('bymonth', 1), + 'day': c.get('bymonthday', 1), + 'hour': 0, 'minute': 0, 'second': 0} - ticks = [vmin] - - while True: - dt = ticks[-1] - if dt.year >= ymax: - return date2num(ticks) - year = dt.year + self.base.step - dt = dt.replace(year=year, **self.replaced) - if hasattr(self.tz, 'localize'): - # look after pytz - if not dt.tzinfo: - dt = self.tz.localize(dt, is_dst=True) + start = vmin.replace(**replace) + stop = start.replace(year=ymax) + self.rule.set(dtstart=start, until=stop) - ticks.append(dt) + return start, stop class MonthLocator(RRuleLocator): @@ -1541,23 +1558,25 @@ class MonthLocator(RRuleLocator): """ def __init__(self, bymonth=None, bymonthday=1, interval=1, tz=None): """ - Mark every month in *bymonth*; *bymonth* can be an int or - sequence. Default is ``range(1, 13)``, i.e. every month. - - *interval* is the interval between each iteration. For - example, if ``interval=2``, mark every second occurrence. + Parameters + ---------- + bymonth : int or list of int, default: all months + Ticks will be placed on every month in *bymonth*. Default is + ``range(1, 13)``, i.e. every month. + bymonthday : int, default: 1 + The day on which to place the ticks. + interval : int, default: 1 + The interval between each iteration. For example, if + ``interval=2``, mark every second occurrence. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ if bymonth is None: bymonth = range(1, 13) - elif isinstance(bymonth, np.ndarray): - # This fixes a bug in dateutil <= 2.3 which prevents the use of - # numpy arrays in (among other things) the bymonthday, byweekday - # and bymonth parameters. - bymonth = [x.item() for x in bymonth.astype(int)] rule = rrulewrapper(MONTHLY, bymonth=bymonth, bymonthday=bymonthday, interval=interval, **self.hms0d) - super().__init__(rule, tz) + super().__init__(rule, tz=tz) class WeekdayLocator(RRuleLocator): @@ -1567,25 +1586,24 @@ class WeekdayLocator(RRuleLocator): def __init__(self, byweekday=1, interval=1, tz=None): """ - Mark every weekday in *byweekday*; *byweekday* can be a number or - sequence. - - Elements of *byweekday* must be one of MO, TU, WE, TH, FR, SA, - SU, the constants from :mod:`dateutil.rrule`, which have been - imported into the :mod:`matplotlib.dates` namespace. - - *interval* specifies the number of weeks to skip. For example, - ``interval=2`` plots every second week. + Parameters + ---------- + byweekday : int or list of int, default: all days + Ticks will be placed on every weekday in *byweekday*. Default is + every day. + + Elements of *byweekday* must be one of MO, TU, WE, TH, FR, SA, + SU, the constants from :mod:`dateutil.rrule`, which have been + imported into the :mod:`matplotlib.dates` namespace. + interval : int, default: 1 + The interval between each iteration. For example, if + ``interval=2``, mark every second occurrence. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ - if isinstance(byweekday, np.ndarray): - # This fixes a bug in dateutil <= 2.3 which prevents the use of - # numpy arrays in (among other things) the bymonthday, byweekday - # and bymonth parameters. - [x.item() for x in byweekday.astype(int)] - rule = rrulewrapper(DAILY, byweekday=byweekday, interval=interval, **self.hms0d) - super().__init__(rule, tz) + super().__init__(rule, tz=tz) class DayLocator(RRuleLocator): @@ -1595,23 +1613,25 @@ class DayLocator(RRuleLocator): """ def __init__(self, bymonthday=None, interval=1, tz=None): """ - Mark every day in *bymonthday*; *bymonthday* can be an int or sequence. - - Default is to tick every day of the month: ``bymonthday=range(1, 32)``. + Parameters + ---------- + bymonthday : int or list of int, default: all days + Ticks will be placed on every day in *bymonthday*. Default is + ``bymonthday=range(1, 32)``, i.e., every day of the month. + interval : int, default: 1 + The interval between each iteration. For example, if + ``interval=2``, mark every second occurrence. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ if interval != int(interval) or interval < 1: raise ValueError("interval must be an integer greater than 0") if bymonthday is None: bymonthday = range(1, 32) - elif isinstance(bymonthday, np.ndarray): - # This fixes a bug in dateutil <= 2.3 which prevents the use of - # numpy arrays in (among other things) the bymonthday, byweekday - # and bymonth parameters. - bymonthday = [x.item() for x in bymonthday.astype(int)] rule = rrulewrapper(DAILY, bymonthday=bymonthday, interval=interval, **self.hms0d) - super().__init__(rule, tz) + super().__init__(rule, tz=tz) class HourLocator(RRuleLocator): @@ -1620,18 +1640,23 @@ class HourLocator(RRuleLocator): """ def __init__(self, byhour=None, interval=1, tz=None): """ - Mark every hour in *byhour*; *byhour* can be an int or sequence. - Default is to tick every hour: ``byhour=range(24)`` - - *interval* is the interval between each iteration. For - example, if ``interval=2``, mark every second occurrence. + Parameters + ---------- + byhour : int or list of int, default: all hours + Ticks will be placed on every hour in *byhour*. Default is + ``byhour=range(24)``, i.e., every hour. + interval : int, default: 1 + The interval between each iteration. For example, if + ``interval=2``, mark every second occurrence. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ if byhour is None: byhour = range(24) rule = rrulewrapper(HOURLY, byhour=byhour, interval=interval, byminute=0, bysecond=0) - super().__init__(rule, tz) + super().__init__(rule, tz=tz) class MinuteLocator(RRuleLocator): @@ -1640,18 +1665,23 @@ class MinuteLocator(RRuleLocator): """ def __init__(self, byminute=None, interval=1, tz=None): """ - Mark every minute in *byminute*; *byminute* can be an int or - sequence. Default is to tick every minute: ``byminute=range(60)`` - - *interval* is the interval between each iteration. For - example, if ``interval=2``, mark every second occurrence. + Parameters + ---------- + byminute : int or list of int, default: all minutes + Ticks will be placed on every minute in *byminute*. Default is + ``byminute=range(60)``, i.e., every minute. + interval : int, default: 1 + The interval between each iteration. For example, if + ``interval=2``, mark every second occurrence. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ if byminute is None: byminute = range(60) rule = rrulewrapper(MINUTELY, byminute=byminute, interval=interval, bysecond=0) - super().__init__(rule, tz) + super().__init__(rule, tz=tz) class SecondLocator(RRuleLocator): @@ -1660,18 +1690,22 @@ class SecondLocator(RRuleLocator): """ def __init__(self, bysecond=None, interval=1, tz=None): """ - Mark every second in *bysecond*; *bysecond* can be an int or - sequence. Default is to tick every second: ``bysecond = range(60)`` - - *interval* is the interval between each iteration. For - example, if ``interval=2``, mark every second occurrence. - + Parameters + ---------- + bysecond : int or list of int, default: all seconds + Ticks will be placed on every second in *bysecond*. Default is + ``bysecond = range(60)``, i.e., every second. + interval : int, default: 1 + The interval between each iteration. For example, if + ``interval=2``, mark every second occurrence. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ if bysecond is None: bysecond = range(60) rule = rrulewrapper(SECONDLY, bysecond=bysecond, interval=interval) - super().__init__(rule, tz) + super().__init__(rule, tz=tz) class MicrosecondLocator(DateLocator): @@ -1693,31 +1727,27 @@ class MicrosecondLocator(DateLocator): If you really must use datetime.datetime() or similar and still need microsecond precision, change the time origin via `.dates.set_epoch` to something closer to the dates being plotted. - See :doc:`/gallery/ticks_and_spines/date_precision_and_epochs`. + See :doc:`/gallery/ticks/date_precision_and_epochs`. """ def __init__(self, interval=1, tz=None): """ - *interval* is the interval between each iteration. For - example, if ``interval=2``, mark every second microsecond. - + Parameters + ---------- + interval : int, default: 1 + The interval between each iteration. For example, if + ``interval=2``, mark every second occurrence. + tz : str or `~datetime.tzinfo`, default: :rc:`timezone` + Ticks timezone. If a string, *tz* is passed to `dateutil.tz`. """ + super().__init__(tz=tz) self._interval = interval self._wrapped_locator = ticker.MultipleLocator(interval) - self.tz = tz def set_axis(self, axis): self._wrapped_locator.set_axis(axis) return super().set_axis(axis) - def set_view_interval(self, vmin, vmax): - self._wrapped_locator.set_view_interval(vmin, vmax) - return super().set_view_interval(vmin, vmax) - - def set_data_interval(self, vmin, vmax): - self._wrapped_locator.set_data_interval(vmin, vmax) - return super().set_data_interval(vmin, vmax) - def __call__(self): # if no data have been set, this will tank with a ValueError try: @@ -1749,47 +1779,8 @@ def _get_interval(self): return self._interval -def epoch2num(e): - """ - Convert UNIX time to days since Matplotlib epoch. - - Parameters - ---------- - e : list of floats - Time in seconds since 1970-01-01. - - Returns - ------- - `numpy.array` - Time in days since Matplotlib epoch (see `~.dates.get_epoch()`). - """ - - dt = (np.datetime64('1970-01-01T00:00:00', 's') - - np.datetime64(get_epoch(), 's')).astype(float) - - return (dt + np.asarray(e)) / SEC_PER_DAY - - -def num2epoch(d): - """ - Convert days since Matplotlib epoch to UNIX time. - - Parameters - ---------- - d : list of floats - Time in days since Matplotlib epoch (see `~.dates.get_epoch()`). - - Returns - ------- - `numpy.array` - Time in seconds since 1970-01-01. - """ - dt = (np.datetime64('1970-01-01T00:00:00', 's') - - np.datetime64(get_epoch(), 's')).astype(float) - - return np.asarray(d) * SEC_PER_DAY - dt - - +@_api.deprecated("3.6", alternative="`AutoDateLocator` and `AutoDateFormatter`" + " or vendor the code") def date_ticker_factory(span, tz=None, numticks=5): """ Create a date locator with *numticks* (approx) and a date formatter @@ -1837,7 +1828,7 @@ class DateConverter(units.ConversionInterface): Converter for `datetime.date` and `datetime.datetime` data, or for date/time data represented as it would be converted by `date2num`. - The 'unit' tag for such data is None or a tzinfo instance. + The 'unit' tag for such data is None or a `~datetime.tzinfo` instance. """ def __init__(self, *, interval_multiples=True): @@ -1848,7 +1839,7 @@ def axisinfo(self, unit, axis): """ Return the `~matplotlib.units.AxisInfo` for *unit*. - *unit* is a tzinfo instance or None. + *unit* is a `~datetime.tzinfo` instance or None. The *axis* argument is required but not used. """ tz = unit @@ -1856,8 +1847,8 @@ def axisinfo(self, unit, axis): majloc = AutoDateLocator(tz=tz, interval_multiples=self._interval_multiples) majfmt = AutoDateFormatter(majloc, tz=tz) - datemin = datetime.date(2000, 1, 1) - datemax = datetime.date(2010, 1, 1) + datemin = datetime.date(1970, 1, 1) + datemax = datetime.date(1970, 1, 2) return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='', default_limits=(datemin, datemax)) @@ -1875,13 +1866,14 @@ def convert(value, unit, axis): @staticmethod def default_units(x, axis): """ - Return the tzinfo instance of *x* or of its first element, or None + Return the `~datetime.tzinfo` instance of *x* or of its first element, + or None """ if isinstance(x, np.ndarray): x = x.ravel() try: - x = cbook.safe_first_element(x) + x = cbook._safe_first_finite(x) except (TypeError, StopIteration): pass @@ -1913,50 +1905,38 @@ def axisinfo(self, unit, axis): zero_formats=self._zero_formats, offset_formats=self._offset_formats, show_offset=self._show_offset) - datemin = datetime.date(2000, 1, 1) - datemax = datetime.date(2010, 1, 1) + datemin = datetime.date(1970, 1, 1) + datemax = datetime.date(1970, 1, 2) return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='', default_limits=(datemin, datemax)) -class _rcParam_helper: +class _SwitchableDateConverter: """ - This helper class is so that we can set the converter for dates - via the validator for the rcParams `date.converter` and - `date.interval_multiples`. Never instatiated. + Helper converter-like object that generates and dispatches to + temporary ConciseDateConverter or DateConverter instances based on + :rc:`date.converter` and :rc:`date.interval_multiples`. """ - conv_st = 'auto' - int_mult = True - - @classmethod - def set_converter(cls, s): - """Called by validator for rcParams date.converter""" - if s not in ['concise', 'auto']: - raise ValueError('Converter must be one of "concise" or "auto"') - cls.conv_st = s - cls.register_converters() - - @classmethod - def set_int_mult(cls, b): - """Called by validator for rcParams date.interval_multiples""" - cls.int_mult = b - cls.register_converters() - - @classmethod - def register_converters(cls): - """ - Helper to register the date converters when rcParams `date.converter` - and `date.interval_multiples` are changed. Called by the helpers - above. - """ - if cls.conv_st == 'concise': - converter = ConciseDateConverter - else: - converter = DateConverter + @staticmethod + def _get_converter(): + converter_cls = { + "concise": ConciseDateConverter, "auto": DateConverter}[ + mpl.rcParams["date.converter"]] + interval_multiples = mpl.rcParams["date.interval_multiples"] + return converter_cls(interval_multiples=interval_multiples) + + def axisinfo(self, *args, **kwargs): + return self._get_converter().axisinfo(*args, **kwargs) + + def default_units(self, *args, **kwargs): + return self._get_converter().default_units(*args, **kwargs) + + def convert(self, *args, **kwargs): + return self._get_converter().convert(*args, **kwargs) + - interval_multiples = cls.int_mult - convert = converter(interval_multiples=interval_multiples) - units.registry[np.datetime64] = convert - units.registry[datetime.date] = convert - units.registry[datetime.datetime] = convert +units.registry[np.datetime64] = \ + units.registry[datetime.date] = \ + units.registry[datetime.datetime] = \ + _SwitchableDateConverter() diff --git a/lib/matplotlib/docstring.py b/lib/matplotlib/docstring.py index d0e661ffffd9..b6ddcf5acd10 100644 --- a/lib/matplotlib/docstring.py +++ b/lib/matplotlib/docstring.py @@ -1,77 +1,4 @@ -import inspect - +from matplotlib._docstring import * # noqa: F401, F403 from matplotlib import _api - - -class Substitution: - """ - A decorator that performs %-substitution on an object's docstring. - - This decorator should be robust even if ``obj.__doc__`` is None (for - example, if -OO was passed to the interpreter). - - Usage: construct a docstring.Substitution with a sequence or dictionary - suitable for performing substitution; then decorate a suitable function - with the constructed object, e.g.:: - - sub_author_name = Substitution(author='Jason') - - @sub_author_name - def some_function(x): - "%(author)s wrote this function" - - # note that some_function.__doc__ is now "Jason wrote this function" - - One can also use positional arguments:: - - sub_first_last_names = Substitution('Edgar Allen', 'Poe') - - @sub_first_last_names - def some_function(x): - "%s %s wrote the Raven" - """ - def __init__(self, *args, **kwargs): - if args and kwargs: - raise TypeError("Only positional or keyword args are allowed") - self.params = args or kwargs - - def __call__(self, func): - if func.__doc__: - func.__doc__ = inspect.cleandoc(func.__doc__) % self.params - return func - - def update(self, *args, **kwargs): - """ - Update ``self.params`` (which must be a dict) with the supplied args. - """ - self.params.update(*args, **kwargs) - - @classmethod - @_api.deprecated("3.3", alternative="assign to the params attribute") - def from_params(cls, params): - """ - In the case where the params is a mutable sequence (list or - dictionary) and it may change before this class is called, one may - explicitly use a reference to the params rather than using *args or - **kwargs which will copy the values and not reference them. - - :meta private: - """ - result = cls() - result.params = params - return result - - -def copy(source): - """Copy a docstring from another source function (if present).""" - def do_copy(target): - if source.__doc__: - target.__doc__ = source.__doc__ - return target - return do_copy - - -# Create a decorator that will house the various docstring snippets reused -# throughout Matplotlib. -interpd = Substitution() -dedent_interpd = interpd +_api.warn_deprecated( + "3.6", obj_type='module', name=f"{__name__}") diff --git a/lib/matplotlib/dviread.py b/lib/matplotlib/dviread.py index 579fe0e11ece..cbd3b542a003 100644 --- a/lib/matplotlib/dviread.py +++ b/lib/matplotlib/dviread.py @@ -25,12 +25,12 @@ from pathlib import Path import re import struct +import subprocess import sys -import textwrap import numpy as np -from matplotlib import _api, cbook, rcParams +from matplotlib import _api, cbook _log = logging.getLogger(__name__) @@ -39,8 +39,8 @@ # are cached using lru_cache(). # Dvi is a bytecode format documented in -# http://mirrors.ctan.org/systems/knuth/dist/texware/dvitype.web -# http://texdoc.net/texmf-dist/doc/generic/knuth/texware/dvitype.pdf +# https://ctan.org/pkg/dvitype +# https://texdoc.org/serve/dvitype.pdf/0 # # The file consists of a preamble, some number of pages, a postamble, # and a finale. Different opcodes are allowed in different contexts, @@ -58,10 +58,71 @@ # The marks on a page consist of text and boxes. A page also has dimensions. Page = namedtuple('Page', 'text boxes height width descent') -Text = namedtuple('Text', 'x y font glyph width') Box = namedtuple('Box', 'x y height width') +# Also a namedtuple, for backcompat. +class Text(namedtuple('Text', 'x y font glyph width')): + """ + A glyph in the dvi file. + + The *x* and *y* attributes directly position the glyph. The *font*, + *glyph*, and *width* attributes are kept public for back-compatibility, + but users wanting to draw the glyph themselves are encouraged to instead + load the font specified by `font_path` at `font_size`, warp it with the + effects specified by `font_effects`, and load the glyph specified by + `glyph_name_or_index`. + """ + + def _get_pdftexmap_entry(self): + return PsfontsMap(find_tex_file("pdftex.map"))[self.font.texname] + + @property + def font_path(self): + """The `~pathlib.Path` to the font for this glyph.""" + psfont = self._get_pdftexmap_entry() + if psfont.filename is None: + raise ValueError("No usable font file found for {} ({}); " + "the font may lack a Type-1 version" + .format(psfont.psname.decode("ascii"), + psfont.texname.decode("ascii"))) + return Path(psfont.filename) + + @property + def font_size(self): + """The font size.""" + return self.font.size + + @property + def font_effects(self): + """ + The "font effects" dict for this glyph. + + This dict contains the values for this glyph of SlantFont and + ExtendFont (if any), read off :file:`pdftex.map`. + """ + return self._get_pdftexmap_entry().effects + + @property + def glyph_name_or_index(self): + """ + Either the glyph name or the native charmap glyph index. + + If :file:`pdftex.map` specifies an encoding for this glyph's font, that + is a mapping of glyph indices to Adobe glyph names; use it to convert + dvi indices to glyph names. Callers can then convert glyph names to + glyph indices (with FT_Get_Name_Index/get_name_index), and load the + glyph using FT_Load_Glyph/load_glyph. + + If :file:`pdftex.map` specifies no encoding, the indices directly map + to the font's "native" charmap; glyphs should directly load using + FT_Load_Char/load_char after selecting the native charmap. + """ + entry = self._get_pdftexmap_entry() + return (_parse_enc(entry.encoding)[self.glyph] + if entry.encoding is not None else self.glyph) + + # Opcode argument parsing # # Each of the following functions takes a Dvi object and delta, @@ -84,8 +145,6 @@ def _arg(nbytes, signed, dvi, _): def _arg_slen(dvi, delta): """ - Signed, length *delta* - Read *delta* bytes, returning None if *delta* is zero, and the bytes interpreted as a signed integer otherwise. """ @@ -96,26 +155,20 @@ def _arg_slen(dvi, delta): def _arg_slen1(dvi, delta): """ - Signed, length *delta*+1 - Read *delta*+1 bytes, returning the bytes interpreted as signed. """ - return dvi._arg(delta+1, True) + return dvi._arg(delta + 1, True) def _arg_ulen1(dvi, delta): """ - Unsigned length *delta*+1 - Read *delta*+1 bytes, returning the bytes interpreted as unsigned. """ - return dvi._arg(delta+1, False) + return dvi._arg(delta + 1, False) def _arg_olen1(dvi, delta): """ - Optionally signed, length *delta*+1 - Read *delta*+1 bytes, returning the bytes interpreted as unsigned integer for 0<=*delta*<3 and signed if *delta*==3. """ @@ -139,30 +192,30 @@ def _dispatch(table, min, max=None, state=None, args=('raw',)): matches *state* if not None, reads arguments from the file according to *args*. - *table* - the dispatch table to be filled in - - *min* - minimum opcode for calling this function - - *max* - maximum opcode for calling this function, None if only *min* is allowed - - *state* - state of the Dvi object in which these opcodes are allowed - - *args* - sequence of argument specifications: - - ``'raw'``: opcode minus minimum - ``'u1'``: read one unsigned byte - ``'u4'``: read four bytes, treat as an unsigned number - ``'s4'``: read four bytes, treat as a signed number - ``'slen'``: read (opcode - minimum) bytes, treat as signed - ``'slen1'``: read (opcode - minimum + 1) bytes, treat as signed - ``'ulen1'``: read (opcode - minimum + 1) bytes, treat as unsigned - ``'olen1'``: read (opcode - minimum + 1) bytes, treat as unsigned - if under four bytes, signed if four bytes + Parameters + ---------- + table : dict[int, callable] + The dispatch table to be filled in. + + min, max : int + Range of opcodes that calls the registered function; *max* defaults to + *min*. + + state : _dvistate, optional + State of the Dvi object in which these opcodes are allowed. + + args : list[str], default: ['raw'] + Sequence of argument specifications: + + - 'raw': opcode minus minimum + - 'u1': read one unsigned byte + - 'u4': read four bytes, treat as an unsigned number + - 's4': read four bytes, treat as a signed number + - 'slen': read (opcode - minimum) bytes, treat as signed + - 'slen1': read (opcode - minimum + 1) bytes, treat as signed + - 'ulen1': read (opcode - minimum + 1) bytes, treat as unsigned + - 'olen1': read (opcode - minimum + 1) bytes, treat as unsigned + if under four bytes, signed if four bytes """ def decorate(method): get_args = [_arg_mapping[x] for x in args] @@ -185,6 +238,7 @@ def wrapper(self, byte): class Dvi: """ A reader for a dvi ("device-independent") file, as produced by TeX. + The current implementation can only iterate through pages in order, and does not even attempt to verify the postamble. @@ -212,15 +266,6 @@ def __init__(self, filename, dpi): self.dpi = dpi self.fonts = {} self.state = _dvistate.pre - self.baseline = self._get_baseline(filename) - - def _get_baseline(self, filename): - if dict.__getitem__(rcParams, 'text.latex.preview'): - baseline = Path(filename).with_suffix(".baseline") - if baseline.exists(): - height, depth, width = baseline.read_bytes().split() - return float(depth) - return None def __enter__(self): """Context manager enter method, does nothing.""" @@ -266,8 +311,8 @@ def _output(self): for elt in self.text + self.boxes: if isinstance(elt, Box): x, y, h, w = elt - e = 0 # zero depth - else: # glyph + e = 0 # zero depth + else: # glyph x, y, font, g, w = elt h, e = font._height_depth_of(g) minx = min(minx, x) @@ -290,10 +335,7 @@ def _output(self): # convert from TeX's "scaled points" to dpi units d = self.dpi / (72.27 * 2**16) - if self.baseline is None: - descent = (maxy - maxy_pure) * d - else: - descent = self.baseline + descent = (maxy - maxy_pure) * d text = [Text((x-minx)*d, (maxy-y)*d - descent, f, g, w*d) for (x, y, f, g, w) in self.text] @@ -311,6 +353,7 @@ def _read(self): # Pages appear to start with the sequence # bop (begin of page) # xxx comment + # # if using chemformula # down # push # down @@ -322,17 +365,24 @@ def _read(self): # etc. # (dviasm is useful to explore this structure.) # Thus, we use the vertical position at the first time the stack depth - # reaches 3, while at least three "downs" have been executed, as the + # reaches 3, while at least three "downs" have been executed (excluding + # those popped out (corresponding to the chemformula preamble)), as the # baseline (the "down" count is necessary to handle xcolor). - downs = 0 + down_stack = [0] self._baseline_v = None while True: byte = self.file.read(1)[0] self._dtable[byte](self, byte) - downs += self._dtable[byte].__name__ == "_down" + name = self._dtable[byte].__name__ + if name == "_push": + down_stack.append(down_stack[-1]) + elif name == "_pop": + down_stack.pop() + elif name == "_down": + down_stack[-1] += 1 if (self._baseline_v is None and len(getattr(self, "stack", [])) == 3 - and downs >= 4): + and down_stack[-1] >= 4): self._baseline_v = self.v if byte == 140: # end of page return True @@ -479,13 +529,12 @@ def _fnt_def_real(self, k, c, s, d, a, l): n = self.file.read(a + l) fontname = n[-l:].decode('ascii') tfm = _tfmfile(fontname) - if tfm is None: - raise FileNotFoundError("missing font metrics file: %s" % fontname) if c != 0 and tfm.checksum != 0 and c != tfm.checksum: raise ValueError('tfm checksum mismatch: %s' % n) - - vf = _vffile(fontname) - + try: + vf = _vffile(fontname) + except FileNotFoundError: + vf = None self.fonts[k] = DviFont(scale=s, tfm=tfm, texname=n, vf=vf) @_dispatch(247, state=_dvistate.pre, args=('u1', 'u4', 'u4', 'u4', 'u1')) @@ -604,7 +653,7 @@ def _height_depth_of(self, char): result.append(_mul2012(value, self._scale)) # cmsyXX (symbols font) glyph 0 ("minus") has a nonzero descent # so that TeX aligns equations properly - # (https://tex.stackexchange.com/questions/526103/), + # (https://tex.stackexchange.com/q/526103/) # but we actually care about the rasterization depth to align # the dvipng-generated images. if re.match(br'^cmsy\d+$', self.texname) and char == 0: @@ -724,15 +773,6 @@ def _pre(self, i, x, cs, ds): # cs = checksum, ds = design size -def _fix2comp(num): - """Convert from two's complement to negative.""" - assert 0 <= num < 2**32 - if num & 2**31: - return num - 2**32 - else: - return num - - def _mul2012(num1, num2): """Multiply two numbers in 20.12 fixed point format.""" # Separated into a function because >> has surprising precedence @@ -766,29 +806,23 @@ def __init__(self, filename): _log.debug('opening tfm file %s', filename) with open(filename, 'rb') as file: header1 = file.read(24) - lh, bc, ec, nw, nh, nd = \ - struct.unpack('!6H', header1[2:14]) + lh, bc, ec, nw, nh, nd = struct.unpack('!6H', header1[2:14]) _log.debug('lh=%d, bc=%d, ec=%d, nw=%d, nh=%d, nd=%d', lh, bc, ec, nw, nh, nd) header2 = file.read(4*lh) - self.checksum, self.design_size = \ - struct.unpack('!2I', header2[:8]) + self.checksum, self.design_size = struct.unpack('!2I', header2[:8]) # there is also encoding information etc. char_info = file.read(4*(ec-bc+1)) - widths = file.read(4*nw) - heights = file.read(4*nh) - depths = file.read(4*nd) - + widths = struct.unpack(f'!{nw}i', file.read(4*nw)) + heights = struct.unpack(f'!{nh}i', file.read(4*nh)) + depths = struct.unpack(f'!{nd}i', file.read(4*nd)) self.width, self.height, self.depth = {}, {}, {} - widths, heights, depths = \ - [struct.unpack('!%dI' % (len(x)/4), x) - for x in (widths, heights, depths)] for idx, char in enumerate(range(bc, ec+1)): byte0 = char_info[4*idx] byte1 = char_info[4*idx+1] - self.width[char] = _fix2comp(widths[byte0]) - self.height[char] = _fix2comp(heights[byte1 >> 4]) - self.depth[char] = _fix2comp(depths[byte1 & 0xf]) + self.width[char] = widths[byte0] + self.height[char] = heights[byte1 >> 4] + self.depth[char] = depths[byte1 & 0xf] PsFont = namedtuple('PsFont', 'texname psname effects encoding filename') @@ -838,7 +872,7 @@ class PsfontsMap: {'slant': 0.16700000000000001} >>> entry.filename """ - __slots__ = ('_font', '_filename') + __slots__ = ('_filename', '_unparsed', '_parsed') # Create a filename -> PsfontsMap cache, so that calling # `PsfontsMap(filename)` with the same filename a second time immediately @@ -846,183 +880,140 @@ class PsfontsMap: @lru_cache() def __new__(cls, filename): self = object.__new__(cls) - self._font = {} self._filename = os.fsdecode(filename) + # Some TeX distributions have enormous pdftex.map files which would + # take hundreds of milliseconds to parse, but it is easy enough to just + # store the unparsed lines (keyed by the first word, which is the + # texname) and parse them on-demand. with open(filename, 'rb') as file: - self._parse(file) + self._unparsed = {} + for line in file: + tfmname = line.split(b' ', 1)[0] + self._unparsed.setdefault(tfmname, []).append(line) + self._parsed = {} return self def __getitem__(self, texname): assert isinstance(texname, bytes) + if texname in self._unparsed: + for line in self._unparsed.pop(texname): + if self._parse_and_cache_line(line): + break try: - result = self._font[texname] + return self._parsed[texname] except KeyError: - fmt = ('A PostScript file for the font whose TeX name is "{0}" ' - 'could not be found in the file "{1}". The dviread module ' - 'can only handle fonts that have an associated PostScript ' - 'font file. ' - 'This problem can often be solved by installing ' - 'a suitable PostScript font package in your (TeX) ' - 'package manager.') - msg = fmt.format(texname.decode('ascii'), self._filename) - msg = textwrap.fill(msg, break_on_hyphens=False, - break_long_words=False) - _log.info(msg) - raise - fn, enc = result.filename, result.encoding - if fn is not None and not fn.startswith(b'/'): - fn = find_tex_file(fn) - if enc is not None and not enc.startswith(b'/'): - enc = find_tex_file(result.encoding) - return result._replace(filename=fn, encoding=enc) - - def _parse(self, file): + raise LookupError( + f"An associated PostScript font (required by Matplotlib) " + f"could not be found for TeX font {texname.decode('ascii')!r} " + f"in {self._filename!r}; this problem can often be solved by " + f"installing a suitable PostScript font package in your TeX " + f"package manager") from None + + def _parse_and_cache_line(self, line): """ - Parse the font mapping file. - - The format is, AFAIK: texname fontname [effects and filenames] - Effects are PostScript snippets like ".177 SlantFont", - filenames begin with one or two less-than signs. A filename - ending in enc is an encoding file, other filenames are font - files. This can be overridden with a left bracket: <[foobar - indicates an encoding file named foobar. - - There is some difference between [^"]+ )" | # quoted encoding marked by [ - "< (?P [^"]+.enc)" | # quoted encoding, ends in .enc - "< [^"]+ )" | # quoted font file name - " (?P [^"]+ )" | # quoted effects or font name - <\[ (?P \S+ ) | # encoding marked by [ - < (?P \S+ .enc) | # encoding, ends in .enc - < \S+ ) | # font file name - (?P \S+ ) # effects or font name - )''') - effects_re = re.compile( - br'''(?x) (?P -?[0-9]*(?:\.[0-9]+)) \s* SlantFont - | (?P-?[0-9]*(?:\.[0-9]+)) \s* ExtendFont''') - - lines = (line.strip() - for line in file - if not empty_re.match(line)) - for line in lines: - effects, encoding, filename = b'', None, None - words = word_re.finditer(line) - - # The named groups are mutually exclusive and are - # referenced below at an estimated order of probability of - # occurrence based on looking at my copy of pdftex.map. - # The font names are probably unquoted: - w = next(words) - texname = w.group('eff2') or w.group('eff1') - w = next(words) - psname = w.group('eff2') or w.group('eff1') - - for w in words: - # Any effects are almost always quoted: - eff = w.group('eff1') or w.group('eff2') - if eff: - effects = eff - continue - # Encoding files usually have the .enc suffix - # and almost never need quoting: - enc = (w.group('enc4') or w.group('enc3') or - w.group('enc2') or w.group('enc1')) - if enc: - if encoding is not None: - _log.debug('Multiple encodings for %s = %s', - texname, psname) - encoding = enc - continue - # File names are probably unquoted: - filename = w.group('file2') or w.group('file1') - - effects_dict = {} - for match in effects_re.finditer(effects): - slant = match.group('slant') - if slant: - effects_dict['slant'] = float(slant) - else: - effects_dict['extend'] = float(match.group('extend')) - - self._font[texname] = PsFont( - texname=texname, psname=psname, effects=effects_dict, - encoding=encoding, filename=filename) + # https://tex.stackexchange.com/q/10826/ + + if not line or line.startswith((b" ", b"%", b"*", b";", b"#")): + return + tfmname = basename = special = encodingfile = fontfile = None + is_subsetted = is_t1 = is_truetype = False + matches = re.finditer(br'"([^"]*)(?:"|$)|(\S+)', line) + for match in matches: + quoted, unquoted = match.groups() + if unquoted: + if unquoted.startswith(b"<<"): # font + fontfile = unquoted[2:] + elif unquoted.startswith(b"<["): # encoding + encodingfile = unquoted[2:] + elif unquoted.startswith(b"<"): # font or encoding + word = ( + # foo + unquoted[1:] + # < by itself => read the next word + or next(filter(None, next(matches).groups()))) + if word.endswith(b".enc"): + encodingfile = word + else: + fontfile = word + is_subsetted = True + elif tfmname is None: + tfmname = unquoted + elif basename is None: + basename = unquoted + elif quoted: + special = quoted + effects = {} + if special: + words = reversed(special.split()) + for word in words: + if word == b"SlantFont": + effects["slant"] = float(next(words)) + elif word == b"ExtendFont": + effects["extend"] = float(next(words)) + + # Verify some properties of the line that would cause it to be ignored + # otherwise. + if fontfile is not None: + if fontfile.endswith((b".ttf", b".ttc")): + is_truetype = True + elif not fontfile.endswith(b".otf"): + is_t1 = True + elif basename is not None: + is_t1 = True + if is_truetype and is_subsetted and encodingfile is None: + return + if not is_t1 and ("slant" in effects or "extend" in effects): + return + if abs(effects.get("slant", 0)) > 1: + return + if abs(effects.get("extend", 0)) > 2: + return + + if basename is None: + basename = tfmname + if encodingfile is not None: + encodingfile = _find_tex_file(encodingfile) + if fontfile is not None: + fontfile = _find_tex_file(fontfile) + self._parsed[tfmname] = PsFont( + texname=tfmname, psname=basename, effects=effects, + encoding=encodingfile, filename=fontfile) + return True -@_api.deprecated("3.3") -class Encoding: +def _parse_enc(path): r""" 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):: - - for name in Encoding(filename): - whatever(name) - - Parameters - ---------- - filename : str or path-like - - Attributes - ---------- - encoding : list - List of character names - """ - __slots__ = ('encoding',) - - def __init__(self, filename): - with open(filename, 'rb') as file: - _log.debug('Parsing TeX encoding %s', filename) - self.encoding = self._parse(file) - _log.debug('Result: %s', self.encoding) - - def __iter__(self): - yield from self.encoding - - @staticmethod - def _parse(file): - lines = (line.split(b'%', 1)[0].strip() for line in file) - data = b''.join(lines) - beginning = data.find(b'[') - if beginning < 0: - raise ValueError("Cannot locate beginning of encoding in {}" - .format(file)) - data = data[beginning:] - end = data.find(b']') - if end < 0: - raise ValueError("Cannot locate end of encoding in {}" - .format(file)) - data = data[:end] - return re.findall(br'/([^][{}<>\s]+)', data) - - -# Note: this function should ultimately replace the Encoding class, which -# appears to be mostly broken: because it uses b''.join(), there is no -# whitespace left between glyph names (only slashes) so the final re.findall -# returns a single string with all glyph names. However this does not appear -# to bother backend_pdf, so that needs to be investigated more. (The fixed -# version below is necessary for textpath/backend_svg, though.) -def _parse_enc(path): - r""" - Parses a \*.enc file referenced from a psfonts.map style file. - The format this class understands is a very limited subset of PostScript. + The format supported by this function is a tiny subset of PostScript. Parameters ---------- - path : os.PathLike + path : `os.PathLike` Returns ------- @@ -1040,63 +1031,105 @@ def _parse_enc(path): "Failed to parse {} as Postscript encoding".format(path)) +class _LuatexKpsewhich: + @lru_cache() # A singleton. + def __new__(cls): + self = object.__new__(cls) + self._proc = self._new_proc() + return self + + def _new_proc(self): + return subprocess.Popen( + ["luatex", "--luaonly", + str(cbook._get_data_path("kpsewhich.lua"))], + stdin=subprocess.PIPE, stdout=subprocess.PIPE) + + def search(self, filename): + if self._proc.poll() is not None: # Dead, restart it. + self._proc = self._new_proc() + self._proc.stdin.write(os.fsencode(filename) + b"\n") + self._proc.stdin.flush() + out = self._proc.stdout.readline().rstrip() + return None if out == b"nil" else os.fsdecode(out) + + @lru_cache() -def find_tex_file(filename, format=None): +def _find_tex_file(filename): """ - Find a file in the texmf tree. + Find a file in the texmf tree using kpathsea_. - Calls :program:`kpsewhich` which is an interface to the kpathsea - library [1]_. Most existing TeX distributions on Unix-like systems use - kpathsea. It is also available as part of MikTeX, a popular - distribution on Windows. + The kpathsea library, provided by most existing TeX distributions, both + on Unix-like systems and on Windows (MikTeX), is invoked via a long-lived + luatex process if luatex is installed, or via kpsewhich otherwise. - *If the file is not found, an empty string is returned*. + .. _kpathsea: https://www.tug.org/kpathsea/ Parameters ---------- filename : str or path-like - format : str or bytes - Used as the value of the ``--format`` option to :program:`kpsewhich`. - Could be e.g. 'tfm' or 'vf' to limit the search to that type of files. - References - ---------- - .. [1] `Kpathsea documentation `_ - The library that :program:`kpsewhich` is part of. + Raises + ------ + FileNotFoundError + If the file is not found. """ # we expect these to always be ascii encoded, but use utf-8 # out of caution if isinstance(filename, bytes): filename = filename.decode('utf-8', errors='replace') - if isinstance(format, bytes): - format = format.decode('utf-8', errors='replace') - - if os.name == 'nt': - # On Windows only, kpathsea can use utf-8 for cmd args and output. - # The `command_line_encoding` environment variable is set to force it - # to always use utf-8 encoding. See Matplotlib issue #11848. - kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'}, - 'encoding': 'utf-8'} - else: # On POSIX, run through the equivalent of os.fsdecode(). - kwargs = {'encoding': sys.getfilesystemencoding(), - 'errors': 'surrogatescape'} - - cmd = ['kpsewhich'] - if format is not None: - cmd += ['--format=' + format] - cmd += [filename] + try: - result = cbook._check_and_log_subprocess(cmd, _log, **kwargs) - except (FileNotFoundError, RuntimeError): - return '' - return result.rstrip('\n') + lk = _LuatexKpsewhich() + except FileNotFoundError: + lk = None # Fallback to directly calling kpsewhich, as below. + + if lk: + path = lk.search(filename) + else: + if os.name == 'nt': + # On Windows only, kpathsea can use utf-8 for cmd args and output. + # The `command_line_encoding` environment variable is set to force + # it to always use utf-8 encoding. See Matplotlib issue #11848. + kwargs = {'env': {**os.environ, 'command_line_encoding': 'utf-8'}, + 'encoding': 'utf-8'} + else: # On POSIX, run through the equivalent of os.fsdecode(). + kwargs = {'encoding': sys.getfilesystemencoding(), + 'errors': 'surrogateescape'} + + try: + path = (cbook._check_and_log_subprocess(['kpsewhich', filename], + _log, **kwargs) + .rstrip('\n')) + except (FileNotFoundError, RuntimeError): + path = None + + if path: + return path + else: + raise FileNotFoundError( + f"Matplotlib's TeX implementation searched for a file named " + f"{filename!r} in your texmf tree, but could not find it") + + +# After the deprecation period elapses, delete this shim and rename +# _find_tex_file to find_tex_file everywhere. +def find_tex_file(filename): + try: + return _find_tex_file(filename) + except FileNotFoundError as exc: + _api.warn_deprecated( + "3.6", message=f"{exc.args[0]}; in the future, this will raise a " + f"FileNotFoundError.") + return "" + + +find_tex_file.__doc__ = _find_tex_file.__doc__ @lru_cache() def _fontfile(cls, suffix, texname): - filename = find_tex_file(texname + suffix) - return cls(filename) if filename else None + return cls(_find_tex_file(texname + suffix)) _tfmfile = partial(_fontfile, Tfm, ".tfm") @@ -1112,16 +1145,21 @@ def _fontfile(cls, suffix, texname): parser.add_argument("dpi", nargs="?", type=float, default=None) args = parser.parse_args() with Dvi(args.filename, args.dpi) as dvi: - fontmap = PsfontsMap(find_tex_file('pdftex.map')) + fontmap = PsfontsMap(_find_tex_file('pdftex.map')) for page in dvi: - print('=== new page ===') + print(f"=== new page === " + f"(w: {page.width}, h: {page.height}, d: {page.descent})") for font, group in itertools.groupby( page.text, lambda text: text.font): - print('font', font.texname, 'scaled', font._scale / 2 ** 20) + print(f"font: {font.texname.decode('latin-1')!r}\t" + f"scale: {font._scale / 2 ** 20}") + print("x", "y", "glyph", "chr", "w", "(glyphs)", sep="\t") for text in group: print(text.x, text.y, text.glyph, chr(text.glyph) if chr(text.glyph).isprintable() else ".", - text.width) - for x, y, w, h in page.boxes: - print(x, y, 'BOX', w, h) + text.width, sep="\t") + if page.boxes: + print("x", "y", "h", "w", "", "(boxes)", sep="\t") + for box in page.boxes: + print(box.x, box.y, box.height, box.width, sep="\t") diff --git a/lib/matplotlib/figure.py b/lib/matplotlib/figure.py index 9e4e07b606f4..c6df929e04ee 100644 --- a/lib/matplotlib/figure.py +++ b/lib/matplotlib/figure.py @@ -12,37 +12,55 @@ `SubplotParams` Control the default spacing between subplots. + +Figures are typically created using pyplot methods `~.pyplot.figure`, +`~.pyplot.subplots`, and `~.pyplot.subplot_mosaic`. + +.. plot:: + :include-source: + + fig, ax = plt.subplots(figsize=(2, 2), facecolor='lightskyblue', + layout='constrained') + fig.suptitle('Figure') + ax.set_title('Axes', loc='left', fontstyle='oblique', fontsize='medium') + +Some situations call for directly instantiating a `~.figure.Figure` class, +usually inside an application of some sort (see :ref:`user_interfaces` for a +list of examples) . More information about Figures can be found at +:ref:`figure_explanation`. + """ +from contextlib import ExitStack import inspect +import itertools import logging from numbers import Integral import numpy as np import matplotlib as mpl -from matplotlib import docstring, projections -from matplotlib import __version__ as _mpl_version - -import matplotlib.artist as martist +from matplotlib import _blocking_input, backend_bases, _docstring, projections from matplotlib.artist import ( Artist, allow_rasterization, _finalize_rasterization) from matplotlib.backend_bases import ( - FigureCanvasBase, NonGuiException, MouseButton) + DrawEvent, FigureCanvasBase, NonGuiException, MouseButton, _get_renderer) import matplotlib._api as _api import matplotlib.cbook as cbook import matplotlib.colorbar as cbar import matplotlib.image as mimage -from matplotlib.axes import Axes, SubplotBase, subplot_class_factory -from matplotlib.blocking_input import BlockingMouseInput, BlockingKeyMouseInput +from matplotlib.axes import Axes from matplotlib.gridspec import GridSpec +from matplotlib.layout_engine import ( + ConstrainedLayoutEngine, TightLayoutEngine, LayoutEngine, + PlaceHolderLayoutEngine +) import matplotlib.legend as mlegend from matplotlib.patches import Rectangle from matplotlib.text import Text from matplotlib.transforms import (Affine2D, Bbox, BboxTransformTo, TransformedBbox) -import matplotlib._layoutgrid as layoutgrid _log = logging.getLogger(__name__) @@ -52,75 +70,48 @@ def _stale_figure_callback(self, val): self.figure.stale = val -class _AxesStack(cbook.Stack): +class _AxesStack: """ - Specialization of Stack, to handle all tracking of Axes in a Figure. + Helper class to track axes in a figure. - This stack stores ``ind, axes`` pairs, where ``ind`` is a serial index - tracking the order in which axes were added. - - AxesStack is a callable; calling it returns the current axes. + Axes are tracked both in the order in which they have been added + (``self._axes`` insertion/iteration order) and in the separate "gca" stack + (which is the index to which they map in the ``self._axes`` dict). """ def __init__(self): - super().__init__() - self._ind = 0 + self._axes = {} # Mapping of axes to "gca" order. + self._counter = itertools.count() def as_list(self): - """ - Return a list of the Axes instances that have been added to the figure. - """ - return [a for i, a in sorted(self._elements)] - - def _entry_from_axes(self, e): - return next(((ind, a) for ind, a in self._elements if a == e), None) + """List the axes that have been added to the figure.""" + return [*self._axes] # This relies on dict preserving order. def remove(self, a): """Remove the axes from the stack.""" - super().remove(self._entry_from_axes(a)) + self._axes.pop(a) def bubble(self, a): - """ - Move the given axes, which must already exist in the stack, to the top. - """ - return super().bubble(self._entry_from_axes(a)) + """Move an axes, which must already exist in the stack, to the top.""" + if a not in self._axes: + raise ValueError("Axes has not been added yet") + self._axes[a] = next(self._counter) def add(self, a): - """ - Add Axes *a* to the stack. - - If *a* is already on the stack, don't add it again. - """ - # All the error checking may be unnecessary; but this method - # is called so seldom that the overhead is negligible. - _api.check_isinstance(Axes, a=a) - - if a in self: - return + """Add an axes to the stack, ignoring it if already present.""" + if a not in self._axes: + self._axes[a] = next(self._counter) - self._ind += 1 - super().push((self._ind, a)) - - def __call__(self): - """ - Return the active axes. - - If no axes exists on the stack, then returns None. - """ - if not len(self._elements): - return None - else: - index, axes = self._elements[self._pos] - return axes - - def __contains__(self, a): - return a in self.as_list() + def current(self): + """Return the active axes, or None if the stack is empty.""" + return max(self._axes, key=self._axes.__getitem__, default=None) class SubplotParams: """ A class to hold the parameters for a subplot. """ + def __init__(self, left=None, bottom=None, right=None, top=None, wspace=None, hspace=None): """ @@ -147,7 +138,6 @@ def __init__(self, left=None, bottom=None, right=None, top=None, The height of the padding between subplots, as a fraction of the average Axes height. """ - self.validate = True for key in ["left", "bottom", "right", "top", "wspace", "hspace"]: setattr(self, key, mpl.rcParams[f"figure.subplot.{key}"]) self.update(left, bottom, right, top, wspace, hspace) @@ -157,13 +147,12 @@ def update(self, left=None, bottom=None, right=None, top=None, """ Update the dimensions of the passed parameters. *None* means unchanged. """ - if self.validate: - if ((left if left is not None else self.left) - >= (right if right is not None else self.right)): - raise ValueError('left cannot be >= right') - if ((bottom if bottom is not None else self.bottom) - >= (top if top is not None else self.top)): - raise ValueError('bottom cannot be >= top') + if ((left if left is not None else self.left) + >= (right if right is not None else self.right)): + raise ValueError('left cannot be >= right') + if ((bottom if bottom is not None else self.bottom) + >= (top if top is not None else self.top)): + raise ValueError('bottom cannot be >= top') if left is not None: self.left = left if right is not None: @@ -180,13 +169,13 @@ def update(self, left=None, bottom=None, right=None, top=None, class FigureBase(Artist): """ - Base class for `.figure.Figure` and `.figure.SubFigure` containing the - methods that add artists to the figure or subfigure, create Axes, etc. + Base class for `.Figure` and `.SubFigure` containing the methods that add + artists to the figure or subfigure, create Axes, etc. """ - def __init__(self): + def __init__(self, **kwargs): super().__init__() # remove the non-figure artist _axes property - # as it makes no sense for a figure to be _in_ an axes + # as it makes no sense for a figure to be _in_ an Axes # this is used by the property methods in the artist base class # which are over-ridden in this class del self._axes @@ -195,18 +184,13 @@ def __init__(self): self._supxlabel = None self._supylabel = None - # constrained_layout: - self._layoutgrid = None - # groupers to keep track of x and y labels we want to align. # see self.align_xlabels and self.align_ylabels and # axis._get_tick_boxes_siblings self._align_label_groups = {"x": cbook.Grouper(), "y": cbook.Grouper()} self.figure = self - # list of child gridspecs for this figure - self._gridspecs = [] - self._localaxes = _AxesStack() # track all axes and current axes + self._localaxes = [] # track all axes self.artists = [] self.lines = [] self.patches = [] @@ -216,6 +200,7 @@ def __init__(self): self.subfigs = [] self.stale = True self.suppressComposite = None + self.set(**kwargs) def _get_draw_artists(self, renderer): """Also runs apply_aspect""" @@ -231,22 +216,15 @@ def _get_draw_artists(self, renderer): artists = sorted( (artist for artist in artists if not artist.get_animated()), key=lambda artist: artist.get_zorder()) - for ax in self._localaxes.as_list(): + for ax in self._localaxes: locator = ax.get_axes_locator() - if locator: - pos = locator(ax, renderer) - ax.apply_aspect(pos) - else: - ax.apply_aspect() + ax.apply_aspect(locator(ax, renderer) if locator else None) for child in ax.get_children(): if hasattr(child, 'apply_aspect'): locator = child.get_axes_locator() - if locator: - pos = locator(child, renderer) - child.apply_aspect(pos) - else: - child.apply_aspect() + child.apply_aspect( + locator(child, renderer) if locator else None) return artists def autofmt_xdate( @@ -270,12 +248,8 @@ def autofmt_xdate( which : {'major', 'minor', 'both'}, default: 'major' Selects which ticklabels to rotate. """ - if which is None: - _api.warn_deprecated( - "3.3", message="Support for passing which=None to mean " - "which='major' is deprecated since %(since)s and will be " - "removed %(removal)s.") - allsubplots = all(hasattr(ax, 'get_subplotspec') for ax in self.axes) + _api.check_in_list(['major', 'minor', 'both'], which=which) + allsubplots = all(ax.get_subplotspec() for ax in self.axes) if len(self.axes) == 1: for label in self.axes[0].get_xticklabels(which=which): label.set_ha(ha) @@ -300,7 +274,7 @@ def get_children(self): """Get a list of artists contained in the figure.""" return [self.patch, *self.artists, - *self._localaxes.as_list(), + *self._localaxes, *self.lines, *self.patches, *self.texts, @@ -322,10 +296,10 @@ def contains(self, mouseevent): inside = self.bbox.contains(mouseevent.x, mouseevent.y) return inside, {} - def get_window_extent(self, *args, **kwargs): - """ - Return the figure bounding box in display space. Arguments are ignored. - """ + @_api.delete_parameter("3.6", "args") + @_api.delete_parameter("3.6", "kwargs") + def get_window_extent(self, renderer=None, *args, **kwargs): + # docstring inherited return self.bbox def _suplabels(self, t, info, **kwargs): @@ -345,10 +319,10 @@ def _suplabels(self, t, info, **kwargs): verticalalignment, va : {'top', 'center', 'bottom', 'baseline'}, \ default: %(va)s The vertical alignment of the text relative to (*x*, *y*). - fontsize, size : default: :rc:`figure.titlesize` + fontsize, size : default: :rc:`figure.%(rc)ssize` The font size of the text. See `.Text.set_size` for possible values. - fontweight, weight : default: :rc:`figure.titleweight` + fontweight, weight : default: :rc:`figure.%(rc)sweight` The font weight of the text. See `.Text.set_weight` for possible values. @@ -362,8 +336,8 @@ def _suplabels(self, t, info, **kwargs): fontproperties : None or dict, optional A dict of font properties. If *fontproperties* is given the default values for font size and weight are taken from the - `.FontProperties` defaults. :rc:`figure.titlesize` and - :rc:`figure.titleweight` are ignored in this case. + `.FontProperties` defaults. :rc:`figure.%(rc)ssize` and + :rc:`figure.%(rc)sweight` are ignored in this case. **kwargs Additional kwargs are `matplotlib.text.Text` properties. @@ -391,9 +365,9 @@ def _suplabels(self, t, info, **kwargs): if 'fontproperties' not in kwargs: if 'fontsize' not in kwargs and 'size' not in kwargs: - kwargs['size'] = mpl.rcParams['figure.titlesize'] + kwargs['size'] = mpl.rcParams[info['size']] if 'fontweight' not in kwargs and 'weight' not in kwargs: - kwargs['weight'] = mpl.rcParams['figure.titleweight'] + kwargs['weight'] = mpl.rcParams[info['weight']] sup = self.text(x, y, t, **kwargs) if suplab is not None: @@ -408,32 +382,35 @@ def _suplabels(self, t, info, **kwargs): self.stale = True return suplab - @docstring.Substitution(x0=0.5, y0=0.98, name='suptitle', ha='center', - va='top') - @docstring.copy(_suplabels) + @_docstring.Substitution(x0=0.5, y0=0.98, name='suptitle', ha='center', + va='top', rc='title') + @_docstring.copy(_suplabels) def suptitle(self, t, **kwargs): # docstring from _suplabels... info = {'name': '_suptitle', 'x0': 0.5, 'y0': 0.98, - 'ha': 'center', 'va': 'top', 'rotation': 0} + 'ha': 'center', 'va': 'top', 'rotation': 0, + 'size': 'figure.titlesize', 'weight': 'figure.titleweight'} return self._suplabels(t, info, **kwargs) - @docstring.Substitution(x0=0.5, y0=0.01, name='supxlabel', ha='center', - va='bottom') - @docstring.copy(_suplabels) + @_docstring.Substitution(x0=0.5, y0=0.01, name='supxlabel', ha='center', + va='bottom', rc='label') + @_docstring.copy(_suplabels) def supxlabel(self, t, **kwargs): # docstring from _suplabels... info = {'name': '_supxlabel', 'x0': 0.5, 'y0': 0.01, - 'ha': 'center', 'va': 'bottom', 'rotation': 0} + 'ha': 'center', 'va': 'bottom', 'rotation': 0, + 'size': 'figure.labelsize', 'weight': 'figure.labelweight'} return self._suplabels(t, info, **kwargs) - @docstring.Substitution(x0=0.02, y0=0.5, name='supylabel', ha='left', - va='center') - @docstring.copy(_suplabels) + @_docstring.Substitution(x0=0.02, y0=0.5, name='supylabel', ha='left', + va='center', rc='label') + @_docstring.copy(_suplabels) def supylabel(self, t, **kwargs): # docstring from _suplabels... info = {'name': '_supylabel', 'x0': 0.02, 'y0': 0.5, 'ha': 'left', 'va': 'center', 'rotation': 'vertical', - 'rotation_mode': 'anchor'} + 'rotation_mode': 'anchor', 'size': 'figure.labelsize', + 'weight': 'figure.labelweight'} return self._suplabels(t, info, **kwargs) def get_edgecolor(self): @@ -507,9 +484,9 @@ def add_artist(self, artist, clip=False): """ Add an `.Artist` to the figure. - Usually artists are added to Axes objects using `.Axes.add_artist`; - this method can be used in the rare cases where one needs to add - artists directly to the figure instead. + Usually artists are added to `~.axes.Axes` objects using + `.Axes.add_artist`; this method can be used in the rare cases where + one needs to add artists directly to the figure instead. Parameters ---------- @@ -538,10 +515,10 @@ def add_artist(self, artist, clip=False): self.stale = True return artist - @docstring.dedent_interpd + @_docstring.dedent_interpd def add_axes(self, *args, **kwargs): """ - Add an Axes to the figure. + Add an `~.axes.Axes` to the figure. Call signatures:: @@ -550,9 +527,10 @@ def add_axes(self, *args, **kwargs): Parameters ---------- - rect : sequence of float - The dimensions [left, bottom, width, height] of the new Axes. All - quantities are in fractions of figure width and height. + rect : tuple (left, bottom, width, height) + The dimensions (left, bottom, width, height) of the new + `~.axes.Axes`. All quantities are in fractions of figure width and + height. projection : {None, 'aitoff', 'hammer', 'lambert', 'mollweide', \ 'polar', 'rectilinear', str}, optional @@ -593,7 +571,7 @@ def add_axes(self, *args, **kwargs): arguments if another projection is used, see the actual Axes class. - %(Axes_kwdoc)s + %(Axes:kwdoc)s Notes ----- @@ -624,12 +602,8 @@ def add_axes(self, *args, **kwargs): """ if not len(args) and 'rect' not in kwargs: - _api.warn_deprecated( - "3.3", - message="Calling add_axes() without argument is " - "deprecated since %(since)s and will be removed %(removal)s. " - "You may want to use add_subplot() instead.") - return + raise TypeError( + "add_axes() missing 1 required positional argument: 'rect'") elif 'rect' in kwargs: if len(args): raise TypeError( @@ -655,7 +629,7 @@ def add_axes(self, *args, **kwargs): key = (projection_class, pkw) return self._add_axes_internal(a, key) - @docstring.dedent_interpd + @_docstring.dedent_interpd def add_subplot(self, *args, **kwargs): """ Add an `~.axes.Axes` to the figure as part of a subplot arrangement. @@ -714,13 +688,11 @@ def add_subplot(self, *args, **kwargs): Returns ------- - `.axes.SubplotBase`, or another subclass of `~.axes.Axes` + `~.axes.Axes` - The Axes of the subplot. The returned Axes base class depends on - the projection used. It is `~.axes.Axes` if rectilinear projection - is used and `.projections.polar.PolarAxes` if polar projection - is used. The returned Axes is then a subplot subclass of the - base class. + The Axes of the subplot. The returned Axes can actually be an + instance of a subclass, such as `.projections.polar.PolarAxes` for + polar projections. Other Parameters ---------------- @@ -731,7 +703,7 @@ def add_subplot(self, *args, **kwargs): the following table but there might also be other keyword arguments if another projection is used. - %(Axes_kwdoc)s + %(Axes:kwdoc)s See Also -------- @@ -761,14 +733,15 @@ def add_subplot(self, *args, **kwargs): if 'figure' in kwargs: # Axes itself allows for a 'figure' kwarg, but since we want to # bind the created Axes to self, it is not allowed here. - raise TypeError( - "add_subplot() got an unexpected keyword argument 'figure'") + raise _api.kwarg_error("add_subplot", "figure") - if len(args) == 1 and isinstance(args[0], SubplotBase): + if (len(args) == 1 + and isinstance(args[0], mpl.axes._base._AxesBase) + and args[0].get_subplotspec()): ax = args[0] key = ax._projection_init if ax.get_figure() is not self: - raise ValueError("The Subplot must have been created in " + raise ValueError("The Axes must have been created in " "the present figure") else: if not args: @@ -781,14 +754,15 @@ def add_subplot(self, *args, **kwargs): args = tuple(map(int, str(args[0]))) projection_class, pkw = self._process_projection_requirements( *args, **kwargs) - ax = subplot_class_factory(projection_class)(self, *args, **pkw) + ax = projection_class(self, *args, **pkw) key = (projection_class, pkw) return self._add_axes_internal(ax, key) def _add_axes_internal(self, ax, key): """Private helper for `add_axes` and `add_subplot`.""" self._axstack.add(ax) - self._localaxes.add(ax) + if ax not in self._localaxes: + self._localaxes.append(ax) self.sca(ax) ax._remove_method = self.delaxes # this is to support plt.subplot's re-selection logic @@ -797,9 +771,9 @@ def _add_axes_internal(self, ax, key): ax.stale_callback = _stale_figure_callback return ax - @_api.make_keyword_only("3.3", "sharex") - def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, - squeeze=True, subplot_kw=None, gridspec_kw=None): + def subplots(self, nrows=1, ncols=1, *, sharex=False, sharey=False, + squeeze=True, width_ratios=None, height_ratios=None, + subplot_kw=None, gridspec_kw=None): """ Add a set of subplots to this figure. @@ -812,8 +786,7 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, Number of rows/columns of the subplot grid. sharex, sharey : bool or {'none', 'all', 'row', 'col'}, default: False - Controls sharing of properties among x (*sharex*) or y (*sharey*) - axes: + Controls sharing of x-axis (*sharex*) or y-axis (*sharey*): - True or 'all': x- or y-axis will be shared among all subplots. - False or 'none': each subplot x- or y-axis will be independent. @@ -843,6 +816,18 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, is always a 2D array containing Axes instances, even if it ends up being 1x1. + width_ratios : array-like of length *ncols*, optional + Defines the relative widths of the columns. Each column gets a + relative width of ``width_ratios[i] / sum(width_ratios)``. + If not given, all columns will have the same width. Equivalent + to ``gridspec_kw={'width_ratios': [...]}``. + + height_ratios : array-like of length *nrows*, optional + Defines the relative heights of the rows. Each row gets a + relative height of ``height_ratios[i] / sum(height_ratios)``. + If not given, all rows will have the same height. Equivalent + to ``gridspec_kw={'height_ratios': [...]}``. + subplot_kw : dict, optional Dict with keywords passed to the `.Figure.add_subplot` call used to create each subplot. @@ -893,20 +878,30 @@ def subplots(self, nrows=1, ncols=1, sharex=False, sharey=False, axes[0, 0].plot(x, y) axes[1, 1].scatter(x, y) - # Share a X axis with each column of subplots + # Share an X-axis with each column of subplots fig.subplots(2, 2, sharex='col') - # Share a Y axis with each row of subplots + # Share a Y-axis with each row of subplots fig.subplots(2, 2, sharey='row') - # Share both X and Y axes with all subplots + # Share both X- and Y-axes with all subplots fig.subplots(2, 2, sharex='all', sharey='all') # Note that this is the same as fig.subplots(2, 2, sharex=True, sharey=True) """ - if gridspec_kw is None: - gridspec_kw = {} + gridspec_kw = dict(gridspec_kw or {}) + if height_ratios is not None: + if 'height_ratios' in gridspec_kw: + raise ValueError("'height_ratios' must not be defined both as " + "parameter and as key in 'gridspec_kw'") + gridspec_kw['height_ratios'] = height_ratios + if width_ratios is not None: + if 'width_ratios' in gridspec_kw: + raise ValueError("'width_ratios' must not be defined both as " + "parameter and as key in 'gridspec_kw'") + gridspec_kw['width_ratios'] = width_ratios + gs = self.add_gridspec(nrows, ncols, figure=self, **gridspec_kw) axs = gs.subplots(sharex=sharex, sharey=sharey, squeeze=squeeze, subplot_kw=subplot_kw) @@ -921,33 +916,10 @@ def _reset_locators_and_formatters(axis): # Set the formatters and locators to be associated with axis # (where previously they may have been associated with another # Axis instance) - # - # Because set_major_formatter() etc. force isDefault_* to be False, - # we have to manually check if the original formatter was a - # default and manually set isDefault_* if that was the case. - majfmt = axis.get_major_formatter() - isDefault = majfmt.axis.isDefault_majfmt - axis.set_major_formatter(majfmt) - if isDefault: - majfmt.axis.isDefault_majfmt = True - - majloc = axis.get_major_locator() - isDefault = majloc.axis.isDefault_majloc - axis.set_major_locator(majloc) - if isDefault: - majloc.axis.isDefault_majloc = True - - minfmt = axis.get_minor_formatter() - isDefault = majloc.axis.isDefault_minfmt - axis.set_minor_formatter(minfmt) - if isDefault: - minfmt.axis.isDefault_minfmt = True - - minloc = axis.get_minor_locator() - isDefault = majloc.axis.isDefault_minloc - axis.set_minor_locator(minloc) - if isDefault: - minloc.axis.isDefault_minloc = True + axis.get_major_formatter().set_axis(axis) + axis.get_major_locator().set_axis(axis) + axis.get_minor_formatter().set_axis(axis) + axis.get_minor_locator().set_axis(axis) def _break_share_link(ax, grouper): siblings = grouper.get_siblings(ax) @@ -963,19 +935,74 @@ def _break_share_link(ax, grouper): self.stale = True self._localaxes.remove(ax) - last_ax = _break_share_link(ax, ax._shared_y_axes) - if last_ax is not None: - _reset_locators_and_formatters(last_ax.yaxis) + # Break link between any shared axes + for name in ax._axis_names: + last_ax = _break_share_link(ax, ax._shared_axes[name]) + if last_ax is not None: + _reset_locators_and_formatters(getattr(last_ax, f"{name}axis")) + + # Break link between any twinned axes + _break_share_link(ax, ax._twinned_axes) - last_ax = _break_share_link(ax, ax._shared_x_axes) - if last_ax is not None: - _reset_locators_and_formatters(last_ax.xaxis) + def clear(self, keep_observers=False): + """ + Clear the figure. + + Parameters + ---------- + keep_observers : bool, default: False + Set *keep_observers* to True if, for example, + a gui widget is tracking the Axes in the figure. + """ + self.suppressComposite = None + + # first clear the axes in any subfigures + for subfig in self.subfigs: + subfig.clear(keep_observers=keep_observers) + self.subfigs = [] + + for ax in tuple(self.axes): # Iterate over the copy. + ax.clear() + self.delaxes(ax) # Remove ax from self._axstack. - # Note: in the docstring below, the newlines in the examples after the - # calls to legend() allow replacing it with figlegend() to generate the - # docstring of pyplot.figlegend. + self.artists = [] + self.lines = [] + self.patches = [] + self.texts = [] + self.images = [] + self.legends = [] + if not keep_observers: + self._axobservers = cbook.CallbackRegistry() + self._suptitle = None + self._supxlabel = None + self._supylabel = None - @docstring.dedent_interpd + self.stale = True + + # synonym for `clear`. + def clf(self, keep_observers=False): + """ + [*Discouraged*] Alias for the `clear()` method. + + .. admonition:: Discouraged + + The use of ``clf()`` is discouraged. Use ``clear()`` instead. + + Parameters + ---------- + keep_observers : bool, default: False + Set *keep_observers* to True if, for example, + a gui widget is tracking the Axes in the figure. + """ + return self.clear(keep_observers=keep_observers) + + # Note: the docstring below is modified with replace for the pyplot + # version of this function because the method name differs (plt.figlegend) + # the replacements are: + # " legend(" -> " figlegend(" for the signatures + # "fig.legend(" -> "plt.figlegend" for the code examples + # "ax.plot" -> "plt.plot" for consistency in using pyplot when able + @_docstring.dedent_interpd def legend(self, *args, **kwargs): """ Place a legend on the figure. @@ -983,10 +1010,11 @@ def legend(self, *args, **kwargs): Call signatures:: legend() - legend(labels) legend(handles, labels) + legend(handles=handles) + legend(labels) - The call signatures correspond to these three different ways to use + The call signatures correspond to the following different ways to use this method: **1. Automatic detection of elements to be shown in the legend** @@ -1014,28 +1042,41 @@ def legend(self, *args, **kwargs): no legend being drawn. - **2. Labeling existing plot elements** + **2. Explicitly listing the artists and labels in the legend** + + For full control of which artists have a legend entry, it is possible + to pass an iterable of legend artists followed by an iterable of + legend labels respectively:: + + fig.legend([line1, line2, line3], ['label1', 'label2', 'label3']) + + + **3. Explicitly listing the artists in the legend** + + This is similar to 2, but the labels are taken from the artists' + label properties. Example:: + + line1, = ax1.plot([1, 2, 3], label='label1') + line2, = ax2.plot([1, 2, 3], label='label2') + fig.legend(handles=[line1, line2]) + + + **4. Labeling existing plot elements** + + .. admonition:: Discouraged + + This call signature is discouraged, because the relation between + plot elements and labels is only implicit by their order and can + easily be mixed up. To make a legend for all artists on all Axes, call this function with an iterable of strings, one for each legend item. For example:: - fig, (ax1, ax2) = plt.subplots(1, 2) + fig, (ax1, ax2) = plt.subplots(1, 2) ax1.plot([1, 3, 5], color='blue') ax2.plot([2, 4, 6], color='red') fig.legend(['the blues', 'the reds']) - Note: This call signature is discouraged, because the relation between - plot elements and labels is only implicit by their order and can - easily be mixed up. - - - **3. Explicitly defining the elements in the legend** - - For full control of which artists have a legend entry, it is possible - to pass an iterable of legend artists followed by an iterable of - legend labels respectively:: - - fig.legend([line1, line2, line3], ['label1', 'label2', 'label3']) Parameters ---------- @@ -1060,7 +1101,7 @@ def legend(self, *args, **kwargs): Other Parameters ---------------- - %(_legend_kw_doc)s + %(_legend_kw_figure)s See Also -------- @@ -1096,7 +1137,7 @@ def legend(self, *args, **kwargs): self.stale = True return l - @docstring.dedent_interpd + @_docstring.dedent_interpd def text(self, x, y, s, fontdict=None, **kwargs): """ Add text to figure. @@ -1125,7 +1166,7 @@ def text(self, x, y, s, fontdict=None, **kwargs): **kwargs : `~matplotlib.text.Text` properties Other miscellaneous text parameters. - %(Text_kwdoc)s + %(Text:kwdoc)s See Also -------- @@ -1146,36 +1187,113 @@ def text(self, x, y, s, fontdict=None, **kwargs): self.stale = True return text - @docstring.dedent_interpd - def colorbar(self, mappable, cax=None, ax=None, use_gridspec=True, **kw): - """%(colorbar_doc)s""" + @_docstring.dedent_interpd + def colorbar( + self, mappable, cax=None, ax=None, use_gridspec=True, **kwargs): + """ + Add a colorbar to a plot. + + Parameters + ---------- + mappable + The `matplotlib.cm.ScalarMappable` (i.e., `.AxesImage`, + `.ContourSet`, etc.) described by this colorbar. This argument is + mandatory for the `.Figure.colorbar` method but optional for the + `.pyplot.colorbar` function, which sets the default to the current + image. + + Note that one can create a `.ScalarMappable` "on-the-fly" to + generate colorbars not attached to a previously drawn artist, e.g. + :: + + fig.colorbar(cm.ScalarMappable(norm=norm, cmap=cmap), ax=ax) + + cax : `~matplotlib.axes.Axes`, optional + Axes into which the colorbar will be drawn. + + ax : `~.axes.Axes` or iterable or `numpy.ndarray` of Axes, optional + One or more parent axes from which space for a new colorbar axes + will be stolen, if *cax* is None. This has no effect if *cax* is + set. + + use_gridspec : bool, optional + If *cax* is ``None``, a new *cax* is created as an instance of + Axes. If *ax* is positioned with a subplotspec and *use_gridspec* + is ``True``, then *cax* is also positioned with a subplotspec. + + Returns + ------- + colorbar : `~matplotlib.colorbar.Colorbar` + + Other Parameters + ---------------- + %(_make_axes_kw_doc)s + %(_colormap_kw_doc)s + + Notes + ----- + If *mappable* is a `~.contour.ContourSet`, its *extend* kwarg is + included automatically. + + The *shrink* kwarg provides a simple way to scale the colorbar with + respect to the axes. Note that if *cax* is specified, it determines the + size of the colorbar and *shrink* and *aspect* kwargs are ignored. + + For more precise control, you can manually specify the positions of the + axes objects in which the mappable and the colorbar are drawn. In this + case, do not use any of the axes properties kwargs. + + It is known that some vector graphics viewers (svg and pdf) renders + white gaps between segments of the colorbar. This is due to bugs in + the viewers, not Matplotlib. As a workaround, the colorbar can be + rendered with overlapping segments:: + + cbar = colorbar() + cbar.solids.set_edgecolor("face") + draw() + + However, this has negative consequences in other circumstances, e.g. + with semi-transparent images (alpha < 1) and colorbar extensions; + therefore, this workaround is not used by default (see issue #1188). + """ + if ax is None: - ax = self.gca() - if (hasattr(mappable, "axes") and ax is not mappable.axes - and cax is None): - _api.warn_deprecated( - "3.4", message="Starting from Matplotlib 3.6, colorbar() " - "will steal space from the mappable's axes, rather than " - "from the current axes, to place the colorbar. To " - "silence this warning, explicitly pass the 'ax' argument " - "to colorbar().") + ax = getattr(mappable, "axes", None) + if (self.get_layout_engine() is not None and + not self.get_layout_engine().colorbar_gridspec): + use_gridspec = False # Store the value of gca so that we can set it back later on. - current_ax = self.gca() if cax is None: - if (use_gridspec and isinstance(ax, SubplotBase) - and not self.get_constrained_layout()): - cax, kw = cbar.make_axes_gridspec(ax, **kw) + if ax is None: + _api.warn_deprecated("3.6", message=( + 'Unable to determine Axes to steal space for Colorbar. ' + 'Using gca(), but will raise in the future. ' + 'Either provide the *cax* argument to use as the Axes for ' + 'the Colorbar, provide the *ax* argument to steal space ' + 'from it, or add *mappable* to an Axes.')) + ax = self.gca() + current_ax = self.gca() + userax = False + if (use_gridspec + and isinstance(ax, mpl.axes._base._AxesBase) + and ax.get_subplotspec()): + cax, kwargs = cbar.make_axes_gridspec(ax, **kwargs) else: - cax, kw = cbar.make_axes(ax, **kw) + cax, kwargs = cbar.make_axes(ax, **kwargs) + cax.grid(visible=False, which='both', axis='both') + else: + userax = True # need to remove kws that cannot be passed to Colorbar NON_COLORBAR_KEYS = ['fraction', 'pad', 'shrink', 'aspect', 'anchor', 'panchor'] - cb_kw = {k: v for k, v in kw.items() if k not in NON_COLORBAR_KEYS} + cb_kw = {k: v for k, v in kwargs.items() if k not in NON_COLORBAR_KEYS} + cb = cbar.Colorbar(cax, mappable, **cb_kw) - self.sca(current_ax) + if not userax: + self.sca(current_ax) self.stale = True return cb @@ -1208,15 +1326,16 @@ def subplots_adjust(self, left=None, bottom=None, right=None, top=None, The height of the padding between subplots, as a fraction of the average Axes height. """ - if self.get_constrained_layout(): - self.set_constrained_layout(False) + if (self.get_layout_engine() is not None and + not self.get_layout_engine().adjust_compatible): _api.warn_external( - "This figure was using constrained_layout, but that is " + "This figure was using a layout engine that is " "incompatible with subplots_adjust and/or tight_layout; " - "disabling constrained_layout.") + "not calling subplots_adjust.") + return self.subplotpars.update(left, bottom, right, top, wspace, hspace) for ax in self.axes: - if isinstance(ax, SubplotBase): + if ax.get_subplotspec() is not None: ax._set_position(ax.get_subplotspec().get_position(self)) self.stale = True @@ -1236,7 +1355,7 @@ def align_xlabels(self, axs=None): Parameters ---------- axs : list of `~matplotlib.axes.Axes` - Optional list of (or ndarray) `~matplotlib.axes.Axes` + Optional list of (or `~numpy.ndarray`) `~matplotlib.axes.Axes` to align the xlabels. Default is to align all Axes on the figure. @@ -1263,7 +1382,7 @@ def align_xlabels(self, axs=None): """ if axs is None: axs = self.axes - axs = np.ravel(axs) + axs = [ax for ax in np.ravel(axs) if ax.get_subplotspec() is not None] for ax in axs: _log.debug(' Working on: %s', ax.get_xlabel()) rowspan = ax.get_subplotspec().rowspan @@ -1297,7 +1416,7 @@ def align_ylabels(self, axs=None): Parameters ---------- axs : list of `~matplotlib.axes.Axes` - Optional list (or ndarray) of `~matplotlib.axes.Axes` + Optional list (or `~numpy.ndarray`) of `~matplotlib.axes.Axes` to align the ylabels. Default is to align all Axes on the figure. @@ -1323,7 +1442,7 @@ def align_ylabels(self, axs=None): """ if axs is None: axs = self.axes - axs = np.ravel(axs) + axs = [ax for ax in np.ravel(axs) if ax.get_subplotspec() is not None] for ax in axs: _log.debug(' Working on: %s', ax.get_ylabel()) colspan = ax.get_subplotspec().colspan @@ -1352,7 +1471,7 @@ def align_labels(self, axs=None): Parameters ---------- axs : list of `~matplotlib.axes.Axes` - Optional list (or ndarray) of `~matplotlib.axes.Axes` + Optional list (or `~numpy.ndarray`) of `~matplotlib.axes.Axes` to align the labels. Default is to align all Axes on the figure. @@ -1376,7 +1495,7 @@ def add_gridspec(self, nrows=1, ncols=1, **kwargs): Number of rows in grid. ncols : int, default: 1 - Number or columns in grid. + Number of columns in grid. Returns ------- @@ -1406,7 +1525,6 @@ def add_gridspec(self, nrows=1, ncols=1, **kwargs): _ = kwargs.pop('figure', None) # pop in case user has added this... gs = GridSpec(nrows=nrows, ncols=ncols, figure=self, **kwargs) - self._gridspecs.append(gs) return gs def subfigures(self, nrows=1, ncols=1, squeeze=True, @@ -1414,7 +1532,7 @@ def subfigures(self, nrows=1, ncols=1, squeeze=True, width_ratios=None, height_ratios=None, **kwargs): """ - Add a subfigure to this figure or subfigure. + Add a set of subfigures to this figure or subfigure. A subfigure has the same artist methods as a figure, and is logically the same as a figure, but cannot print itself. @@ -1463,12 +1581,9 @@ def subfigures(self, nrows=1, ncols=1, squeeze=True, # Returned axis array will be always 2-d, even if nrows=ncols=1. return sfarr - return sfarr - def add_subfigure(self, subplotspec, **kwargs): """ - Add a `~.figure.SubFigure` to the figure as part of a subplot - arrangement. + Add a `.SubFigure` to the figure as part of a subplot arrangement. Parameters ---------- @@ -1478,12 +1593,12 @@ def add_subfigure(self, subplotspec, **kwargs): Returns ------- - `.figure.SubFigure` + `.SubFigure` Other Parameters ---------------- **kwargs - Are passed to the `~.figure.SubFigure` object. + Are passed to the `.SubFigure` object. See Also -------- @@ -1499,32 +1614,18 @@ def sca(self, a): self._axobservers.process("_axes_change_event", self) return a - @docstring.dedent_interpd - def gca(self, **kwargs): + def gca(self): """ - Get the current Axes, creating one if necessary. - - The following kwargs are supported for ensuring the returned Axes - adheres to the given projection etc., and for Axes creation if - the active Axes does not exist: - - %(Axes_kwdoc)s + Get the current Axes. + If there is currently no Axes on this Figure, a new one is created + using `.Figure.add_subplot`. (To test whether there is currently an + Axes on a Figure, check whether ``figure.axes`` is empty. To test + whether there is currently a Figure on the pyplot figure stack, check + whether `.pyplot.get_fignums()` is empty.) """ - if kwargs: - _api.warn_deprecated( - "3.4", - message="Calling gca() with keyword arguments was deprecated " - "in Matplotlib %(since)s. Starting %(removal)s, gca() will " - "take no keyword arguments. The gca() function should only be " - "used to get the current axes, or if no axes exist, create " - "new axes with default keyword arguments. To create a new " - "axes with non-default arguments, use plt.axes() or " - "plt.subplot().") - if self._axstack.empty(): - return self.add_subplot(1, 1, 1, **kwargs) - else: - return self._axstack() + ax = self._axstack.current() + return ax if ax is not None else self.add_subplot() def _gci(self): # Helper for `~matplotlib.pyplot.gci`. Do not use elsewhere. @@ -1543,13 +1644,13 @@ def _gci(self): Historically, the only colorable artists were images; hence the name ``gci`` (get current image). """ - # Look first for an image in the current Axes: - if self._axstack.empty(): + # Look first for an image in the current Axes. + ax = self._axstack.current() + if ax is None: return None - im = self._axstack()._gci() + im = ax._gci() if im is not None: return im - # If there is no image in the current Axes, search for # one in a previously created Axes. Whether this makes # sense is debatable, but it is the documented behavior. @@ -1593,8 +1694,6 @@ def _process_projection_requirements( raise TypeError( f"projection must be a string, None or implement a " f"_as_mpl_axes method, not {projection!r}") - if projection_class.__name__ == 'Axes3D': - kwargs.setdefault('auto_add_to_figure', False) return projection_class, kwargs def get_default_bbox_extra_artists(self): @@ -1605,9 +1704,12 @@ def get_default_bbox_extra_artists(self): bbox_artists.extend(ax.get_default_bbox_extra_artists()) return bbox_artists - def get_tightbbox(self, renderer, bbox_extra_artists=None): + def get_tightbbox(self, renderer=None, bbox_extra_artists=None): """ - Return a (tight) bounding box of the figure in inches. + Return a (tight) bounding box of the figure *in inches*. + + Note that `.FigureBase` differs from all other artists, which return + their `.Bbox` in pixels. Artists that have ``artist.set_in_layout(False)`` are not included in the bbox. @@ -1615,7 +1717,7 @@ def get_tightbbox(self, renderer, bbox_extra_artists=None): Parameters ---------- renderer : `.RendererBase` subclass - renderer that will be used to draw the figures (i.e. + Renderer that will be used to draw the figures (i.e. ``fig.canvas.get_renderer()``) bbox_extra_artists : list of `.Artist` or ``None`` @@ -1629,6 +1731,9 @@ def get_tightbbox(self, renderer, bbox_extra_artists=None): containing the bounding box (in figure inches). """ + if renderer is None: + renderer = self.figure._get_renderer() + bb = [] if bbox_extra_artists is None: artists = self.get_default_bbox_extra_artists() @@ -1637,7 +1742,7 @@ def get_tightbbox(self, renderer, bbox_extra_artists=None): for a in artists: bbox = a.get_tightbbox(renderer) - if bbox is not None and (bbox.width != 0 or bbox.height != 0): + if bbox is not None: bb.append(bbox) for ax in self.axes: @@ -1654,8 +1759,9 @@ def get_tightbbox(self, renderer, bbox_extra_artists=None): if (np.isfinite(b.width) and np.isfinite(b.height) and (b.width != 0 or b.height != 0))] + isfigure = hasattr(self, 'bbox_inches') if len(bb) == 0: - if hasattr(self, 'bbox_inches'): + if isfigure: return self.bbox_inches else: # subfigures do not have bbox_inches, but do have a bbox @@ -1663,9 +1769,30 @@ def get_tightbbox(self, renderer, bbox_extra_artists=None): _bbox = Bbox.union(bb) - bbox_inches = TransformedBbox(_bbox, Affine2D().scale(1 / self.dpi)) + if isfigure: + # transform from pixels to inches... + _bbox = TransformedBbox(_bbox, self.dpi_scale_trans.inverted()) + + return _bbox - return bbox_inches + @staticmethod + def _norm_per_subplot_kw(per_subplot_kw): + expanded = {} + for k, v in per_subplot_kw.items(): + if isinstance(k, tuple): + for sub_key in k: + if sub_key in expanded: + raise ValueError( + f'The key {sub_key!r} appears multiple times.' + ) + expanded[sub_key] = v + else: + if k in expanded: + raise ValueError( + f'The key {k!r} appears multiple times.' + ) + expanded[k] = v + return expanded @staticmethod def _normalize_grid_string(layout): @@ -1677,18 +1804,17 @@ def _normalize_grid_string(layout): layout = inspect.cleandoc(layout) return [list(ln) for ln in layout.strip('\n').split('\n')] - def subplot_mosaic(self, mosaic, *, subplot_kw=None, gridspec_kw=None, - empty_sentinel='.'): + def subplot_mosaic(self, mosaic, *, sharex=False, sharey=False, + width_ratios=None, height_ratios=None, + empty_sentinel='.', + subplot_kw=None, per_subplot_kw=None, gridspec_kw=None): """ Build a layout of Axes based on ASCII art or nested lists. This is a helper function to build complex GridSpec layouts visually. - .. note :: - - This API is provisional and may be revised in the future based on - early user feedback. - + See :doc:`/gallery/subplots_axes_and_figures/mosaic` + for an example and full API documentation Parameters ---------- @@ -1700,7 +1826,7 @@ def subplot_mosaic(self, mosaic, *, subplot_kw=None, gridspec_kw=None, x = [['A panel', 'A panel', 'edge'], ['C panel', '.', 'edge']] - Produces 4 Axes: + produces 4 Axes: - 'A panel' which is 1 row high and spans the first two columns - 'edge' which is 2 rows high and is on the right edge @@ -1726,13 +1852,53 @@ def subplot_mosaic(self, mosaic, *, subplot_kw=None, gridspec_kw=None, The string notation allows only single character Axes labels and does not support nesting but is very terse. + The Axes identifiers may be `str` or a non-iterable hashable + object (e.g. `tuple` s may not be used). + + sharex, sharey : bool, default: False + If True, the x-axis (*sharex*) or y-axis (*sharey*) will be shared + among all subplots. In that case, tick label visibility and axis + units behave as for `subplots`. If False, each subplot's x- or + y-axis will be independent. + + width_ratios : array-like of length *ncols*, optional + Defines the relative widths of the columns. Each column gets a + relative width of ``width_ratios[i] / sum(width_ratios)``. + If not given, all columns will have the same width. Equivalent + to ``gridspec_kw={'width_ratios': [...]}``. In the case of nested + layouts, this argument applies only to the outer layout. + + height_ratios : array-like of length *nrows*, optional + Defines the relative heights of the rows. Each row gets a + relative height of ``height_ratios[i] / sum(height_ratios)``. + If not given, all rows will have the same height. Equivalent + to ``gridspec_kw={'height_ratios': [...]}``. In the case of nested + layouts, this argument applies only to the outer layout. + subplot_kw : dict, optional Dictionary with keywords passed to the `.Figure.add_subplot` call - used to create each subplot. + used to create each subplot. These values may be overridden by + values in *per_subplot_kw*. + + per_subplot_kw : dict, optional + A dictionary mapping the Axes identifiers or tuples of identifiers + to a dictionary of keyword arguments to be passed to the + `.Figure.add_subplot` call used to create each subplot. The values + in these dictionaries have precedence over the values in + *subplot_kw*. + + If *mosaic* is a string, and thus all keys are single characters, + it is possible to use a single string instead of a tuple as keys; + i.e. ``"AB"`` is equivalent to ``("A", "B")``. + + .. versionadded:: 3.7 gridspec_kw : dict, optional Dictionary with keywords passed to the `.GridSpec` constructor used - to create the grid the subplots are placed on. + to create the grid the subplots are placed on. In the case of + nested layouts, this argument applies only to the outer layout. + For more complex layouts, users should use `.Figure.subfigures` + to create the nesting. empty_sentinel : object, optional Entry in the layout to mean "leave this space empty". Defaults @@ -1749,10 +1915,31 @@ def subplot_mosaic(self, mosaic, *, subplot_kw=None, gridspec_kw=None, """ subplot_kw = subplot_kw or {} - gridspec_kw = gridspec_kw or {} + gridspec_kw = dict(gridspec_kw or {}) + per_subplot_kw = per_subplot_kw or {} + + if height_ratios is not None: + if 'height_ratios' in gridspec_kw: + raise ValueError("'height_ratios' must not be defined both as " + "parameter and as key in 'gridspec_kw'") + gridspec_kw['height_ratios'] = height_ratios + if width_ratios is not None: + if 'width_ratios' in gridspec_kw: + raise ValueError("'width_ratios' must not be defined both as " + "parameter and as key in 'gridspec_kw'") + gridspec_kw['width_ratios'] = width_ratios + # special-case string input if isinstance(mosaic, str): mosaic = self._normalize_grid_string(mosaic) + per_subplot_kw = { + tuple(k): v for k, v in per_subplot_kw.items() + } + + per_subplot_kw = self._norm_per_subplot_kw(per_subplot_kw) + + # Only accept strict bools to allow a possible future API expansion. + _api.check_isinstance(bool, sharex=sharex, sharey=sharey) def _make_array(inp): """ @@ -1833,7 +2020,6 @@ def _do_layout(gs, mosaic, unique_ids, nested): dict[label, Axes] A flat dict of all of the Axes created. """ - rows, cols = mosaic.shape output = dict() # we need to merge together the Axes at this level and the axes @@ -1873,7 +2059,7 @@ def _do_layout(gs, mosaic, unique_ids, nested): name, arg, method = this_level[key] # we are doing some hokey function dispatch here based # on the 'method' string stashed above to sort out if this - # element is an axes or a nested mosaic. + # element is an Axes or a nested mosaic. if method == 'axes': slc = arg # add a single axes @@ -1881,7 +2067,11 @@ def _do_layout(gs, mosaic, unique_ids, nested): raise ValueError(f"There are duplicate keys {name} " f"in the layout\n{mosaic!r}") ax = self.add_subplot( - gs[slc], **{'label': str(name), **subplot_kw} + gs[slc], **{ + 'label': str(name), + **subplot_kw, + **per_subplot_kw.get(name, {}) + } ) output[name] = ax elif method == 'nested': @@ -1890,7 +2080,7 @@ def _do_layout(gs, mosaic, unique_ids, nested): # recursively add the nested mosaic rows, cols = nested_mosaic.shape nested_output = _do_layout( - gs[j, k].subgridspec(rows, cols, **gridspec_kw), + gs[j, k].subgridspec(rows, cols), nested_mosaic, *_identify_keys_and_nested(nested_mosaic) ) @@ -1910,9 +2100,19 @@ def _do_layout(gs, mosaic, unique_ids, nested): rows, cols = mosaic.shape gs = self.add_gridspec(rows, cols, **gridspec_kw) ret = _do_layout(gs, mosaic, *_identify_keys_and_nested(mosaic)) - for k, ax in ret.items(): - if isinstance(k, str): - ax.set_label(k) + ax0 = next(iter(ret.values())) + for ax in ret.values(): + if sharex: + ax.sharex(ax0) + ax._label_outer_xaxis(check_patch=True) + if sharey: + ax.sharey(ax0) + ax._label_outer_yaxis(check_patch=True) + if extra := set(per_subplot_kw) - set(ret): + raise ValueError( + f"The keys {extra} are in *per_subplot_kw* " + "but not in the mosaic." + ) return ret def _set_artist_props(self, a): @@ -1922,6 +2122,7 @@ def _set_artist_props(self, a): a.set_transform(self.transSubfigure) +@_docstring.interpd class SubFigure(FigureBase): """ Logical figure that can be placed inside a figure. @@ -1939,16 +2140,21 @@ class SubFigure(FigureBase): See :doc:`/gallery/subplots_axes_and_figures/subfigures` """ + callbacks = _api.deprecated( + "3.6", alternative=("the 'resize_event' signal in " + "Figure.canvas.callbacks") + )(property(lambda self: self._fig_callbacks)) def __init__(self, parent, subplotspec, *, facecolor=None, edgecolor=None, linewidth=0.0, - frameon=None): + frameon=None, + **kwargs): """ Parameters ---------- - parent : `.figure.Figure` or `.figure.SubFigure` + parent : `.Figure` or `.SubFigure` Figure or subfigure that contains the SubFigure. SubFigures can be nested. @@ -1968,8 +2174,14 @@ def __init__(self, parent, subplotspec, *, frameon : bool, default: :rc:`figure.frameon` If ``False``, suppress drawing the figure background patch. + + Other Parameters + ---------------- + **kwargs : `.SubFigure` properties, optional + + %(SubFigure:kwdoc)s """ - super().__init__() + super().__init__(**kwargs) if facecolor is None: facecolor = mpl.rcParams['figure.facecolor'] if edgecolor is None: @@ -1980,6 +2192,8 @@ def __init__(self, parent, subplotspec, *, self._subplotspec = subplotspec self._parent = parent self.figure = parent.figure + self._fig_callbacks = parent._fig_callbacks + # subfigures use the parent axstack self._axstack = parent._axstack self.subplotpars = parent.subplotpars @@ -2002,9 +2216,6 @@ def __init__(self, parent, subplotspec, *, self._set_artist_props(self.patch) self.patch.set_antialiased(False) - if parent._layoutgrid is not None: - self.init_layoutgrid() - @property def dpi(self): return self._parent.dpi @@ -2013,6 +2224,26 @@ def dpi(self): def dpi(self, value): self._parent.dpi = value + def get_dpi(self): + """ + Return the resolution of the parent figure in dots-per-inch as a float. + """ + return self._parent.dpi + + def set_dpi(self, val): + """ + Set the resolution of parent figure in dots-per-inch. + + Parameters + ---------- + val : float + """ + self._parent.dpi = val + self.stale = True + + def _get_renderer(self): + return self._parent._get_renderer() + def _redo_transform_rel_fig(self, bbox=None): """ Make the transSubfigure bbox relative to Figure transform. @@ -2021,44 +2252,25 @@ def _redo_transform_rel_fig(self, bbox=None): ---------- bbox : bbox or None If not None, then the bbox is used for relative bounding box. - Otherwise it is calculated from the subplotspec. + Otherwise, it is calculated from the subplotspec. """ - if bbox is not None: self.bbox_relative.p0 = bbox.p0 self.bbox_relative.p1 = bbox.p1 return - - gs = self._subplotspec.get_gridspec() # need to figure out *where* this subplotspec is. - wr = gs.get_width_ratios() - hr = gs.get_height_ratios() - nrows, ncols = gs.get_geometry() - if wr is None: - wr = np.ones(ncols) - else: - wr = np.array(wr) - if hr is None: - hr = np.ones(nrows) - else: - hr = np.array(hr) - widthf = np.sum(wr[self._subplotspec.colspan]) / np.sum(wr) - heightf = np.sum(hr[self._subplotspec.rowspan]) / np.sum(hr) - - x0 = 0 - if not self._subplotspec.is_first_col(): - x0 += np.sum(wr[:self._subplotspec.colspan.start]) / np.sum(wr) - - y0 = 0 - if not self._subplotspec.is_last_row(): - y0 += 1 - (np.sum(hr[:self._subplotspec.rowspan.stop]) / - np.sum(hr)) - + gs = self._subplotspec.get_gridspec() + wr = np.asarray(gs.get_width_ratios()) + hr = np.asarray(gs.get_height_ratios()) + dx = wr[self._subplotspec.colspan].sum() / wr.sum() + dy = hr[self._subplotspec.rowspan].sum() / hr.sum() + x0 = wr[:self._subplotspec.colspan.start].sum() / wr.sum() + y0 = 1 - hr[:self._subplotspec.rowspan.stop].sum() / hr.sum() if self.bbox_relative is None: - self.bbox_relative = Bbox.from_bounds(x0, y0, widthf, heightf) + self.bbox_relative = Bbox.from_bounds(x0, y0, dx, dy) else: self.bbox_relative.p0 = (x0, y0) - self.bbox_relative.p1 = (x0 + widthf, y0 + heightf) + self.bbox_relative.p1 = (x0 + dx, y0 + dy) def get_constrained_layout(self): """ @@ -2084,46 +2296,28 @@ def get_constrained_layout_pads(self, relative=False): """ return self._parent.get_constrained_layout_pads(relative=relative) - def init_layoutgrid(self): - """Initialize the layoutgrid for use in constrained_layout.""" - if self._layoutgrid is None: - gs = self._subplotspec.get_gridspec() - parent = gs._layoutgrid - if parent is not None: - self._layoutgrid = layoutgrid.LayoutGrid( - parent=parent, - name=(parent.name + '.' + 'panellb' + - layoutgrid.seq_id()), - parent_inner=True, - nrows=1, ncols=1, - parent_pos=(self._subplotspec.rowspan, - self._subplotspec.colspan)) - - def get_axes(self): - """ - Return a list of Axes in the SubFigure. You can access and modify the - Axes in the Figure through this list. - - Do not modify the list itself. Instead, use `~.SubFigure.add_axes`, - `~.SubFigure.add_subplot` or `~.SubFigure.delaxes` to add or remove an - Axes. + def get_layout_engine(self): + return self._parent.get_layout_engine() - Note: This is equivalent to the property `~.SubFigure.axes`. + @property + def axes(self): """ - return self._localaxes.as_list() - - axes = property(get_axes, doc=""" List of Axes in the SubFigure. You can access and modify the Axes in the SubFigure through this list. - Do not modify the list itself. Instead, use `~.SubFigure.add_axes`, + Modifying this list has no effect. Instead, use `~.SubFigure.add_axes`, `~.SubFigure.add_subplot` or `~.SubFigure.delaxes` to add or remove an Axes. - """) + + Note: The `.SubFigure.axes` property and `~.SubFigure.get_axes` method + are equivalent. + """ + return self._localaxes[:] + + get_axes = axes.fget def draw(self, renderer): # docstring inherited - self._cachedRenderer = renderer # draw the figure bounding box, perhaps none for white figure if not self.get_visible(): @@ -2134,7 +2328,8 @@ def draw(self, renderer): try: renderer.open_group('subfigure', gid=self.get_gid()) self.patch.draw(renderer) - mimage._draw_list_compositing_images(renderer, self, artists) + mimage._draw_list_compositing_images( + renderer, self, artists, self.figure.suppressComposite) for sfig in self.subfigs: sfig.draw(renderer) renderer.close_group('subfigure') @@ -2143,25 +2338,27 @@ def draw(self, renderer): self.stale = False +@_docstring.interpd class Figure(FigureBase): """ The top level container for all the plot elements. - The Figure instance supports callbacks through a *callbacks* attribute - which is a `.CallbackRegistry` instance. The events you can connect to - are 'dpi_changed', and the callback will be called with ``func(fig)`` where - fig is the `Figure` instance. - Attributes ---------- patch The `.Rectangle` instance representing the figure background patch. suppressComposite - For multiple figure images, the figure will make composite images + For multiple images, the figure will make composite images depending on the renderer option_image_nocomposite function. If *suppressComposite* is a boolean, this will override the renderer. """ + # Remove the self._fig_callbacks properties on figure and subfigure + # after the deprecation expires. + callbacks = _api.deprecated( + "3.6", alternative=("the 'resize_event' signal in " + "Figure.canvas.callbacks") + )(property(lambda self: self._fig_callbacks)) def __str__(self): return "Figure(%gx%g)" % tuple(self.bbox.size) @@ -2173,6 +2370,7 @@ def __repr__(self): naxes=len(self.axes), ) + @_api.make_keyword_only("3.6", "facecolor") def __init__(self, figsize=None, dpi=None, @@ -2183,6 +2381,9 @@ def __init__(self, subplotpars=None, # rc figure.subplot.* tight_layout=None, # rc figure.autolayout constrained_layout=None, # rc figure.constrained_layout.use + *, + layout=None, + **kwargs ): """ Parameters @@ -2211,31 +2412,112 @@ def __init__(self, parameters :rc:`figure.subplot.*` are used. tight_layout : bool or dict, default: :rc:`figure.autolayout` - If ``False`` use *subplotpars*. If ``True`` adjust subplot - parameters using `.tight_layout` with default padding. - When providing a dict containing the keys ``pad``, ``w_pad``, - ``h_pad``, and ``rect``, the default `.tight_layout` paddings - will be overridden. + Whether to use the tight layout mechanism. See `.set_tight_layout`. + + .. admonition:: Discouraged + + The use of this parameter is discouraged. Please use + ``layout='tight'`` instead for the common case of + ``tight_layout=True`` and use `.set_tight_layout` otherwise. constrained_layout : bool, default: :rc:`figure.constrained_layout.use` - If ``True`` use constrained layout to adjust positioning of plot - elements. Like ``tight_layout``, but designed to be more - flexible. See - :doc:`/tutorials/intermediate/constrainedlayout_guide` - for examples. (Note: does not work with `add_subplot` or - `~.pyplot.subplot2grid`.) - """ - super().__init__() + This is equal to ``layout='constrained'``. + + .. admonition:: Discouraged + + The use of this parameter is discouraged. Please use + ``layout='constrained'`` instead. + + layout : {'constrained', 'compressed', 'tight', 'none', `.LayoutEngine`, \ +None}, default: None + The layout mechanism for positioning of plot elements to avoid + overlapping Axes decorations (labels, ticks, etc). Note that + layout managers can have significant performance penalties. - self.callbacks = cbook.CallbackRegistry() + - 'constrained': The constrained layout solver adjusts axes sizes + to avoid overlapping axes decorations. Can handle complex plot + layouts and colorbars, and is thus recommended. + + See :doc:`/tutorials/intermediate/constrainedlayout_guide` + for examples. + + - 'compressed': uses the same algorithm as 'constrained', but + removes extra space between fixed-aspect-ratio Axes. Best for + simple grids of axes. + + - 'tight': Use the tight layout mechanism. This is a relatively + simple algorithm that adjusts the subplot parameters so that + decorations do not overlap. See `.Figure.set_tight_layout` for + further details. + + - 'none': Do not use a layout engine. + + - A `.LayoutEngine` instance. Builtin layout classes are + `.ConstrainedLayoutEngine` and `.TightLayoutEngine`, more easily + accessible by 'constrained' and 'tight'. Passing an instance + allows third parties to provide their own layout engine. + + If not given, fall back to using the parameters *tight_layout* and + *constrained_layout*, including their config defaults + :rc:`figure.autolayout` and :rc:`figure.constrained_layout.use`. + + Other Parameters + ---------------- + **kwargs : `.Figure` properties, optional + + %(Figure:kwdoc)s + """ + super().__init__(**kwargs) + self._layout_engine = None + + if layout is not None: + if (tight_layout is not None): + _api.warn_external( + "The Figure parameters 'layout' and 'tight_layout' cannot " + "be used together. Please use 'layout' only.") + if (constrained_layout is not None): + _api.warn_external( + "The Figure parameters 'layout' and 'constrained_layout' " + "cannot be used together. Please use 'layout' only.") + self.set_layout_engine(layout=layout) + elif tight_layout is not None: + if constrained_layout is not None: + _api.warn_external( + "The Figure parameters 'tight_layout' and " + "'constrained_layout' cannot be used together. Please use " + "'layout' parameter") + self.set_layout_engine(layout='tight') + if isinstance(tight_layout, dict): + self.get_layout_engine().set(**tight_layout) + elif constrained_layout is not None: + if isinstance(constrained_layout, dict): + self.set_layout_engine(layout='constrained') + self.get_layout_engine().set(**constrained_layout) + elif constrained_layout: + self.set_layout_engine(layout='constrained') + + else: + # everything is None, so use default: + self.set_layout_engine(layout=layout) + + self._fig_callbacks = cbook.CallbackRegistry(signals=["dpi_changed"]) # Callbacks traditionally associated with the canvas (and exposed with # a proxy property), but that actually need to be on the figure for # pickling. - self._canvas_callbacks = cbook.CallbackRegistry() - self._button_pick_id = self._canvas_callbacks.connect( - 'button_press_event', lambda event: self.canvas.pick(event)) - self._scroll_pick_id = self._canvas_callbacks.connect( - 'scroll_event', lambda event: self.canvas.pick(event)) + self._canvas_callbacks = cbook.CallbackRegistry( + signals=FigureCanvasBase.events) + connect = self._canvas_callbacks._connect_picklable + self._mouse_key_ids = [ + connect('key_press_event', backend_bases._key_handler), + connect('key_release_event', backend_bases._key_handler), + connect('key_release_event', backend_bases._key_handler), + connect('button_press_event', backend_bases._mouse_handler), + connect('button_release_event', backend_bases._mouse_handler), + connect('scroll_event', backend_bases._mouse_handler), + connect('motion_notify_event', backend_bases._mouse_handler), + ] + self._button_pick_id = connect('button_press_event', self.pick) + self._scroll_pick_id = connect('scroll_event', self.pick) if figsize is None: figsize = mpl.rcParams['figure.figsize'] @@ -2276,20 +2558,100 @@ def __init__(self, self.subplotpars = subplotpars - # constrained_layout: - self._layoutgrid = None - self._constrained = False + self._axstack = _AxesStack() # track all figure axes and current axes + self.clear() - self.set_tight_layout(tight_layout) + def pick(self, mouseevent): + if not self.canvas.widgetlock.locked(): + super().pick(mouseevent) - self._axstack = _AxesStack() # track all figure axes and current axes - self.clf() - self._cachedRenderer = None + def _check_layout_engines_compat(self, old, new): + """ + Helper for set_layout engine + + If the figure has used the old engine and added a colorbar then the + value of colorbar_gridspec must be the same on the new engine. + """ + if old is None or new is None: + return True + if old.colorbar_gridspec == new.colorbar_gridspec: + return True + # colorbar layout different, so check if any colorbars are on the + # figure... + for ax in self.axes: + if hasattr(ax, '_colorbar'): + # colorbars list themselves as a colorbar. + return False + return True + + def set_layout_engine(self, layout=None, **kwargs): + """ + Set the layout engine for this figure. + + Parameters + ---------- + layout: {'constrained', 'compressed', 'tight', 'none'} or \ +`LayoutEngine` or None + + - 'constrained' will use `~.ConstrainedLayoutEngine` + - 'compressed' will also use `~.ConstrainedLayoutEngine`, but with + a correction that attempts to make a good layout for fixed-aspect + ratio Axes. + - 'tight' uses `~.TightLayoutEngine` + - 'none' removes layout engine. + + If `None`, the behavior is controlled by :rc:`figure.autolayout` + (which if `True` behaves as if 'tight' was passed) and + :rc:`figure.constrained_layout.use` (which if `True` behaves as if + 'constrained' was passed). If both are `True`, + :rc:`figure.autolayout` takes priority. + + Users and libraries can define their own layout engines and pass + the instance directly as well. + + kwargs: dict + The keyword arguments are passed to the layout engine to set things + like padding and margin sizes. Only used if *layout* is a string. + + """ + if layout is None: + if mpl.rcParams['figure.autolayout']: + layout = 'tight' + elif mpl.rcParams['figure.constrained_layout.use']: + layout = 'constrained' + else: + self._layout_engine = None + return + if layout == 'tight': + new_layout_engine = TightLayoutEngine(**kwargs) + elif layout == 'constrained': + new_layout_engine = ConstrainedLayoutEngine(**kwargs) + elif layout == 'compressed': + new_layout_engine = ConstrainedLayoutEngine(compress=True, + **kwargs) + elif layout == 'none': + if self._layout_engine is not None: + new_layout_engine = PlaceHolderLayoutEngine( + self._layout_engine.adjust_compatible, + self._layout_engine.colorbar_gridspec + ) + else: + new_layout_engine = None + elif isinstance(layout, LayoutEngine): + new_layout_engine = layout + else: + raise ValueError(f"Invalid value for 'layout': {layout!r}") - self.set_constrained_layout(constrained_layout) + if self._check_layout_engines_compat(self._layout_engine, + new_layout_engine): + self._layout_engine = new_layout_engine + else: + raise RuntimeError('Colorbar layout of new layout engine not ' + 'compatible with old engine, and a colorbar ' + 'has been created. Engine not changed.') - # list of child gridspecs for this figure - self._gridspecs = [] + def get_layout_engine(self): + return self._layout_engine # TODO: I'd like to dynamically add the _repr_html_ method # to the figure in the right context, but then IPython doesn't @@ -2316,18 +2678,25 @@ def show(self, warn=True): may only be shown briefly or not shown at all if you or your environment are not managing an event loop. - Proper use cases for `.Figure.show` include running this from a - GUI application or an IPython shell. + Use cases for `.Figure.show` include running this from a GUI + application (where there is persistently an event loop running) or + from a shell, like IPython, that install an input hook to allow the + interactive shell to accept input while the figure is also being + shown and interactive. Some, but not all, GUI toolkits will + register an input hook on import. See :ref:`cp_integration` for + more details. - If you're running a pure python shell or executing a non-GUI - python script, you should use `matplotlib.pyplot.show` instead, - which takes care of managing the event loop for you. + If you're in a shell without input hook integration or executing a + python script, you should use `matplotlib.pyplot.show` with + ``block=True`` instead, which takes care of starting and running + the event loop for you. Parameters ---------- warn : bool, default: True If ``True`` and we are not running headless (i.e. on Linux with an unset DISPLAY), issue warning when called on a non-GUI backend. + """ if self.canvas.manager is None: raise AttributeError( @@ -2339,25 +2708,27 @@ def show(self, warn=True): if warn: _api.warn_external(str(exc)) - def get_axes(self): + @property + def axes(self): """ - Return a list of Axes in the Figure. You can access and modify the - Axes in the Figure through this list. + List of Axes in the Figure. You can access and modify the Axes in the + Figure through this list. Do not modify the list itself. Instead, use `~Figure.add_axes`, `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an Axes. - Note: This is equivalent to the property `~.Figure.axes`. + Note: The `.Figure.axes` property and `~.Figure.get_axes` method are + equivalent. """ return self._axstack.as_list() - axes = property(get_axes, doc=""" - List of Axes in the Figure. You can access and modify the Axes in the - Figure through this list. + get_axes = axes.fget - Do not modify the list itself. Instead, use "`~Figure.add_axes`, - `~.Figure.add_subplot` or `~.Figure.delaxes` to add or remove an Axes. - """) + def _get_renderer(self): + if hasattr(self.canvas, 'get_renderer'): + return self.canvas.get_renderer() + else: + return _get_renderer(self) def _get_dpi(self): return self._dpi @@ -2378,30 +2749,38 @@ def _set_dpi(self, dpi, forward=True): self.dpi_scale_trans.clear().scale(dpi) w, h = self.get_size_inches() self.set_size_inches(w, h, forward=forward) - self.callbacks.process('dpi_changed', self) + self._fig_callbacks.process('dpi_changed', self) dpi = property(_get_dpi, _set_dpi, doc="The resolution in dots per inch.") def get_tight_layout(self): """Return whether `.tight_layout` is called when drawing.""" - return self._tight + return isinstance(self.get_layout_engine(), TightLayoutEngine) + @_api.deprecated("3.6", alternative="set_layout_engine", + pending=True) def set_tight_layout(self, tight): """ - Set whether and how `.tight_layout` is called when drawing. + [*Discouraged*] Set whether and how `.tight_layout` is called when + drawing. + + .. admonition:: Discouraged + + This method is discouraged in favor of `~.set_layout_engine`. Parameters ---------- tight : bool or dict with keys "pad", "w_pad", "h_pad", "rect" or None If a bool, sets whether to call `.tight_layout` upon drawing. - If ``None``, use the ``figure.autolayout`` rcparam instead. + If ``None``, use :rc:`figure.autolayout` instead. If a dict, pass it as kwargs to `.tight_layout`, overriding the default paddings. """ if tight is None: tight = mpl.rcParams['figure.autolayout'] - self._tight = bool(tight) - self._tight_parameters = tight if isinstance(tight, dict) else {} + _tight = 'tight' if bool(tight) else 'none' + _tight_parameters = tight if isinstance(tight, dict) else {} + self.set_layout_engine(_tight, **_tight_parameters) self.stale = True def get_constrained_layout(self): @@ -2410,82 +2789,80 @@ def get_constrained_layout(self): See :doc:`/tutorials/intermediate/constrainedlayout_guide`. """ - return self._constrained + return isinstance(self.get_layout_engine(), ConstrainedLayoutEngine) + @_api.deprecated("3.6", alternative="set_layout_engine('constrained')", + pending=True) def set_constrained_layout(self, constrained): """ - Set whether ``constrained_layout`` is used upon drawing. If None, - :rc:`figure.constrained_layout.use` value will be used. + [*Discouraged*] Set whether ``constrained_layout`` is used upon + drawing. - When providing a dict containing the keys `w_pad`, `h_pad` + If None, :rc:`figure.constrained_layout.use` value will be used. + + When providing a dict containing the keys ``w_pad``, ``h_pad`` the default ``constrained_layout`` paddings will be overridden. These pads are in inches and default to 3.0/72.0. ``w_pad`` is the width padding and ``h_pad`` is the height padding. - See :doc:`/tutorials/intermediate/constrainedlayout_guide`. + .. admonition:: Discouraged + + This method is discouraged in favor of `~.set_layout_engine`. Parameters ---------- constrained : bool or dict or None """ - self._constrained_layout_pads = dict() - self._constrained_layout_pads['w_pad'] = None - self._constrained_layout_pads['h_pad'] = None - self._constrained_layout_pads['wspace'] = None - self._constrained_layout_pads['hspace'] = None if constrained is None: constrained = mpl.rcParams['figure.constrained_layout.use'] - self._constrained = bool(constrained) - if isinstance(constrained, dict): - self.set_constrained_layout_pads(**constrained) - else: - self.set_constrained_layout_pads() - - self.init_layoutgrid() - + _constrained = 'constrained' if bool(constrained) else 'none' + _parameters = constrained if isinstance(constrained, dict) else {} + self.set_layout_engine(_constrained, **_parameters) self.stale = True + @_api.deprecated( + "3.6", alternative="figure.get_layout_engine().set()", + pending=True) def set_constrained_layout_pads(self, **kwargs): """ - Set padding for ``constrained_layout``. Note the kwargs can be passed - as a dictionary ``fig.set_constrained_layout(**paddict)``. + Set padding for ``constrained_layout``. + + Tip: The parameters can be passed from a dictionary by using + ``fig.set_constrained_layout(**pad_dict)``. See :doc:`/tutorials/intermediate/constrainedlayout_guide`. Parameters ---------- - w_pad : float + w_pad : float, default: :rc:`figure.constrained_layout.w_pad` Width padding in inches. This is the pad around Axes and is meant to make sure there is enough room for fonts to look good. Defaults to 3 pts = 0.04167 inches - h_pad : float + h_pad : float, default: :rc:`figure.constrained_layout.h_pad` Height padding in inches. Defaults to 3 pts. - wspace : float + wspace : float, default: :rc:`figure.constrained_layout.wspace` Width padding between subplots, expressed as a fraction of the subplot width. The total padding ends up being w_pad + wspace. - hspace : float + hspace : float, default: :rc:`figure.constrained_layout.hspace` Height padding between subplots, expressed as a fraction of the subplot width. The total padding ends up being h_pad + hspace. """ + if isinstance(self.get_layout_engine(), ConstrainedLayoutEngine): + self.get_layout_engine().set(**kwargs) - todo = ['w_pad', 'h_pad', 'wspace', 'hspace'] - for td in todo: - if td in kwargs and kwargs[td] is not None: - self._constrained_layout_pads[td] = kwargs[td] - else: - self._constrained_layout_pads[td] = ( - mpl.rcParams['figure.constrained_layout.' + td]) - + @_api.deprecated("3.6", alternative="fig.get_layout_engine().get()", + pending=True) def get_constrained_layout_pads(self, relative=False): """ Get padding for ``constrained_layout``. Returns a list of ``w_pad, h_pad`` in inches and ``wspace`` and ``hspace`` as fractions of the subplot. + All values are None if ``constrained_layout`` is not used. See :doc:`/tutorials/intermediate/constrainedlayout_guide`. @@ -2494,16 +2871,19 @@ def get_constrained_layout_pads(self, relative=False): relative : bool If `True`, then convert from inches to figure relative. """ - w_pad = self._constrained_layout_pads['w_pad'] - h_pad = self._constrained_layout_pads['h_pad'] - wspace = self._constrained_layout_pads['wspace'] - hspace = self._constrained_layout_pads['hspace'] + if not isinstance(self.get_layout_engine(), ConstrainedLayoutEngine): + return None, None, None, None + info = self.get_layout_engine().get_info() + w_pad = info['w_pad'] + h_pad = info['h_pad'] + wspace = info['wspace'] + hspace = info['hspace'] if relative and (w_pad is not None or h_pad is not None): - renderer0 = layoutgrid.get_renderer(self) - dpi = renderer0.dpi - w_pad = w_pad * dpi / renderer0.width - h_pad = h_pad * dpi / renderer0.height + renderer = self._get_renderer() + dpi = renderer.dpi + w_pad = w_pad * dpi / renderer.width + h_pad = h_pad * dpi / renderer.height return w_pad, h_pad, wspace, hspace @@ -2517,6 +2897,7 @@ def set_canvas(self, canvas): """ self.canvas = canvas + @_docstring.interpd def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, vmin=None, vmax=None, origin=None, resize=False, **kwargs): """ @@ -2530,9 +2911,11 @@ def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, X The image data. This is an array of one of the following shapes: - - MxN: luminance (grayscale) values - - MxNx3: RGB values - - MxNx4: RGBA values + - (M, N): an image with scalar data. Color-mapping is controlled + by *cmap*, *norm*, *vmin*, and *vmax*. + - (M, N, 3): an image with RGB values (0-1 float or 0-255 int). + - (M, N, 4): an image with RGBA values (0-1 float or 0-255 int), + i.e. including transparency. xo, yo : int The *x*/*y* image offset in pixels. @@ -2540,16 +2923,17 @@ def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, alpha : None or float The alpha blending value. - norm : `matplotlib.colors.Normalize` - A `.Normalize` instance to map the luminance to the - interval [0, 1]. + %(cmap_doc)s + + This parameter is ignored if *X* is RGB(A). - cmap : str or `matplotlib.colors.Colormap`, default: :rc:`image.cmap` - The colormap to use. + %(norm_doc)s - vmin, vmax : float - If *norm* is not given, these values set the data limits for the - colormap. + This parameter is ignored if *X* is RGB(A). + + %(vmin_vmax_doc)s + + This parameter is ignored if *X* is RGB(A). origin : {'upper', 'lower'}, default: :rc:`image.origin` Indicates where the [0, 0] index of the array is in the upper left @@ -2590,7 +2974,9 @@ def figimage(self, X, xo=0, yo=0, alpha=None, norm=None, cmap=None, figsize = [x / dpi for x in (X.shape[1], X.shape[0])] self.set_size_inches(figsize, forward=True) - im = mimage.FigureImage(self, cmap, norm, xo, yo, origin, **kwargs) + im = mimage.FigureImage(self, cmap=cmap, norm=norm, + offsetx=xo, offsety=yo, + origin=origin, **kwargs) im.stale_callback = _stale_figure_callback im.set_array(X) @@ -2639,12 +3025,9 @@ def set_size_inches(self, w, h=None, forward=True): raise ValueError(f'figure size must be positive finite not {size}') self.bbox_inches.p1 = size if forward: - canvas = getattr(self, 'canvas') - if canvas is not None: - dpi_ratio = getattr(canvas, '_dpi_ratio', 1) - manager = getattr(canvas, 'manager', None) - if manager is not None: - manager.resize(*(size * self.dpi / dpi_ratio).astype(int)) + manager = self.canvas.manager + if manager is not None: + manager.resize(*(size * self.dpi).astype(int)) self.stale = True def get_size_inches(self): @@ -2725,63 +3108,30 @@ def set_figheight(self, val, forward=True): """ self.set_size_inches(self.get_figwidth(), val, forward=forward) - def clf(self, keep_observers=False): - """ - Clear the figure. - - Set *keep_observers* to True if, for example, - a gui widget is tracking the Axes in the figure. - """ - self.suppressComposite = None - self.callbacks = cbook.CallbackRegistry() - - for ax in tuple(self.axes): # Iterate over the copy. - ax.cla() - self.delaxes(ax) # removes ax from self._axstack - - toolbar = getattr(self.canvas, 'toolbar', None) + def clear(self, keep_observers=False): + # docstring inherited + super().clear(keep_observers=keep_observers) + # FigureBase.clear does not clear toolbars, as + # only Figure can have toolbars + toolbar = self.canvas.toolbar if toolbar is not None: toolbar.update() - self._axstack.clear() - self.artists = [] - self.lines = [] - self.patches = [] - self.texts = [] - self.images = [] - self.legends = [] - if not keep_observers: - self._axobservers = cbook.CallbackRegistry() - self._suptitle = None - self._supxlabel = None - self._supylabel = None - - if self.get_constrained_layout(): - self.init_layoutgrid() - self.stale = True - - def clear(self, keep_observers=False): - """Clear the figure -- synonym for `clf`.""" - self.clf(keep_observers=keep_observers) @_finalize_rasterization @allow_rasterization def draw(self, renderer): # docstring inherited - self._cachedRenderer = renderer # draw the figure bounding box, perhaps none for white figure if not self.get_visible(): return artists = self._get_draw_artists(renderer) - try: renderer.open_group('figure', gid=self.get_gid()) - if self.get_constrained_layout() and self.axes: - self.execute_constrained_layout(renderer) - if self.get_tight_layout() and self.axes: + if self.axes and self.get_layout_engine() is not None: try: - self.tight_layout(**self._tight_parameters) + self.get_layout_engine().execute(self) except ValueError: pass # ValueError can occur when resizing a window. @@ -2797,19 +3147,22 @@ def draw(self, renderer): finally: self.stale = False - self.canvas.draw_event(renderer) + DrawEvent("draw_event", self.canvas, renderer)._process() + + def draw_without_rendering(self): + """ + Draw the figure with no output. Useful to get the final size of + artists that require a draw before their size is known (e.g. text). + """ + renderer = _get_renderer(self) + with renderer._draw_disabled(): + self.draw(renderer) def draw_artist(self, a): """ Draw `.Artist` *a* only. - - This method can only be used after an initial draw of the figure, - because that creates and caches the renderer needed here. """ - if self._cachedRenderer is None: - raise AttributeError("draw_artist can only be used after an " - "initial draw which caches the renderer") - a.draw(self._cachedRenderer) + a.draw(self.canvas.get_renderer()) def __getstate__(self): state = super().__getstate__() @@ -2819,29 +3172,23 @@ def __getstate__(self): # re-attached to another. state.pop("canvas") - # Set cached renderer to None -- it can't be pickled. - state["_cachedRenderer"] = None + # discard any changes to the dpi due to pixel ratio changes + state["_dpi"] = state.get('_original_dpi', state['_dpi']) # add version information to the state - state['__mpl_version__'] = _mpl_version + state['__mpl_version__'] = mpl.__version__ # check whether the figure manager (if any) is registered with pyplot from matplotlib import _pylab_helpers - if getattr(self.canvas, 'manager', None) \ - in _pylab_helpers.Gcf.figs.values(): + if self.canvas.manager in _pylab_helpers.Gcf.figs.values(): state['_restore_to_pylab'] = True - - # set all the layoutgrid information to None. kiwisolver objects can't - # be pickled, so we lose the layout options at this point. - state.pop('_layoutgrid', None) - return state def __setstate__(self, state): version = state.pop('__mpl_version__') restore_to_pylab = state.pop('_restore_to_pylab', False) - if version != _mpl_version: + if version != mpl.__version__: _api.warn_external( f"This figure was saved with matplotlib version {version} and " f"is unlikely to function correctly.") @@ -2850,7 +3197,6 @@ def __setstate__(self, state): # re-initialise some of the unstored state information FigureCanvasBase(self) # Set self.canvas. - self._layoutgrid = None if restore_to_pylab: # lazy import to avoid circularity @@ -2858,7 +3204,8 @@ def __setstate__(self, state): import matplotlib._pylab_helpers as pylab_helpers allnums = plt.get_fignums() num = max(allnums) + 1 if allnums else 1 - mgr = plt._backend_mod.new_figure_manager_given_figure(num, self) + backend = plt._get_backend_mod() + mgr = backend.new_figure_manager_given_figure(num, self) pylab_helpers.Gcf._set_new_active_manager(mgr) plt.draw_if_interactive() @@ -2876,10 +3223,11 @@ def savefig(self, fname, *, transparent=None, **kwargs): Call signature:: - savefig(fname, dpi=None, facecolor='w', edgecolor='w', - orientation='portrait', papertype=None, format=None, - transparent=False, bbox_inches=None, pad_inches=0.1, - frameon=None, metadata=None) + savefig(fname, *, dpi='figure', format=None, metadata=None, + bbox_inches=None, pad_inches=0.1, + facecolor='auto', edgecolor='auto', + backend=None, **kwargs + ) The available output formats depend on the backend being used. @@ -2907,30 +3255,28 @@ def savefig(self, fname, *, transparent=None, **kwargs): The resolution in dots per inch. If 'figure', use the figure's dpi value. - quality : int, default: :rc:`savefig.jpeg_quality` - Applicable only if *format* is 'jpg' or 'jpeg', ignored otherwise. - - The image quality, on a scale from 1 (worst) to 95 (best). - Values above 95 should be avoided; 100 disables portions of - the JPEG compression algorithm, and results in large files - with hardly any gain in image quality. - - This parameter is deprecated. - - optimize : bool, default: False - Applicable only if *format* is 'jpg' or 'jpeg', ignored otherwise. - - Whether the encoder should make an extra pass over the image - in order to select optimal encoder settings. + format : str + The file format, e.g. 'png', 'pdf', 'svg', ... The behavior when + this is unset is documented under *fname*. - This parameter is deprecated. + metadata : dict, optional + Key/value pairs to store in the image metadata. The supported keys + and defaults depend on the image format and backend: - progressive : bool, default: False - Applicable only if *format* is 'jpg' or 'jpeg', ignored otherwise. + - 'png' with Agg backend: See the parameter ``metadata`` of + `~.FigureCanvasAgg.print_png`. + - 'pdf' with pdf backend: See the parameter ``metadata`` of + `~.backend_pdf.PdfPages`. + - 'svg' with svg backend: See the parameter ``metadata`` of + `~.FigureCanvasSVG.print_svg`. + - 'eps' and 'ps' with PS backend: Only 'Creator' is supported. - Whether the image should be stored as a progressive JPEG file. + bbox_inches : str or `.Bbox`, default: :rc:`savefig.bbox` + Bounding box in inches: only the given portion of the figure is + saved. If 'tight', try to figure out the tight bbox of the figure. - This parameter is deprecated. + pad_inches : float, default: :rc:`savefig.pad_inches` + Amount of padding around the figure when bbox_inches is 'tight'. facecolor : color or 'auto', default: :rc:`savefig.facecolor` The facecolor of the figure. If 'auto', use the current figure @@ -2940,6 +3286,14 @@ def savefig(self, fname, *, transparent=None, **kwargs): The edgecolor of the figure. If 'auto', use the current figure edgecolor. + backend : str, optional + Use a non-default backend to render the file, e.g. to render a + png file with the "cairo" backend rather than the default "agg", + or a pdf file with the "pgf" backend rather than the default + "pdf". Note that the default backend is normally sufficient. See + :ref:`the-builtin-backends` for a list of valid backends for each + file format. Custom backends can be referenced as "module://...". + orientation : {'landscape', 'portrait'} Currently only supported by the postscript backend. @@ -2948,76 +3302,45 @@ def savefig(self, fname, *, transparent=None, **kwargs): 'a10', 'b0' through 'b10'. Only supported for postscript output. - format : str - The file format, e.g. 'png', 'pdf', 'svg', ... The behavior when - this is unset is documented under *fname*. - transparent : bool If *True*, the Axes patches will all be transparent; the - figure patch will also be transparent unless facecolor - and/or edgecolor are specified via kwargs. - This is useful, for example, for displaying - a plot on top of a colored background on a web page. The - transparency of these patches will be restored to their - original values upon exit of this function. + Figure patch will also be transparent unless *facecolor* + and/or *edgecolor* are specified via kwargs. - bbox_inches : str or `.Bbox`, default: :rc:`savefig.bbox` - Bounding box in inches: only the given portion of the figure is - saved. If 'tight', try to figure out the tight bbox of the figure. + If *False* has no effect and the color of the Axes and + Figure patches are unchanged (unless the Figure patch + is specified via the *facecolor* and/or *edgecolor* keyword + arguments in which case those colors are used). - pad_inches : float, default: :rc:`savefig.pad_inches` - Amount of padding around the figure when bbox_inches is 'tight'. + The transparency of these patches will be restored to their + original values upon exit of this function. + + This is useful, for example, for displaying + a plot on top of a colored background on a web page. bbox_extra_artists : list of `~matplotlib.artist.Artist`, optional A list of extra artists that will be considered when the tight bbox is calculated. - backend : str, optional - Use a non-default backend to render the file, e.g. to render a - png file with the "cairo" backend rather than the default "agg", - or a pdf file with the "pgf" backend rather than the default - "pdf". Note that the default backend is normally sufficient. See - :ref:`the-builtin-backends` for a list of valid backends for each - file format. Custom backends can be referenced as "module://...". - - metadata : dict, optional - Key/value pairs to store in the image metadata. The supported keys - and defaults depend on the image format and backend: - - - 'png' with Agg backend: See the parameter ``metadata`` of - `~.FigureCanvasAgg.print_png`. - - 'pdf' with pdf backend: See the parameter ``metadata`` of - `~.backend_pdf.PdfPages`. - - 'svg' with svg backend: See the parameter ``metadata`` of - `~.FigureCanvasSVG.print_svg`. - - 'eps' and 'ps' with PS backend: Only 'Creator' is supported. - pil_kwargs : dict, optional Additional keyword arguments that are passed to `PIL.Image.Image.save` when saving the figure. + """ kwargs.setdefault('dpi', mpl.rcParams['savefig.dpi']) if transparent is None: transparent = mpl.rcParams['savefig.transparent'] - if transparent: - kwargs.setdefault('facecolor', 'none') - kwargs.setdefault('edgecolor', 'none') - original_axes_colors = [] - for ax in self.axes: - patch = ax.patch - original_axes_colors.append((patch.get_facecolor(), - patch.get_edgecolor())) - patch.set_facecolor('none') - patch.set_edgecolor('none') + with ExitStack() as stack: + if transparent: + kwargs.setdefault('facecolor', 'none') + kwargs.setdefault('edgecolor', 'none') + for ax in self.axes: + stack.enter_context( + ax.patch._cm_set(facecolor='none', edgecolor='none')) - self.canvas.print_figure(fname, **kwargs) - - if transparent: - for ax, cc in zip(self.axes, original_axes_colors): - ax.patch.set_facecolor(cc[0]) - ax.patch.set_edgecolor(cc[1]) + self.canvas.print_figure(fname, **kwargs) def ginput(self, n=1, timeout=30, show_clicks=True, mouse_add=MouseButton.LEFT, @@ -3045,7 +3368,7 @@ def ginput(self, n=1, timeout=30, show_clicks=True, clicks until the input is terminated manually. timeout : float, default: 30 seconds Number of seconds to wait before timing out. If zero or negative - will never timeout. + will never time out. show_clicks : bool, default: True If True, show a red cross at the location of each click. mouse_add : `.MouseButton` or None, default: `.MouseButton.LEFT` @@ -3064,16 +3387,56 @@ def ginput(self, n=1, timeout=30, show_clicks=True, ----- The keyboard can also be used to select points in case your mouse does not have one or more of the buttons. The delete and backspace - keys act like right clicking (i.e., remove last point), the enter key + keys act like right-clicking (i.e., remove last point), the enter key terminates input and any other key (not already used by the window manager) selects a point. """ - blocking_mouse_input = BlockingMouseInput(self, - mouse_add=mouse_add, - mouse_pop=mouse_pop, - mouse_stop=mouse_stop) - return blocking_mouse_input(n=n, timeout=timeout, - show_clicks=show_clicks) + clicks = [] + marks = [] + + def handler(event): + is_button = event.name == "button_press_event" + is_key = event.name == "key_press_event" + # Quit (even if not in infinite mode; this is consistent with + # MATLAB and sometimes quite useful, but will require the user to + # test how many points were actually returned before using data). + if (is_button and event.button == mouse_stop + or is_key and event.key in ["escape", "enter"]): + self.canvas.stop_event_loop() + # Pop last click. + elif (is_button and event.button == mouse_pop + or is_key and event.key in ["backspace", "delete"]): + if clicks: + clicks.pop() + if show_clicks: + marks.pop().remove() + self.canvas.draw() + # Add new click. + elif (is_button and event.button == mouse_add + # On macOS/gtk, some keys return None. + or is_key and event.key is not None): + if event.inaxes: + clicks.append((event.xdata, event.ydata)) + _log.info("input %i: %f, %f", + len(clicks), event.xdata, event.ydata) + if show_clicks: + line = mpl.lines.Line2D([event.xdata], [event.ydata], + marker="+", color="r") + event.inaxes.add_line(line) + marks.append(line) + self.canvas.draw() + if len(clicks) == n and n > 0: + self.canvas.stop_event_loop() + + _blocking_input.blocking_input_loop( + self, ["button_press_event", "key_press_event"], timeout, handler) + + # Cleanup. + for mark in marks: + mark.remove() + self.canvas.draw() + + return clicks def waitforbuttonpress(self, timeout=-1): """ @@ -3083,43 +3446,32 @@ def waitforbuttonpress(self, timeout=-1): mouse button was pressed and None if no input was given within *timeout* seconds. Negative values deactivate *timeout*. """ - blocking_input = BlockingKeyMouseInput(self) - return blocking_input(timeout=timeout) + event = None + + def handler(ev): + nonlocal event + event = ev + self.canvas.stop_event_loop() + + _blocking_input.blocking_input_loop( + self, ["button_press_event", "key_press_event"], timeout, handler) - def init_layoutgrid(self): - """Initialize the layoutgrid for use in constrained_layout.""" - del(self._layoutgrid) - self._layoutgrid = layoutgrid.LayoutGrid( - parent=None, name='figlb') + return None if event is None else event.name == "key_press_event" + @_api.deprecated("3.6", alternative="figure.get_layout_engine().execute()") def execute_constrained_layout(self, renderer=None): """ Use ``layoutgrid`` to determine pos positions within Axes. See also `.set_constrained_layout_pads`. - """ - - from matplotlib._constrained_layout import do_constrained_layout - from matplotlib.tight_layout import get_renderer - _log.debug('Executing constrainedlayout') - if self._layoutgrid is None: - _api.warn_external("Calling figure.constrained_layout, but " - "figure not setup to do constrained layout. " - "You either called GridSpec without the " - "figure keyword, you are using plt.subplot, " - "or you need to call figure or subplots " - "with the constrained_layout=True kwarg.") - return - w_pad, h_pad, wspace, hspace = self.get_constrained_layout_pads() - # convert to unit-relative lengths - fig = self - width, height = fig.get_size_inches() - w_pad = w_pad / width - h_pad = h_pad / height - if renderer is None: - renderer = get_renderer(fig) - do_constrained_layout(fig, renderer, h_pad, w_pad, hspace, wspace) + Returns + ------- + layoutgrid : private debugging object + """ + if not isinstance(self.get_layout_engine(), ConstrainedLayoutEngine): + return None + return self.get_layout_engine().execute(self) def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None): """ @@ -3143,29 +3495,23 @@ def tight_layout(self, *, pad=1.08, h_pad=None, w_pad=None, rect=None): See Also -------- - .Figure.set_tight_layout + .Figure.set_layout_engine .pyplot.tight_layout """ - - from .tight_layout import ( - get_renderer, get_subplotspec_list, get_tight_layout_figure) - from contextlib import suppress - subplotspec_list = get_subplotspec_list(self.axes) - if None in subplotspec_list: - _api.warn_external("This figure includes Axes that are not " - "compatible with tight_layout, so results " - "might be incorrect.") - - renderer = get_renderer(self) - ctx = (renderer._draw_disabled() - if hasattr(renderer, '_draw_disabled') - else suppress()) - with ctx: - kwargs = get_tight_layout_figure( - self, self.axes, subplotspec_list, renderer, - pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) - if kwargs: - self.subplots_adjust(**kwargs) + # note that here we do not permanently set the figures engine to + # tight_layout but rather just perform the layout in place and remove + # any previous engines. + engine = TightLayoutEngine(pad=pad, h_pad=h_pad, w_pad=w_pad, + rect=rect) + try: + previous_engine = self.get_layout_engine() + self.set_layout_engine(engine) + engine.execute(self) + if not isinstance(previous_engine, TightLayoutEngine) \ + and previous_engine is not None: + _api.warn_external('The figure layout has changed to tight') + finally: + self.set_layout_engine(None) def figaspect(arg): @@ -3246,6 +3592,3 @@ def figaspect(arg): # the min/max dimensions (we don't want figures 10 feet tall!) newsize = np.clip(newsize, figsize_min, figsize_max) return newsize - - -docstring.interpd.update(Figure_kwdoc=martist.kwdoc(Figure)) diff --git a/lib/matplotlib/font_manager.py b/lib/matplotlib/font_manager.py index 2b65c36bf916..8a4b52e96f32 100644 --- a/lib/matplotlib/font_manager.py +++ b/lib/matplotlib/font_manager.py @@ -1,7 +1,7 @@ """ A module for finding, managing, and using fonts across platforms. -This module provides a single `FontManager` instance that can +This module provides a single `FontManager` instance, ``fontManager``, that can be shared across backends and platforms. The `findfont` function returns the best TrueType (TTF) font file in the local or system font path that matches the specified `FontProperties` @@ -23,7 +23,11 @@ # - setWeights function needs improvement # - 'light' is an invalid weight value, remove it. +from base64 import b64encode +import copy +import dataclasses from functools import lru_cache +from io import BytesIO import json import logging from numbers import Number @@ -32,16 +36,11 @@ import re import subprocess import sys -try: - import threading - from threading import Timer -except ImportError: - import dummy_threading as threading - from dummy_threading import Timer +import threading import matplotlib as mpl -from matplotlib import _api, afm, cbook, ft2font, rcParams -from matplotlib.fontconfig_pattern import ( +from matplotlib import _api, _afm, cbook, ft2font +from matplotlib._fontconfig_pattern import ( parse_fontconfig_pattern, generate_fontconfig_pattern) from matplotlib.rcsetup import _validators @@ -168,14 +167,9 @@ ] -@lru_cache(64) -def _cached_realpath(path): - return os.path.realpath(path) - - def get_fontext_synonyms(fontext): """ - Return a list of file extensions extensions that are synonyms for + Return a list of file extensions that are synonyms for the given file extension *fileext*. """ return { @@ -216,88 +210,41 @@ def win32FontDirectory(): return os.path.join(os.environ['WINDIR'], 'Fonts') -def _win32RegistryFonts(reg_domain, base_dir): - r""" - Search for fonts in the Windows registry. - - Parameters - ---------- - reg_domain : int - The top level registry domain (e.g. HKEY_LOCAL_MACHINE). - - base_dir : str - The path to the folder where the font files are usually located (e.g. - C:\Windows\Fonts). If only the filename of the font is stored in the - registry, the absolute path is built relative to this base directory. - - Returns - ------- - `set` - `pathlib.Path` objects with the absolute path to the font files found. - - """ +def _get_win32_installed_fonts(): + """List the font paths known to the Windows registry.""" import winreg items = set() - - for reg_path in MSFontDirectories: - try: - with winreg.OpenKey(reg_domain, reg_path) as local: - for j in range(winreg.QueryInfoKey(local)[1]): - # value may contain the filename of the font or its - # absolute path. - key, value, tp = winreg.EnumValue(local, j) - if not isinstance(value, str): - continue - - # Work around for https://bugs.python.org/issue25778, which - # is fixed in Py>=3.6.1. - value = value.split("\0", 1)[0] - - try: - # If value contains already an absolute path, then it - # is not changed further. - path = Path(base_dir, value).resolve() - except RuntimeError: - # Don't fail with invalid entries. - continue - - items.add(path) - except (OSError, MemoryError): - continue - + # Search and resolve fonts listed in the registry. + for domain, base_dirs in [ + (winreg.HKEY_LOCAL_MACHINE, [win32FontDirectory()]), # System. + (winreg.HKEY_CURRENT_USER, MSUserFontDirectories), # User. + ]: + for base_dir in base_dirs: + for reg_path in MSFontDirectories: + try: + with winreg.OpenKey(domain, reg_path) as local: + for j in range(winreg.QueryInfoKey(local)[1]): + # value may contain the filename of the font or its + # absolute path. + key, value, tp = winreg.EnumValue(local, j) + if not isinstance(value, str): + continue + try: + # If value contains already an absolute path, + # then it is not changed further. + path = Path(base_dir, value).resolve() + except RuntimeError: + # Don't fail with invalid entries. + continue + items.add(path) + except (OSError, MemoryError): + continue return items -def win32InstalledFonts(directory=None, fontext='ttf'): - """ - Search for fonts in the specified font directory, or use the - system directories if none given. Additionally, it is searched for user - fonts installed. A list of TrueType font filenames are returned by default, - or AFM fonts if *fontext* == 'afm'. - """ - import winreg - - if directory is None: - directory = win32FontDirectory() - - fontext = ['.' + ext for ext in get_fontext_synonyms(fontext)] - - items = set() - - # System fonts - items.update(_win32RegistryFonts(winreg.HKEY_LOCAL_MACHINE, directory)) - - # User fonts - for userdir in MSUserFontDirectories: - items.update(_win32RegistryFonts(winreg.HKEY_CURRENT_USER, userdir)) - - # Keep only paths with matching file extension. - return [str(path) for path in items if path.suffix.lower() in fontext] - - @lru_cache() -def _call_fc_list(): - """Cache and list the font filenames known to `fc-list`.""" +def _get_fontconfig_fonts(): + """Cache and list the font paths known to ``fc-list``.""" try: if b'--format' not in subprocess.check_output(['fc-list', '--help']): _log.warning( # fontconfig 2.7 implemented --format. @@ -306,14 +253,7 @@ def _call_fc_list(): out = subprocess.check_output(['fc-list', '--format=%{file}\\n']) except (OSError, subprocess.CalledProcessError): return [] - return [os.fsdecode(fname) for fname in out.split(b'\n')] - - -def get_fontconfig_fonts(fontext='ttf'): - """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] + return [Path(os.fsdecode(fname)) for fname in out.split(b'\n')] def findSystemFonts(fontpaths=None, fontext='ttf'): @@ -329,14 +269,16 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): if fontpaths is None: if sys.platform == 'win32': - fontpaths = MSUserFontDirectories + [win32FontDirectory()] - # now get all installed fonts directly... - fontfiles.update(win32InstalledFonts(fontext=fontext)) + installed_fonts = _get_win32_installed_fonts() + fontpaths = [] else: - fontpaths = X11FontDirectories + installed_fonts = _get_fontconfig_fonts() if sys.platform == 'darwin': fontpaths = [*X11FontDirectories, *OSXFontDirectories] - fontfiles.update(get_fontconfig_fonts(fontext)) + else: + fontpaths = X11FontDirectories + fontfiles.update(str(path) for path in installed_fonts + if path.suffix.lower()[1:] in fontexts) elif isinstance(fontpaths, str): fontpaths = [fontpaths] @@ -347,35 +289,42 @@ def findSystemFonts(fontpaths=None, fontext='ttf'): return [fname for fname in fontfiles if os.path.exists(fname)] -class FontEntry: - """ - A class for storing Font properties. It is used when populating - the font lookup dictionary. - """ - def __init__(self, - fname ='', - name ='', - style ='normal', - variant='normal', - weight ='normal', - stretch='normal', - size ='medium', - ): - self.fname = fname - self.name = name - self.style = style - self.variant = variant - self.weight = weight - self.stretch = stretch - try: - self.size = str(float(size)) - except ValueError: - self.size = size - - def __repr__(self): - return "" % ( - self.name, os.path.basename(self.fname), self.style, self.variant, - self.weight, self.stretch) +def _fontentry_helper_repr_png(fontent): + from matplotlib.figure import Figure # Circular import. + fig = Figure() + font_path = Path(fontent.fname) if fontent.fname != '' else None + fig.text(0, 0, fontent.name, font=font_path) + with BytesIO() as buf: + fig.savefig(buf, bbox_inches='tight', transparent=True) + return buf.getvalue() + + +def _fontentry_helper_repr_html(fontent): + png_stream = _fontentry_helper_repr_png(fontent) + png_b64 = b64encode(png_stream).decode() + return f"" + + +FontEntry = dataclasses.make_dataclass( + 'FontEntry', [ + ('fname', str, dataclasses.field(default='')), + ('name', str, dataclasses.field(default='')), + ('style', str, dataclasses.field(default='normal')), + ('variant', str, dataclasses.field(default='normal')), + ('weight', str, dataclasses.field(default='normal')), + ('stretch', str, dataclasses.field(default='normal')), + ('size', str, dataclasses.field(default='medium')), + ], + namespace={ + '__doc__': """ + A class for storing Font properties. + + It is used when populating the font lookup dictionary. + """, + '_repr_html_': lambda self: _fontentry_helper_repr_html(self), + '_repr_png_': lambda self: _fontentry_helper_repr_png(self), + } +) def ttfFontProperty(font): @@ -511,7 +460,7 @@ def afmFontProperty(fontpath, font): Parameters ---------- - font : `.AFM` + font : AFM The AFM font file from which information will be extracted. Returns @@ -583,33 +532,34 @@ class FontProperties: specification and *math_fontfamily* for math fonts: - family: A list of font names in decreasing order of priority. - The items may include a generic font family name, either - 'sans-serif' (default), 'serif', 'cursive', 'fantasy', or 'monospace'. - In that case, the actual font to be used will be looked up - from the associated rcParam. + The items may include a generic font family name, either 'sans-serif', + 'serif', 'cursive', 'fantasy', or 'monospace'. In that case, the actual + font to be used will be looked up from the associated rcParam during the + search process in `.findfont`. Default: :rc:`font.family` - - style: Either 'normal' (default), 'italic' or 'oblique'. + - style: Either 'normal', 'italic' or 'oblique'. + Default: :rc:`font.style` - - variant: Either 'normal' (default) or 'small-caps'. + - variant: Either 'normal' or 'small-caps'. + Default: :rc:`font.variant` - stretch: A numeric value in the range 0-1000 or one of 'ultra-condensed', 'extra-condensed', 'condensed', - 'semi-condensed', 'normal' (default), 'semi-expanded', 'expanded', - 'extra-expanded' or 'ultra-expanded'. + 'semi-condensed', 'normal', 'semi-expanded', 'expanded', + 'extra-expanded' or 'ultra-expanded'. Default: :rc:`font.stretch` - weight: A numeric value in the range 0-1000 or one of - 'ultralight', 'light', 'normal' (default), 'regular', 'book', 'medium', + 'ultralight', 'light', 'normal', 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', - 'extra bold', 'black'. + 'extra bold', 'black'. Default: :rc:`font.weight` - - size: Either an relative value of 'xx-small', 'x-small', + - size: Either a relative value of 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large' or an - absolute font size, e.g., 10 (default). + absolute font size, e.g., 10. Default: :rc:`font.size` - - math_fontfamily: The family of fonts used to render math text; overrides - :rc:`mathtext.fontset`. Supported values are the same as the ones - supported by :rc:`mathtext.fontset`: 'dejavusans', 'dejavuserif', 'cm', - 'stix', 'stixsans' and 'custom'. + - math_fontfamily: The family of fonts used to render math text. + Supported values are: 'dejavusans', 'dejavuserif', 'cm', + 'stix', 'stixsans' and 'custom'. Default: :rc:`mathtext.fontset` Alternatively, a font may be specified using the absolute path to a font file, by using the *fname* kwarg. However, in this case, it is typically @@ -635,33 +585,10 @@ class FontProperties: fontconfig. """ - def __init__(self, - family = None, - style = None, - variant= None, - weight = None, - stretch= None, - size = None, - fname = None, # if set, it's a hardcoded filename to use - math_fontfamily = None, - ): - self._family = _normalize_font_family(rcParams['font.family']) - self._slant = rcParams['font.style'] - self._variant = rcParams['font.variant'] - self._weight = rcParams['font.weight'] - self._stretch = rcParams['font.stretch'] - self._size = rcParams['font.size'] - self._file = None - self._math_fontfamily = None - - if isinstance(family, str): - # Treat family as a fontconfig pattern if it is the only - # parameter provided. - if (style is None and variant is None and weight is None and - stretch is None and size is None and fname is None): - self.set_fontconfig_pattern(family) - return - + def __init__(self, family=None, style=None, variant=None, weight=None, + stretch=None, size=None, + fname=None, # if set, it's a hardcoded filename to use + math_fontfamily=None): self.set_family(family) self.set_style(style) self.set_variant(variant) @@ -670,6 +597,13 @@ def __init__(self, self.set_file(fname) self.set_size(size) self.set_math_fontfamily(math_fontfamily) + # Treat family as a fontconfig pattern if it is the only parameter + # provided. Even in that case, call the other setters first to set + # attributes not specified by the pattern to the rcParams defaults. + if (isinstance(family, str) + and style is None and variant is None and weight is None + and stretch is None and size is None and fname is None): + self.set_fontconfig_pattern(family) @classmethod def _from_any(cls, arg): @@ -700,7 +634,7 @@ def __hash__(self): self.get_variant(), self.get_weight(), self.get_stretch(), - self.get_size_in_points(), + self.get_size(), self.get_file(), self.get_math_fontfamily()) return hash(l) @@ -713,7 +647,11 @@ def __str__(self): def get_family(self): """ - Return a list of font names that comprise the font family. + Return a list of individual font family names or generic family names. + + The font families or generic font families (which will be resolved + from their respective rcParams when searching for a matching font) in + the order of preference. """ return self._family @@ -728,7 +666,6 @@ def get_style(self): Return the font style. Values are: 'normal', 'italic' or 'oblique'. """ return self._slant - get_slant = get_style def get_variant(self): """ @@ -759,9 +696,6 @@ def get_size(self): """ return self._size - def get_size_in_points(self): - return self._size - def get_file(self): """ Return the filename of the associated font. @@ -780,80 +714,109 @@ def get_fontconfig_pattern(self): def set_family(self, family): """ - Change the font family. May be either an alias (generic name + Change the font family. Can be either an alias (generic name is CSS parlance), such as: 'serif', 'sans-serif', 'cursive', 'fantasy', or 'monospace', a real font name or a list of real font names. Real font names are not supported when - :rc:`text.usetex` is `True`. + :rc:`text.usetex` is `True`. Default: :rc:`font.family` """ if family is None: - family = rcParams['font.family'] - self._family = _normalize_font_family(family) - set_name = set_family + family = mpl.rcParams['font.family'] + if isinstance(family, str): + family = [family] + self._family = family def set_style(self, style): """ - Set the font style. Values are: 'normal', 'italic' or 'oblique'. + Set the font style. + + Parameters + ---------- + style : {'normal', 'italic', 'oblique'}, default: :rc:`font.style` """ if style is None: - style = rcParams['font.style'] + style = mpl.rcParams['font.style'] _api.check_in_list(['normal', 'italic', 'oblique'], style=style) self._slant = style - set_slant = set_style def set_variant(self, variant): """ - Set the font variant. Values are: 'normal' or 'small-caps'. + Set the font variant. + + Parameters + ---------- + variant : {'normal', 'small-caps'}, default: :rc:`font.variant` """ if variant is None: - variant = rcParams['font.variant'] + variant = mpl.rcParams['font.variant'] _api.check_in_list(['normal', 'small-caps'], variant=variant) self._variant = variant def set_weight(self, weight): """ - Set the font weight. May be either a numeric value in the - range 0-1000 or one of 'ultralight', 'light', 'normal', - 'regular', 'book', 'medium', 'roman', 'semibold', 'demibold', - 'demi', 'bold', 'heavy', 'extra bold', 'black' + Set the font weight. + + Parameters + ---------- + weight : int or {'ultralight', 'light', 'normal', 'regular', 'book', \ +'medium', 'roman', 'semibold', 'demibold', 'demi', 'bold', 'heavy', \ +'extra bold', 'black'}, default: :rc:`font.weight` + If int, must be in the range 0-1000. """ if weight is None: - weight = rcParams['font.weight'] + weight = mpl.rcParams['font.weight'] + if weight in weight_dict: + self._weight = weight + return try: weight = int(weight) - if weight < 0 or weight > 1000: - raise ValueError() except ValueError: - if weight not in weight_dict: - raise ValueError("weight is invalid") - self._weight = weight + pass + else: + if 0 <= weight <= 1000: + self._weight = weight + return + raise ValueError(f"{weight=} is invalid") def set_stretch(self, stretch): """ - Set the font stretch or width. Options are: 'ultra-condensed', - 'extra-condensed', 'condensed', 'semi-condensed', 'normal', - 'semi-expanded', 'expanded', 'extra-expanded' or - 'ultra-expanded', or a numeric value in the range 0-1000. + Set the font stretch or width. + + Parameters + ---------- + stretch : int or {'ultra-condensed', 'extra-condensed', 'condensed', \ +'semi-condensed', 'normal', 'semi-expanded', 'expanded', 'extra-expanded', \ +'ultra-expanded'}, default: :rc:`font.stretch` + If int, must be in the range 0-1000. """ if stretch is None: - stretch = rcParams['font.stretch'] + stretch = mpl.rcParams['font.stretch'] + if stretch in stretch_dict: + self._stretch = stretch + return try: stretch = int(stretch) - if stretch < 0 or stretch > 1000: - raise ValueError() - except ValueError as err: - if stretch not in stretch_dict: - raise ValueError("stretch is invalid") from err - self._stretch = stretch + except ValueError: + pass + else: + if 0 <= stretch <= 1000: + self._stretch = stretch + return + raise ValueError(f"{stretch=} is invalid") def set_size(self, size): """ - Set the font size. Either an relative value of 'xx-small', - 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large' - or an absolute font size, e.g., 12. + Set the font size. + + Parameters + ---------- + size : float or {'xx-small', 'x-small', 'small', 'medium', \ +'large', 'x-large', 'xx-large'}, default: :rc:`font.size` + If a float, the font size in points. The string values denote + sizes relative to the default font size. """ if size is None: - size = rcParams['font.size'] + size = mpl.rcParams['font.size'] try: size = float(size) except ValueError: @@ -919,7 +882,7 @@ def set_math_fontfamily(self, fontfamily): .text.Text.get_math_fontfamily """ if fontfamily is None: - fontfamily = rcParams['mathtext.fontset'] + fontfamily = mpl.rcParams['mathtext.fontset'] else: valid_fonts = _validators['mathtext.fontset'].valid.values() # _check_in_list() Validates the parameter math_fontfamily as @@ -929,9 +892,13 @@ def set_math_fontfamily(self, fontfamily): def copy(self): """Return a copy of self.""" - new = type(self)() - vars(new).update(vars(self)) - return new + return copy.copy(self) + + # Aliases + set_name = set_family + get_slant = get_style + set_slant = set_style + get_size_in_points = get_size class _JSONEncoder(json.JSONEncoder): @@ -1002,16 +969,10 @@ def json_load(filename): -------- json_dump """ - with open(filename, 'r') as fh: + with open(filename) as fh: return json.load(fh, object_hook=_json_decode) -def _normalize_font_family(family): - if isinstance(family, str): - family = [family] - return family - - class FontManager: """ On import, the `FontManager` singleton instance creates a list of ttf and @@ -1021,7 +982,7 @@ class FontManager: font is returned. """ # Increment this version number whenever the font cache data - # format or behavior has changed and requires a existing font + # format or behavior has changed and requires an existing font # cache files to be rebuilt. __version__ = 330 @@ -1031,23 +992,10 @@ def __init__(self, size=None, weight='normal'): self.__default_weight = weight self.default_size = size + # Create list of font paths. paths = [cbook._get_data_path('fonts', subdir) for subdir in ['ttf', 'afm', 'pdfcorefonts']] - # Create list of font paths - for pathname in ['TTFPATH', 'AFMPATH']: - if pathname in os.environ: - ttfpath = os.environ[pathname] - if ttfpath.find(';') >= 0: # win32 style - paths.extend(ttfpath.split(';')) - elif ttfpath.find(':') >= 0: # unix style - paths.extend(ttfpath.split(':')) - else: - paths.append(ttfpath) - _api.warn_deprecated( - "3.3", name=pathname, obj_type="environment variable", - alternative="FontManager.addfont()") - _log.debug('font search path %s', str(paths)) - # Load TrueType fonts and create font dictionary. + _log.debug('font search path %s', paths) self.defaultFamily = { 'ttf': 'DejaVu Sans', @@ -1057,7 +1005,7 @@ def __init__(self, size=None, weight='normal'): self.ttflist = [] # Delay the warning by 5s. - timer = Timer(5, lambda: _log.warning( + timer = threading.Timer(5, lambda: _log.warning( 'Matplotlib is building the font cache; this may take a moment.')) timer.start() try: @@ -1083,15 +1031,19 @@ def addfont(self, path): ---------- path : str or path-like """ + # Convert to string in case of a path as + # afmFontProperty and FT2Font expect this + path = os.fsdecode(path) if Path(path).suffix.lower() == ".afm": with open(path, "rb") as fh: - font = afm.AFM(fh) + font = _afm.AFM(fh) prop = afmFontProperty(path, font) self.afmlist.append(prop) else: font = ft2font.FT2Font(path) prop = ttfFontProperty(font) self.ttflist.append(prop) + self._findfont_cached.cache_clear() @property def defaultFont(self): @@ -1111,7 +1063,7 @@ def get_default_size(): """ Return the default font size. """ - return rcParams['font.size'] + return mpl.rcParams['font.size'] def set_default_weight(self, weight): """ @@ -1123,7 +1075,7 @@ def set_default_weight(self, weight): def _expand_aliases(family): if family in ('sans', 'sans serif'): family = 'sans-serif' - return rcParams['font.' + family] + return mpl.rcParams['font.' + family] # Each of the scoring functions below should return a value between # 0.0 (perfect match) and 1.0 (terrible match) @@ -1267,7 +1219,7 @@ def findfont(self, prop, fontext='ttf', directory=None, If given, only search this directory and its subdirectories. fallback_to_default : bool - If True, will fallback to the default font family (usually + If True, will fall back to the default font family (usually "DejaVu Sans" or "Helvetica") if the first lookup hard-fails. rebuild_if_missing : bool @@ -1301,14 +1253,111 @@ def findfont(self, prop, fontext='ttf', directory=None, # Pass the relevant rcParams (and the font manager, as `self`) to # _findfont_cached so to prevent using a stale cache entry after an # rcParam was changed. - rc_params = tuple(tuple(rcParams[key]) for key in [ + rc_params = tuple(tuple(mpl.rcParams[key]) for key in [ "font.serif", "font.sans-serif", "font.cursive", "font.fantasy", "font.monospace"]) - return self._findfont_cached( + ret = self._findfont_cached( prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params) + if isinstance(ret, Exception): + raise ret + return ret - @lru_cache() + def get_font_names(self): + """Return the list of available fonts.""" + return list(set([font.name for font in self.ttflist])) + + def _find_fonts_by_props(self, prop, fontext='ttf', directory=None, + fallback_to_default=True, rebuild_if_missing=True): + """ + Find font families that most closely match the given properties. + + Parameters + ---------- + prop : str or `~matplotlib.font_manager.FontProperties` + The font properties to search for. This can be either a + `.FontProperties` object or a string defining a + `fontconfig patterns`_. + + fontext : {'ttf', 'afm'}, default: 'ttf' + The extension of the font file: + + - 'ttf': TrueType and OpenType fonts (.ttf, .ttc, .otf) + - 'afm': Adobe Font Metrics (.afm) + + directory : str, optional + If given, only search this directory and its subdirectories. + + fallback_to_default : bool + If True, will fall back to the default font family (usually + "DejaVu Sans" or "Helvetica") if none of the families were found. + + rebuild_if_missing : bool + Whether to rebuild the font cache and search again if the first + match appears to point to a nonexisting font (i.e., the font cache + contains outdated entries). + + Returns + ------- + list[str] + The paths of the fonts found + + Notes + ----- + This is an extension/wrapper of the original findfont API, which only + returns a single font for given font properties. Instead, this API + returns a dict containing multiple fonts and their filepaths + which closely match the given font properties. Since this internally + uses the original API, there's no change to the logic of performing the + nearest neighbor search. See `findfont` for more details. + """ + + prop = FontProperties._from_any(prop) + + fpaths = [] + for family in prop.get_family(): + cprop = prop.copy() + cprop.set_family(family) # set current prop's family + + try: + fpaths.append( + self.findfont( + cprop, fontext, directory, + fallback_to_default=False, # don't fallback to default + rebuild_if_missing=rebuild_if_missing, + ) + ) + except ValueError: + if family in font_family_aliases: + _log.warning( + "findfont: Generic family %r not found because " + "none of the following families were found: %s", + family, ", ".join(self._expand_aliases(family)) + ) + else: + _log.warning("findfont: Font family %r not found.", family) + + # only add default family if no other font was found and + # fallback_to_default is enabled + if not fpaths: + if fallback_to_default: + dfamily = self.defaultFamily[fontext] + cprop = prop.copy() + cprop.set_family(dfamily) + fpaths.append( + self.findfont( + cprop, fontext, directory, + fallback_to_default=True, + rebuild_if_missing=rebuild_if_missing, + ) + ) + else: + raise ValueError("Failed to find any font, and fallback " + "to the default font was disabled") + + return fpaths + + @lru_cache(1024) def _findfont_cached(self, prop, fontext, directory, fallback_to_default, rebuild_if_missing, rc_params): @@ -1361,8 +1410,11 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, return self.findfont(default_prop, fontext, directory, fallback_to_default=False) else: - raise ValueError(f"Failed to find font {prop}, and fallback " - f"to the default font was disabled") + # This return instead of raise is intentional, as we wish to + # cache the resulting exception, which will not occur if it was + # actually raised. + return ValueError(f"Failed to find font {prop}, and fallback " + f"to the default font was disabled") else: _log.debug('findfont: Matching %s to %s (%r) with score of %f.', prop, best_font.name, best_font.fname, best_score) @@ -1381,7 +1433,10 @@ def _findfont_cached(self, prop, fontext, directory, fallback_to_default, return self.findfont( prop, fontext, directory, rebuild_if_missing=False) else: - raise ValueError("No valid font could be found") + # This return instead of raise is intentional, as we wish to + # cache the resulting exception, which will not occur if it was + # actually raised. + return ValueError("No valid font could be found") return _cached_realpath(result) @@ -1401,29 +1456,75 @@ def is_opentype_cff_font(filename): @lru_cache(64) -def _get_font(filename, hinting_factor, *, _kerning_factor, thread_id): +def _get_font(font_filepaths, hinting_factor, *, _kerning_factor, thread_id): + first_fontpath, *rest = font_filepaths return ft2font.FT2Font( - filename, hinting_factor, _kerning_factor=_kerning_factor) + first_fontpath, hinting_factor, + _fallback_list=[ + ft2font.FT2Font( + fpath, hinting_factor, + _kerning_factor=_kerning_factor + ) + for fpath in rest + ], + _kerning_factor=_kerning_factor + ) # FT2Font objects cannot be used across fork()s because they reference the same # FT_Library object. While invalidating *all* existing FT2Fonts after a fork # would be too complicated to be worth it, the main way FT2Fonts get reused is -# via the cache of _get_font, which we can empty upon forking (in Py3.7+). +# via the cache of _get_font, which we can empty upon forking (not on Windows, +# which has no fork() or register_at_fork()). if hasattr(os, "register_at_fork"): os.register_at_fork(after_in_child=_get_font.cache_clear) -def get_font(filename, hinting_factor=None): +@lru_cache(64) +def _cached_realpath(path): # Resolving the path avoids embedding the font twice in pdf/ps output if a # single font is selected using two different relative paths. - filename = _cached_realpath(filename) + return os.path.realpath(path) + + +@_api.rename_parameter('3.6', "filepath", "font_filepaths") +def get_font(font_filepaths, hinting_factor=None): + """ + Get an `.ft2font.FT2Font` object given a list of file paths. + + Parameters + ---------- + font_filepaths : Iterable[str, Path, bytes], str, Path, bytes + Relative or absolute paths to the font files to be used. + + If a single string, bytes, or `pathlib.Path`, then it will be treated + as a list with that entry only. + + If more than one filepath is passed, then the returned FT2Font object + will fall back through the fonts, in the order given, to find a needed + glyph. + + Returns + ------- + `.ft2font.FT2Font` + + """ + if isinstance(font_filepaths, (str, Path, bytes)): + paths = (_cached_realpath(font_filepaths),) + else: + paths = tuple(_cached_realpath(fname) for fname in font_filepaths) + if hinting_factor is None: - hinting_factor = rcParams['text.hinting_factor'] - # also key on the thread ID to prevent segfaults with multi-threading - return _get_font(filename, hinting_factor, - _kerning_factor=rcParams['text.kerning_factor'], - thread_id=threading.get_ident()) + hinting_factor = mpl.rcParams['text.hinting_factor'] + + return _get_font( + # must be a tuple to be cached + paths, + hinting_factor, + _kerning_factor=mpl.rcParams['text.kerning_factor'], + # also key on the thread ID to prevent segfaults with multi-threading + thread_id=threading.get_ident() + ) def _load_fontmanager(*, try_read_cache=True): @@ -1432,7 +1533,7 @@ def _load_fontmanager(*, try_read_cache=True): if try_read_cache: try: fm = json_load(fm_path) - except Exception as exc: + except Exception: pass else: if getattr(fm, "_version", object()) == FontManager.__version__: @@ -1446,3 +1547,4 @@ def _load_fontmanager(*, try_read_cache=True): fontManager = _load_fontmanager() findfont = fontManager.findfont +get_font_names = fontManager.get_font_names diff --git a/lib/matplotlib/fontconfig_pattern.py b/lib/matplotlib/fontconfig_pattern.py index c47e19bf99dc..292435b1487a 100644 --- a/lib/matplotlib/fontconfig_pattern.py +++ b/lib/matplotlib/fontconfig_pattern.py @@ -1,209 +1,20 @@ -""" -A module for parsing and generating `fontconfig patterns`_. - -.. _fontconfig patterns: - https://www.freedesktop.org/software/fontconfig/fontconfig-user.html -""" - -# This class logically belongs in `matplotlib.font_manager`, but placing it -# there would have created cyclical dependency problems, because it also needs -# to be available from `matplotlib.rcsetup` (for parsing matplotlibrc files). - -from functools import lru_cache import re -import numpy as np -from pyparsing import (Literal, ZeroOrMore, Optional, Regex, StringEnd, - ParseException, Suppress) - -family_punc = r'\\\-:,' -family_unescape = re.compile(r'\\([%s])' % family_punc).sub -family_escape = re.compile(r'([%s])' % family_punc).sub - -value_punc = r'\\=_:,' -value_unescape = re.compile(r'\\([%s])' % value_punc).sub -value_escape = re.compile(r'([%s])' % value_punc).sub - - -class FontconfigPatternParser: - """ - A simple pyparsing-based parser for `fontconfig patterns`_. - - .. _fontconfig patterns: - https://www.freedesktop.org/software/fontconfig/fontconfig-user.html - """ - - _constants = { - 'thin': ('weight', 'light'), - 'extralight': ('weight', 'light'), - 'ultralight': ('weight', 'light'), - 'light': ('weight', 'light'), - 'book': ('weight', 'book'), - 'regular': ('weight', 'regular'), - 'normal': ('weight', 'normal'), - 'medium': ('weight', 'medium'), - 'demibold': ('weight', 'demibold'), - 'semibold': ('weight', 'semibold'), - 'bold': ('weight', 'bold'), - 'extrabold': ('weight', 'extra bold'), - 'black': ('weight', 'black'), - 'heavy': ('weight', 'heavy'), - 'roman': ('slant', 'normal'), - 'italic': ('slant', 'italic'), - 'oblique': ('slant', 'oblique'), - 'ultracondensed': ('width', 'ultra-condensed'), - 'extracondensed': ('width', 'extra-condensed'), - 'condensed': ('width', 'condensed'), - 'semicondensed': ('width', 'semi-condensed'), - 'expanded': ('width', 'expanded'), - 'extraexpanded': ('width', 'extra-expanded'), - 'ultraexpanded': ('width', 'ultra-expanded') - } - - def __init__(self): - - family = Regex( - r'([^%s]|(\\[%s]))*' % (family_punc, family_punc) - ).setParseAction(self._family) - - size = Regex( - r"([0-9]+\.?[0-9]*|\.[0-9]+)" - ).setParseAction(self._size) - - name = Regex( - r'[a-z]+' - ).setParseAction(self._name) - - value = Regex( - r'([^%s]|(\\[%s]))*' % (value_punc, value_punc) - ).setParseAction(self._value) - - families = ( - family - + ZeroOrMore( - Literal(',') - + family) - ).setParseAction(self._families) - - point_sizes = ( - size - + ZeroOrMore( - Literal(',') - + size) - ).setParseAction(self._point_sizes) - - property = ( - (name - + Suppress(Literal('=')) - + value - + ZeroOrMore( - Suppress(Literal(',')) - + value)) - | name - ).setParseAction(self._property) +from pyparsing import ParseException - pattern = ( - Optional( - families) - + Optional( - Literal('-') - + point_sizes) - + ZeroOrMore( - Literal(':') - + property) - + StringEnd() - ) +from matplotlib._fontconfig_pattern import * # noqa: F401, F403 +from matplotlib._fontconfig_pattern import ( + parse_fontconfig_pattern, _family_punc, _value_punc) +from matplotlib import _api +_api.warn_deprecated("3.6", name=__name__, obj_type="module") - self._parser = pattern - self.ParseException = ParseException - def parse(self, pattern): - """ - Parse the given fontconfig *pattern* and return a dictionary - of key/value pairs useful for initializing a - `.font_manager.FontProperties` object. - """ - props = self._properties = {} - try: - self._parser.parseString(pattern) - except self.ParseException as e: - raise ValueError( - "Could not parse font string: '%s'\n%s" % (pattern, e)) from e +family_unescape = re.compile(r'\\([%s])' % _family_punc).sub +value_unescape = re.compile(r'\\([%s])' % _value_punc).sub +family_escape = re.compile(r'([%s])' % _family_punc).sub +value_escape = re.compile(r'([%s])' % _value_punc).sub - self._properties = None - self._parser.resetCache() - - return props - - def _family(self, s, loc, tokens): - return [family_unescape(r'\1', str(tokens[0]))] - - def _size(self, s, loc, tokens): - return [float(tokens[0])] - - def _name(self, s, loc, tokens): - return [str(tokens[0])] - - def _value(self, s, loc, tokens): - return [value_unescape(r'\1', str(tokens[0]))] - - def _families(self, s, loc, tokens): - self._properties['family'] = [str(x) for x in tokens] - return [] - - def _point_sizes(self, s, loc, tokens): - self._properties['size'] = [str(x) for x in tokens] - return [] - - def _property(self, s, loc, tokens): - if len(tokens) == 1: - if tokens[0] in self._constants: - key, val = self._constants[tokens[0]] - self._properties.setdefault(key, []).append(val) - else: - key = tokens[0] - val = tokens[1:] - self._properties.setdefault(key, []).extend(val) - return [] - - -# `parse_fontconfig_pattern` is a bottleneck during the tests because it is -# repeatedly called when the rcParams are reset (to validate the default -# fonts). In practice, the cache size doesn't grow beyond a few dozen entries -# during the test suite. -parse_fontconfig_pattern = lru_cache()(FontconfigPatternParser().parse) - - -def _escape_val(val, escape_func): - """ - Given a string value or a list of string values, run each value through - the input escape function to make the values into legal font config - strings. The result is returned as a string. - """ - if not np.iterable(val) or isinstance(val, str): - val = [val] - - return ','.join(escape_func(r'\\\1', str(x)) for x in val - if x is not None) - - -def generate_fontconfig_pattern(d): - """ - Given a dictionary of key/value pairs, generates a fontconfig - pattern string. - """ - props = [] - - # Family is added first w/o a keyword - family = d.get_family() - if family is not None and family != []: - props.append(_escape_val(family, family_escape)) - - # The other keys are added as key=value - for key in ['style', 'variant', 'weight', 'stretch', 'file', 'size']: - val = getattr(d, 'get_' + key)() - # Don't use 'if not val' because 0 is a valid input. - if val is not None and val != []: - props.append(":%s=%s" % (key, _escape_val(val, value_escape))) +class FontconfigPatternParser: + ParseException = ParseException - return ''.join(props) + def parse(self, pattern): return parse_fontconfig_pattern(pattern) diff --git a/lib/matplotlib/gridspec.py b/lib/matplotlib/gridspec.py index 973d3d66b157..d4eecaf4b5a2 100644 --- a/lib/matplotlib/gridspec.py +++ b/lib/matplotlib/gridspec.py @@ -5,8 +5,10 @@ The `GridSpec` specifies the overall grid structure. Individual cells within the grid are referenced by `SubplotSpec`\s. -See the tutorial :doc:`/tutorials/intermediate/gridspec` for a comprehensive -usage guide. +Often, users need not access this module directly, and can use higher-level +methods like `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` and +`~.Figure.subfigures`. See the tutorial +:doc:`/tutorials/intermediate/arranging_axes` for a guide. """ import copy @@ -16,10 +18,8 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, _pylab_helpers, tight_layout, rcParams +from matplotlib import _api, _pylab_helpers, _tight_layout from matplotlib.transforms import Bbox -import matplotlib._layoutgrid as layoutgrid - _log = logging.getLogger(__name__) @@ -41,16 +41,16 @@ def __init__(self, nrows, ncols, height_ratios=None, width_ratios=None): relative width of ``width_ratios[i] / sum(width_ratios)``. If not given, all columns will have the same width. height_ratios : array-like of length *nrows*, optional - Defines the relative heights of the rows. Each column gets a + Defines the relative heights of the rows. Each row gets a relative height of ``height_ratios[i] / sum(height_ratios)``. If not given, all rows will have the same height. """ if not isinstance(nrows, Integral) or nrows <= 0: raise ValueError( - f"Number of rows must be a positive integer, not {nrows}") + f"Number of rows must be a positive integer, not {nrows!r}") if not isinstance(ncols, Integral) or ncols <= 0: raise ValueError( - f"Number of columns must be a positive integer, not {ncols}") + f"Number of columns must be a positive integer, not {ncols!r}") self._nrows, self._ncols = nrows, ncols self.set_height_ratios(height_ratios) self.set_width_ratios(width_ratios) @@ -142,6 +142,7 @@ def get_height_ratios(self): """ return self._row_height_ratios + @_api.delete_parameter("3.7", "raw") def get_grid_positions(self, fig, raw=False): """ Return the positions of the grid cells in figure coordinates. @@ -210,8 +211,8 @@ def _check_gridspec_exists(figure, nrows, ncols): or create a new one """ for ax in figure.get_axes(): - if hasattr(ax, 'get_subplotspec'): - gs = ax.get_subplotspec().get_gridspec() + gs = ax.get_gridspec() + if gs is not None: if hasattr(gs, 'get_topmost_subplotspec'): # This is needed for colorbar gridspec layouts. # This is probably OK because this whole logic tree @@ -275,21 +276,12 @@ def subplots(self, *, sharex=False, sharey=False, squeeze=True, raise ValueError("GridSpec.subplots() only works for GridSpecs " "created with a parent figure") - if isinstance(sharex, bool): + if not isinstance(sharex, str): sharex = "all" if sharex else "none" - if isinstance(sharey, bool): + if not isinstance(sharey, str): sharey = "all" if sharey else "none" - # This check was added because it is very easy to type - # `subplots(1, 2, 1)` when `subplot(1, 2, 1)` was intended. - # In most cases, no error will ever occur, but mysterious behavior - # will result because what was intended to be the subplot index is - # instead treated as a bool for sharex. This check should go away - # once sharex becomes kwonly. - if isinstance(sharex, Integral): - _api.warn_external( - "sharex argument to subplots() was an integer. Did you " - "intend to use subplot() (without 's')?") - _api.check_in_list(["all", "row", "col", "none"], + + _api.check_in_list(["all", "row", "col", "none", False, True], sharex=sharex, sharey=sharey) if subplot_kw is None: subplot_kw = {} @@ -309,23 +301,11 @@ def subplots(self, *, sharex=False, sharey=False, squeeze=True, # turn off redundant tick labeling if sharex in ["col", "all"]: - for ax in axarr[:-1, :].flat: # Remove bottom labels/offsettexts. - ax.xaxis.set_tick_params(which="both", labelbottom=False) - if ax.xaxis.offsetText.get_position()[1] == 0: - ax.xaxis.offsetText.set_visible(False) - for ax in axarr[1:, :].flat: # Remove top labels/offsettexts. - ax.xaxis.set_tick_params(which="both", labeltop=False) - if ax.xaxis.offsetText.get_position()[1] == 1: - ax.xaxis.offsetText.set_visible(False) + for ax in axarr.flat: + ax._label_outer_xaxis(check_patch=True) if sharey in ["row", "all"]: - for ax in axarr[:, 1:].flat: # Remove left labels/offsettexts. - ax.yaxis.set_tick_params(which="both", labelleft=False) - if ax.yaxis.offsetText.get_position()[0] == 0: - ax.yaxis.offsetText.set_visible(False) - for ax in axarr[:, :-1].flat: # Remove right labels/offsettexts. - ax.yaxis.set_tick_params(which="both", labelright=False) - if ax.yaxis.offsetText.get_position()[0] == 1: - ax.yaxis.offsetText.set_visible(False) + for ax in axarr.flat: + ax._label_outer_yaxis(check_patch=True) if squeeze: # Discarding unneeded dimensions that equal 1. If we only have one @@ -343,6 +323,8 @@ class GridSpec(GridSpecBase): The location of the grid cells is determined in a similar way to `~.figure.SubplotParams` using *left*, *right*, *top*, *bottom*, *wspace* and *hspace*. + + Indexing a GridSpec instance returns a `.SubplotSpec`. """ def __init__(self, nrows, ncols, figure=None, left=None, bottom=None, right=None, top=None, @@ -354,7 +336,7 @@ def __init__(self, nrows, ncols, figure=None, nrows, ncols : int The number of rows and columns of the grid. - figure : `~.figure.Figure`, optional + figure : `.Figure`, optional Only used for constrained layout to create a proper layoutgrid. left, right, top, bottom : float, optional @@ -381,7 +363,7 @@ def __init__(self, nrows, ncols, figure=None, If not given, all columns will have the same width. height_ratios : array-like of length *nrows*, optional - Defines the relative heights of the rows. Each column gets a + Defines the relative heights of the rows. Each row gets a relative height of ``height_ratios[i] / sum(height_ratios)``. If not given, all rows will have the same height. @@ -398,25 +380,8 @@ def __init__(self, nrows, ncols, figure=None, width_ratios=width_ratios, height_ratios=height_ratios) - # set up layoutgrid for constrained_layout: - self._layoutgrid = None - if self.figure is None or not self.figure.get_constrained_layout(): - self._layoutgrid = None - else: - self._toplayoutbox = self.figure._layoutgrid - self._layoutgrid = layoutgrid.LayoutGrid( - parent=self.figure._layoutgrid, - parent_inner=True, - name=(self.figure._layoutgrid.name + '.gridspec' + - layoutgrid.seq_id()), - ncols=ncols, nrows=nrows, width_ratios=width_ratios, - height_ratios=height_ratios) - _AllowedKeys = ["left", "bottom", "right", "top", "wspace", "hspace"] - def __getstate__(self): - return {**self.__dict__, "_layoutgrid": None} - def update(self, **kwargs): """ Update the subplot parameters of the grid. @@ -439,7 +404,7 @@ def update(self, **kwargs): raise AttributeError(f"{k} is an unknown keyword") for figmanager in _pylab_helpers.Gcf.figs.values(): for ax in figmanager.canvas.figure.axes: - if isinstance(ax, mpl.axes.SubplotBase): + if ax.get_subplotspec() is not None: ss = ax.get_subplotspec().get_topmost_subplotspec() if ss.get_gridspec() == self: ax._set_position( @@ -447,7 +412,7 @@ def update(self, **kwargs): def get_subplot_params(self, figure=None): """ - Return the `~.SubplotParams` for the GridSpec. + Return the `.SubplotParams` for the GridSpec. In order of precedence the values are taken from @@ -456,7 +421,8 @@ def get_subplot_params(self, figure=None): - :rc:`figure.subplot.*` """ if figure is None: - kw = {k: rcParams["figure.subplot."+k] for k in self._AllowedKeys} + kw = {k: mpl.rcParams["figure.subplot."+k] + for k in self._AllowedKeys} subplotpars = mpl.figure.SubplotParams(**kw) else: subplotpars = copy.copy(figure.subplotpars) @@ -481,31 +447,27 @@ def tight_layout(self, figure, renderer=None, Parameters ---------- + figure : `.Figure` + The figure. + renderer : `.RendererBase` subclass, optional + The renderer to be used. pad : float Padding between the figure edge and the edges of subplots, as a fraction of the font-size. h_pad, w_pad : float, optional Padding (height/width) between edges of adjacent subplots. Defaults to *pad*. - rect : tuple of 4 floats, default: (0, 0, 1, 1), i.e. the whole figure + rect : tuple (left, bottom, right, top), default: None (left, bottom, right, top) rectangle in normalized figure coordinates that the whole subplots area (including labels) will - fit into. + fit into. Default (None) is the whole figure. """ - - subplotspec_list = tight_layout.get_subplotspec_list( - figure.axes, grid_spec=self) - if None in subplotspec_list: - _api.warn_external("This figure includes Axes that are not " - "compatible with tight_layout, so results " - "might be incorrect.") - if renderer is None: - renderer = tight_layout.get_renderer(figure) - - kwargs = tight_layout.get_tight_layout_figure( - figure, figure.axes, subplotspec_list, renderer, - pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) + renderer = figure._get_renderer() + kwargs = _tight_layout.get_tight_layout_figure( + figure, figure.axes, + _tight_layout.get_subplotspec_list(figure.axes, grid_spec=self), + renderer, pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) if kwargs: self.update(**kwargs) @@ -520,11 +482,19 @@ def __init__(self, nrows, ncols, wspace=None, hspace=None, height_ratios=None, width_ratios=None): """ - The number of rows and number of columns of the grid need to - be set. An instance of SubplotSpec is also needed to be set - from which the layout parameters will be inherited. The wspace - and hspace of the layout can be optionally specified or the - default values (from the figure or rcParams) will be used. + Parameters + ---------- + nrows, ncols : int + Number of rows and number of columns of the grid. + subplot_spec : SubplotSpec + Spec from which the layout parameters are inherited. + wspace, hspace : float, optional + See `GridSpec` for more details. If not specified default values + (from the figure or rcParams) are used. + height_ratios : array-like of length *nrows*, optional + See `GridSpecBase` for details. + width_ratios : array-like of length *ncols*, optional + See `GridSpecBase` for details. """ self._wspace = wspace self._hspace = hspace @@ -533,35 +503,15 @@ def __init__(self, nrows, ncols, super().__init__(nrows, ncols, width_ratios=width_ratios, height_ratios=height_ratios) - # do the layoutgrids for constrained_layout: - subspeclb = subplot_spec.get_gridspec()._layoutgrid - if subspeclb is None: - self._layoutgrid = None - else: - # this _toplayoutbox is a container that spans the cols and - # rows in the parent gridspec. Not yet implemented, - # but we do this so that it is possible to have subgridspec - # level artists. - self._toplayoutgrid = layoutgrid.LayoutGrid( - parent=subspeclb, - name=subspeclb.name + '.top' + layoutgrid.seq_id(), - nrows=1, ncols=1, - parent_pos=(subplot_spec.rowspan, subplot_spec.colspan)) - self._layoutgrid = layoutgrid.LayoutGrid( - parent=self._toplayoutgrid, - name=(self._toplayoutgrid.name + '.gridspec' + - layoutgrid.seq_id()), - nrows=nrows, ncols=ncols, - width_ratios=width_ratios, height_ratios=height_ratios) def get_subplot_params(self, figure=None): """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"]) + else mpl.rcParams["figure.subplot.hspace"]) wspace = (self._wspace if self._wspace is not None else figure.subplotpars.wspace if figure is not None - else rcParams["figure.subplot.wspace"]) + else mpl.rcParams["figure.subplot.wspace"]) figbox = self._subplot_spec.get_position(figure) left, bottom, right, top = figbox.extents @@ -579,21 +529,21 @@ def get_topmost_subplotspec(self): class SubplotSpec: """ - Specifies the location of a subplot in a `GridSpec`. + The location of a subplot in a `GridSpec`. .. note:: - Likely, you'll never instantiate a `SubplotSpec` yourself. Instead you - will typically obtain one from a `GridSpec` using item-access. + Likely, you will never instantiate a `SubplotSpec` yourself. Instead, + you will typically obtain one from a `GridSpec` using item-access. Parameters ---------- gridspec : `~matplotlib.gridspec.GridSpec` The GridSpec, which the subplot is referencing. num1, num2 : int - The subplot will occupy the num1-th cell of the given - gridspec. If num2 is provided, the subplot will span between - num1-th cell and num2-th cell *inclusive*. + The subplot will occupy the *num1*-th cell of the given + *gridspec*. If *num2* is provided, the subplot will span between + *num1*-th cell and *num2*-th cell **inclusive**. The index starts from 0. """ @@ -615,51 +565,41 @@ def _from_subplot_args(figure, args): - a `.SubplotSpec` -- returned as is; - one or three numbers -- a MATLAB-style subplot specifier. """ - message = ("Passing non-integers as three-element position " - "specification is deprecated since %(since)s and will be " - "removed %(removal)s.") if len(args) == 1: arg, = args if isinstance(arg, SubplotSpec): return arg - else: - if not isinstance(arg, Integral): - _api.warn_deprecated("3.3", message=message) - arg = str(arg) - try: - rows, cols, num = map(int, str(arg)) - except ValueError: - raise ValueError( - f"Single argument to subplot must be a three-digit " - f"integer, not {arg}") from None - i = j = num + elif not isinstance(arg, Integral): + raise ValueError( + f"Single argument to subplot must be a three-digit " + f"integer, not {arg!r}") + try: + rows, cols, num = map(int, str(arg)) + except ValueError: + raise ValueError( + f"Single argument to subplot must be a three-digit " + f"integer, not {arg!r}") from None elif len(args) == 3: rows, cols, num = args - if not (isinstance(rows, Integral) and isinstance(cols, Integral)): - _api.warn_deprecated("3.3", message=message) - rows, cols = map(int, [rows, cols]) - gs = GridSpec(rows, cols, figure=figure) - if isinstance(num, tuple) and len(num) == 2: - if not all(isinstance(n, Integral) for n in num): - _api.warn_deprecated("3.3", message=message) - i, j = map(int, num) - else: - i, j = num - else: - if not isinstance(num, Integral): - _api.warn_deprecated("3.3", message=message) - num = int(num) - if num < 1 or num > rows*cols: - raise ValueError( - f"num must be 1 <= num <= {rows*cols}, not {num}") - i = j = num else: - raise TypeError(f"subplot() takes 1 or 3 positional arguments but " - f"{len(args)} were given") + raise _api.nargs_error("subplot", takes="1 or 3", given=len(args)) gs = GridSpec._check_gridspec_exists(figure, rows, cols) if gs is None: gs = GridSpec(rows, cols, figure=figure) + if isinstance(num, tuple) and len(num) == 2: + if not all(isinstance(n, Integral) for n in num): + raise ValueError( + f"Subplot specifier tuple must contain integers, not {num}" + ) + i, j = num + else: + if not isinstance(num, Integral) or num < 1 or num > rows*cols: + raise ValueError( + f"num must be an integer with 1 <= num <= {rows*cols}, " + f"not {num!r}" + ) + i = j = num return gs[i-1:j] # num2 is a property only to handle the case where it is None and someone @@ -673,9 +613,6 @@ def num2(self): def num2(self, value): self._num2 = value - def __getstate__(self): - return {**self.__dict__} - def get_gridspec(self): return self._gridspec @@ -690,18 +627,6 @@ def get_geometry(self): rows, cols = self.get_gridspec().get_geometry() return rows, cols, self.num1, self.num2 - @_api.deprecated("3.3", alternative="rowspan, colspan") - def get_rows_columns(self): - """ - Return the subplot row and column numbers as a tuple - ``(n_rows, n_cols, row_start, row_stop, col_start, col_stop)``. - """ - gridspec = self.get_gridspec() - nrows, ncols = gridspec.get_geometry() - row_start, col_start = divmod(self.num1, ncols) - row_stop, col_stop = divmod(self.num2, ncols) - return nrows, ncols, row_start, row_stop, col_start, col_stop - @property def rowspan(self): """The rows spanned by this subplot, as a `range` object.""" @@ -729,8 +654,7 @@ def is_first_col(self): def is_last_col(self): return self.colspan.stop == self.get_gridspec().ncols - @_api.delete_parameter("3.4", "return_all") - def get_position(self, figure, return_all=False): + def get_position(self, figure): """ Update the subplot position from ``figure.subplotpars``. """ @@ -744,12 +668,7 @@ def get_position(self, figure, return_all=False): fig_top = fig_tops[rows].max() fig_left = fig_lefts[cols].min() fig_right = fig_rights[cols].max() - figbox = Bbox.from_extents(fig_left, fig_bottom, fig_right, fig_top) - - if return_all: - return figbox, rows[0], cols[0], nrows, ncols - else: - return figbox + return Bbox.from_extents(fig_left, fig_bottom, fig_right, fig_top) def get_topmost_subplotspec(self): """ @@ -788,7 +707,7 @@ def subgridspec(self, nrows, ncols, **kwargs): Number of rows in grid. ncols : int - Number or columns in grid. + Number of columns in grid. Returns ------- diff --git a/lib/matplotlib/hatch.py b/lib/matplotlib/hatch.py index af674decd41f..396baa55dbbb 100644 --- a/lib/matplotlib/hatch.py +++ b/lib/matplotlib/hatch.py @@ -101,12 +101,13 @@ def __init__(self, hatch, density): def set_vertices_and_codes(self, vertices, codes): offset = 1.0 / self.num_rows shape_vertices = self.shape_vertices * offset * self.size - if not self.filled: - inner_vertices = shape_vertices[::-1] * 0.9 shape_codes = self.shape_codes - shape_size = len(shape_vertices) - - cursor = 0 + if not self.filled: + shape_vertices = np.concatenate( # Forward, then backward. + [shape_vertices, shape_vertices[::-1] * 0.9]) + shape_codes = np.concatenate([shape_codes, shape_codes]) + vertices_parts = [] + codes_parts = [] for row in range(self.num_rows + 1): if row % 2 == 0: cols = np.linspace(0, 1, self.num_rows + 1) @@ -114,15 +115,10 @@ def set_vertices_and_codes(self, vertices, codes): cols = np.linspace(offset / 2, 1 - offset / 2, self.num_rows) row_pos = row * offset for col_pos in cols: - vertices[cursor:cursor + shape_size] = (shape_vertices + - (col_pos, row_pos)) - codes[cursor:cursor + shape_size] = shape_codes - cursor += shape_size - if not self.filled: - vertices[cursor:cursor + shape_size] = (inner_vertices + - (col_pos, row_pos)) - codes[cursor:cursor + shape_size] = shape_codes - cursor += shape_size + vertices_parts.append(shape_vertices + [col_pos, row_pos]) + codes_parts.append(shape_codes) + np.concatenate(vertices_parts, out=vertices) + np.concatenate(codes_parts, out=codes) class Circles(Shapes): @@ -149,16 +145,13 @@ def __init__(self, hatch, density): super().__init__(hatch, density) -# TODO: __init__ and class attributes override all attributes set by -# SmallCircles. Should this class derive from Circles instead? -class SmallFilledCircles(SmallCircles): +class SmallFilledCircles(Circles): size = 0.1 filled = True def __init__(self, hatch, density): self.num_rows = (hatch.count('.')) * density - # Not super().__init__! - Circles.__init__(self, hatch, density) + super().__init__(hatch, density) class Stars(Shapes): @@ -195,6 +188,7 @@ def _validate_hatch_pattern(hatch): invalids = ''.join(sorted(invalids)) _api.warn_deprecated( '3.4', + removal='3.8', # one release after custom hatches (#20690) message=f'hatch must consist of a string of "{valid}" or ' 'None, but found the following invalid values ' f'"{invalids}". Passing invalid values is deprecated ' diff --git a/lib/matplotlib/image.py b/lib/matplotlib/image.py index 2adb92bdea5c..51db3fa5c3d4 100644 --- a/lib/matplotlib/image.py +++ b/lib/matplotlib/image.py @@ -7,22 +7,21 @@ import os import logging from pathlib import Path +import warnings import numpy as np import PIL.PngImagePlugin import matplotlib as mpl -from matplotlib import _api +from matplotlib import _api, cbook, cm +# For clarity, names from _image are given explicitly in this module +from matplotlib import _image +# For user convenience, the names from _image are also imported into +# the image namespace +from matplotlib._image import * import matplotlib.artist as martist from matplotlib.backend_bases import FigureCanvasBase import matplotlib.colors as mcolors -import matplotlib.cm as cm -import matplotlib.cbook as cbook -# For clarity, names from _image are given explicitly in this module: -import matplotlib._image as _image -# For user convenience, the names from _image are also imported into -# the image namespace: -from matplotlib._image import * from matplotlib.transforms import ( Affine2D, BboxBase, Bbox, BboxTransform, BboxTransformTo, IdentityTransform, TransformedBbox) @@ -150,7 +149,7 @@ def flush_images(): for a in artists: if (isinstance(a, _ImageBase) and a.can_composite() and - a.get_clip_on()): + a.get_clip_on() and not a.get_clip_path()): image_group.append(a) else: flush_images() @@ -166,7 +165,22 @@ def _resample( allocating the output array and fetching the relevant properties from the Image object *image_obj*. """ - + # AGG can only handle coordinates smaller than 24-bit signed integers, + # so raise errors if the input data is larger than _image.resample can + # handle. + msg = ('Data with more than {n} cannot be accurately displayed. ' + 'Downsampling to less than {n} before displaying. ' + 'To remove this warning, manually downsample your data.') + if data.shape[1] > 2**23: + warnings.warn(msg.format(n='2**23 columns')) + step = int(np.ceil(data.shape[1] / 2**23)) + data = data[:, ::step] + transform = Affine2D().scale(step, 1) + transform + if data.shape[0] > 2**24: + warnings.warn(msg.format(n='2**24 rows')) + step = int(np.ceil(data.shape[0] / 2**24)) + data = data[::step, :] + transform = Affine2D().scale(1, step) + transform # decide if we need to apply anti-aliasing if the data is upsampled: # compare the number of displayed pixels to the number of # the data pixels. @@ -238,6 +252,8 @@ def __init__(self, ax, filternorm=True, filterrad=4.0, resample=False, + *, + interpolation_stage=None, **kwargs ): martist.Artist.__init__(self) @@ -249,18 +265,24 @@ def __init__(self, ax, self.set_filternorm(filternorm) self.set_filterrad(filterrad) self.set_interpolation(interpolation) + self.set_interpolation_stage(interpolation_stage) self.set_resample(resample) self.axes = ax self._imcache = None - self.update(kwargs) + self._internal_update(kwargs) + + def __str__(self): + try: + size = self.get_size() + return f"{type(self).__name__}(size={size!r})" + except RuntimeError: + return type(self).__name__ def __getstate__(self): - state = super().__getstate__() - # We can't pickle the C Image cached object. - state['_imcache'] = None - return state + # Save some space on the pickle by not saving the cache. + return {**super().__getstate__(), "_imcache": None} def get_size(self): """Return the size of the image as tuple (numrows, numcols).""" @@ -301,7 +323,6 @@ def changed(self): Call this whenever the mappable is changed so observers can update. """ self._imcache = None - self._rgbacache = None cm.ScalarMappable.changed(self) def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, @@ -392,53 +413,41 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, if not unsampled: if not (A.ndim == 2 or A.ndim == 3 and A.shape[-1] in (3, 4)): raise ValueError(f"Invalid shape {A.shape} for image data") - - if A.ndim == 2: + if A.ndim == 2 and self._interpolation_stage != 'rgba': # if we are a 2D array, then we are running through the # norm + colormap transformation. However, in general the # input data is not going to match the size on the screen so we # have to resample to the correct number of pixels # TODO slice input array first - inp_dtype = A.dtype a_min = A.min() a_max = A.max() - # figure out the type we should scale to. For floats, - # leave as is. For integers cast to an appropriate-sized - # float. Small integers get smaller floats in an attempt - # to keep the memory footprint reasonable. - if a_min is np.ma.masked: - # all masked, so values don't matter + if a_min is np.ma.masked: # All masked; values don't matter. a_min, a_max = np.int32(0), np.int32(1) - if inp_dtype.kind == 'f': - scaled_dtype = A.dtype - # Cast to float64 - if A.dtype not in (np.float32, np.float16): - if A.dtype != np.float64: - _api.warn_external( - f"Casting input data from '{A.dtype}' to " - f"'float64' for imshow") - scaled_dtype = np.float64 - else: - # probably an integer of some type. + if A.dtype.kind == 'f': # Float dtype: scale to same dtype. + scaled_dtype = np.dtype( + np.float64 if A.dtype.itemsize > 4 else np.float32) + if scaled_dtype.itemsize < A.dtype.itemsize: + _api.warn_external(f"Casting input data from {A.dtype}" + f" to {scaled_dtype} for imshow.") + else: # Int dtype, likely. + # Scale to appropriately sized float: use float32 if the + # dynamic range is small, to limit the memory footprint. da = a_max.astype(np.float64) - a_min.astype(np.float64) - # give more breathing room if a big dynamic range scaled_dtype = np.float64 if da > 1e8 else np.float32 - # scale the input data to [.1, .9]. The Agg - # interpolators clip to [0, 1] internally, use a - # smaller input scale to identify which of the - # interpolated points need to be should be flagged as - # over / under. - # This may introduce numeric instabilities in very broadly - # scaled data + # Scale the input data to [.1, .9]. The Agg interpolators clip + # to [0, 1] internally, and we use a smaller input scale to + # identify the interpolated points that need to be flagged as + # over/under. This may introduce numeric instabilities in very + # broadly scaled data. + # Always copy, and don't allow array subtypes. A_scaled = np.array(A, dtype=scaled_dtype) - # clip scaled data around norm if necessary. - # This is necessary for big numbers at the edge of - # float64's ability to represent changes. Applying - # a norm first would be good, but ruins the interpolation - # of over numbers. + # Clip scaled data around norm if necessary. This is necessary + # for big numbers at the edge of float64's ability to represent + # changes. Applying a norm first would be good, but ruins the + # interpolation of over numbers. self.norm.autoscale_None(A) dv = np.float64(self.norm.vmax) - np.float64(self.norm.vmin) vmid = np.float64(self.norm.vmin) + dv / 2 @@ -456,21 +465,17 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, if newmax is not None or newmin is not None: np.clip(A_scaled, newmin, newmax, out=A_scaled) - # used to rescale the raw data to [offset, 1-offset] - # so that the resampling code will run cleanly. Using - # dyadic numbers here could reduce the error, but - # would not full eliminate it and breaks a number of - # tests (due to the slightly different error bouncing - # some pixels across a boundary in the (very + # Rescale the raw data to [offset, 1-offset] so that the + # resampling code will run cleanly. Using dyadic numbers here + # could reduce the error, but would not fully eliminate it and + # breaks a number of tests (due to the slightly different + # error bouncing some pixels across a boundary in the (very # quantized) colormapping step). offset = .1 frac = .8 - # we need to run the vmin/vmax through the same rescaling - # that we run the raw data through because there are small - # errors in the round-trip due to float precision. If we - # 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. + # Run vmin/vmax through the same rescaling as the raw data; + # otherwise, data values close or equal to the boundaries can + # end up on the wrong side due to floating point error. vmin, vmax = self.norm.vmin, self.norm.vmax if vmin is np.ma.masked: vmin, vmax = a_min, a_max @@ -478,8 +483,7 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, A_scaled -= a_min vrange -= a_min - # a_min and a_max might be ndarray subclasses so use - # item to avoid errors + # .item() handles a_min/a_max being ndarray subclasses. a_min = a_min.astype(scaled_dtype).item() a_max = a_max.astype(scaled_dtype).item() @@ -490,13 +494,11 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, vrange += offset # resample the input data to the correct resolution and shape A_resampled = _resample(self, A_scaled, out_shape, t) - # done with A_scaled now, remove from namespace to be sure! - del A_scaled - # un-scale the resampled data to approximately the - # original range things that interpolated to above / - # below the original min/max will still be above / - # below, but possibly clipped in the case of higher order - # interpolation + drastically changing data. + del A_scaled # Make sure we don't use A_scaled anymore! + # Un-scale the resampled data to approximately the original + # range. Things that interpolated to outside the original range + # will still be outside, but possibly clipped in the case of + # higher order interpolation + drastically changing data. A_resampled -= offset vrange -= offset if a_min != a_max: @@ -514,8 +516,7 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, # we always have to interpolate the mask to account for # non-affine transformations out_alpha = _resample(self, mask, out_shape, t, resample=True) - # done with the mask now, delete from namespace to be sure! - del mask + del mask # Make sure we don't use mask anymore! # Agg updates out_alpha in place. If the pixel has no image # data it will not be updated (and still be 0 as we initialized # it), if input data that would go into that output pixel than @@ -537,12 +538,15 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, if isinstance(self.norm, mcolors.LogNorm) and s_vmin <= 0: # Don't give 0 or negative values to LogNorm s_vmin = np.finfo(scaled_dtype).eps - with cbook._setattr_cm(self.norm, - vmin=s_vmin, - vmax=s_vmax, - ): + # Block the norm from sending an update signal during the + # temporary vmin/vmax change + with self.norm.callbacks.blocked(), \ + cbook._setattr_cm(self.norm, vmin=s_vmin, vmax=s_vmax): output = self.norm(resampled_masked) else: + if A.ndim == 2: # _interpolation_stage == 'rgba' + self.norm.autoscale_None(A) + A = self.to_rgba(A) if A.shape[2] == 3: A = _rgb_to_rgba(A) alpha = self._get_scalar_alpha() @@ -552,8 +556,7 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, self, _rgb_to_rgba(A[..., :3]), out_shape, t, alpha=alpha) output[..., 3] = output_alpha # recombine rgb and alpha - # at this point output is either a 2D array of normed data - # (of int or float) + # output is now either a 2D array of normed (int or float) data # or an RGBA array of re-sampled input output = self.to_rgba(output, bytes=True, norm=False) # output is now a correctly sized RGBA array of uint8 @@ -562,17 +565,15 @@ def _make_image(self, A, in_bbox, out_bbox, clip_bbox, magnification=1.0, if A.ndim == 2: alpha = self._get_scalar_alpha() alpha_channel = output[:, :, 3] - alpha_channel[:] = np.asarray( - np.asarray(alpha_channel, np.float32) * out_alpha * alpha, - np.uint8) + alpha_channel[:] = ( # Assignment will cast to uint8. + alpha_channel.astype(np.float32) * out_alpha * alpha) else: if self._imcache is None: self._imcache = self.to_rgba(A, bytes=True, norm=(A.ndim == 2)) output = self._imcache - # Subset the input image to only the part that will be - # displayed + # Subset the input image to only the part that will be displayed. subset = TransformedBbox(clip_bbox, t0.inverted()).frozen() output = output[ int(max(subset.ymin, 0)): @@ -727,7 +728,6 @@ def set_data(self, A): self._A = self._A.astype(np.uint8) self._imcache = None - self._rgbacache = None self.stale = True def set_array(self, A): @@ -775,6 +775,22 @@ def set_interpolation(self, s): self._interpolation = s self.stale = True + def set_interpolation_stage(self, s): + """ + Set when interpolation happens during the transform to RGBA. + + Parameters + ---------- + s : {'data', 'rgba'} or None + Whether to apply up/downsampling interpolation in data or rgba + space. + """ + if s is None: + s = "data" # placeholder for maybe having rcParam + _api.check_in_list(['data', 'rgba'], s=s) + self._interpolation_stage = s + self.stale = True + def can_composite(self): """Return whether the image can be composited with its neighbors.""" trans = self.get_transform() @@ -849,13 +865,18 @@ class AxesImage(_ImageBase): cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` The Colormap instance or registered colormap name used to map scalar data to colors. - norm : `~matplotlib.colors.Normalize` + norm : str or `~matplotlib.colors.Normalize` Maps luminance to 0-1. interpolation : str, default: :rc:`image.interpolation` Supported values are 'none', 'antialiased', 'nearest', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'blackman'. + interpolation_stage : {'data', 'rgba'}, default: 'data' + If 'data', interpolation + is carried out on the data provided by the user. If 'rgba', the + interpolation is carried out after the colormapping has been + applied (visual interpolation). origin : {'upper', 'lower'}, default: :rc:`image.origin` Place the [0, 0] index of the array in the upper left or lower left corner of the axes. The convention 'upper' is typically used for @@ -880,9 +901,8 @@ class AxesImage(_ImageBase): the output image is larger than the input image. **kwargs : `.Artist` properties """ - def __str__(self): - return "AxesImage(%g,%g;%gx%g)" % tuple(self.axes.bbox.bounds) + @_api.make_keyword_only("3.6", name="cmap") def __init__(self, ax, cmap=None, norm=None, @@ -892,6 +912,8 @@ def __init__(self, ax, filternorm=True, filterrad=4.0, resample=False, + *, + interpolation_stage=None, **kwargs ): @@ -906,13 +928,14 @@ def __init__(self, ax, filternorm=filternorm, filterrad=filterrad, resample=resample, + interpolation_stage=interpolation_stage, **kwargs ) def get_window_extent(self, renderer=None): x0, x1, y0, y1 = self._extent bbox = Bbox.from_extents([x0, y0, x1, y1]) - return bbox.transformed(self.axes.transData) + return bbox.transformed(self.get_transform()) def make_image(self, renderer, magnification=1.0, unsampled=False): # docstring inherited @@ -930,7 +953,7 @@ def _check_unsampled_image(self): """Return whether the image would be better drawn unsampled.""" return self.get_interpolation() == "none" - def set_extent(self, extent): + def set_extent(self, extent, **kwargs): """ Set the image extent. @@ -939,6 +962,10 @@ def set_extent(self, extent): extent : 4-tuple of float The position and size of the image as tuple ``(left, right, bottom, top)`` in data coordinates. + **kwargs + Other parameters from which unit info (i.e., the *xunits*, + *yunits*, *zunits* (for 3D axes), *runits* and *thetaunits* (for + polar axes) entries are applied, if present. Notes ----- @@ -947,14 +974,30 @@ def set_extent(self, extent): state is not changed, so following this with ``ax.autoscale_view()`` will redo the autoscaling in accord with ``dataLim``. """ - self._extent = xmin, xmax, ymin, ymax = extent + (xmin, xmax), (ymin, ymax) = self.axes._process_unit_info( + [("x", [extent[0], extent[1]]), + ("y", [extent[2], extent[3]])], + kwargs) + if kwargs: + raise _api.kwarg_error("set_extent", kwargs) + xmin = self.axes._validate_converted_limits( + xmin, self.convert_xunits) + xmax = self.axes._validate_converted_limits( + xmax, self.convert_xunits) + ymin = self.axes._validate_converted_limits( + ymin, self.convert_yunits) + ymax = self.axes._validate_converted_limits( + ymax, self.convert_yunits) + extent = [xmin, xmax, ymin, ymax] + + self._extent = extent corners = (xmin, ymin), (xmax, ymax) self.axes.update_datalim(corners) self.sticky_edges.x[:] = [xmin, xmax] self.sticky_edges.y[:] = [ymin, ymax] - if self.axes._autoscaleXon: + if self.axes.get_autoscalex_on(): self.axes.set_xlim((xmin, xmax), auto=None) - if self.axes._autoscaleYon: + if self.axes.get_autoscaley_on(): self.axes.set_ylim((ymin, ymax), auto=None) self.stale = True @@ -997,16 +1040,6 @@ def get_cursor_data(self, event): else: return arr[i, j] - def format_cursor_data(self, data): - if np.ndim(data) == 0 and self.colorbar: - return ( - "[" - + cbook.strip_math( - self.colorbar.formatter.format_data_short(data)).strip() - + "]") - else: - return super().format_cursor_data(data) - class NonUniformImage(AxesImage): mouseover = False # This class still needs its own get_cursor_data impl. @@ -1015,8 +1048,10 @@ def __init__(self, ax, *, interpolation='nearest', **kwargs): """ Parameters ---------- + ax : `~.axes.Axes` + The axes the image will belong to. interpolation : {'nearest', 'bilinear'}, default: 'nearest' - + The interpolation scheme used in the resampling. **kwargs All other keyword arguments are identical to those of `.AxesImage`. """ @@ -1027,8 +1062,6 @@ def _check_unsampled_image(self): """Return False. Do not use unsampled image.""" return False - is_grayscale = _api.deprecate_privatize_attribute("3.3") - def make_image(self, renderer, magnification=1.0, unsampled=False): # docstring inherited if self._A is None: @@ -1039,11 +1072,9 @@ def make_image(self, renderer, magnification=1.0, unsampled=False): if A.ndim == 2: if A.dtype != np.uint8: A = self.to_rgba(A, bytes=True) - self._is_grayscale = self.cmap.is_gray() else: A = np.repeat(A[:, :, np.newaxis], 4, 2) A[:, :, 3] = 255 - self._is_grayscale = True else: if A.dtype != np.uint8: A = (255*A).astype(np.uint8) @@ -1052,17 +1083,53 @@ def make_image(self, renderer, magnification=1.0, unsampled=False): B[:, :, 0:3] = A B[:, :, 3] = 255 A = B - self._is_grayscale = False vl = self.axes.viewLim l, b, r, t = self.axes.bbox.extents - width = (round(r) + 0.5) - (round(l) - 0.5) - height = (round(t) + 0.5) - (round(b) - 0.5) - width *= magnification - height *= magnification - im = _image.pcolor(self._Ax, self._Ay, A, - int(height), int(width), - (vl.x0, vl.x1, vl.y0, vl.y1), - _interpd_[self._interpolation]) + width = int(((round(r) + 0.5) - (round(l) - 0.5)) * magnification) + height = int(((round(t) + 0.5) - (round(b) - 0.5)) * magnification) + x_pix = np.linspace(vl.x0, vl.x1, width) + y_pix = np.linspace(vl.y0, vl.y1, height) + if self._interpolation == "nearest": + x_mid = (self._Ax[:-1] + self._Ax[1:]) / 2 + y_mid = (self._Ay[:-1] + self._Ay[1:]) / 2 + x_int = x_mid.searchsorted(x_pix) + y_int = y_mid.searchsorted(y_pix) + # The following is equal to `A[y_int[:, None], x_int[None, :]]`, + # but many times faster. Both casting to uint32 (to have an + # effectively 1D array) and manual index flattening matter. + im = ( + np.ascontiguousarray(A).view(np.uint32).ravel()[ + np.add.outer(y_int * A.shape[1], x_int)] + .view(np.uint8).reshape((height, width, 4))) + else: # self._interpolation == "bilinear" + # Use np.interp to compute x_int/x_float has similar speed. + x_int = np.clip( + self._Ax.searchsorted(x_pix) - 1, 0, len(self._Ax) - 2) + y_int = np.clip( + self._Ay.searchsorted(y_pix) - 1, 0, len(self._Ay) - 2) + idx_int = np.add.outer(y_int * A.shape[1], x_int) + x_frac = np.clip( + np.divide(x_pix - self._Ax[x_int], np.diff(self._Ax)[x_int], + dtype=np.float32), # Downcasting helps with speed. + 0, 1) + y_frac = np.clip( + np.divide(y_pix - self._Ay[y_int], np.diff(self._Ay)[y_int], + dtype=np.float32), + 0, 1) + f00 = np.outer(1 - y_frac, 1 - x_frac) + f10 = np.outer(y_frac, 1 - x_frac) + f01 = np.outer(1 - y_frac, x_frac) + f11 = np.outer(y_frac, x_frac) + im = np.empty((height, width, 4), np.uint8) + for chan in range(4): + ac = A[:, :, chan].reshape(-1) # reshape(-1) avoids a copy. + # Shifting the buffer start (`ac[offset:]`) avoids an array + # addition (`ac[idx_int + offset]`). + buf = f00 * ac[idx_int] + buf += f10 * ac[A.shape[1]:][idx_int] + buf += f01 * ac[1:][idx_int] + buf += f11 * ac[A.shape[1] + 1:][idx_int] + im[:, :, chan] = buf # Implicitly casts to uint8. return im, l, b, IdentityTransform() def set_data(self, x, y, A): @@ -1075,8 +1142,8 @@ def set_data(self, x, y, A): Monotonic arrays of shapes (N,) and (M,), respectively, specifying pixel centers. A : array-like - (M, N) ndarray or masked array of values to be colormapped, or - (M, N, 3) RGB array, or (M, N, 4) RGBA array. + (M, N) `~numpy.ndarray` or masked array of values to be + colormapped, or (M, N, 3) RGB array, or (M, N, 4) RGBA array. """ x = np.array(x, np.float32) y = np.array(y, np.float32) @@ -1141,6 +1208,8 @@ class PcolorImage(AxesImage): This uses a variation of the original irregular image code, and it is used by pcolorfast for the corresponding grid type. """ + + @_api.make_keyword_only("3.6", name="cmap") def __init__(self, ax, x=None, y=None, @@ -1162,51 +1231,53 @@ def __init__(self, ax, The data to be color-coded. The interpretation depends on the shape: - - (M, N) ndarray or masked array: values to be colormapped + - (M, N) `~numpy.ndarray` or masked array: values to be colormapped - (M, N, 3): RGB array - (M, N, 4): RGBA array cmap : str or `~matplotlib.colors.Colormap`, default: :rc:`image.cmap` The Colormap instance or registered colormap name used to map scalar data to colors. - norm : `~matplotlib.colors.Normalize` + norm : str or `~matplotlib.colors.Normalize` Maps luminance to 0-1. **kwargs : `.Artist` properties """ super().__init__(ax, norm=norm, cmap=cmap) - self.update(kwargs) + self._internal_update(kwargs) if A is not None: self.set_data(x, y, A) - is_grayscale = _api.deprecate_privatize_attribute("3.3") - def make_image(self, renderer, magnification=1.0, unsampled=False): # docstring inherited if self._A is None: raise RuntimeError('You must first set the image array') if unsampled: raise ValueError('unsampled not supported on PColorImage') - fc = self.axes.patch.get_facecolor() - bg = mcolors.to_rgba(fc, 0) - bg = (np.array(bg)*255).astype(np.uint8) + + if self._imcache is None: + A = self.to_rgba(self._A, bytes=True) + self._imcache = np.pad(A, [(1, 1), (1, 1), (0, 0)], "constant") + padded_A = self._imcache + bg = mcolors.to_rgba(self.axes.patch.get_facecolor(), 0) + bg = (np.array(bg) * 255).astype(np.uint8) + if (padded_A[0, 0] != bg).all(): + padded_A[[0, -1], :] = padded_A[:, [0, -1]] = bg + l, b, r, t = self.axes.bbox.extents width = (round(r) + 0.5) - (round(l) - 0.5) height = (round(t) + 0.5) - (round(b) - 0.5) - width = int(round(width * magnification)) - height = int(round(height * magnification)) - if self._rgbacache is None: - A = self.to_rgba(self._A, bytes=True) - self._rgbacache = A - if self._A.ndim == 2: - self._is_grayscale = self.cmap.is_gray() - else: - A = self._rgbacache + width = round(width * magnification) + height = round(height * magnification) vl = self.axes.viewLim - im = _image.pcolor2(self._Ax, self._Ay, A, - height, - width, - (vl.x0, vl.x1, vl.y0, vl.y1), - bg) + + x_pix = np.linspace(vl.x0, vl.x1, width) + y_pix = np.linspace(vl.y0, vl.y1, height) + x_int = self._Ax.searchsorted(x_pix) + y_int = self._Ay.searchsorted(y_pix) + im = ( # See comment in NonUniformImage.make_image re: performance. + padded_A.view(np.uint32).ravel()[ + np.add.outer(y_int * padded_A.shape[1], x_int)] + .view(np.uint8).reshape((height, width, 4))) return im, l, b, IdentityTransform() def _check_unsampled_image(self): @@ -1226,7 +1297,7 @@ def set_data(self, x, y, A): The data to be color-coded. The interpretation depends on the shape: - - (M, N) ndarray or masked array: values to be colormapped + - (M, N) `~numpy.ndarray` or masked array: values to be colormapped - (M, N, 3): RGB array - (M, N, 4): RGBA array """ @@ -1246,15 +1317,10 @@ def set_data(self, x, y, A): (A.shape[:2], (y.size - 1, x.size - 1))) if A.ndim not in [2, 3]: raise ValueError("A must be 2D or 3D") - if A.ndim == 3 and A.shape[2] == 1: - A = A.squeeze(axis=-1) - self._is_grayscale = False if A.ndim == 3: - if A.shape[2] in [3, 4]: - if ((A[:, :, 0] == A[:, :, 1]).all() and - (A[:, :, 0] == A[:, :, 2]).all()): - self._is_grayscale = True - else: + if A.shape[2] == 1: + A = A.squeeze(axis=-1) + elif A.shape[2] not in [3, 4]: raise ValueError("3D arrays must have RGB or RGBA as last dim") # For efficient cursor readout, ensure x and y are increasing. @@ -1268,7 +1334,7 @@ def set_data(self, x, y, A): self._A = A self._Ax = x self._Ay = y - self._rgbacache = None + self._imcache = None self.stale = True def set_array(self, *args): @@ -1295,6 +1361,7 @@ class FigureImage(_ImageBase): _interpolation = 'nearest' + @_api.make_keyword_only("3.6", name="cmap") def __init__(self, fig, cmap=None, norm=None, @@ -1318,7 +1385,7 @@ def __init__(self, fig, self.figure = fig self.ox = offsetx self.oy = offsety - self.update(kwargs) + self._internal_update(kwargs) self.magnification = 1.0 def get_extent(self): @@ -1346,14 +1413,14 @@ def make_image(self, renderer, magnification=1.0, unsampled=False): def set_data(self, A): """Set the image array.""" - cm.ScalarMappable.set_array(self, - cbook.safe_masked_invalid(A, copy=True)) + cm.ScalarMappable.set_array(self, A) self.stale = True class BboxImage(_ImageBase): """The Image class whose size is determined by the given bbox.""" + @_api.make_keyword_only("3.6", name="cmap") def __init__(self, bbox, cmap=None, norm=None, @@ -1385,7 +1452,7 @@ def __init__(self, bbox, def get_window_extent(self, renderer=None): if renderer is None: - renderer = self.get_figure()._cachedRenderer + renderer = self.get_figure()._get_renderer() if isinstance(self.bbox, BboxBase): return self.bbox @@ -1425,6 +1492,11 @@ def imread(fname, format=None): """ Read an image from a file into an array. + .. note:: + + This function exists for historical reasons. It is recommended to + use `PIL.Image.open` instead for loading images. + Parameters ---------- fname : str or file-like @@ -1433,11 +1505,13 @@ def imread(fname, format=None): Passing a URL is deprecated. Please open the URL for reading and pass the result to Pillow, e.g. with - ``PIL.Image.open(urllib.request.urlopen(url))``. + ``np.array(PIL.Image.open(urllib.request.urlopen(url)))``. format : str, optional - The image file format assumed for reading the data. If not - given, the format is deduced from the filename. If nothing can - be deduced, PNG is tried. + The image file format assumed for reading the data. The image is + loaded as a PNG file if *format* is set to "png", if *fname* is a path + or opened file with a ".png" extension, or if it is a URL. In all + other cases, *format* is ignored and the format is auto-detected by + `PIL.Image.open`. Returns ------- @@ -1447,6 +1521,10 @@ def imread(fname, format=None): - (M, N) for grayscale images. - (M, N, 3) for RGB images. - (M, N, 4) for RGBA images. + + PNG images are returned as float arrays (0-1). All other formats are + returned as int arrays, with a bit depth determined by the file's + contents. """ # hide imports to speed initial import on systems with slow linkers from urllib import parse @@ -1475,29 +1553,13 @@ def imread(fname, format=None): ext = format img_open = ( PIL.PngImagePlugin.PngImageFile if ext == 'png' else PIL.Image.open) - if isinstance(fname, str): - parsed = parse.urlparse(fname) - if len(parsed.scheme) > 1: # Pillow doesn't handle URLs directly. - _api.warn_deprecated( - "3.4", message="Directly reading images from URLs is " - "deprecated since %(since)s and will no longer be supported " - "%(removal)s. Please open the URL for reading and pass the " - "result to Pillow, e.g. with " - "``PIL.Image.open(urllib.request.urlopen(url))``.") - # hide imports to speed initial import on systems with slow linkers - from urllib import request - 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) - except (AttributeError, io.UnsupportedOperation): - response = io.BytesIO(response.read()) - return imread(response, format=ext) + if isinstance(fname, str) and len(parse.urlparse(fname).scheme) > 1: + # Pillow doesn't handle URLs directly. + raise ValueError( + "Please open the URL for reading and pass the " + "result to Pillow, e.g. with " + "``np.array(PIL.Image.open(urllib.request.urlopen(url)))``." + ) with img_open(fname) as image: return (_pil_png_to_float_array(image) if isinstance(image, PIL.PngImagePlugin.PngImageFile) else @@ -1507,7 +1569,15 @@ def imread(fname, format=None): def imsave(fname, arr, vmin=None, vmax=None, cmap=None, format=None, origin=None, dpi=100, *, metadata=None, pil_kwargs=None): """ - Save an array as an image file. + Colormap and save an array as an image file. + + RGB(A) images are passed through. Single channel images will be + colormapped according to *cmap* and *norm*. + + .. note:: + + If you want to save a single channel image as gray scale please use an + image I/O library (such as pillow, tifffile, or imageio) directly. Parameters ---------- @@ -1565,23 +1635,26 @@ def imsave(fname, arr, vmin=None, vmax=None, cmap=None, format=None, else: # Don't bother creating an image; this avoids rounding errors on the # size when dividing and then multiplying by dpi. - sm = cm.ScalarMappable(cmap=cmap) - sm.set_clim(vmin, vmax) if origin is None: origin = mpl.rcParams["image.origin"] if origin == "lower": arr = arr[::-1] if (isinstance(arr, memoryview) and arr.format == "B" and arr.ndim == 3 and arr.shape[-1] == 4): - # Such an ``arr`` would also be handled fine by sm.to_rgba (after - # casting with asarray), but it is useful to special-case it + # Such an ``arr`` would also be handled fine by sm.to_rgba below + # (after casting with asarray), but it is useful to special-case it # because that's what backend_agg passes, and can be in fact used # as is, saving a few operations. rgba = arr else: + sm = cm.ScalarMappable(cmap=cmap) + sm.set_clim(vmin, vmax) rgba = sm.to_rgba(arr, bytes=True) if pil_kwargs is None: pil_kwargs = {} + else: + # we modify this below, so make a copy (don't modify caller's dict) + pil_kwargs = pil_kwargs.copy() pil_shape = (rgba.shape[1], rgba.shape[0]) image = PIL.Image.frombuffer( "RGBA", pil_shape, rgba, "raw", "RGBA", 0, 1) @@ -1661,7 +1734,7 @@ def _pil_png_to_float_array(pil_png): mode = pil_png.mode rawmode = pil_png.png.im_rawmode if rawmode == "1": # Grayscale. - return np.asarray(pil_png).astype(np.float32) + return np.asarray(pil_png, np.float32) if rawmode == "L;2": # Grayscale. return np.divide(pil_png, 2**2 - 1, dtype=np.float32) if rawmode == "L;4": # Grayscale. @@ -1716,7 +1789,7 @@ def thumbnail(infile, thumbfile, scale=0.1, interpolation='bilinear', Returns ------- - `~.figure.Figure` + `.Figure` The figure instance containing the thumbnail. """ diff --git a/lib/matplotlib/layout_engine.py b/lib/matplotlib/layout_engine.py new file mode 100644 index 000000000000..248ad13757f8 --- /dev/null +++ b/lib/matplotlib/layout_engine.py @@ -0,0 +1,284 @@ +""" +Classes to layout elements in a `.Figure`. + +Figures have a ``layout_engine`` property that holds a subclass of +`~.LayoutEngine` defined here (or *None* for no layout). At draw time +``figure.get_layout_engine().execute()`` is called, the goal of which is +usually to rearrange Axes on the figure to produce a pleasing layout. This is +like a ``draw`` callback but with two differences. First, when printing we +disable the layout engine for the final draw. Second, it is useful to know the +layout engine while the figure is being created. In particular, colorbars are +made differently with different layout engines (for historical reasons). + +Matplotlib supplies two layout engines, `.TightLayoutEngine` and +`.ConstrainedLayoutEngine`. Third parties can create their own layout engine +by subclassing `.LayoutEngine`. +""" + +from contextlib import nullcontext + +import matplotlib as mpl + +from matplotlib._constrained_layout import do_constrained_layout +from matplotlib._tight_layout import (get_subplotspec_list, + get_tight_layout_figure) + + +class LayoutEngine: + """ + Base class for Matplotlib layout engines. + + A layout engine can be passed to a figure at instantiation or at any time + with `~.figure.Figure.set_layout_engine`. Once attached to a figure, the + layout engine ``execute`` function is called at draw time by + `~.figure.Figure.draw`, providing a special draw-time hook. + + .. note:: + + However, note that layout engines affect the creation of colorbars, so + `~.figure.Figure.set_layout_engine` should be called before any + colorbars are created. + + Currently, there are two properties of `LayoutEngine` classes that are + consulted while manipulating the figure: + + - ``engine.colorbar_gridspec`` tells `.Figure.colorbar` whether to make the + axes using the gridspec method (see `.colorbar.make_axes_gridspec`) or + not (see `.colorbar.make_axes`); + - ``engine.adjust_compatible`` stops `.Figure.subplots_adjust` from being + run if it is not compatible with the layout engine. + + To implement a custom `LayoutEngine`: + + 1. override ``_adjust_compatible`` and ``_colorbar_gridspec`` + 2. override `LayoutEngine.set` to update *self._params* + 3. override `LayoutEngine.execute` with your implementation + + """ + # override these in subclass + _adjust_compatible = None + _colorbar_gridspec = None + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._params = {} + + def set(self, **kwargs): + raise NotImplementedError + + @property + def colorbar_gridspec(self): + """ + Return a boolean if the layout engine creates colorbars using a + gridspec. + """ + if self._colorbar_gridspec is None: + raise NotImplementedError + return self._colorbar_gridspec + + @property + def adjust_compatible(self): + """ + Return a boolean if the layout engine is compatible with + `~.Figure.subplots_adjust`. + """ + if self._adjust_compatible is None: + raise NotImplementedError + return self._adjust_compatible + + def get(self): + """ + Return copy of the parameters for the layout engine. + """ + return dict(self._params) + + def execute(self, fig): + """ + Execute the layout on the figure given by *fig*. + """ + # subclasses must implement this. + raise NotImplementedError + + +class PlaceHolderLayoutEngine(LayoutEngine): + """ + This layout engine does not adjust the figure layout at all. + + The purpose of this `.LayoutEngine` is to act as a placeholder when the + user removes a layout engine to ensure an incompatible `.LayoutEngine` can + not be set later. + + Parameters + ---------- + adjust_compatible, colorbar_gridspec : bool + Allow the PlaceHolderLayoutEngine to mirror the behavior of whatever + layout engine it is replacing. + + """ + def __init__(self, adjust_compatible, colorbar_gridspec, **kwargs): + self._adjust_compatible = adjust_compatible + self._colorbar_gridspec = colorbar_gridspec + super().__init__(**kwargs) + + def execute(self, fig): + return + + +class TightLayoutEngine(LayoutEngine): + """ + Implements the ``tight_layout`` geometry management. See + :doc:`/tutorials/intermediate/tight_layout_guide` for details. + """ + _adjust_compatible = True + _colorbar_gridspec = True + + def __init__(self, *, pad=1.08, h_pad=None, w_pad=None, + rect=(0, 0, 1, 1), **kwargs): + """ + Initialize tight_layout engine. + + Parameters + ---------- + pad : float, 1.08 + Padding between the figure edge and the edges of subplots, as a + fraction of the font size. + h_pad, w_pad : float + Padding (height/width) between edges of adjacent subplots. + Defaults to *pad*. + rect : tuple (left, bottom, right, top), default: (0, 0, 1, 1). + rectangle in normalized figure coordinates that the subplots + (including labels) will fit into. + """ + super().__init__(**kwargs) + for td in ['pad', 'h_pad', 'w_pad', 'rect']: + # initialize these in case None is passed in above: + self._params[td] = None + self.set(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) + + def execute(self, fig): + """ + Execute tight_layout. + + This decides the subplot parameters given the padding that + will allow the axes labels to not be covered by other labels + and axes. + + Parameters + ---------- + fig : `.Figure` to perform layout on. + + See Also + -------- + .figure.Figure.tight_layout + .pyplot.tight_layout + """ + info = self._params + renderer = fig._get_renderer() + with getattr(renderer, "_draw_disabled", nullcontext)(): + kwargs = get_tight_layout_figure( + fig, fig.axes, get_subplotspec_list(fig.axes), renderer, + pad=info['pad'], h_pad=info['h_pad'], w_pad=info['w_pad'], + rect=info['rect']) + if kwargs: + fig.subplots_adjust(**kwargs) + + def set(self, *, pad=None, w_pad=None, h_pad=None, rect=None): + for td in self.set.__kwdefaults__: + if locals()[td] is not None: + self._params[td] = locals()[td] + + +class ConstrainedLayoutEngine(LayoutEngine): + """ + Implements the ``constrained_layout`` geometry management. See + :doc:`/tutorials/intermediate/constrainedlayout_guide` for details. + """ + + _adjust_compatible = False + _colorbar_gridspec = False + + def __init__(self, *, h_pad=None, w_pad=None, + hspace=None, wspace=None, rect=(0, 0, 1, 1), + compress=False, **kwargs): + """ + Initialize ``constrained_layout`` settings. + + Parameters + ---------- + h_pad, w_pad : float + Padding around the axes elements in figure-normalized units. + Default to :rc:`figure.constrained_layout.h_pad` and + :rc:`figure.constrained_layout.w_pad`. + hspace, wspace : float + Fraction of the figure to dedicate to space between the + axes. These are evenly spread between the gaps between the axes. + A value of 0.2 for a three-column layout would have a space + of 0.1 of the figure width between each column. + If h/wspace < h/w_pad, then the pads are used instead. + Default to :rc:`figure.constrained_layout.hspace` and + :rc:`figure.constrained_layout.wspace`. + rect : tuple of 4 floats + Rectangle in figure coordinates to perform constrained layout in + (left, bottom, width, height), each from 0-1. + compress : bool + Whether to shift Axes so that white space in between them is + removed. This is useful for simple grids of fixed-aspect Axes (e.g. + a grid of images). See :ref:`compressed_layout`. + """ + super().__init__(**kwargs) + # set the defaults: + self.set(w_pad=mpl.rcParams['figure.constrained_layout.w_pad'], + h_pad=mpl.rcParams['figure.constrained_layout.h_pad'], + wspace=mpl.rcParams['figure.constrained_layout.wspace'], + hspace=mpl.rcParams['figure.constrained_layout.hspace'], + rect=(0, 0, 1, 1)) + # set anything that was passed in (None will be ignored): + self.set(w_pad=w_pad, h_pad=h_pad, wspace=wspace, hspace=hspace, + rect=rect) + self._compress = compress + + def execute(self, fig): + """ + Perform constrained_layout and move and resize axes accordingly. + + Parameters + ---------- + fig : `.Figure` to perform layout on. + """ + width, height = fig.get_size_inches() + # pads are relative to the current state of the figure... + w_pad = self._params['w_pad'] / width + h_pad = self._params['h_pad'] / height + + return do_constrained_layout(fig, w_pad=w_pad, h_pad=h_pad, + wspace=self._params['wspace'], + hspace=self._params['hspace'], + rect=self._params['rect'], + compress=self._compress) + + def set(self, *, h_pad=None, w_pad=None, + hspace=None, wspace=None, rect=None): + """ + Set the pads for constrained_layout. + + Parameters + ---------- + h_pad, w_pad : float + Padding around the axes elements in figure-normalized units. + Default to :rc:`figure.constrained_layout.h_pad` and + :rc:`figure.constrained_layout.w_pad`. + hspace, wspace : float + Fraction of the figure to dedicate to space between the + axes. These are evenly spread between the gaps between the axes. + A value of 0.2 for a three-column layout would have a space + of 0.1 of the figure width between each column. + If h/wspace < h/w_pad, then the pads are used instead. + Default to :rc:`figure.constrained_layout.hspace` and + :rc:`figure.constrained_layout.wspace`. + rect : tuple of 4 floats + Rectangle in figure coordinates to perform constrained layout in + (left, bottom, width, height), each from 0-1. + """ + for td in self.set.__kwdefaults__: + if locals()[td] is not None: + self._params[td] = locals()[td] diff --git a/lib/matplotlib/legend.py b/lib/matplotlib/legend.py index 05453584f629..2d41189b898a 100644 --- a/lib/matplotlib/legend.py +++ b/lib/matplotlib/legend.py @@ -28,22 +28,24 @@ import numpy as np import matplotlib as mpl -from matplotlib import _api, docstring, colors +from matplotlib import _api, _docstring, colors, offsetbox from matplotlib.artist import Artist, allow_rasterization from matplotlib.cbook import silent_list from matplotlib.font_manager import FontProperties from matplotlib.lines import Line2D from matplotlib.patches import (Patch, Rectangle, Shadow, FancyBboxPatch, StepPatch) -from matplotlib.collections import (LineCollection, RegularPolyCollection, - CircleCollection, PathCollection, - PolyCollection) +from matplotlib.collections import ( + Collection, CircleCollection, LineCollection, PathCollection, + PolyCollection, RegularPolyCollection) +from matplotlib.text import Text from matplotlib.transforms import Bbox, BboxBase, TransformedBbox from matplotlib.transforms import BboxTransformTo, BboxTransformFrom - -from matplotlib.offsetbox import HPacker, VPacker, TextArea, DrawingArea -from matplotlib.offsetbox import DraggableOffsetBox - +from matplotlib.offsetbox import ( + AnchoredOffsetbox, DraggableOffsetBox, + HPacker, VPacker, + DrawingArea, TextArea, +) from matplotlib.container import ErrorbarContainer, BarContainer, StemContainer from . import legend_handler @@ -92,51 +94,7 @@ def _update_bbox_to_anchor(self, loc_in_canvas): self.legend.set_bbox_to_anchor(loc_in_bbox) -docstring.interpd.update(_legend_kw_doc=""" -loc : str or pair of floats, default: :rc:`legend.loc` ('best' for axes, \ -'upper right' for figures) - The location of the legend. - - The strings - ``'upper left', 'upper right', 'lower left', 'lower right'`` - place the legend at the corresponding corner of the axes/figure. - - The strings - ``'upper center', 'lower center', 'center left', 'center right'`` - place the legend at the center of the corresponding edge of the - axes/figure. - - The string ``'center'`` places the legend at the center of the axes/figure. - - The string ``'best'`` places the legend at the location, among the nine - locations defined so far, with the minimum overlap with other drawn - artists. This option can be quite slow for plots with large amounts of - data; your plotting speed may benefit from providing a specific location. - - The location can also be a 2-tuple giving the coordinates of the lower-left - corner of the legend in axes coordinates (in which case *bbox_to_anchor* - will be ignored). - - For back-compatibility, ``'center right'`` (but no other location) can also - be spelled ``'right'``, and each "string" locations can also be given as a - numeric value: - - =============== ============= - Location String Location Code - =============== ============= - 'best' 0 - 'upper right' 1 - 'upper left' 2 - 'lower left' 3 - 'lower right' 4 - 'right' 5 - 'center left' 6 - 'center right' 7 - 'lower center' 8 - 'upper center' 9 - 'center' 10 - =============== ============= - +_legend_kw_doc_base = """ bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats Box that is used to position the legend in conjunction with *loc*. Defaults to `axes.bbox` (if called as a method to `.Axes.legend`) or @@ -160,9 +118,12 @@ def _update_bbox_to_anchor(self, loc_in_canvas): loc='upper right', bbox_to_anchor=(0.5, 0.5) -ncol : int, default: 1 +ncols : int, default: 1 The number of columns that the legend has. + For backward compatibility, the spelling *ncol* is also supported + but it is discouraged. If both are given, *ncols* takes precedence. + prop : None or `matplotlib.font_manager.FontProperties` or dict The font properties of the legend. If None (default), the current :data:`matplotlib.rcParams` will be used. @@ -173,12 +134,15 @@ def _update_bbox_to_anchor(self, loc_in_canvas): absolute font size in points. String values are relative to the current default font size. This argument is only used if *prop* is not specified. -labelcolor : str or list +labelcolor : str or list, default: :rc:`legend.labelcolor` The color of the text in the legend. Either a valid color string (for example, 'red'), or a list of color strings. The labelcolor can also be made to match the color of the line or marker using 'linecolor', 'markerfacecolor' (or 'mfc'), or 'markeredgecolor' (or 'mec'). + Labelcolor can be set globally using :rc:`legend.labelcolor`. If None, + use :rc:`text.color`. + numpoints : int, default: :rc:`legend.numpoints` The number of marker points in the legend when creating a legend entry for a `.Line2D` (line). @@ -194,18 +158,23 @@ def _update_bbox_to_anchor(self, loc_in_canvas): same height, set to ``[0.5]``. markerscale : float, default: :rc:`legend.markerscale` - The relative size of legend markers compared with the originally - drawn ones. + The relative size of legend markers compared to the originally drawn ones. markerfirst : bool, default: True If *True*, legend marker is placed to the left of the legend label. If *False*, legend marker is placed to the right of the legend label. +reverse : bool, default: False + If *True*, the legend labels are displayed in reverse order from the input. + If *False*, the legend labels are displayed in the same order as the input. + + .. versionadded:: 3.7 + frameon : bool, default: :rc:`legend.frameon` Whether the legend should be drawn on a patch (frame). fancybox : bool, default: :rc:`legend.fancybox` - Whether round edges should be enabled around the `~.FancyBboxPatch` which + Whether round edges should be enabled around the `.FancyBboxPatch` which makes up the legend's background. shadow : bool, default: :rc:`legend.shadow` @@ -237,9 +206,21 @@ def _update_bbox_to_anchor(self, loc_in_canvas): title : str or None The legend's title. Default is no title (``None``). +title_fontproperties : None or `matplotlib.font_manager.FontProperties` or dict + The font properties of the legend's title. If None (default), the + *title_fontsize* argument will be used if present; if *title_fontsize* is + also None, the current :rc:`legend.title_fontsize` will be used. + title_fontsize : int or {'xx-small', 'x-small', 'small', 'medium', 'large', \ 'x-large', 'xx-large'}, default: :rc:`legend.title_fontsize` The font size of the legend's title. + Note: This cannot be combined with *title_fontproperties*. If you want + to set the fontsize alongside other font properties, use the *size* + parameter in *title_fontproperties*. + +alignment : {'center', 'left', 'right'}, default: 'center' + The alignment of the legend title and the box of entries. The entries + are aligned as a single block, so that markers always lined up. borderpad : float, default: :rc:`legend.borderpad` The fractional whitespace inside the legend border, in font-size units. @@ -250,6 +231,9 @@ def _update_bbox_to_anchor(self, loc_in_canvas): handlelength : float, default: :rc:`legend.handlelength` The length of the legend handles, in font-size units. +handleheight : float, default: :rc:`legend.handleheight` + The height of the legend handles, in font-size units. + handletextpad : float, default: :rc:`legend.handletextpad` The pad between the legend handle and text, in font-size units. @@ -263,76 +247,146 @@ def _update_bbox_to_anchor(self, loc_in_canvas): The custom dictionary mapping instances or types to a legend handler. This *handler_map* updates the default handler map found at `matplotlib.legend.Legend.get_legend_handler_map`. -""") + +draggable : bool, default: False + Whether the legend can be dragged with the mouse. +""" + +_loc_doc_base = """ +loc : str or pair of floats, default: {default} + The location of the legend. + + The strings ``'upper left'``, ``'upper right'``, ``'lower left'``, + ``'lower right'`` place the legend at the corresponding corner of the + {parent}. + + The strings ``'upper center'``, ``'lower center'``, ``'center left'``, + ``'center right'`` place the legend at the center of the corresponding edge + of the {parent}. + + The string ``'center'`` places the legend at the center of the {parent}. +{best} + The location can also be a 2-tuple giving the coordinates of the lower-left + corner of the legend in {parent} coordinates (in which case *bbox_to_anchor* + will be ignored). + + For back-compatibility, ``'center right'`` (but no other location) can also + be spelled ``'right'``, and each "string" location can also be given as a + numeric value: + + ================== ============= + Location String Location Code + ================== ============= + 'best' (Axes only) 0 + 'upper right' 1 + 'upper left' 2 + 'lower left' 3 + 'lower right' 4 + 'right' 5 + 'center left' 6 + 'center right' 7 + 'lower center' 8 + 'upper center' 9 + 'center' 10 + ================== ============= + {outside}""" + +_loc_doc_best = """ + The string ``'best'`` places the legend at the location, among the nine + locations defined so far, with the minimum overlap with other drawn + artists. This option can be quite slow for plots with large amounts of + data; your plotting speed may benefit from providing a specific location. +""" + +_legend_kw_axes_st = ( + _loc_doc_base.format(parent='axes', default=':rc:`legend.loc`', + best=_loc_doc_best, outside='') + + _legend_kw_doc_base) +_docstring.interpd.update(_legend_kw_axes=_legend_kw_axes_st) + +_outside_doc = """ + If a figure is using the constrained layout manager, the string codes + of the *loc* keyword argument can get better layout behaviour using the + prefix 'outside'. There is ambiguity at the corners, so 'outside + upper right' will make space for the legend above the rest of the + axes in the layout, and 'outside right upper' will make space on the + right side of the layout. In addition to the values of *loc* + listed above, we have 'outside right upper', 'outside right lower', + 'outside left upper', and 'outside left lower'. See + :doc:`/tutorials/intermediate/legend_guide` for more details. +""" + +_legend_kw_figure_st = ( + _loc_doc_base.format(parent='figure', default="'upper right'", + best='', outside=_outside_doc) + + _legend_kw_doc_base) +_docstring.interpd.update(_legend_kw_figure=_legend_kw_figure_st) + +_legend_kw_both_st = ( + _loc_doc_base.format(parent='axes/figure', + default=":rc:`legend.loc` for Axes, 'upper right' for Figure", + best=_loc_doc_best, outside=_outside_doc) + + _legend_kw_doc_base) +_docstring.interpd.update(_legend_kw_doc=_legend_kw_both_st) class Legend(Artist): """ - Place a legend on the axes at location loc. - + Place a legend on the figure/axes. """ - codes = {'best': 0, # only implemented for axes legends - 'upper right': 1, - 'upper left': 2, - 'lower left': 3, - 'lower right': 4, - 'right': 5, - 'center left': 6, - 'center right': 7, - 'lower center': 8, - 'upper center': 9, - 'center': 10, - } + # 'best' is only implemented for axes legends + codes = {'best': 0, **AnchoredOffsetbox.codes} zorder = 5 def __str__(self): return "Legend" - @docstring.dedent_interpd - def __init__(self, parent, handles, labels, - loc=None, - numpoints=None, # the number of points in the legend line - markerscale=None, # the relative size of legend markers - # vs. original - markerfirst=True, # controls ordering (left-to-right) of - # legend marker and label - scatterpoints=None, # number of scatter points - scatteryoffsets=None, - prop=None, # properties for the legend texts - fontsize=None, # keyword to set font size directly - labelcolor=None, # keyword to set the text color - - # spacing & pad defined as a fraction of the font-size - borderpad=None, # the whitespace inside the legend border - labelspacing=None, # the vertical space between the legend - # entries - handlelength=None, # the length of the legend handles - handleheight=None, # the height of the legend handles - handletextpad=None, # the pad between the legend handle - # and text - borderaxespad=None, # the pad between the axes and legend - # border - columnspacing=None, # spacing between columns - - ncol=1, # number of columns - mode=None, # mode for horizontal distribution of columns. - # None, "expand" - - fancybox=None, # True use a fancy box, false use a rounded - # box, none use rc - shadow=None, - title=None, # set a title for the legend - title_fontsize=None, # the font size for the title - framealpha=None, # set frame alpha - edgecolor=None, # frame patch edgecolor - facecolor=None, # frame patch facecolor - - bbox_to_anchor=None, # bbox that the legend will be anchored. - bbox_transform=None, # transform for the bbox - frameon=None, # draw frame - handler_map=None, - ): + @_api.make_keyword_only("3.6", "loc") + @_docstring.dedent_interpd + def __init__( + self, parent, handles, labels, + loc=None, + numpoints=None, # number of points in the legend line + markerscale=None, # relative size of legend markers vs. original + markerfirst=True, # left/right ordering of legend marker and label + reverse=False, # reverse ordering of legend marker and label + scatterpoints=None, # number of scatter points + scatteryoffsets=None, + prop=None, # properties for the legend texts + fontsize=None, # keyword to set font size directly + labelcolor=None, # keyword to set the text color + + # spacing & pad defined as a fraction of the font-size + borderpad=None, # whitespace inside the legend border + labelspacing=None, # vertical space between the legend entries + handlelength=None, # length of the legend handles + handleheight=None, # height of the legend handles + handletextpad=None, # pad between the legend handle and text + borderaxespad=None, # pad between the axes and legend border + columnspacing=None, # spacing between columns + + ncols=1, # number of columns + mode=None, # horizontal distribution of columns: None or "expand" + + fancybox=None, # True: fancy box, False: rounded box, None: rcParam + shadow=None, + title=None, # legend title + title_fontsize=None, # legend title font size + framealpha=None, # set frame alpha + edgecolor=None, # frame patch edgecolor + facecolor=None, # frame patch facecolor + + bbox_to_anchor=None, # bbox to which the legend will be anchored + bbox_transform=None, # transform for the bbox + frameon=None, # draw frame + handler_map=None, + title_fontproperties=None, # properties for the legend title + alignment="center", # control the alignment within the legend box + *, + ncol=1, # synonym for ncols (backward compatibility) + draggable=False # whether the legend can be dragged with the mouse + ): """ Parameters ---------- @@ -345,26 +399,22 @@ def __init__(self, parent, handles, labels, labels : list of str A list of labels to show next to the artists. The length of handles and labels should be the same. If they are not, they are truncated - to the smaller of both lengths. + to the length of the shorter list. Other Parameters ---------------- %(_legend_kw_doc)s - Notes - ----- - Users can specify any arbitrary location for the legend using the - *bbox_to_anchor* keyword argument. *bbox_to_anchor* can be a - `.BboxBase` (or derived therefrom) or a tuple of 2 or 4 floats. - See `set_bbox_to_anchor` for more detail. + Attributes + ---------- + legend_handles + List of `.Artist` objects added as legend entries. - The legend location can be specified by setting *loc* with a tuple of - 2 floats, which is interpreted as the lower-left corner of the legend - in the normalized axes coordinate. + .. versionadded:: 3.7 """ # local import only to avoid circularity from matplotlib.axes import Axes - from matplotlib.figure import Figure + from matplotlib.figure import FigureBase super().__init__() @@ -382,40 +432,46 @@ def __init__(self, parent, handles, labels, self._fontsize = self.prop.get_size_in_points() self.texts = [] - self.legendHandles = [] + self.legend_handles = [] self._legend_title_box = None #: A dictionary with the extra handler mappings for this Legend #: instance. self._custom_handler_map = handler_map - locals_view = locals() - for name in ["numpoints", "markerscale", "shadow", "columnspacing", - "scatterpoints", "handleheight", 'borderpad', - 'labelspacing', 'handlelength', 'handletextpad', - 'borderaxespad']: - if locals_view[name] is None: - value = mpl.rcParams["legend." + name] - else: - value = locals_view[name] - setattr(self, name, value) - del locals_view + def val_or_rc(val, rc_name): + return val if val is not None else mpl.rcParams[rc_name] + + self.numpoints = val_or_rc(numpoints, 'legend.numpoints') + self.markerscale = val_or_rc(markerscale, 'legend.markerscale') + self.scatterpoints = val_or_rc(scatterpoints, 'legend.scatterpoints') + self.borderpad = val_or_rc(borderpad, 'legend.borderpad') + self.labelspacing = val_or_rc(labelspacing, 'legend.labelspacing') + self.handlelength = val_or_rc(handlelength, 'legend.handlelength') + self.handleheight = val_or_rc(handleheight, 'legend.handleheight') + self.handletextpad = val_or_rc(handletextpad, 'legend.handletextpad') + self.borderaxespad = val_or_rc(borderaxespad, 'legend.borderaxespad') + self.columnspacing = val_or_rc(columnspacing, 'legend.columnspacing') + self.shadow = val_or_rc(shadow, 'legend.shadow') # trim handles and labels if illegal label... _lab, _hand = [], [] for label, handle in zip(labels, handles): if isinstance(label, str) and label.startswith('_'): - _api.warn_external('The handle {!r} has a label of {!r} ' - 'which cannot be automatically added to' - ' the legend.'.format(handle, label)) + _api.warn_external(f"The label {label!r} of {handle!r} starts " + "with '_'. It is thus excluded from the " + "legend.") else: _lab.append(label) _hand.append(handle) labels, handles = _lab, _hand - handles = list(handles) + if reverse: + labels.reverse() + handles.reverse() + if len(handles) < 2: - ncol = 1 - self._ncol = ncol + ncols = 1 + self._ncols = ncols if ncols != 1 else ncol if self.numpoints <= 0: raise ValueError("numpoints must be > 0; it was %d" % numpoints) @@ -438,29 +494,50 @@ def __init__(self, parent, handles, labels, self.isaxes = True self.axes = parent self.set_figure(parent.figure) - elif isinstance(parent, Figure): + elif isinstance(parent, FigureBase): self.isaxes = False self.set_figure(parent) else: - raise TypeError("Legend needs either Axes or Figure as parent") + raise TypeError( + "Legend needs either Axes or FigureBase as parent" + ) self.parent = parent + loc0 = loc self._loc_used_default = loc is None if loc is None: loc = mpl.rcParams["legend.loc"] if not self.isaxes and loc in [0, 'best']: loc = 'upper right' + + # handle outside legends: + self._outside_loc = None if isinstance(loc, str): - if loc not in self.codes: - raise ValueError( - "Unrecognized location {!r}. Valid locations are\n\t{}\n" - .format(loc, '\n\t'.join(self.codes))) - else: - loc = self.codes[loc] + if loc.split()[0] == 'outside': + # strip outside: + loc = loc.split('outside ')[1] + # strip "center" at the beginning + self._outside_loc = loc.replace('center ', '') + # strip first + self._outside_loc = self._outside_loc.split()[0] + locs = loc.split() + if len(locs) > 1 and locs[0] in ('right', 'left'): + # locs doesn't accept "left upper", etc, so swap + if locs[0] != 'center': + locs = locs[::-1] + loc = locs[0] + ' ' + locs[1] + # check that loc is in acceptable strings + loc = _api.check_getitem(self.codes, loc=loc) + + if self.isaxes and self._outside_loc: + raise ValueError( + f"'outside' option for loc='{loc0}' keyword argument only " + "works for figure legends") + if not self.isaxes and loc == 0: raise ValueError( "Automatic legend placement (loc='best') not implemented for " - "figure legend.") + "figure legend") self._mode = mode self.set_bbox_to_anchor(bbox_to_anchor, bbox_transform) @@ -499,6 +576,9 @@ def __init__(self, parent, handles, labels, ) self._set_artist_props(self.legendPatch) + _api.check_in_list(["center", "left", "right"], alignment=alignment) + self._alignment = alignment + # init with null renderer self._init_legend_box(handles, labels, markerfirst) @@ -506,12 +586,26 @@ def __init__(self, parent, handles, labels, self._set_loc(loc) self._loc_used_default = tmp # ignore changes done by _set_loc - # figure out title fontsize: - if title_fontsize is None: - title_fontsize = mpl.rcParams['legend.title_fontsize'] - tprop = FontProperties(size=title_fontsize) - self.set_title(title, prop=tprop) + # figure out title font properties: + if title_fontsize is not None and title_fontproperties is not None: + raise ValueError( + "title_fontsize and title_fontproperties can't be specified " + "at the same time. Only use one of them. ") + title_prop_fp = FontProperties._from_any(title_fontproperties) + if isinstance(title_fontproperties, dict): + if "size" not in title_fontproperties: + title_fontsize = mpl.rcParams["legend.title_fontsize"] + title_prop_fp.set_size(title_fontsize) + elif title_fontsize is not None: + title_prop_fp.set_size(title_fontsize) + elif not isinstance(title_fontproperties, FontProperties): + title_fontsize = mpl.rcParams["legend.title_fontsize"] + title_prop_fp.set_size(title_fontsize) + + self.set_title(title, prop=title_prop_fp) + self._draggable = None + self.set_draggable(state=draggable) # set the text color @@ -523,14 +617,31 @@ def __init__(self, parent, handles, labels, 'mec': ['get_markeredgecolor', 'get_edgecolor'], } if labelcolor is None: - pass - elif isinstance(labelcolor, str) and labelcolor in color_getters: + if mpl.rcParams['legend.labelcolor'] is not None: + labelcolor = mpl.rcParams['legend.labelcolor'] + else: + labelcolor = mpl.rcParams['text.color'] + if isinstance(labelcolor, str) and labelcolor in color_getters: getter_names = color_getters[labelcolor] - for handle, text in zip(self.legendHandles, self.texts): + for handle, text in zip(self.legend_handles, self.texts): + try: + if handle.get_array() is not None: + continue + except AttributeError: + pass for getter_name in getter_names: try: color = getattr(handle, getter_name)() - text.set_color(color) + if isinstance(color, np.ndarray): + if ( + color.shape[0] == 1 + or np.isclose(color, color[0]).all() + ): + text.set_color(color[0]) + else: + pass + else: + text.set_color(color) break except AttributeError: pass @@ -543,8 +654,10 @@ def __init__(self, parent, handles, labels, colors.to_rgba_array(labelcolor))): text.set_color(color) else: - raise ValueError("Invalid argument for labelcolor : %s" % - str(labelcolor)) + raise ValueError(f"Invalid labelcolor: {labelcolor!r}") + + legendHandles = _api.deprecated('3.7', alternative="legend_handles")( + property(lambda self: self.legend_handles)) def _set_artist_props(self, a): """ @@ -566,6 +679,10 @@ def _set_loc(self, loc): self.stale = True self._legend_box.set_offset(self._findoffset) + def set_ncols(self, ncols): + """Set the number of columns.""" + self._ncols = ncols + def _get_loc(self): return self._loc_real @@ -607,7 +724,7 @@ def draw(self, renderer): # update the location and size of the legend. This needs to # be done in any case to clip the figure right. bbox = self._legend_box.get_window_extent(renderer) - self.legendPatch.set_bounds(bbox.x0, bbox.y0, bbox.width, bbox.height) + self.legendPatch.set_bounds(bbox.bounds) self.legendPatch.set_mutation_scale(fontsize) if self.shadow: @@ -643,38 +760,24 @@ def draw(self, renderer): @classmethod def get_default_handler_map(cls): - """ - A class method that returns the default handler map. - """ + """Return the global default handler map, shared by all legends.""" return cls._default_handler_map @classmethod def set_default_handler_map(cls, handler_map): - """ - A class method to set the default handler map. - """ + """Set the global default handler map, shared by all legends.""" cls._default_handler_map = handler_map @classmethod def update_default_handler_map(cls, handler_map): - """ - A class method to update the default handler map. - """ + """Update the global default handler map, shared by all legends.""" cls._default_handler_map.update(handler_map) def get_legend_handler_map(self): - """ - Return the handler map. - """ - + """Return this legend instance's handler map.""" default_handler_map = self.get_default_handler_map() - - if self._custom_handler_map: - hm = default_handler_map.copy() - hm.update(self._custom_handler_map) - return hm - else: - return default_handler_map + return ({**default_handler_map, **self._custom_handler_map} + if self._custom_handler_map else default_handler_map) @staticmethod def get_legend_handler(legend_handler_map, orig_handle): @@ -712,27 +815,19 @@ def _init_legend_box(self, handles, labels, markerfirst=True): fontsize = self._fontsize - # legend_box is a HPacker, horizontally packed with - # columns. Each column is a VPacker, vertically packed with - # legend items. Each legend item is HPacker packed with - # legend handleBox and labelBox. handleBox is an instance of - # offsetbox.DrawingArea which contains legend handle. labelBox - # is an instance of offsetbox.TextArea which contains legend - # text. + # legend_box is a HPacker, horizontally packed with columns. + # Each column is a VPacker, vertically packed with legend items. + # Each legend item is a HPacker packed with: + # - handlebox: a DrawingArea which contains the legend handle. + # - labelbox: a TextArea which contains the legend text. text_list = [] # the list of text instances - handle_list = [] # the list of text instances + handle_list = [] # the list of handle instances handles_and_labels = [] - label_prop = dict(verticalalignment='baseline', - horizontalalignment='left', - fontproperties=self.prop, - ) - # The approximate height and descent of text. These values are # only used for plotting the legend handle. - descent = 0.35 * fontsize * (self.handleheight - 0.7) - # 0.35 and 0.7 are just heuristic numbers and may need to be improved. + descent = 0.35 * fontsize * (self.handleheight - 0.7) # heuristic. height = fontsize * self.handleheight - descent # each handle needs to be drawn inside a box of (x, y, w, h) = # (0, -descent, width, height). And their coordinates should @@ -744,21 +839,24 @@ def _init_legend_box(self, handles, labels, markerfirst=True): # manually set their transform to the self.get_transform(). legend_handler_map = self.get_legend_handler_map() - for orig_handle, lab in zip(handles, labels): + for orig_handle, label in zip(handles, labels): handler = self.get_legend_handler(legend_handler_map, orig_handle) if handler is None: _api.warn_external( - "Legend does not support {!r} instances.\nA proxy artist " - "may be used instead.\nSee: " - "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 - # to None. + "Legend does not support handles for {0} " + "instances.\nA proxy artist may be used " + "instead.\nSee: https://matplotlib.org/" + "stable/tutorials/intermediate/legend_guide.html" + "#controlling-the-legend-entries".format( + type(orig_handle).__name__)) + # No handle for this artist, so we just defer to None. handle_list.append(None) else: - textbox = TextArea(lab, textprops=label_prop, - multilinebaseline=True) + textbox = TextArea(label, multilinebaseline=True, + textprops=dict( + verticalalignment='baseline', + horizontalalignment='left', + fontproperties=self.prop)) handlebox = DrawingArea(width=self.handlelength * fontsize, height=height, xdescent=0., ydescent=descent) @@ -770,34 +868,25 @@ def _init_legend_box(self, handles, labels, markerfirst=True): fontsize, handlebox)) handles_and_labels.append((handlebox, textbox)) - if handles_and_labels: - # We calculate number of rows in each column. The first - # (num_largecol) columns will have (nrows+1) rows, and remaining - # (num_smallcol) columns will have (nrows) rows. - ncol = min(self._ncol, len(handles_and_labels)) - nrows, num_largecol = divmod(len(handles_and_labels), ncol) - num_smallcol = ncol - num_largecol - # starting index of each column and number of rows in it. - rows_per_col = [nrows + 1] * num_largecol + [nrows] * num_smallcol - start_idxs = np.concatenate([[0], np.cumsum(rows_per_col)[:-1]]) - cols = zip(start_idxs, rows_per_col) - else: - cols = [] - columnbox = [] - for i0, di in cols: - # pack handleBox and labelBox into itemBox - itemBoxes = [HPacker(pad=0, + # array_split splits n handles_and_labels into ncols columns, with the + # first n%ncols columns having an extra entry. filter(len, ...) + # handles the case where n < ncols: the last ncols-n columns are empty + # and get filtered out. + for handles_and_labels_column in filter( + len, np.array_split(handles_and_labels, self._ncols)): + # pack handlebox and labelbox into itembox + itemboxes = [HPacker(pad=0, sep=self.handletextpad * fontsize, children=[h, t] if markerfirst else [t, h], align="baseline") - for h, t in handles_and_labels[i0:i0 + di]] - # pack columnBox + for h, t in handles_and_labels_column] + # pack columnbox alignment = "baseline" if markerfirst else "right" columnbox.append(VPacker(pad=0, sep=self.labelspacing * fontsize, align=alignment, - children=itemBoxes)) + children=itemboxes)) mode = "expand" if self._mode == "expand" else "fixed" sep = self.columnspacing * fontsize @@ -808,12 +897,13 @@ def _init_legend_box(self, handles, labels, markerfirst=True): self._legend_title_box = TextArea("") self._legend_box = VPacker(pad=self.borderpad * fontsize, sep=self.labelspacing * fontsize, - align="center", + align=self._alignment, children=[self._legend_title_box, self._legend_handle_box]) self._legend_box.set_figure(self.figure) + self._legend_box.axes = self.axes self.texts = text_list - self.legendHandles = handle_list + self.legend_handles = handle_list def _auto_legend_data(self): """ @@ -829,18 +919,25 @@ def _auto_legend_data(self): List of (x, y) offsets of all collection. """ assert self.isaxes # always holds, as this is only called internally - ax = self.parent - lines = [line.get_transform().transform_path(line.get_path()) - for line in ax.lines] - bboxes = [patch.get_bbox().transformed(patch.get_data_transform()) - if isinstance(patch, Rectangle) else - patch.get_path().get_extents(patch.get_transform()) - for patch in ax.patches] + bboxes = [] + lines = [] offsets = [] - for handle in ax.collections: - _, transOffset, hoffsets, _ = handle._prepare_points() - for offset in transOffset.transform(hoffsets): - offsets.append(offset) + for artist in self.parent._children: + if isinstance(artist, Line2D): + lines.append( + artist.get_transform().transform_path(artist.get_path())) + elif isinstance(artist, Rectangle): + bboxes.append( + artist.get_bbox().transformed(artist.get_data_transform())) + elif isinstance(artist, Patch): + lines.append( + artist.get_transform().transform_path(artist.get_path())) + elif isinstance(artist, Collection): + transform, transOffset, hoffsets, _ = artist._prepare_points() + if len(hoffsets): + for offset in transOffset.transform(hoffsets): + offsets.append(offset) + return bboxes, lines, offsets def get_children(self): @@ -853,22 +950,53 @@ def get_frame(self): def get_lines(self): r"""Return the list of `~.lines.Line2D`\s in the legend.""" - return [h for h in self.legendHandles if isinstance(h, Line2D)] + return [h for h in self.legend_handles if isinstance(h, Line2D)] def get_patches(self): r"""Return the list of `~.patches.Patch`\s in the legend.""" return silent_list('Patch', - [h for h in self.legendHandles + [h for h in self.legend_handles if isinstance(h, Patch)]) def get_texts(self): r"""Return the list of `~.text.Text`\s in the legend.""" return silent_list('Text', self.texts) + def set_alignment(self, alignment): + """ + Set the alignment of the legend title and the box of entries. + + The entries are aligned as a single block, so that markers always + lined up. + + Parameters + ---------- + alignment : {'center', 'left', 'right'}. + + """ + _api.check_in_list(["center", "left", "right"], alignment=alignment) + self._alignment = alignment + self._legend_box.align = alignment + + def get_alignment(self): + """Get the alignment value of the legend box""" + return self._legend_box.align + def set_title(self, title, prop=None): """ - Set the legend title. Fontproperties can be optionally set - with *prop* parameter. + Set legend title and title style. + + Parameters + ---------- + title : str + The legend title. + + prop : `.font_manager.FontProperties` or `str` or `pathlib.Path` + The font properties of the legend title. + If a `str`, it is interpreted as a fontconfig pattern parsed by + `.FontProperties`. If a `pathlib.Path`, it is interpreted as the + absolute path to a font file. + """ self._legend_title_box._text.set_text(title) if title: @@ -890,24 +1018,11 @@ def get_title(self): def get_window_extent(self, renderer=None): # docstring inherited if renderer is None: - renderer = self.figure._cachedRenderer + renderer = self.figure._get_renderer() return self._legend_box.get_window_extent(renderer=renderer) - def get_tightbbox(self, renderer): - """ - Like `.Legend.get_window_extent`, but uses the box for the legend. - - Parameters - ---------- - renderer : `.RendererBase` subclass - renderer that will be used to draw the figures (i.e. - ``fig.canvas.get_renderer()``) - - Returns - ------- - `.BboxBase` - The bounding box in figure pixel coordinates. - """ + def get_tightbbox(self, renderer=None): + # docstring inherited return self._legend_box.get_window_extent(renderer) def get_frame_on(self): @@ -963,8 +1078,7 @@ def set_bbox_to_anchor(self, bbox, transform=None): try: l = len(bbox) except TypeError as err: - raise ValueError("Invalid argument for bbox : %s" % - str(bbox)) from err + raise ValueError(f"Invalid bbox: {bbox}") from err if l == 2: bbox = [bbox[0], bbox[1], 0, 0] @@ -992,29 +1106,10 @@ def _get_anchored_bbox(self, loc, bbox, parentbbox, renderer): bbox to be placed, in display coordinates. parentbbox : `~matplotlib.transforms.Bbox` A parent box which will contain the bbox, in display coordinates. - """ - assert loc in range(1, 11) # called only internally - - BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) - - anchor_coefs = {UR: "NE", - UL: "NW", - LL: "SW", - LR: "SE", - R: "E", - CL: "W", - CR: "E", - LC: "S", - UC: "N", - C: "C"} - - c = anchor_coefs[loc] - - fontsize = renderer.points_to_pixels(self._fontsize) - container = parentbbox.padded(-self.borderaxespad * fontsize) - anchored_box = bbox.anchored(c, container=container) - return anchored_box.x0, anchored_box.y0 + return offsetbox._get_anchored_bbox( + loc, bbox, parentbbox, + self.borderaxespad * renderer.points_to_pixels(self._fontsize)) def _find_best_position(self, width, height, renderer, consider=None): """ @@ -1110,43 +1205,43 @@ def get_draggable(self): # Helper functions to parse legend arguments for both `figure.legend` and # `axes.legend`: def _get_legend_handles(axs, legend_handler_map=None): - """ - Return a generator of artists that can be used as handles in - a legend. - - """ + """Yield artists that can be used as handles in a legend.""" handles_original = [] for ax in axs: - handles_original += (ax.lines + ax.patches + - ax.collections + ax.containers) + handles_original += [ + *(a for a in ax._children + if isinstance(a, (Line2D, Patch, Collection, Text))), + *ax.containers] # support parasite axes: if hasattr(ax, 'parasites'): for axx in ax.parasites: - handles_original += (axx.lines + axx.patches + - axx.collections + axx.containers) - - handler_map = Legend.get_default_handler_map() - - if legend_handler_map is not None: - handler_map = handler_map.copy() - handler_map.update(legend_handler_map) + handles_original += [ + *(a for a in axx._children + if isinstance(a, (Line2D, Patch, Collection, Text))), + *axx.containers] + handler_map = {**Legend.get_default_handler_map(), + **(legend_handler_map or {})} has_handler = Legend.get_legend_handler - for handle in handles_original: label = handle.get_label() if label != '_nolegend_' and has_handler(handler_map, handle): yield handle + elif (label and not label.startswith('_') and + not has_handler(handler_map, handle)): + _api.warn_external( + "Legend does not support handles for {0} " + "instances.\nSee: https://matplotlib.org/stable/" + "tutorials/intermediate/legend_guide.html" + "#implementing-a-custom-legend-handler".format( + type(handle).__name__)) + continue def _get_legend_handles_labels(axs, legend_handler_map=None): - """ - Return handles and labels for legend, internal method. - - """ + """Return handles and labels for legend.""" handles = [] labels = [] - for handle in _get_legend_handles(axs, legend_handler_map): label = handle.get_label() if label and not label.startswith('_'): @@ -1202,7 +1297,7 @@ def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs): """ log = logging.getLogger(__name__) - handlers = kwargs.get('handler_map', {}) or {} + handlers = kwargs.get('handler_map') extra_args = () if (handles is not None or labels is not None) and args: @@ -1225,7 +1320,10 @@ def _parse_legend_args(axs, *args, handles=None, labels=None, **kwargs): elif len(args) == 0: handles, labels = _get_legend_handles_labels(axs, handlers) if not handles: - log.warning('No handles with labels found to put in legend.') + log.warning( + "No artists with labels found to put in legend. Note that " + "artists whose label start with an underscore are ignored " + "when legend() is called with no argument.") # One argument. User defined labels - automatic handle detection. elif len(args) == 1: diff --git a/lib/matplotlib/legend_handler.py b/lib/matplotlib/legend_handler.py index 016bf8d3adf8..849644145856 100644 --- a/lib/matplotlib/legend_handler.py +++ b/lib/matplotlib/legend_handler.py @@ -1,20 +1,24 @@ """ Default legend handlers. -It is strongly encouraged to have read the :doc:`legend guide -` before this documentation. +.. important:: + + This is a low-level legend API, which most end users do not need. + + We recommend that you are familiar with the :doc:`legend guide + ` before reading this documentation. Legend handlers are expected to be a callable object with a following -signature. :: +signature:: legend_handler(legend, orig_handle, fontsize, handlebox) Where *legend* is the legend itself, *orig_handle* is the original -plot, *fontsize* is the fontsize in pixels, and *handlebox* is a -OffsetBox instance. Within the call, you should create relevant +plot, *fontsize* is the fontsize in pixels, and *handlebox* is an +`.OffsetBox` instance. Within the call, you should create relevant artists (using relevant properties from the *legend* and/or -*orig_handle*) and add them into the handlebox. The artists needs to -be scaled according to the fontsize (note that the size is in pixel, +*orig_handle*) and add them into the *handlebox*. The artists need to +be scaled according to the *fontsize* (note that the size is in pixels, i.e., this is dpi-scaled value). This module includes definition of several legend handler classes @@ -27,7 +31,7 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox) import numpy as np -from matplotlib import cbook +from matplotlib import _api, cbook from matplotlib.lines import Line2D from matplotlib.patches import Rectangle import matplotlib.collections as mcoll @@ -41,10 +45,10 @@ def update_from_first_child(tgt, src): class HandlerBase: """ - A Base class for default legend handlers. + A base class for default legend handlers. The derived classes are meant to override *create_artists* method, which - has a following signature.:: + has the following signature:: def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, @@ -56,6 +60,18 @@ def create_artists(self, legend, orig_handle, """ def __init__(self, xpad=0., ypad=0., update_func=None): + """ + Parameters + ---------- + + xpad : float, optional + Padding in x-direction. + ypad : float, optional + Padding in y-direction. + update_func : callable, optional + Function for updating the legend handler properties from another + legend handler, used by `~HandlerBase.update_prop`. + """ self._xpad, self._ypad = xpad, ypad self._update_prop_func = update_func @@ -125,6 +141,26 @@ def legend_artist(self, legend, orig_handle, def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): + """ + Return the legend artists generated. + + Parameters + ---------- + legend : `~matplotlib.legend.Legend` + The legend for which these legend artists are being created. + orig_handle : `~matplotlib.artist.Artist` or similar + The object for which these legend artists are being created. + xdescent, ydescent, width, height : int + The rectangle (*xdescent*, *ydescent*, *width*, *height*) that the + legend artists being created should fit within. + fontsize : int + The fontsize in pixels. The legend artists being created should + be scaled according to the given fontsize. + trans : `~matplotlib.transforms.Transform` + The transform that is applied to the legend artists being created. + Typically from unit coordinates in the handler box to screen + coordinates. + """ raise NotImplementedError('Derived must override') @@ -132,21 +168,19 @@ class HandlerNpoints(HandlerBase): """ A legend handler that shows *numpoints* points in the legend entry. """ - def __init__(self, marker_pad=0.3, numpoints=None, **kw): + + def __init__(self, marker_pad=0.3, numpoints=None, **kwargs): """ Parameters ---------- marker_pad : float Padding between points in legend entry. - numpoints : int Number of points to show in legend entry. - - Notes - ----- - Any other keyword arguments are given to `HandlerBase`. + **kwargs + Keyword arguments forwarded to `.HandlerBase`. """ - super().__init__(**kw) + super().__init__(**kwargs) self._numpoints = numpoints self._marker_pad = marker_pad @@ -177,22 +211,20 @@ class HandlerNpointsYoffsets(HandlerNpoints): A legend handler that shows *numpoints* in the legend, and allows them to be individually offset in the y-direction. """ - def __init__(self, numpoints=None, yoffsets=None, **kw): + + def __init__(self, numpoints=None, yoffsets=None, **kwargs): """ Parameters ---------- numpoints : int Number of points to show in legend entry. - yoffsets : array of floats Length *numpoints* list of y offsets for each point in legend entry. - - Notes - ----- - Any other keyword arguments are given to `HandlerNpoints`. + **kwargs + Keyword arguments forwarded to `.HandlerNpoints`. """ - super().__init__(numpoints=numpoints, **kw) + super().__init__(numpoints=numpoints, **kwargs) self._yoffsets = yoffsets def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): @@ -204,30 +236,16 @@ def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): return ydata -class HandlerLine2D(HandlerNpoints): +class HandlerLine2DCompound(HandlerNpoints): """ - Handler for `.Line2D` instances. + Original handler for `.Line2D` instances, that relies on combining + a line-only with a marker-only artist. May be deprecated in the future. """ - def __init__(self, marker_pad=0.3, numpoints=None, **kw): - """ - Parameters - ---------- - marker_pad : float - Padding between points in legend entry. - - numpoints : int - Number of points to show in legend entry. - - Notes - ----- - Any other keyword arguments are given to `HandlerNpoints`. - """ - super().__init__(marker_pad=marker_pad, numpoints=numpoints, **kw) def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): - + # docstring inherited xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent, width, height, fontsize) @@ -255,11 +273,51 @@ def create_artists(self, legend, orig_handle, return [legline, legline_marker] +class HandlerLine2D(HandlerNpoints): + """ + Handler for `.Line2D` instances. + + See Also + -------- + HandlerLine2DCompound : An earlier handler implementation, which used one + artist for the line and another for the marker(s). + """ + + def create_artists(self, legend, orig_handle, + xdescent, ydescent, width, height, fontsize, + trans): + # docstring inherited + xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent, + width, height, fontsize) + + markevery = None + if self.get_numpoints(legend) == 1: + # Special case: one wants a single marker in the center + # and a line that extends on both sides. One will use a + # 3 points line, but only mark the #1 (i.e. middle) point. + xdata = np.linspace(xdata[0], xdata[-1], 3) + markevery = [1] + + ydata = np.full_like(xdata, (height - ydescent) / 2) + legline = Line2D(xdata, ydata, markevery=markevery) + + self.update_prop(legline, orig_handle, legend) + + if legend.markerscale != 1: + newsz = legline.get_markersize() * legend.markerscale + legline.set_markersize(newsz) + + legline.set_transform(trans) + + return [legline] + + class HandlerPatch(HandlerBase): """ Handler for `.Patch` instances. """ - def __init__(self, patch_func=None, **kw): + + def __init__(self, patch_func=None, **kwargs): """ Parameters ---------- @@ -271,14 +329,13 @@ def patch_func(legend=legend, orig_handle=orig_handle, xdescent=xdescent, ydescent=ydescent, width=width, height=height, fontsize=fontsize) - Subsequently the created artist will have its ``update_prop`` + Subsequently, the created artist will have its ``update_prop`` method called and the appropriate transform will be applied. - Notes - ----- - Any other keyword arguments are given to `HandlerBase`. + **kwargs + Keyword arguments forwarded to `.HandlerBase`. """ - super().__init__(**kw) + super().__init__(**kwargs) self._patch_func = patch_func def _create_patch(self, legend, orig_handle, @@ -294,6 +351,7 @@ def _create_patch(self, legend, orig_handle, def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): + # docstring inherited p = self._create_patch(legend, orig_handle, xdescent, ydescent, width, height, fontsize) self.update_prop(p, orig_handle, legend) @@ -305,43 +363,35 @@ class HandlerStepPatch(HandlerBase): """ Handler for `~.matplotlib.patches.StepPatch` instances. """ - def __init__(self, **kw): - """ - Any other keyword arguments are given to `HandlerBase`. - """ - super().__init__(**kw) - - def _create_patch(self, legend, orig_handle, - xdescent, ydescent, width, height, fontsize): - p = Rectangle(xy=(-xdescent, -ydescent), - color=orig_handle.get_facecolor(), - width=width, height=height) - return p - # Unfilled StepPatch should show as a line - def _create_line(self, legend, orig_handle, - xdescent, ydescent, width, height, fontsize): + @staticmethod + def _create_patch(orig_handle, xdescent, ydescent, width, height): + return Rectangle(xy=(-xdescent, -ydescent), width=width, + height=height, color=orig_handle.get_facecolor()) - # Overwrite manually because patch and line properties don't mix + @staticmethod + def _create_line(orig_handle, width, height): + # Unfilled StepPatch should show as a line legline = Line2D([0, width], [height/2, height/2], color=orig_handle.get_edgecolor(), linestyle=orig_handle.get_linestyle(), linewidth=orig_handle.get_linewidth(), ) + # Overwrite manually because patch and line properties don't mix legline.set_drawstyle('default') legline.set_marker("") return legline def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): + # docstring inherited if orig_handle.get_fill() or (orig_handle.get_hatch() is not None): - p = self._create_patch(legend, orig_handle, - xdescent, ydescent, width, height, fontsize) + p = self._create_patch(orig_handle, xdescent, ydescent, width, + height) self.update_prop(p, orig_handle, legend) else: - p = self._create_line(legend, orig_handle, - xdescent, ydescent, width, height, fontsize) + p = self._create_line(orig_handle, width, height) p.set_transform(trans) return [p] @@ -366,7 +416,7 @@ def _default_update_prop(self, legend_handle, orig_handle): def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): - + # docstring inherited xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent, width, height, fontsize) ydata = np.full_like(xdata, (height - ydescent) / 2) @@ -381,8 +431,8 @@ def create_artists(self, legend, orig_handle, class HandlerRegularPolyCollection(HandlerNpointsYoffsets): r"""Handler for `.RegularPolyCollection`\s.""" - def __init__(self, yoffsets=None, sizes=None, **kw): - super().__init__(yoffsets=yoffsets, **kw) + def __init__(self, yoffsets=None, sizes=None, **kwargs): + super().__init__(yoffsets=yoffsets, **kwargs) self._sizes = sizes @@ -422,18 +472,18 @@ def update_prop(self, legend_handle, orig_handle, legend): legend_handle.set_clip_box(None) legend_handle.set_clip_path(None) - def create_collection(self, orig_handle, sizes, offsets, transOffset): - p = type(orig_handle)(orig_handle.get_numsides(), - rotation=orig_handle.get_rotation(), - sizes=sizes, - offsets=offsets, - transOffset=transOffset, - ) - return p + @_api.rename_parameter("3.6", "transOffset", "offset_transform") + def create_collection(self, orig_handle, sizes, offsets, offset_transform): + return type(orig_handle)( + orig_handle.get_numsides(), + rotation=orig_handle.get_rotation(), sizes=sizes, + offsets=offsets, offset_transform=offset_transform, + ) def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): + # docstring inherited xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent, width, height, fontsize) @@ -443,46 +493,45 @@ def create_artists(self, legend, orig_handle, sizes = self.get_sizes(legend, orig_handle, xdescent, ydescent, width, height, fontsize) - p = self.create_collection(orig_handle, sizes, - offsets=list(zip(xdata_marker, ydata)), - transOffset=trans) + p = self.create_collection( + orig_handle, sizes, + offsets=list(zip(xdata_marker, ydata)), offset_transform=trans) self.update_prop(p, orig_handle, legend) - p._transOffset = trans + p.set_offset_transform(trans) return [p] class HandlerPathCollection(HandlerRegularPolyCollection): r"""Handler for `.PathCollection`\s, which are used by `~.Axes.scatter`.""" - def create_collection(self, orig_handle, sizes, offsets, transOffset): - p = type(orig_handle)([orig_handle.get_paths()[0]], - sizes=sizes, - offsets=offsets, - transOffset=transOffset, - ) - return p + + @_api.rename_parameter("3.6", "transOffset", "offset_transform") + def create_collection(self, orig_handle, sizes, offsets, offset_transform): + return type(orig_handle)( + [orig_handle.get_paths()[0]], sizes=sizes, + offsets=offsets, offset_transform=offset_transform, + ) class HandlerCircleCollection(HandlerRegularPolyCollection): r"""Handler for `.CircleCollection`\s.""" - def create_collection(self, orig_handle, sizes, offsets, transOffset): - p = type(orig_handle)(sizes, - offsets=offsets, - transOffset=transOffset, - ) - return p + + @_api.rename_parameter("3.6", "transOffset", "offset_transform") + def create_collection(self, orig_handle, sizes, offsets, offset_transform): + return type(orig_handle)( + sizes, offsets=offsets, offset_transform=offset_transform) class HandlerErrorbar(HandlerLine2D): """Handler for Errorbars.""" def __init__(self, xerr_size=0.5, yerr_size=None, - marker_pad=0.3, numpoints=None, **kw): + marker_pad=0.3, numpoints=None, **kwargs): self._xerr_size = xerr_size self._yerr_size = yerr_size - super().__init__(marker_pad=marker_pad, numpoints=numpoints, **kw) + super().__init__(marker_pad=marker_pad, numpoints=numpoints, **kwargs) def get_err_size(self, legend, xdescent, ydescent, width, height, fontsize): @@ -498,7 +547,7 @@ def get_err_size(self, legend, xdescent, ydescent, def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): - + # docstring inherited plotlines, caplines, barlinecols = orig_handle xdata, xdata_marker = self.get_xdata(legend, xdescent, ydescent, @@ -524,7 +573,7 @@ def create_artists(self, legend, orig_handle, self.update_prop(legline, plotlines, legend) legline.set_drawstyle('default') - legline.set_marker('None') + legline.set_marker('none') self.update_prop(legline_marker, plotlines, legend) legline_marker.set_linestyle('None') @@ -584,30 +633,26 @@ class HandlerStem(HandlerNpointsYoffsets): """ Handler for plots produced by `~.Axes.stem`. """ + def __init__(self, marker_pad=0.3, numpoints=None, - bottom=None, yoffsets=None, **kw): + bottom=None, yoffsets=None, **kwargs): """ Parameters ---------- marker_pad : float, default: 0.3 Padding between points in legend entry. - numpoints : int, optional Number of points to show in legend entry. - bottom : float, optional yoffsets : array of floats, optional Length *numpoints* list of y offsets for each point in legend entry. - - Notes - ----- - Any other keyword arguments are given to `HandlerNpointsYoffsets`. + **kwargs + Keyword arguments forwarded to `.HandlerNpointsYoffsets`. """ - super().__init__(marker_pad=marker_pad, numpoints=numpoints, - yoffsets=yoffsets, **kw) + yoffsets=yoffsets, **kwargs) self._bottom = bottom def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): @@ -621,6 +666,7 @@ def get_ydata(self, legend, xdescent, ydescent, width, height, fontsize): def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): + # docstring inherited markerline, stemlines, baseline = orig_handle # Check to see if the stemcontainer is storing lines as a list or a # LineCollection. Eventually using a list will be removed, and this @@ -677,19 +723,20 @@ def _copy_collection_props(self, legend_handle, orig_handle): class HandlerTuple(HandlerBase): """ Handler for Tuple. - - Additional kwargs are passed through to `HandlerBase`. - - Parameters - ---------- - ndivide : int, default: 1 - The number of sections to divide the legend area into. If None, - use the length of the input tuple. - pad : float, default: :rc:`legend.borderpad` - Padding in units of fraction of font size. """ def __init__(self, ndivide=1, pad=None, **kwargs): + """ + Parameters + ---------- + ndivide : int, default: 1 + The number of sections to divide the legend area into. If None, + use the length of the input tuple. + pad : float, default: :rc:`legend.borderpad` + Padding in units of fraction of font size. + **kwargs + Keyword arguments forwarded to `.HandlerBase`. + """ self._ndivide = ndivide self._pad = pad super().__init__(**kwargs) @@ -697,7 +744,7 @@ def __init__(self, ndivide=1, pad=None, **kwargs): def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): - + # docstring inherited handler_map = legend.get_legend_handler_map() if self._ndivide is None: @@ -747,6 +794,8 @@ def get_first(prop_array): # Directly set Patch color attributes (must be RGBA tuples). legend_handle._facecolor = first_color(orig_handle.get_facecolor()) legend_handle._edgecolor = first_color(orig_handle.get_edgecolor()) + legend_handle._original_facecolor = orig_handle._original_facecolor + legend_handle._original_edgecolor = orig_handle._original_edgecolor legend_handle._fill = orig_handle.get_fill() legend_handle._hatch = orig_handle.get_hatch() # Hatch color is anomalous in having no getters and setters. @@ -760,6 +809,7 @@ def get_first(prop_array): def create_artists(self, legend, orig_handle, xdescent, ydescent, width, height, fontsize, trans): + # docstring inherited p = Rectangle(xy=(-xdescent, -ydescent), width=width, height=height) self.update_prop(p, orig_handle, legend) diff --git a/lib/matplotlib/lines.py b/lib/matplotlib/lines.py index 1203862a3f50..db0ce3ba0cea 100644 --- a/lib/matplotlib/lines.py +++ b/lib/matplotlib/lines.py @@ -1,15 +1,16 @@ """ -The 2D line class which can draw with a variety of line styles, markers and -colors. +2D lines with support for a variety of line styles, markers, colors, etc. """ +import copy + from numbers import Integral, Number, Real import logging import numpy as np import matplotlib as mpl -from . import _api, artist, cbook, colors as mcolors, docstring, rcParams +from . import _api, cbook, colors as mcolors, _docstring from .artist import Artist, allow_rasterization from .cbook import ( _to_unmasked_float_array, ls_mapper, ls_mapper_r, STEP_LOOKUP_MAP) @@ -21,7 +22,7 @@ # Imported here for backward compatibility, even though they don't # really belong. from . import _path -from .markers import ( +from .markers import ( # noqa CARETLEFT, CARETRIGHT, CARETUP, CARETDOWN, CARETLEFTBASE, CARETRIGHTBASE, CARETUPBASE, CARETDOWNBASE, TICKLEFT, TICKRIGHT, TICKUP, TICKDOWN) @@ -41,18 +42,14 @@ def _get_dash_pattern(style): # dashed styles elif style in ['dashed', 'dashdot', 'dotted']: offset = 0 - dashes = tuple(rcParams['lines.{}_pattern'.format(style)]) + dashes = tuple(mpl.rcParams['lines.{}_pattern'.format(style)]) # elif isinstance(style, tuple): offset, dashes = style if offset is None: - _api.warn_deprecated( - "3.3", message="Passing the dash offset as None is deprecated " - "since %(since)s and support for it will be removed " - "%(removal)s; pass it as zero instead.") - offset = 0 + raise ValueError(f'Unrecognized linestyle: {style!r}') else: - raise ValueError('Unrecognized linestyle: %s' % str(style)) + raise ValueError(f'Unrecognized linestyle: {style!r}') # normalize offset to be positive and shorter than the dash cycle if dashes is not None: @@ -64,7 +61,7 @@ def _get_dash_pattern(style): def _scale_dashes(offset, dashes, lw): - if not rcParams['lines.scale_dashes']: + if not mpl.rcParams['lines.scale_dashes']: return offset, dashes scaled_offset = offset * lw scaled_dashes = ([x * lw if x is not None else None for x in dashes] @@ -109,7 +106,7 @@ def segment_hits(cx, cy, x, y, radius): return np.concatenate((points, lines)) -def _mark_every_path(markevery, tpath, affine, ax_transform): +def _mark_every_path(markevery, tpath, affine, ax): """ Helper function that sorts out how to deal the input `markevery` and returns the points where markers should be drawn. @@ -157,15 +154,23 @@ def _slice_or_none(in_v, slc): '`markevery` is a tuple with len 2 and second element is ' 'a float, but the first element is not a float or an int; ' 'markevery={}'.format(markevery)) + if ax is None: + raise ValueError( + "markevery is specified relative to the axes size, but " + "the line does not have a Axes as parent") + # calc cumulative distance along path (in display coords): - disp_coords = affine.transform(tpath.vertices) + fin = np.isfinite(verts).all(axis=1) + fverts = verts[fin] + disp_coords = affine.transform(fverts) + delta = np.empty((len(disp_coords), 2)) delta[0, :] = 0 delta[1:, :] = disp_coords[1:, :] - disp_coords[:-1, :] delta = np.hypot(*delta.T).cumsum() # calc distance between markers along path based on the axes # bounding box diagonal being a distance of unity: - (x0, y0), (x1, y1) = ax_transform.transform([[0, 0], [1, 1]]) + (x0, y0), (x1, y1) = ax.transAxes.transform([[0, 0], [1, 1]]) scale = np.hypot(x1 - x0, y1 - y0) marker_delta = np.arange(start * scale, delta[-1], step * scale) # find closest actual data point that is closest to @@ -174,7 +179,7 @@ def _slice_or_none(in_v, slc): inds = inds.argmin(axis=1) inds = np.unique(inds) # return, we are done here - return Path(verts[inds], _slice_or_none(codes, inds)) + return Path(fverts[inds], _slice_or_none(codes, inds)) else: raise ValueError( f"markevery={markevery!r} is a tuple with len 2, but its " @@ -196,7 +201,8 @@ def _slice_or_none(in_v, slc): raise ValueError(f"markevery={markevery!r} is not a recognized value") -@cbook._define_aliases({ +@_docstring.interpd +@_api.define_aliases({ "antialiased": ["aa"], "color": ["c"], "drawstyle": ["ds"], @@ -250,16 +256,6 @@ class Line2D(Artist): zorder = 2 - @_api.deprecated("3.4") - @_api.classproperty - def validCap(cls): - return tuple(cs.value for cs in CapStyle) - - @_api.deprecated("3.4") - @_api.classproperty - def validJoin(cls): - return tuple(js.value for js in JoinStyle) - def __str__(self): if self._label != "": return f"Line2D({self._label})" @@ -273,10 +269,12 @@ def __str__(self): return "Line2D(%s)" % ",".join( map("({:g},{:g})".format, self._x, self._y)) + @_api.make_keyword_only("3.6", name="linewidth") def __init__(self, xdata, ydata, linewidth=None, # all Nones default to rc linestyle=None, color=None, + gapcolor=None, marker=None, markersize=None, markeredgewidth=None, @@ -300,7 +298,7 @@ def __init__(self, xdata, ydata, Additional keyword arguments are `.Line2D` properties: - %(Line2D_kwdoc)s + %(Line2D:kwdoc)s See :meth:`set_linestyle` for a description of the line styles, :meth:`set_marker` for a description of the markers, and @@ -309,38 +307,34 @@ def __init__(self, xdata, ydata, """ super().__init__() - #convert sequences to numpy arrays + # Convert sequences to NumPy arrays. if not np.iterable(xdata): raise RuntimeError('xdata must be a sequence') if not np.iterable(ydata): raise RuntimeError('ydata must be a sequence') if linewidth is None: - linewidth = rcParams['lines.linewidth'] + linewidth = mpl.rcParams['lines.linewidth'] if linestyle is None: - linestyle = rcParams['lines.linestyle'] + linestyle = mpl.rcParams['lines.linestyle'] if marker is None: - marker = rcParams['lines.marker'] - if markerfacecolor is None: - markerfacecolor = rcParams['lines.markerfacecolor'] - if markeredgecolor is None: - markeredgecolor = rcParams['lines.markeredgecolor'] + marker = mpl.rcParams['lines.marker'] if color is None: - color = rcParams['lines.color'] + color = mpl.rcParams['lines.color'] if markersize is None: - markersize = rcParams['lines.markersize'] + markersize = mpl.rcParams['lines.markersize'] if antialiased is None: - antialiased = rcParams['lines.antialiased'] + antialiased = mpl.rcParams['lines.antialiased'] if dash_capstyle is None: - dash_capstyle = rcParams['lines.dash_capstyle'] + dash_capstyle = mpl.rcParams['lines.dash_capstyle'] if dash_joinstyle is None: - dash_joinstyle = rcParams['lines.dash_joinstyle'] + dash_joinstyle = mpl.rcParams['lines.dash_joinstyle'] if solid_capstyle is None: - solid_capstyle = rcParams['lines.solid_capstyle'] + solid_capstyle = mpl.rcParams['lines.solid_capstyle'] if solid_joinstyle is None: - solid_joinstyle = rcParams['lines.solid_joinstyle'] + solid_joinstyle = mpl.rcParams['lines.solid_joinstyle'] if drawstyle is None: drawstyle = 'default' @@ -357,14 +351,8 @@ def __init__(self, xdata, ydata, self._linestyles = None self._drawstyle = None self._linewidth = linewidth - - # scaled dash + offset - self._dashSeq = None - self._dashOffset = 0 - # unscaled dash + offset - # this is needed scaling the dash pattern by linewidth - self._us_dashSeq = None - self._us_dashOffset = 0 + self._unscaled_dash_pattern = (0, None) # offset, dash + self._dash_pattern = (0, None) # offset, dash (scaled by linewidth) self.set_linewidth(linewidth) self.set_linestyle(linestyle) @@ -372,7 +360,15 @@ def __init__(self, xdata, ydata, self._color = None self.set_color(color) - self._marker = MarkerStyle(marker, fillstyle) + if marker is None: + marker = 'none' # Default. + if not isinstance(marker, MarkerStyle): + self._marker = MarkerStyle(marker, fillstyle) + else: + self._marker = marker + + self._gapcolor = None + self.set_gapcolor(gapcolor) self._markevery = None self._markersize = None @@ -387,19 +383,19 @@ def __init__(self, xdata, ydata, self._markerfacecolor = None self._markerfacecoloralt = None - self.set_markerfacecolor(markerfacecolor) + self.set_markerfacecolor(markerfacecolor) # Normalizes None to rc. self.set_markerfacecoloralt(markerfacecoloralt) - self.set_markeredgecolor(markeredgecolor) + self.set_markeredgecolor(markeredgecolor) # Normalizes None to rc. self.set_markeredgewidth(markeredgewidth) # update kwargs before updating data to give the caller a # chance to init axes (and hence unit support) - self.update(kwargs) - self.pickradius = pickradius + self._internal_update(kwargs) + self._pickradius = pickradius self.ind_offset = 0 if (isinstance(self._picker, Number) and not isinstance(self._picker, bool)): - self.pickradius = self._picker + self._pickradius = self._picker self._xorig = np.asarray([]) self._yorig = np.asarray([]) @@ -460,9 +456,9 @@ def contains(self, mouseevent): # Convert pick radius from points to pixels if self.figure is None: _log.warning('no figure set when check if mouse is on line') - pixels = self.pickradius + pixels = self._pickradius else: - pixels = self.figure.dpi / 72. * self.pickradius + pixels = self.figure.dpi / 72. * self._pickradius # The math involved in checking for containment (here and inside of # segment_hits) assumes that it is OK to overflow, so temporarily set @@ -493,7 +489,8 @@ def get_pickradius(self): """ return self._pickradius - def set_pickradius(self, d): + @_api.rename_parameter("3.6", "d", "pickradius") + def set_pickradius(self, pickradius): """ Set the pick radius used for containment tests. @@ -501,12 +498,12 @@ def set_pickradius(self, d): Parameters ---------- - d : float + pickradius : float Pick radius, in points. """ - if not isinstance(d, Number) or d < 0: + if not isinstance(pickradius, Number) or pickradius < 0: raise ValueError("pick radius should be a distance") - self._pickradius = d + self._pickradius = pickradius pickradius = property(get_pickradius, set_pickradius) @@ -550,37 +547,39 @@ def set_markevery(self, every): (float, float) or list[bool] Which markers to plot. - - every=None, every point will be plotted. - - every=N, every N-th marker will be plotted starting with + - ``every=None``: every point will be plotted. + - ``every=N``: every N-th marker will be plotted starting with marker 0. - - every=(start, N), every N-th marker, starting at point - start, will be plotted. - - every=slice(start, end, N), every N-th marker, starting at - point start, up to but not including point end, will be plotted. - - every=[i, j, m, n], only markers at points i, j, m, and n - will be plotted. - - every=[True, False, True], positions that are True will be + - ``every=(start, N)``: every N-th marker, starting at index + *start*, will be plotted. + - ``every=slice(start, end, N)``: every N-th marker, starting at + index *start*, up to but not including index *end*, will be plotted. - - every=0.1, (i.e. a float) then markers will be spaced at - approximately equal distances along the line; the distance + - ``every=[i, j, m, ...]``: only markers at the given indices + will be plotted. + - ``every=[True, False, True, ...]``: only positions that are True + will be plotted. The list must have the same length as the data + points. + - ``every=0.1``, (i.e. a float): markers will be spaced at + approximately equal visual distances along the line; the distance along the line between markers is determined by multiplying the display-coordinate distance of the axes bounding-box diagonal - by the value of every. - - every=(0.5, 0.1) (i.e. a length-2 tuple of float), the same - functionality as every=0.1 is exhibited but the first marker will - be 0.5 multiplied by the display-coordinate-diagonal-distance - along the line. + by the value of *every*. + - ``every=(0.5, 0.1)`` (i.e. a length-2 tuple of float): similar + to ``every=0.1`` but the first marker will be offset along the + line by 0.5 multiplied by the + display-coordinate-diagonal-distance along the line. For examples see :doc:`/gallery/lines_bars_and_markers/markevery_demo`. Notes ----- - Setting the markevery property will only show markers at actual data - points. When using float arguments to set the markevery property - on irregularly spaced data, the markers will likely not appear evenly - spaced because the actual data points do not coincide with the - theoretical spacing between markers. + Setting *markevery* will still only draw markers at actual data points. + While the float argument form aims for uniform visual spacing, it has + to coerce from the ideal spacing to the nearest available data point. + Depending on the number and distribution of data points, the result + may still not look evenly spaced. When using a start offset to specify the first marker, the offset will be from the first data point which may be different from the first @@ -605,20 +604,24 @@ def get_markevery(self): def set_picker(self, p): """ - Sets the event picker details for the line. + Set the event picker details for the line. Parameters ---------- p : float or callable[[Artist, Event], tuple[bool, dict]] If a float, it is used as the pick radius in points. """ - if callable(p): - self._contains = p - else: - self.pickradius = p + if not callable(p): + self.set_pickradius(p) self._picker = p - def get_window_extent(self, renderer): + def get_bbox(self): + """Get the bounding box of this line.""" + bbox = Bbox([[0, 0], [0, 0]]) + bbox.update_from_data_xy(self.get_xydata()) + return bbox + + def get_window_extent(self, renderer=None): bbox = Bbox([[0, 0], [0, 0]]) trans_data_to_xy = self.get_transform().transform bbox.update_from_data_xy(trans_data_to_xy(self.get_xydata()), @@ -629,15 +632,6 @@ def get_window_extent(self, renderer): bbox = bbox.padded(ms) return bbox - @Artist.axes.setter - def axes(self, ax): - # call the set method from the base-class property - Artist.axes.fset(self, ax) - if ax is not None: - for axis in ax._get_axis_map().values(): - axis.callbacks._pickled_cids.add( - axis.callbacks.connect('units', self.recache_always)) - def set_data(self, *args): """ Set the x and y data. @@ -677,7 +671,8 @@ def recache(self, always=False): self.axes.name == 'rectilinear' and self.axes.get_xscale() == 'linear' and self._markevery is None and - self.get_clip_on()): + self.get_clip_on() and + self.get_transform() == self.axes.transData): self._subslice = True nanmask = np.isnan(x) if nanmask.any(): @@ -701,7 +696,7 @@ def recache(self, always=False): def _transform_path(self, subslice=None): """ - Puts a TransformedPath instance at self._transformed_path; + Put a TransformedPath instance at self._transformed_path; all invalidation of the transform is then handled by the TransformedPath instance. """ @@ -715,26 +710,16 @@ def _transform_path(self, subslice=None): self._transformed_path = TransformedPath(_path, self.get_transform()) def _get_transformed_path(self): - """ - Return the :class:`~matplotlib.transforms.TransformedPath` instance - of this line. - """ + """Return this line's `~matplotlib.transforms.TransformedPath`.""" if self._transformed_path is None: self._transform_path() return self._transformed_path def set_transform(self, t): - """ - Set the Transformation instance used by this artist. - - Parameters - ---------- - t : `matplotlib.transforms.Transform` - """ - super().set_transform(t) + # docstring inherited self._invalidx = True self._invalidy = True - self.stale = True + super().set_transform(t) def _is_sorted(self, x): """Return whether x is sorted in ascending order.""" @@ -774,9 +759,6 @@ def draw(self, renderer): self._set_gc_clip(gc) gc.set_url(self.get_url()) - lc_rgba = mcolors.to_rgba(self._color, self._alpha) - gc.set_foreground(lc_rgba, isRGBA=True) - gc.set_antialiased(self._antialiased) gc.set_linewidth(self._linewidth) @@ -792,7 +774,27 @@ def draw(self, renderer): if self.get_sketch_params() is not None: gc.set_sketch_params(*self.get_sketch_params()) - gc.set_dashes(self._dashOffset, self._dashSeq) + # We first draw a path within the gaps if needed. + if self.is_dashed() and self._gapcolor is not None: + lc_rgba = mcolors.to_rgba(self._gapcolor, self._alpha) + gc.set_foreground(lc_rgba, isRGBA=True) + + # Define the inverse pattern by moving the last gap to the + # start of the sequence. + dashes = self._dash_pattern[1] + gaps = dashes[-1:] + dashes[:-1] + # Set the offset so that this new first segment is skipped + # (see backend_bases.GraphicsContextBase.set_dashes for + # offset definition). + offset_gaps = self._dash_pattern[0] + dashes[-1] + + gc.set_dashes(offset_gaps, gaps) + renderer.draw_path(gc, tpath, affine.frozen()) + + lc_rgba = mcolors.to_rgba(self._color, self._alpha) + gc.set_foreground(lc_rgba, isRGBA=True) + + gc.set_dashes(*self._dash_pattern) renderer.draw_path(gc, tpath, affine.frozen()) gc.restore() @@ -839,8 +841,8 @@ def draw(self, renderer): # subsample the markers if markevery is not None markevery = self.get_markevery() if markevery is not None: - subsampled = _mark_every_path(markevery, tpath, - affine, self.axes.transAxes) + subsampled = _mark_every_path( + markevery, tpath, affine, self.axes) else: subsampled = tpath @@ -896,6 +898,14 @@ def get_drawstyle(self): """ return self._drawstyle + def get_gapcolor(self): + """ + Return the line gapcolor. + + See also `~.Line2D.set_gapcolor`. + """ + return self._gapcolor + def get_linestyle(self): """ Return the linestyle. @@ -928,7 +938,7 @@ def get_markeredgecolor(self): """ mec = self._markeredgecolor if cbook._str_equal(mec, 'auto'): - if rcParams['_internal.classic_mode']: + if mpl.rcParams['_internal.classic_mode']: if self._marker.get_marker() in ('.', ','): return self._color if (self._marker.is_filled() @@ -1014,10 +1024,7 @@ def get_ydata(self, orig=True): return self._y def get_path(self): - """ - Return the :class:`~matplotlib.path.Path` object associated - with this line. - """ + """Return the `~matplotlib.path.Path` associated with this line.""" if self._invalidy or self._invalidx: self.recache() return self._path @@ -1050,8 +1057,7 @@ def set_color(self, color): ---------- color : color """ - if not cbook._str_equal(color, 'auto'): - mcolors._check_color_like(color=color) + mcolors._check_color_like(color=color) self._color = color self.stale = True @@ -1090,6 +1096,29 @@ def set_drawstyle(self, drawstyle): self._invalidx = True self._drawstyle = drawstyle + def set_gapcolor(self, gapcolor): + """ + Set a color to fill the gaps in the dashed line style. + + .. note:: + + Striped lines are created by drawing two interleaved dashed lines. + There can be overlaps between those two, which may result in + artifacts when using transparency. + + This functionality is experimental and may change. + + Parameters + ---------- + gapcolor : color or None + The color with which to fill the gaps. If None, the gaps are + unfilled. + """ + if gapcolor is not None: + mcolors._check_color_like(color=gapcolor) + self._gapcolor = gapcolor + self.stale = True + def set_linewidth(self, w): """ Set the line width in points. @@ -1100,13 +1129,10 @@ def set_linewidth(self, w): Line width, in points. """ w = float(w) - if self._linewidth != w: self.stale = True self._linewidth = w - # rescale the dashes + offset - self._dashOffset, self._dashSeq = _scale_dashes( - self._us_dashOffset, self._us_dashSeq, self._linewidth) + self._dash_pattern = _scale_dashes(*self._unscaled_dash_pattern, w) def set_linestyle(self, ls): """ @@ -1119,15 +1145,15 @@ def set_linestyle(self, ls): - A string: - =============================== ================= - Linestyle Description - =============================== ================= - ``'-'`` or ``'solid'`` solid line - ``'--'`` or ``'dashed'`` dashed line - ``'-.'`` or ``'dashdot'`` dash-dotted line - ``':'`` or ``'dotted'`` dotted line - ``'None'`` or ``' '`` or ``''`` draw nothing - =============================== ================= + ========================================== ================= + linestyle description + ========================================== ================= + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing + ========================================== ================= - Alternatively a dash tuple of the following form can be provided:: @@ -1142,21 +1168,18 @@ def set_linestyle(self, ls): if isinstance(ls, str): if ls in [' ', '', 'none']: ls = 'None' - _api.check_in_list([*self._lineStyles, *ls_mapper_r], ls=ls) if ls not in self._lineStyles: ls = ls_mapper_r[ls] self._linestyle = ls else: self._linestyle = '--' + self._unscaled_dash_pattern = _get_dash_pattern(ls) + self._dash_pattern = _scale_dashes( + *self._unscaled_dash_pattern, self._linewidth) + self.stale = True - # get the unscaled dashes - self._us_dashOffset, self._us_dashSeq = _get_dash_pattern(ls) - # compute the linewidth scaled dashes - self._dashOffset, self._dashSeq = _scale_dashes( - self._us_dashOffset, self._us_dashSeq, self._linewidth) - - @docstring.interpd + @_docstring.interpd def set_marker(self, marker): """ Set the line marker. @@ -1170,6 +1193,20 @@ def set_marker(self, marker): self._marker = MarkerStyle(marker, self._marker.get_fillstyle()) self.stale = True + def _set_markercolor(self, name, has_rcdefault, val): + if val is None: + val = mpl.rcParams[f"lines.{name}"] if has_rcdefault else "auto" + attr = f"_{name}" + current = getattr(self, attr) + if current is None: + self.stale = True + else: + neq = current != val + # Much faster than `np.any(current != val)` if no arrays are used. + if neq.any() if isinstance(neq, np.ndarray) else neq: + self.stale = True + setattr(self, attr, val) + def set_markeredgecolor(self, ec): """ Set the marker edge color. @@ -1178,55 +1215,42 @@ def set_markeredgecolor(self, ec): ---------- ec : color """ - if ec is None: - ec = 'auto' - if (self._markeredgecolor is None - or np.any(self._markeredgecolor != ec)): - self.stale = True - self._markeredgecolor = ec + self._set_markercolor("markeredgecolor", True, ec) - def set_markeredgewidth(self, ew): + def set_markerfacecolor(self, fc): """ - Set the marker edge width in points. + Set the marker face color. Parameters ---------- - ew : float - Marker edge width, in points. + fc : color """ - if ew is None: - ew = rcParams['lines.markeredgewidth'] - if self._markeredgewidth != ew: - self.stale = True - self._markeredgewidth = ew + self._set_markercolor("markerfacecolor", True, fc) - def set_markerfacecolor(self, fc): + def set_markerfacecoloralt(self, fc): """ - Set the marker face color. + Set the alternate marker face color. Parameters ---------- fc : color """ - if fc is None: - fc = 'auto' - if np.any(self._markerfacecolor != fc): - self.stale = True - self._markerfacecolor = fc + self._set_markercolor("markerfacecoloralt", False, fc) - def set_markerfacecoloralt(self, fc): + def set_markeredgewidth(self, ew): """ - Set the alternate marker face color. + Set the marker edge width in points. Parameters ---------- - fc : color + ew : float + Marker edge width, in points. """ - if fc is None: - fc = 'auto' - if np.any(self._markerfacecoloralt != fc): + if ew is None: + ew = mpl.rcParams['lines.markeredgewidth'] + if self._markeredgewidth != ew: self.stale = True - self._markerfacecoloralt = fc + self._markeredgewidth = ew def set_markersize(self, sz): """ @@ -1250,7 +1274,16 @@ def set_xdata(self, x): ---------- x : 1D array """ - self._xorig = x + if not np.iterable(x): + # When deprecation cycle is completed + # raise RuntimeError('x must be a sequence') + _api.warn_deprecated( + since=3.7, + message="Setting data with a non sequence type " + "is deprecated since %(since)s and will be " + "remove %(removal)s") + x = [x, ] + self._xorig = copy.copy(x) self._invalidx = True self.stale = True @@ -1262,7 +1295,16 @@ def set_ydata(self, y): ---------- y : 1D array """ - self._yorig = y + if not np.iterable(y): + # When deprecation cycle is completed + # raise RuntimeError('y must be a sequence') + _api.warn_deprecated( + since=3.7, + message="Setting data with a non sequence type " + "is deprecated since %(since)s and will be " + "remove %(removal)s") + y = [y, ] + self._yorig = copy.copy(y) self._invalidy = True self.stale = True @@ -1276,6 +1318,9 @@ def set_dashes(self, seq): For example, (5, 2, 1, 2) describes a sequence of 5 point and 1 point dashes separated by 2 point spaces. + See also `~.Line2D.set_gapcolor`, which allows those spaces to be + filled with a color. + Parameters ---------- seq : sequence of floats (on/off ink in points) or (None, None) @@ -1293,15 +1338,14 @@ def update_from(self, other): self._linestyle = other._linestyle self._linewidth = other._linewidth self._color = other._color + self._gapcolor = other._gapcolor self._markersize = other._markersize self._markerfacecolor = other._markerfacecolor self._markerfacecoloralt = other._markerfacecoloralt self._markeredgecolor = other._markeredgecolor self._markeredgewidth = other._markeredgewidth - self._dashSeq = other._dashSeq - self._us_dashSeq = other._us_dashSeq - self._dashOffset = other._dashOffset - self._us_dashOffset = other._us_dashOffset + self._unscaled_dash_pattern = other._unscaled_dash_pattern + self._dash_pattern = other._dash_pattern self._dashcapstyle = other._dashcapstyle self._dashjoinstyle = other._dashjoinstyle self._solidcapstyle = other._solidcapstyle @@ -1311,11 +1355,13 @@ def update_from(self, other): self._marker = MarkerStyle(marker=other._marker) self._drawstyle = other._drawstyle - @docstring.interpd + @_docstring.interpd def set_dash_joinstyle(self, s): """ How to join segments of the line if it `~Line2D.is_dashed`. + The default joinstyle is :rc:`lines.dash_joinstyle`. + Parameters ---------- s : `.JoinStyle` or %(JoinStyle)s @@ -1325,11 +1371,13 @@ def set_dash_joinstyle(self, s): self.stale = True self._dashjoinstyle = js - @docstring.interpd + @_docstring.interpd def set_solid_joinstyle(self, s): """ How to join segments if the line is solid (not `~Line2D.is_dashed`). + The default joinstyle is :rc:`lines.solid_joinstyle`. + Parameters ---------- s : `.JoinStyle` or %(JoinStyle)s @@ -1355,11 +1403,13 @@ def get_solid_joinstyle(self): """ return self._solidjoinstyle.name - @docstring.interpd + @_docstring.interpd def set_dash_capstyle(self, s): """ How to draw the end caps if the line is `~Line2D.is_dashed`. + The default capstyle is :rc:`lines.dash_capstyle`. + Parameters ---------- s : `.CapStyle` or %(CapStyle)s @@ -1369,11 +1419,13 @@ def set_dash_capstyle(self, s): self.stale = True self._dashcapstyle = cs - @docstring.interpd + @_docstring.interpd def set_solid_capstyle(self, s): """ How to draw the end caps if the line is solid (not `~Line2D.is_dashed`) + The default capstyle is :rc:`lines.solid_capstyle`. + Parameters ---------- s : `.CapStyle` or %(CapStyle)s @@ -1477,13 +1529,11 @@ def draw(self, renderer): class VertexSelector: """ - Manage the callbacks to maintain a list of selected vertices for - `.Line2D`. Derived classes should override - :meth:`~matplotlib.lines.VertexSelector.process_selected` to do + Manage the callbacks to maintain a list of selected vertices for `.Line2D`. + Derived classes should override the `process_selected` method to do something with the picks. - Here is an example which highlights the selected verts with red - circles:: + Here is an example which highlights the selected verts with red circles:: import numpy as np import matplotlib.pyplot as plt @@ -1491,7 +1541,7 @@ class VertexSelector: class HighlightSelected(lines.VertexSelector): def __init__(self, line, fmt='ro', **kwargs): - lines.VertexSelector.__init__(self, line) + super().__init__(line) self.markers, = self.axes.plot([], [], fmt, **kwargs) def process_selected(self, ind, xs, ys): @@ -1504,32 +1554,32 @@ def process_selected(self, ind, xs, ys): selector = HighlightSelected(line) plt.show() - """ + def __init__(self, line): """ - Initialize the class with a `.Line2D` instance. The line should - already be added to some :class:`matplotlib.axes.Axes` instance and - should have the picker property set. + Parameters + ---------- + line : `.Line2D` + The line must already have been added to an `~.axes.Axes` and must + have its picker property set. """ if line.axes is None: raise RuntimeError('You must first add the line to the Axes') - if line.get_picker() is None: raise RuntimeError('You must first set the picker property ' 'of the line') - self.axes = line.axes self.line = line - self.canvas = self.axes.figure.canvas - self.cid = self.canvas.mpl_connect('pick_event', self.onpick) - + self.cid = self.canvas.callbacks._connect_picklable( + 'pick_event', self.onpick) self.ind = set() + canvas = property(lambda self: self.axes.figure.canvas) + def process_selected(self, ind, xs, ys): """ - Default "do nothing" implementation of the - :meth:`process_selected` method. + Default "do nothing" implementation of the `process_selected` method. Parameters ---------- @@ -1554,9 +1604,3 @@ def onpick(self, event): lineMarkers = MarkerStyle.markers drawStyles = Line2D.drawStyles fillStyles = MarkerStyle.fillstyles - -docstring.interpd.update(Line2D_kwdoc=artist.kwdoc(Line2D)) - -# You can not set the docstring of an instancemethod, -# but you can on the underlying function. Go figure. -docstring.interpd(Line2D.__init__) diff --git a/lib/matplotlib/markers.py b/lib/matplotlib/markers.py index ae2bb3da83ac..c9fc0141939d 100644 --- a/lib/matplotlib/markers.py +++ b/lib/matplotlib/markers.py @@ -45,7 +45,8 @@ ``9`` (``CARETRIGHTBASE``) |m34| caretright (centered at base) ``10`` (``CARETUPBASE``) |m35| caretup (centered at base) ``11`` (``CARETDOWNBASE``) |m36| caretdown (centered at base) -``"None"``, ``" "`` or ``""`` nothing +``"none"`` or ``"None"`` nothing +``" "`` or ``""`` nothing ``'$...$'`` |m37| Render the string using mathtext. E.g ``"$f$"`` for marker showing the letter ``f``. @@ -63,9 +64,10 @@ rotated by ``angle``. ============================== ====== ========================================= -``None`` is the default which means 'nothing', however this table is -referred to from other docs for the valid inputs from marker inputs and in -those cases ``None`` still means 'default'. +As a deprecated feature, ``None`` also means 'nothing' when directly +constructing a `.MarkerStyle`, but note that there are other contexts where +``marker=None`` instead means "the default marker" (e.g. :rc:`scatter.marker` +for `.Axes.scatter`). Note that special symbols can be defined via the :doc:`STIX math font `, @@ -80,12 +82,16 @@ plt.plot([1, 2, 3], marker=11) plt.plot([1, 2, 3], marker=matplotlib.markers.CARETDOWNBASE) +Markers join and cap styles can be customized by creating a new instance of +MarkerStyle. +A MarkerStyle can also have a custom `~matplotlib.transforms.Transform` +allowing it to be arbitrarily rotated or offset. + Examples showing the use of markers: * :doc:`/gallery/lines_bars_and_markers/marker_reference` -* :doc:`/gallery/shapes_and_collections/marker_path` * :doc:`/gallery/lines_bars_and_markers/scatter_star_poly` - +* :doc:`/gallery/lines_bars_and_markers/multivariate_marker_plot` .. |m00| image:: /_static/markers/m00.png .. |m01| image:: /_static/markers/m01.png @@ -126,12 +132,15 @@ .. |m36| image:: /_static/markers/m36.png .. |m37| image:: /_static/markers/m37.png """ +import copy from collections.abc import Sized +import inspect import numpy as np -from . import _api, cbook, rcParams +import matplotlib as mpl +from . import _api, cbook from .path import Path from .transforms import IdentityTransform, Affine2D from ._enums import JoinStyle, CapStyle @@ -200,7 +209,7 @@ class MarkerStyle: CARETUPBASE: 'caretupbase', CARETDOWNBASE: 'caretdownbase', "None": 'nothing', - None: 'nothing', + "none": 'nothing', ' ': 'nothing', '': 'nothing' } @@ -208,33 +217,63 @@ class MarkerStyle: # Just used for informational purposes. is_filled() # is calculated in the _set_* functions. filled_markers = ( - 'o', 'v', '^', '<', '>', '8', 's', 'p', '*', 'h', 'H', 'D', 'd', + '.', 'o', 'v', '^', '<', '>', '8', 's', 'p', '*', 'h', 'H', 'D', 'd', 'P', 'X') fillstyles = ('full', 'left', 'right', 'bottom', 'top', 'none') _half_fillstyles = ('left', 'right', 'bottom', 'top') - # TODO: Is this ever used as a non-constant? - _point_size_reduction = 0.5 + _unset = object() # For deprecation of MarkerStyle(). - def __init__(self, marker=None, fillstyle=None): + def __init__(self, marker=_unset, fillstyle=None, + transform=None, capstyle=None, joinstyle=None): """ Parameters ---------- - marker : str, array-like, Path, MarkerStyle, or None, default: None + marker : str, array-like, Path, MarkerStyle, or None - Another instance of *MarkerStyle* copies the details of that ``marker``. - - *None* means no marker. - - For other possible marker values see the module docstring + - *None* means no marker. This is the deprecated default. + - For other possible marker values, see the module docstring `matplotlib.markers`. - fillstyle : str, default: 'full' + fillstyle : str, default: :rc:`markers.fillstyle` One of 'full', 'left', 'right', 'bottom', 'top', 'none'. + + transform : transforms.Transform, default: None + Transform that will be combined with the native transform of the + marker. + + capstyle : CapStyle, default: None + Cap style that will override the default cap style of the marker. + + joinstyle : JoinStyle, default: None + Join style that will override the default join style of the marker. """ self._marker_function = None + self._user_transform = transform + self._user_capstyle = capstyle + self._user_joinstyle = joinstyle self._set_fillstyle(fillstyle) + # Remove _unset and signature rewriting after deprecation elapses. + if marker is self._unset: + marker = "" + _api.warn_deprecated( + "3.6", message="Calling MarkerStyle() with no parameters is " + "deprecated since %(since)s; support will be removed " + "%(removal)s. Use MarkerStyle('') to construct an empty " + "MarkerStyle.") + if marker is None: + marker = "" + _api.warn_deprecated( + "3.6", message="MarkerStyle(None) is deprecated since " + "%(since)s; support will be removed %(removal)s. Use " + "MarkerStyle('') to construct an empty MarkerStyle.") self._set_marker(marker) + __init__.__signature__ = inspect.signature( # Only for deprecation period. + lambda self, marker, fillstyle=None: None) + def _recache(self): if self._marker_function is None: return @@ -244,7 +283,7 @@ def _recache(self): self._alt_transform = None self._snap_threshold = None self._joinstyle = JoinStyle.round - self._capstyle = CapStyle.butt + self._capstyle = self._user_capstyle or CapStyle.butt # Initial guess: Assume the marker is filled unless the fillstyle is # set to 'none'. The marker function will override this for unfilled # markers. @@ -260,10 +299,6 @@ def is_filled(self): def get_fillstyle(self): return self._fillstyle - @_api.deprecated("3.4", alternative="a new marker") - def set_fillstyle(self, fillstyle): - return self._set_fillstyle(fillstyle) - def _set_fillstyle(self, fillstyle): """ Set the fillstyle. @@ -275,24 +310,20 @@ def _set_fillstyle(self, fillstyle): markerfacecolor. """ if fillstyle is None: - fillstyle = rcParams['markers.fillstyle'] + fillstyle = mpl.rcParams['markers.fillstyle'] _api.check_in_list(self.fillstyles, fillstyle=fillstyle) self._fillstyle = fillstyle self._recache() def get_joinstyle(self): - return self._joinstyle + return self._joinstyle.name def get_capstyle(self): - return self._capstyle + return self._capstyle.name def get_marker(self): return self._marker - @_api.deprecated("3.4", alternative="a new marker") - def set_marker(self, marker): - return self._set_marker(marker) - def _set_marker(self, marker): """ Set the marker. @@ -321,7 +352,8 @@ def _set_marker(self, marker): self._marker_function = getattr( self, '_set_' + self.markers[marker]) elif isinstance(marker, MarkerStyle): - self.__dict__.update(marker.__dict__) + self.__dict__ = copy.deepcopy(marker.__dict__) + else: try: Path(marker) @@ -348,7 +380,10 @@ def get_transform(self): Return the transform to be applied to the `.Path` from `MarkerStyle.get_path()`. """ - return self._transform.frozen() + if self._user_transform is None: + return self._transform.frozen() + else: + return (self._transform + self._user_transform).frozen() def get_alt_path(self): """ @@ -364,11 +399,86 @@ def get_alt_transform(self): Return the transform to be applied to the `.Path` from `MarkerStyle.get_alt_path()`. """ - return self._alt_transform.frozen() + if self._user_transform is None: + return self._alt_transform.frozen() + else: + return (self._alt_transform + self._user_transform).frozen() def get_snap_threshold(self): return self._snap_threshold + def get_user_transform(self): + """Return user supplied part of marker transform.""" + if self._user_transform is not None: + return self._user_transform.frozen() + + def transformed(self, transform: Affine2D): + """ + Return a new version of this marker with the transform applied. + + Parameters + ---------- + transform : Affine2D, default: None + Transform will be combined with current user supplied transform. + """ + new_marker = MarkerStyle(self) + if new_marker._user_transform is not None: + new_marker._user_transform += transform + else: + new_marker._user_transform = transform + return new_marker + + def rotated(self, *, deg=None, rad=None): + """ + Return a new version of this marker rotated by specified angle. + + Parameters + ---------- + deg : float, default: None + Rotation angle in degrees. + + rad : float, default: None + Rotation angle in radians. + + .. note:: You must specify exactly one of deg or rad. + """ + if deg is None and rad is None: + raise ValueError('One of deg or rad is required') + if deg is not None and rad is not None: + raise ValueError('Only one of deg and rad can be supplied') + new_marker = MarkerStyle(self) + if new_marker._user_transform is None: + new_marker._user_transform = Affine2D() + + if deg is not None: + new_marker._user_transform.rotate_deg(deg) + if rad is not None: + new_marker._user_transform.rotate(rad) + + return new_marker + + def scaled(self, sx, sy=None): + """ + Return new marker scaled by specified scale factors. + + If *sy* is None, the same scale is applied in both the *x*- and + *y*-directions. + + Parameters + ---------- + sx : float + *X*-direction scaling factor. + sy : float, default: None + *Y*-direction scaling factor. + """ + if sy is None: + sy = sx + + new_marker = MarkerStyle(self) + _transform = new_marker._user_transform or Affine2D() + new_marker._user_transform = _transform.scale(sx, sy) + return new_marker + def _set_nothing(self): self._filled = False @@ -392,21 +502,21 @@ def _set_tuple_marker(self): symstyle = marker[1] if symstyle == 0: self._path = Path.unit_regular_polygon(numsides) - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter elif symstyle == 1: self._path = Path.unit_regular_star(numsides) - self._joinstyle = JoinStyle.bevel + self._joinstyle = self._user_joinstyle or JoinStyle.bevel elif symstyle == 2: self._path = Path.unit_regular_asterisk(numsides) self._filled = False - self._joinstyle = JoinStyle.bevel + self._joinstyle = self._user_joinstyle or JoinStyle.bevel else: raise ValueError(f"Unexpected tuple marker: {marker}") self._transform = Affine2D().scale(0.5).rotate_deg(rotation) def _set_mathtext_path(self): """ - Draws mathtext markers '$...$' using TextPath object. + Draw mathtext markers '$...$' using `.TextPath` object. Submitted by tcb """ @@ -415,7 +525,7 @@ def _set_mathtext_path(self): # again, the properties could be initialised just once outside # this function text = TextPath(xy=(0, 0), s=self.get_marker(), - usetex=rcParams['text.usetex']) + usetex=mpl.rcParams['text.usetex']) if len(text.vertices) == 0: return @@ -433,8 +543,8 @@ def _set_mathtext_path(self): def _half_fill(self): return self.get_fillstyle() in self._half_fillstyles - def _set_circle(self, reduction=1.0): - self._transform = Affine2D().scale(0.5 * reduction) + def _set_circle(self, size=1.0): + self._transform = Affine2D().scale(0.5 * size) self._snap_threshold = np.inf if not self._half_fill(): self._path = Path.unit_circle() @@ -445,6 +555,9 @@ def _set_circle(self, reduction=1.0): {'right': 0, 'top': 90, 'left': 180, 'bottom': 270}[fs]) self._alt_transform = self._transform.frozen().rotate_deg(180.) + def _set_point(self): + self._set_circle(size=0.5) + def _set_pixel(self): self._path = Path.unit_rectangle() # Ideally, you'd want -0.5, -0.5 here, but then the snapping @@ -459,18 +572,13 @@ def _set_pixel(self): self._transform = Affine2D().translate(-0.49999, -0.49999) self._snap_threshold = None - def _set_point(self): - self._set_circle(reduction=self._point_size_reduction) - - _triangle_path = Path([[0, 1], [-1, -1], [1, -1], [0, 1]], closed=True) + _triangle_path = Path._create_closed([[0, 1], [-1, -1], [1, -1]]) # Going down halfway looks to small. Golden ratio is too far. - _triangle_path_u = Path([[0, 1], [-3/5, -1/5], [3/5, -1/5], [0, 1]], - closed=True) - _triangle_path_d = Path( - [[-3/5, -1/5], [3/5, -1/5], [1, -1], [-1, -1], [-3/5, -1/5]], - closed=True) - _triangle_path_l = Path([[0, 1], [0, -1], [-1, -1], [0, 1]], closed=True) - _triangle_path_r = Path([[0, 1], [0, -1], [1, -1], [0, 1]], closed=True) + _triangle_path_u = Path._create_closed([[0, 1], [-3/5, -1/5], [3/5, -1/5]]) + _triangle_path_d = Path._create_closed( + [[-3/5, -1/5], [3/5, -1/5], [1, -1], [-1, -1]]) + _triangle_path_l = Path._create_closed([[0, 1], [0, -1], [-1, -1]]) + _triangle_path_r = Path._create_closed([[0, 1], [0, -1], [1, -1]]) def _set_triangle(self, rot, skip): self._transform = Affine2D().scale(0.5).rotate_deg(rot) @@ -500,7 +608,7 @@ def _set_triangle(self, rot, skip): self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_triangle_up(self): return self._set_triangle(0.0, 0) @@ -530,7 +638,7 @@ def _set_square(self): self._transform.rotate_deg(rotate) self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_diamond(self): self._transform = Affine2D().translate(-0.5, -0.5).rotate_deg(45) @@ -544,7 +652,7 @@ def _set_diamond(self): rotate = {'right': 0, 'top': 90, 'left': 180, 'bottom': 270}[fs] self._transform.rotate_deg(rotate) self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_thin_diamond(self): self._set_diamond() @@ -571,7 +679,7 @@ def _set_pentagon(self): }[self.get_fillstyle()] self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_star(self): self._transform = Affine2D().scale(0.5) @@ -593,7 +701,7 @@ def _set_star(self): }[self.get_fillstyle()] self._alt_transform = self._transform - self._joinstyle = JoinStyle.bevel + self._joinstyle = self._user_joinstyle or JoinStyle.bevel def _set_hexagon1(self): self._transform = Affine2D().scale(0.5) @@ -617,7 +725,7 @@ def _set_hexagon1(self): }[self.get_fillstyle()] self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_hexagon2(self): self._transform = Affine2D().scale(0.5).rotate_deg(30) @@ -643,7 +751,7 @@ def _set_hexagon2(self): }[self.get_fillstyle()] self._alt_transform = self._transform - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_octagon(self): self._transform = Affine2D().scale(0.5) @@ -664,7 +772,7 @@ def _set_octagon(self): {'left': 0, 'bottom': 90, 'right': 180, 'top': 270}[fs]) self._alt_transform = self._transform.frozen().rotate_deg(180.0) - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter _line_marker_path = Path([[0.0, -1.0], [0.0, 1.0]]) @@ -738,7 +846,7 @@ def _set_caretdown(self): self._snap_threshold = 3.0 self._filled = False self._path = self._caret_path - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter def _set_caretup(self): self._set_caretdown() @@ -792,20 +900,17 @@ def _set_x(self): self._filled = False self._path = self._x_path - _plus_filled_path = Path( - np.array([(-1, -3), (+1, -3), (+1, -1), (+3, -1), (+3, +1), (+1, +1), - (+1, +3), (-1, +3), (-1, +1), (-3, +1), (-3, -1), (-1, -1), - (-1, -3)]) / 6, closed=True) - _plus_filled_path_t = Path( - np.array([(+3, 0), (+3, +1), (+1, +1), (+1, +3), - (-1, +3), (-1, +1), (-3, +1), (-3, 0), - (+3, 0)]) / 6, closed=True) + _plus_filled_path = Path._create_closed(np.array([ + (-1, -3), (+1, -3), (+1, -1), (+3, -1), (+3, +1), (+1, +1), + (+1, +3), (-1, +3), (-1, +1), (-3, +1), (-3, -1), (-1, -1)]) / 6) + _plus_filled_path_t = Path._create_closed(np.array([ + (+3, 0), (+3, +1), (+1, +1), (+1, +3), + (-1, +3), (-1, +1), (-3, +1), (-3, 0)]) / 6) def _set_plus_filled(self): self._transform = Affine2D() self._snap_threshold = 5.0 - self._joinstyle = JoinStyle.miter - fs = self.get_fillstyle() + self._joinstyle = self._user_joinstyle or JoinStyle.miter if not self._half_fill(): self._path = self._plus_filled_path else: @@ -816,20 +921,17 @@ def _set_plus_filled(self): {'top': 0, 'left': 90, 'bottom': 180, 'right': 270}[fs]) self._alt_transform = self._transform.frozen().rotate_deg(180) - _x_filled_path = Path( - np.array([(-1, -2), (0, -1), (+1, -2), (+2, -1), (+1, 0), (+2, +1), - (+1, +2), (0, +1), (-1, +2), (-2, +1), (-1, 0), (-2, -1), - (-1, -2)]) / 4, - closed=True) - _x_filled_path_t = Path( - np.array([(+1, 0), (+2, +1), (+1, +2), (0, +1), - (-1, +2), (-2, +1), (-1, 0), (+1, 0)]) / 4, - closed=True) + _x_filled_path = Path._create_closed(np.array([ + (-1, -2), (0, -1), (+1, -2), (+2, -1), (+1, 0), (+2, +1), + (+1, +2), (0, +1), (-1, +2), (-2, +1), (-1, 0), (-2, -1)]) / 4) + _x_filled_path_t = Path._create_closed(np.array([ + (+1, 0), (+2, +1), (+1, +2), (0, +1), + (-1, +2), (-2, +1), (-1, 0)]) / 4) def _set_x_filled(self): self._transform = Affine2D() self._snap_threshold = 5.0 - self._joinstyle = JoinStyle.miter + self._joinstyle = self._user_joinstyle or JoinStyle.miter if not self._half_fill(): self._path = self._x_filled_path else: diff --git a/lib/matplotlib/mathtext.py b/lib/matplotlib/mathtext.py index f2eb39dd90b3..fc677e83616e 100644 --- a/lib/matplotlib/mathtext.py +++ b/lib/matplotlib/mathtext.py @@ -17,30 +17,24 @@ from collections import namedtuple import functools -from io import StringIO import logging -import types import numpy as np -from PIL import Image -from matplotlib import _api, colors as mcolors, rcParams, _mathtext +import matplotlib as mpl +from matplotlib import _api, _mathtext from matplotlib.ft2font import FT2Image, LOAD_NO_HINTING from matplotlib.font_manager import FontProperties -# Backcompat imports, all are deprecated as of 3.4. -from matplotlib._mathtext import ( # noqa: F401 - SHRINK_FACTOR, GROW_FACTOR, NUM_SIZE_LEVELS) -from matplotlib._mathtext_data import ( # noqa: F401 - latex_to_bakoma, latex_to_cmex, latex_to_standard, stix_virtual_fonts, - tex2uni) +from ._mathtext import ( # noqa: reexported API + RasterParse, VectorParse, get_unicode_index) _log = logging.getLogger(__name__) -get_unicode_index = _mathtext.get_unicode_index get_unicode_index.__module__ = __name__ +@_api.deprecated("3.6") class MathtextBackend: """ The base class for the mathtext backend-specific code. `MathtextBackend` @@ -96,6 +90,7 @@ def get_hinting_type(self): return LOAD_NO_HINTING +@_api.deprecated("3.6") class MathtextBackendAgg(MathtextBackend): """ Render glyphs and rectangles to an FTImage buffer, which is later @@ -129,7 +124,7 @@ def render_glyph(self, ox, oy, info): else: info.font.draw_glyph_to_bitmap( self.image, ox, oy - info.metrics.iceberg, info.glyph, - antialiased=rcParams['text.antialiased']) + antialiased=mpl.rcParams['text.antialiased']) def render_rect_filled(self, x1, y1, x2, y2): if self.mode == 'bbox': @@ -143,147 +138,17 @@ def render_rect_filled(self, x1, y1, x2, y2): y = int(y1) self.image.draw_rect_filled(int(x1), y, np.ceil(x2), y + height) - def get_results(self, box, used_characters): - self.mode = 'bbox' - orig_height = box.height - orig_depth = box.depth - _mathtext.ship(0, 0, box) - bbox = self.bbox - bbox = [bbox[0] - 1, bbox[1] - 1, bbox[2] + 1, bbox[3] + 1] - self.mode = 'render' - self.set_canvas_size( - bbox[2] - bbox[0], - (bbox[3] - bbox[1]) - orig_depth, - (bbox[3] - bbox[1]) - orig_height) - _mathtext.ship(-bbox[0], -bbox[1], box) - result = (self.ox, - self.oy, - self.width, - self.height + self.depth, - self.depth, - self.image, - used_characters) + def get_results(self, box): self.image = None - return result + self.mode = 'render' + return _mathtext.ship(box).to_raster() def get_hinting_type(self): from matplotlib.backends import backend_agg return backend_agg.get_hinting_flag() -@_api.deprecated("3.4", alternative="mathtext.math_to_image") -class MathtextBackendBitmap(MathtextBackendAgg): - def get_results(self, box, used_characters): - ox, oy, width, height, depth, image, characters = \ - super().get_results(box, used_characters) - return image, depth - - -@_api.deprecated("3.4", alternative="MathtextBackendPath") -class MathtextBackendPs(MathtextBackend): - """ - Store information to write a mathtext rendering to the PostScript backend. - """ - - _PSResult = namedtuple( - "_PSResult", "width height depth pswriter used_characters") - - def __init__(self): - self.pswriter = StringIO() - self.lastfont = None - - def render_glyph(self, ox, oy, info): - oy = self.height - oy + info.offset - postscript_name = info.postscript_name - fontsize = info.fontsize - - if (postscript_name, fontsize) != self.lastfont: - self.lastfont = postscript_name, fontsize - self.pswriter.write( - f"/{postscript_name} findfont\n" - f"{fontsize} scalefont\n" - f"setfont\n") - - self.pswriter.write( - f"{ox:f} {oy:f} moveto\n" - f"/{info.symbol_name} glyphshow\n") - - def render_rect_filled(self, x1, y1, x2, y2): - ps = "%f %f %f %f rectfill\n" % ( - x1, self.height - y2, x2 - x1, y2 - y1) - self.pswriter.write(ps) - - def get_results(self, box, used_characters): - _mathtext.ship(0, 0, box) - return self._PSResult(self.width, - self.height + self.depth, - self.depth, - self.pswriter, - used_characters) - - -@_api.deprecated("3.4", alternative="MathtextBackendPath") -class MathtextBackendPdf(MathtextBackend): - """Store information to write a mathtext rendering to the PDF backend.""" - - _PDFResult = namedtuple( - "_PDFResult", "width height depth glyphs rects used_characters") - - def __init__(self): - self.glyphs = [] - self.rects = [] - - def render_glyph(self, ox, oy, info): - filename = info.font.fname - oy = self.height - oy + info.offset - self.glyphs.append( - (ox, oy, filename, info.fontsize, - info.num, info.symbol_name)) - - def render_rect_filled(self, x1, y1, x2, y2): - self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1)) - - def get_results(self, box, used_characters): - _mathtext.ship(0, 0, box) - return self._PDFResult(self.width, - self.height + self.depth, - self.depth, - self.glyphs, - self.rects, - used_characters) - - -@_api.deprecated("3.4", alternative="MathtextBackendPath") -class MathtextBackendSvg(MathtextBackend): - """ - Store information to write a mathtext rendering to the SVG - backend. - """ - def __init__(self): - self.svg_glyphs = [] - self.svg_rects = [] - - def render_glyph(self, ox, oy, info): - oy = self.height - oy + info.offset - - self.svg_glyphs.append( - (info.font, info.fontsize, info.num, ox, oy, info.metrics)) - - def render_rect_filled(self, x1, y1, x2, y2): - self.svg_rects.append( - (x1, self.height - y1 + 1, x2 - x1, y2 - y1)) - - def get_results(self, box, used_characters): - _mathtext.ship(0, 0, box) - svg_elements = types.SimpleNamespace(svg_glyphs=self.svg_glyphs, - svg_rects=self.svg_rects) - return (self.width, - self.height + self.depth, - self.depth, - svg_elements, - used_characters) - - +@_api.deprecated("3.6") class MathtextBackendPath(MathtextBackend): """ Store information to write a mathtext rendering to the text path @@ -293,6 +158,7 @@ class MathtextBackendPath(MathtextBackend): _Result = namedtuple("_Result", "width height depth glyphs rects") def __init__(self): + super().__init__() self.glyphs = [] self.rects = [] @@ -303,114 +169,21 @@ def render_glyph(self, ox, oy, info): def render_rect_filled(self, x1, y1, x2, y2): self.rects.append((x1, self.height - y2, x2 - x1, y2 - y1)) - def get_results(self, box, used_characters): - _mathtext.ship(0, 0, box) - return self._Result(self.width, - self.height + self.depth, - self.depth, - self.glyphs, - self.rects) - - -@_api.deprecated("3.4", alternative="MathtextBackendPath") -class MathtextBackendCairo(MathtextBackend): - """ - Store information to write a mathtext rendering to the Cairo - backend. - """ - - def __init__(self): - self.glyphs = [] - self.rects = [] - - def render_glyph(self, ox, oy, info): - oy = oy - info.offset - self.height - thetext = chr(info.num) - self.glyphs.append( - (info.font, info.fontsize, thetext, ox, oy)) - - def render_rect_filled(self, x1, y1, x2, y2): - self.rects.append( - (x1, y1 - self.height, x2 - x1, y2 - y1)) - - def get_results(self, box, used_characters): - _mathtext.ship(0, 0, box) - return (self.width, - self.height + self.depth, - self.depth, - self.glyphs, - self.rects) - - -for _cls_name in [ - "Fonts", - *[c.__name__ for c in _mathtext.Fonts.__subclasses__()], - "FontConstantsBase", - *[c.__name__ for c in _mathtext.FontConstantsBase.__subclasses__()], - "Node", - *[c.__name__ for c in _mathtext.Node.__subclasses__()], - "Ship", "Parser", -]: - globals()[_cls_name] = _api.deprecated("3.4")( - type(_cls_name, (getattr(_mathtext, _cls_name),), {})) + def get_results(self, box): + return _mathtext.ship(box).to_vector() +@_api.deprecated("3.6") class MathTextWarning(Warning): pass -@_api.deprecated("3.3") -class GlueSpec: - """See `Glue`.""" - - def __init__(self, width=0., stretch=0., stretch_order=0, - shrink=0., shrink_order=0): - self.width = width - self.stretch = stretch - self.stretch_order = stretch_order - self.shrink = shrink - self.shrink_order = shrink_order - - def copy(self): - return GlueSpec( - self.width, - self.stretch, - self.stretch_order, - self.shrink, - self.shrink_order) - - @classmethod - def factory(cls, glue_type): - return cls._types[glue_type] - - -with _api.suppress_matplotlib_deprecation_warning(): - GlueSpec._types = {k: GlueSpec(**v._asdict()) - for k, v in _mathtext._GlueSpec._named.items()} - - -@_api.deprecated("3.4") -def ship(ox, oy, box): - _mathtext.ship(ox, oy, box) - - ############################################################################## # MAIN class MathTextParser: _parser = None - - _backend_mapping = { - 'bitmap': MathtextBackendBitmap, - 'agg': MathtextBackendAgg, - 'ps': MathtextBackendPs, - 'pdf': MathtextBackendPdf, - 'svg': MathtextBackendSvg, - 'path': MathtextBackendPath, - 'cairo': MathtextBackendCairo, - 'macosx': MathtextBackendAgg, - } _font_type_mapping = { 'cm': _mathtext.BakomaFonts, 'dejavuserif': _mathtext.DejaVuSerifFonts, @@ -421,10 +194,20 @@ class MathTextParser: } def __init__(self, output): - """Create a MathTextParser for the given backend *output*.""" - self._output = output.lower() + """ + Create a MathTextParser for the given backend *output*. + + Parameters + ---------- + output : {"path", "agg"} + Whether to return a `VectorParse` ("path") or a + `RasterParse` ("agg", or its synonym "macosx"). + """ + self._output_type = _api.check_getitem( + {"path": "vector", "agg": "raster", "macosx": "raster"}, + output=output.lower()) - def parse(self, s, dpi=72, prop=None, *, _force_standard_ps_fonts=False): + def parse(self, s, dpi=72, prop=None): """ Parse the given math expression *s* at the given *dpi*. If *prop* is provided, it is a `.FontProperties` object specifying the "default" @@ -432,159 +215,45 @@ def parse(self, s, dpi=72, prop=None, *, _force_standard_ps_fonts=False): The results are cached, so multiple calls to `parse` with the same expression should be fast. + + Depending on the *output* type, this returns either a `VectorParse` or + a `RasterParse`. """ - if _force_standard_ps_fonts: - _api.warn_deprecated( - "3.4", - removal="3.5", - message=( - "Mathtext using only standard PostScript fonts has " - "been likely to produce wrong output for a while, " - "has been deprecated in %(since)s and will be removed " - "in %(removal)s, after which ps.useafm will have no " - "effect on mathtext." - ) - ) - - # lru_cache can't decorate parse() directly because the ps.useafm and - # mathtext.fontset rcParams also affect the parse (e.g. by affecting - # the glyph metrics). - return self._parse_cached(s, dpi, prop, _force_standard_ps_fonts) + # lru_cache can't decorate parse() directly because prop + # is mutable; key the cache using an internal copy (see + # text._get_text_metrics_with_cache for a similar case). + prop = prop.copy() if prop is not None else None + return self._parse_cached(s, dpi, prop) @functools.lru_cache(50) - def _parse_cached(self, s, dpi, prop, force_standard_ps_fonts): + def _parse_cached(self, s, dpi, prop): + from matplotlib.backends import backend_agg + if prop is None: prop = FontProperties() - - fontset_class = ( - _mathtext.StandardPsFonts if force_standard_ps_fonts - else _api.check_getitem( - self._font_type_mapping, fontset=prop.get_math_fontfamily())) - backend = self._backend_mapping[self._output]() - font_output = fontset_class(prop, backend) + fontset_class = _api.check_getitem( + self._font_type_mapping, fontset=prop.get_math_fontfamily()) + load_glyph_flags = { + "vector": LOAD_NO_HINTING, + "raster": backend_agg.get_hinting_flag(), + }[self._output_type] + fontset = fontset_class(prop, load_glyph_flags) fontsize = prop.get_size_in_points() - # This is a class variable so we don't rebuild the parser - # with each request. - if self._parser is None: + if self._parser is None: # Cache the parser globally. self.__class__._parser = _mathtext.Parser() - box = self._parser.parse(s, font_output, fontsize, dpi) - font_output.set_canvas_size(box.width, box.height, box.depth) - return font_output.get_results(box) - - @_api.deprecated("3.4", alternative="mathtext.math_to_image") - def to_mask(self, texstr, dpi=120, fontsize=14): - r""" - Convert a mathtext string to a grayscale array and depth. - - Parameters - ---------- - texstr : str - A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'. - dpi : float - The dots-per-inch setting used to render the text. - fontsize : int - The font size in points - - Returns - ------- - array : 2D uint8 alpha - Mask array of rasterized tex. - depth : int - Offset of the baseline from the bottom of the image, in pixels. - """ - assert self._output == "bitmap" - prop = FontProperties(size=fontsize) - ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop) - return np.asarray(ftimage), depth - - @_api.deprecated("3.4", alternative="mathtext.math_to_image") - def to_rgba(self, texstr, color='black', dpi=120, fontsize=14): - r""" - Convert a mathtext string to an RGBA array and depth. - - Parameters - ---------- - texstr : str - A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'. - color : color - The text color. - dpi : float - The dots-per-inch setting used to render the text. - fontsize : int - The font size in points. - - Returns - ------- - array : (M, N, 4) array - RGBA color values of rasterized tex, colorized with *color*. - depth : int - Offset of the baseline from the bottom of the image, in pixels. - """ - x, depth = self.to_mask(texstr, dpi=dpi, fontsize=fontsize) + box = self._parser.parse(s, fontset, fontsize, dpi) + output = _mathtext.ship(box) + if self._output_type == "vector": + return output.to_vector() + elif self._output_type == "raster": + return output.to_raster() - r, g, b, a = mcolors.to_rgba(color) - RGBA = np.zeros((x.shape[0], x.shape[1], 4), dtype=np.uint8) - RGBA[:, :, 0] = 255 * r - RGBA[:, :, 1] = 255 * g - RGBA[:, :, 2] = 255 * b - RGBA[:, :, 3] = x - return RGBA, depth - @_api.deprecated("3.4", alternative="mathtext.math_to_image") - def to_png(self, filename, texstr, color='black', dpi=120, fontsize=14): - r""" - Render a tex expression to a PNG file. - - Parameters - ---------- - filename - A writable filename or fileobject. - texstr : str - A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'. - color : color - The text color. - dpi : float - The dots-per-inch setting used to render the text. - fontsize : int - The font size in points. - - Returns - ------- - int - Offset of the baseline from the bottom of the image, in pixels. - """ - rgba, depth = self.to_rgba( - texstr, color=color, dpi=dpi, fontsize=fontsize) - Image.fromarray(rgba).save(filename, format="png") - return depth - - @_api.deprecated("3.4", alternative="mathtext.math_to_image") - def get_depth(self, texstr, dpi=120, fontsize=14): - r""" - Get the depth of a mathtext string. - - Parameters - ---------- - texstr : str - A valid mathtext string, e.g., r'IQ: $\sigma_i=15$'. - dpi : float - The dots-per-inch setting used to render the text. - - Returns - ------- - int - Offset of the baseline from the bottom of the image, in pixels. - """ - assert self._output == "bitmap" - prop = FontProperties(size=fontsize) - ftimage, depth = self.parse(texstr, dpi=dpi, prop=prop) - return depth - - -def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None): +def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None, + *, color=None): """ Given a math expression, renders it in a closely-clipped bounding box to an image file. @@ -603,20 +272,16 @@ def math_to_image(s, filename_or_obj, prop=None, dpi=None, format=None): format : str, optional The output format, e.g., 'svg', 'pdf', 'ps' or 'png'. If not set, the format is determined as for `.Figure.savefig`. + color : str, optional + Foreground color, defaults to :rc:`text.color`. """ from matplotlib import figure - # backend_agg supports all of the core output formats - from matplotlib.backends import backend_agg - - if prop is None: - prop = FontProperties() parser = MathTextParser('path') width, height, depth, _, _ = parser.parse(s, dpi=72, prop=prop) fig = figure.Figure(figsize=(width / 72.0, height / 72.0)) - fig.text(0, depth/height, s, fontproperties=prop) - backend_agg.FigureCanvasAgg(fig) + fig.text(0, depth/height, s, fontproperties=prop, color=color) fig.savefig(filename_or_obj, dpi=dpi, format=format) return depth diff --git a/lib/matplotlib/mlab.py b/lib/matplotlib/mlab.py index b6f6173254fd..059cf0f1624b 100644 --- a/lib/matplotlib/mlab.py +++ b/lib/matplotlib/mlab.py @@ -1,8 +1,11 @@ """ -Numerical python functions written for compatibility with MATLAB -commands with the same names. Most numerical python functions can be found in -the `numpy` and `scipy` libraries. What remains here is code for performing -spectral computations. +Numerical Python functions written for compatibility with MATLAB +commands with the same names. Most numerical Python functions can be found in +the `NumPy`_ and `SciPy`_ libraries. What remains here is code for performing +spectral computations and kernel density estimations. + +.. _NumPy: https://numpy.org +.. _SciPy: https://www.scipy.org Spectral functions ------------------ @@ -52,14 +55,12 @@ import numpy as np -from matplotlib import _api -import matplotlib.cbook as cbook -from matplotlib import docstring +from matplotlib import _api, _docstring, cbook def window_hanning(x): """ - Return x times the hanning window of len(x). + Return *x* times the Hanning (or Hann) window of len(*x*). See Also -------- @@ -70,7 +71,7 @@ def window_hanning(x): def window_none(x): """ - No window function; simply return x. + No window function; simply return *x*. See Also -------- @@ -81,7 +82,7 @@ def window_none(x): def detrend(x, key=None, axis=None): """ - Return x with its trend removed. + Return *x* with its trend removed. Parameters ---------- @@ -130,7 +131,7 @@ def detrend(x, key=None, axis=None): def detrend_mean(x, axis=None): """ - Return x minus the mean(x). + Return *x* minus the mean(*x*). Parameters ---------- @@ -139,7 +140,7 @@ def detrend_mean(x, axis=None): Can have any dimensionality axis : int - The axis along which to take the mean. See numpy.mean for a + The axis along which to take the mean. See `numpy.mean` for a description of this argument. See Also @@ -158,7 +159,7 @@ def detrend_mean(x, axis=None): def detrend_none(x, axis=None): """ - Return x: no detrending. + Return *x*: no detrending. Parameters ---------- @@ -180,17 +181,13 @@ def detrend_none(x, axis=None): def detrend_linear(y): """ - Return x minus best fit line; 'linear' detrending. + Return *x* minus best fit line; 'linear' detrending. Parameters ---------- y : 0-D or 1-D array or sequence Array or sequence containing the data - axis : int - The axis along which to take the mean. See numpy.mean for a - description of this argument. - See Also -------- detrend_mean : Another detrend algorithm. @@ -216,9 +213,10 @@ def detrend_linear(y): return y - (b*x + a) +@_api.deprecated("3.6") def stride_windows(x, n, noverlap=None, axis=0): """ - Get all windows of x with length n as a single array, + Get all windows of *x* with length *n* as a single array, using strides to avoid data duplication. .. warning:: @@ -241,12 +239,24 @@ def stride_windows(x, n, noverlap=None, axis=0): References ---------- `stackoverflow: Rolling window for 1D arrays in Numpy? - `_ + `_ `stackoverflow: Using strides for an efficient moving average filter - `_ + `_ """ if noverlap is None: noverlap = 0 + if np.ndim(x) != 1: + raise ValueError('only 1-dimensional arrays can be used') + return _stride_windows(x, n, noverlap, axis) + + +def _stride_windows(x, n, noverlap=0, axis=0): + # np>=1.20 provides sliding_window_view, and we only ever use axis=0. + if hasattr(np.lib.stride_tricks, "sliding_window_view") and axis == 0: + if noverlap >= n: + raise ValueError('noverlap must be less than n') + return np.lib.stride_tricks.sliding_window_view( + x, n, axis=0)[::n - noverlap].T if noverlap >= n: raise ValueError('noverlap must be less than n') @@ -255,13 +265,11 @@ def stride_windows(x, n, noverlap=None, axis=0): x = np.asarray(x) - if x.ndim != 1: - raise ValueError('only 1-dimensional arrays can be used') if n == 1 and noverlap == 0: if axis == 0: return x[np.newaxis] else: - return x[np.newaxis].transpose() + return x[np.newaxis].T if n > x.size: raise ValueError('n cannot be greater than the length of x') @@ -371,7 +379,7 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, raise ValueError( "The window length must match the data's first dimension") - result = stride_windows(x, NFFT, noverlap, axis=0) + result = _stride_windows(x, NFFT, noverlap) result = detrend(result, detrend_func, axis=0) result = result * window.reshape((-1, 1)) result = np.fft.fft(result, n=pad_to, axis=0)[:numFreqs, :] @@ -379,7 +387,7 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, if not same_data: # if same_data is False, mode must be 'psd' - resultY = stride_windows(y, NFFT, noverlap) + resultY = _stride_windows(y, NFFT, noverlap) resultY = detrend(resultY, detrend_func, axis=0) resultY = resultY * window.reshape((-1, 1)) resultY = np.fft.fft(resultY, n=pad_to, axis=0)[:numFreqs, :] @@ -387,12 +395,12 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, elif mode == 'psd': result = np.conj(result) * result elif mode == 'magnitude': - result = np.abs(result) / np.abs(window).sum() + result = np.abs(result) / window.sum() elif mode == 'angle' or mode == 'phase': # we unwrap the phase later to handle the onesided vs. twosided case result = np.angle(result) elif mode == 'complex': - result /= np.abs(window).sum() + result /= window.sum() if mode == 'psd': @@ -416,10 +424,10 @@ def _spectral_helper(x, y=None, NFFT=None, Fs=None, detrend_func=None, result /= Fs # Scale the spectrum by the norm of the window to compensate for # windowing loss; see Bendat & Piersol Sec 11.5.2. - result /= (np.abs(window)**2).sum() + result /= (window**2).sum() else: # In this case, preserve power in the segment, not amplitude - result /= np.abs(window).sum()**2 + result /= window.sum()**2 t = np.arange(NFFT/2, len(x) - NFFT/2 + 1, NFFT - noverlap)/Fs @@ -465,7 +473,7 @@ def _single_spectrum_helper( # Split out these keyword docs so that they can be used elsewhere -docstring.interpd.update( +_docstring.interpd.update( Spectral="""\ Fs : float, default: 2 The sampling frequency (samples per time unit). It is used to calculate @@ -489,8 +497,8 @@ def _single_spectrum_helper( the FFT. While not increasing the actual resolution of the spectrum (the minimum distance between resolvable peaks), this can give more points in the plot, allowing for more detail. This corresponds to the *n* parameter - in the call to fft(). The default is None, which sets *pad_to* equal to - the length of the input signal (i.e. no padding).""", + in the call to `~numpy.fft.fft`. The default is None, which sets *pad_to* + equal to the length of the input signal (i.e. no padding).""", PSD="""\ pad_to : int, optional @@ -499,8 +507,8 @@ def _single_spectrum_helper( of data points used. While not increasing the actual resolution of the spectrum (the minimum distance between resolvable peaks), this can give more points in the plot, allowing for more detail. This corresponds to - the *n* parameter in the call to fft(). The default is None, which sets - *pad_to* equal to *NFFT* + the *n* parameter in the call to `~numpy.fft.fft`. The default is None, + which sets *pad_to* equal to *NFFT* NFFT : int, default: 256 The number of data points used in each block for the FFT. A power 2 is @@ -510,7 +518,7 @@ def _single_spectrum_helper( detrend : {'none', 'mean', 'linear'} or callable, default: 'none' The function applied to each segment before fft-ing, designed to remove the mean or linear trend. Unlike in MATLAB, where the *detrend* parameter - is a vector, in Matplotlib is it a function. The :mod:`~matplotlib.mlab` + is a vector, in Matplotlib it is a function. The :mod:`~matplotlib.mlab` module defines `.detrend_none`, `.detrend_mean`, and `.detrend_linear`, but you can use a custom function as well. You can also use a string to choose one of the functions: 'none' calls `.detrend_none`. 'mean' calls @@ -518,12 +526,12 @@ def _single_spectrum_helper( 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 + frequency, which gives density in units of 1/Hz. This allows for integration over the returned frequency values. The default is True for MATLAB compatibility.""") -@docstring.dedent_interpd +@_docstring.dedent_interpd def psd(x, NFFT=None, Fs=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None): r""" @@ -579,7 +587,7 @@ def psd(x, NFFT=None, Fs=None, detrend=None, window=None, return Pxx.real, freqs -@docstring.dedent_interpd +@_docstring.dedent_interpd def csd(x, y, NFFT=None, Fs=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None): """ @@ -684,33 +692,33 @@ def csd(x, y, NFFT=None, Fs=None, detrend=None, window=None, complex_spectrum = functools.partial(_single_spectrum_helper, "complex") complex_spectrum.__doc__ = _single_spectrum_docs.format( quantity="complex-valued frequency spectrum", - **docstring.interpd.params) + **_docstring.interpd.params) magnitude_spectrum = functools.partial(_single_spectrum_helper, "magnitude") magnitude_spectrum.__doc__ = _single_spectrum_docs.format( quantity="magnitude (absolute value) of the frequency spectrum", - **docstring.interpd.params) + **_docstring.interpd.params) angle_spectrum = functools.partial(_single_spectrum_helper, "angle") angle_spectrum.__doc__ = _single_spectrum_docs.format( quantity="angle of the frequency spectrum (wrapped phase spectrum)", - **docstring.interpd.params) + **_docstring.interpd.params) phase_spectrum = functools.partial(_single_spectrum_helper, "phase") phase_spectrum.__doc__ = _single_spectrum_docs.format( quantity="phase of the frequency spectrum (unwrapped phase spectrum)", - **docstring.interpd.params) + **_docstring.interpd.params) -@docstring.dedent_interpd +@_docstring.dedent_interpd def specgram(x, NFFT=None, Fs=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, mode=None): """ Compute a spectrogram. - Compute and plot a spectrogram of data in x. Data are split into - NFFT length segments and the spectrum of each section is - computed. The windowing function window is applied to each + Compute and plot a spectrogram of data in *x*. Data are split into + *NFFT* length segments and the spectrum of each section is + computed. The windowing function *window* is applied to each segment, and the amount of overlap of each segment is - specified with noverlap. + specified with *noverlap*. Parameters ---------- @@ -752,13 +760,13 @@ def specgram(x, NFFT=None, Fs=None, detrend=None, window=None, -------- psd : differs in the overlap and in the return values. complex_spectrum : similar, but with complex valued frequencies. - magnitude_spectrum : similar single segment when mode is 'magnitude'. - angle_spectrum : similar to single segment when mode is 'angle'. - phase_spectrum : similar to single segment when mode is 'phase'. + magnitude_spectrum : similar single segment when *mode* is 'magnitude'. + angle_spectrum : similar to single segment when *mode* is 'angle'. + phase_spectrum : similar to single segment when *mode* is 'phase'. Notes ----- - detrend and scale_by_freq only apply when *mode* is set to 'psd'. + *detrend* and *scale_by_freq* only apply when *mode* is set to 'psd'. """ if noverlap is None: @@ -782,7 +790,7 @@ def specgram(x, NFFT=None, Fs=None, detrend=None, window=None, return spec, freqs, t -@docstring.dedent_interpd +@_docstring.dedent_interpd def cohere(x, y, NFFT=256, Fs=2, detrend=detrend_none, window=window_hanning, noverlap=0, pad_to=None, sides='default', scale_by_freq=None): r""" @@ -841,7 +849,6 @@ class GaussianKDE: dataset : array-like Datapoints to estimate from. In case of univariate data this is a 1-D array, otherwise a 2D array with shape (# of dims, # of data). - bw_method : str, scalar or callable, optional The method used to calculate the estimator bandwidth. This can be 'scott', 'silverman', a scalar constant or a callable. If a @@ -852,22 +859,17 @@ class GaussianKDE: Attributes ---------- dataset : ndarray - The dataset with which `gaussian_kde` was initialized. - + The dataset passed to the constructor. dim : int Number of dimensions. - num_dp : int Number of datapoints. - factor : float The bandwidth factor, obtained from `kde.covariance_factor`, with which the covariance matrix is multiplied. - covariance : ndarray The covariance matrix of *dataset*, scaled by the calculated bandwidth (`kde.factor`). - inv_cov : ndarray The inverse of *covariance*. @@ -875,10 +877,8 @@ class GaussianKDE: ------- kde.evaluate(points) : ndarray Evaluate the estimated pdf on a provided set of points. - kde(points) : ndarray Same as kde.evaluate(points) - """ # This implementation with minor modification was too good to pass up. diff --git a/lib/matplotlib/mpl-data/fonts/ttf/LICENSE_STIX b/lib/matplotlib/mpl-data/fonts/ttf/LICENSE_STIX index 12c454a3e104..6034d9474814 100644 --- a/lib/matplotlib/mpl-data/fonts/ttf/LICENSE_STIX +++ b/lib/matplotlib/mpl-data/fonts/ttf/LICENSE_STIX @@ -34,7 +34,7 @@ Portions copyright (c) 1990 by Elsevier, Inc. This Font Software is licensed under the SIL Open Font License, Version 1.1. This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL +https://scripts.sil.org/OFL ----------------------------------------------------------- SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 diff --git a/lib/matplotlib/mpl-data/images/matplotlib_128.ppm b/lib/matplotlib/mpl-data/images/matplotlib_128.ppm deleted file mode 100644 index d9a647b08a5a..000000000000 --- a/lib/matplotlib/mpl-data/images/matplotlib_128.ppm +++ /dev/null @@ -1,4 +0,0 @@ -P6 -128 128 -255 -ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿþþþðôöØãéÀÒÜ©ÁÏ‘±Ã„§»¥¹}¢·}¢·¥¹„§»‘±Ã©ÁÏÁÓÝØãéðôöþþþÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿõøùÉÙáš·Çj•­ 1: - sep = (total - sum(w_list)) / (len(w_list) - 1) + if len(widths) > 1: + sep = (total - sum(widths)) / (len(widths) - 1) else: sep = 0 - offsets_ = np.cumsum([0] + [w + sep for w in w_list]) + offsets_ = np.cumsum([0] + [w + sep for w in widths]) offsets = offsets_[:-1] return total, offsets elif mode == "equal": - maxh = max(w_list) + maxh = max(widths) if total is None: if sep is None: raise ValueError("total and sep cannot both be None when " "using layout mode 'equal'") - total = (maxh + sep) * len(w_list) + total = (maxh + sep) * len(widths) else: - sep = total / len(w_list) - maxh - offsets = (maxh + sep) * np.arange(len(w_list)) + sep = total / len(widths) - maxh + offsets = (maxh + sep) * np.arange(len(widths)) return total, offsets -def _get_aligned_offsets(hd_list, height, align="baseline"): +def _get_aligned_offsets(yspans, height, align="baseline"): """ - Align boxes each specified by their ``(height, descent)`` pair. + Align boxes each specified by their ``(y0, y1)`` spans. For simplicity of the description, the terminology used here assumes a horizontal layout (i.e., vertical alignment), but the function works @@ -140,46 +164,45 @@ def _get_aligned_offsets(hd_list, height, align="baseline"): Parameters ---------- - hd_list - List of (height, xdescent) of boxes to be aligned. + yspans + List of (y0, y1) spans of boxes to be aligned. height : float or None - Intended total height. If None, the maximum of the heights in *hd_list* - is used. + Intended total height. If None, the maximum of the heights + (``y1 - y0``) in *yspans* is used. align : {'baseline', 'left', 'top', 'right', 'bottom', 'center'} The alignment anchor of the boxes. Returns ------- - height - The total height of the packing (if a value was originally passed in, - it is returned without checking that it is actually large enough). + (y0, y1) + y range spanned by the packing. If a *height* was originally passed + in, then for all alignments other than "baseline", a span of ``(0, + height)`` is used without checking that it is actually large enough). descent The descent of the packing. offsets The bottom offsets of the boxes. """ - if height is None: - height = max(h for h, d in hd_list) _api.check_in_list( ["baseline", "left", "top", "right", "bottom", "center"], align=align) + if height is None: + height = max(y1 - y0 for y0, y1 in yspans) if align == "baseline": - height_descent = max(h - d for h, d in hd_list) - descent = max(d for h, d in hd_list) - height = height_descent + descent - offsets = [0. for h, d in hd_list] - elif align in ["left", "top"]: - descent = 0. - offsets = [d for h, d in hd_list] - elif align in ["right", "bottom"]: - descent = 0. - offsets = [height - h + d for h, d in hd_list] + yspan = (min(y0 for y0, y1 in yspans), max(y1 for y0, y1 in yspans)) + offsets = [0] * len(yspans) + elif align in ["left", "bottom"]: + yspan = (0, height) + offsets = [-y0 for y0, y1 in yspans] + elif align in ["right", "top"]: + yspan = (0, height) + offsets = [height - y1 for y0, y1 in yspans] elif align == "center": - descent = 0. - offsets = [(height - h) * .5 + d for h, d in hd_list] + yspan = (0, height) + offsets = [(height - (y1 - y0)) * .5 - y0 for y0, y1 in yspans] - return height, descent, offsets + return yspan, offsets class OffsetBox(martist.Artist): @@ -192,14 +215,12 @@ class OffsetBox(martist.Artist): Being an artist itself, all parameters are passed on to `.Artist`. """ def __init__(self, *args, **kwargs): - - super().__init__(*args, **kwargs) - - # Clipping has not been implemented in the OffesetBox family, so + super().__init__(*args) + self._internal_update(kwargs) + # Clipping has not been implemented in the OffsetBox family, so # disable the clip flag for consistency. It can always be turned back # on to zero effect. self.set_clip_on(False) - self._children = [] self._offset = (0, 0) @@ -274,7 +295,8 @@ def offset(width, height, xdescent, ydescent, renderer) \ self._offset = xy self.stale = True - def get_offset(self, width, height, xdescent, ydescent, renderer): + @_compat_get_offset + def get_offset(self, bbox, renderer): """ Return the offset as a tuple (x, y). @@ -284,14 +306,13 @@ def get_offset(self, width, height, xdescent, ydescent, renderer): Parameters ---------- - width, height, xdescent, ydescent - Extent parameters. + bbox : `.Bbox` renderer : `.RendererBase` subclass - """ - return (self._offset(width, height, xdescent, ydescent, renderer) - if callable(self._offset) - else self._offset) + return ( + self._offset(bbox.width, bbox.height, -bbox.x0, -bbox.y0, renderer) + if callable(self._offset) + else self._offset) def set_width(self, width): """ @@ -323,6 +344,30 @@ def get_children(self): r"""Return a list of the child `.Artist`\s.""" return self._children + def _get_bbox_and_child_offsets(self, renderer): + """ + Return the bbox of the offsetbox and the child offsets. + + The bbox should satisfy ``x0 <= x1 and y0 <= y1``. + + Parameters + ---------- + renderer : `.RendererBase` subclass + + Returns + ------- + bbox + list of (xoffset, yoffset) pairs + """ + raise NotImplementedError( + "get_bbox_and_offsets must be overridden in derived classes") + + def get_bbox(self, renderer): + """Return the bbox of the offsetbox, ignoring parent offsets.""" + bbox, offsets = self._get_bbox_and_child_offsets(renderer) + return bbox + + @_api.deprecated("3.7", alternative="get_bbox and child.get_offset") def get_extent_offsets(self, renderer): """ Update offset of the children and return the extent of the box. @@ -339,48 +384,50 @@ def get_extent_offsets(self, renderer): ydescent list of (xoffset, yoffset) pairs """ - raise NotImplementedError( - "get_extent_offsets must be overridden in derived classes.") + bbox, offsets = self._get_bbox_and_child_offsets(renderer) + return bbox.width, bbox.height, -bbox.x0, -bbox.y0, offsets + @_api.deprecated("3.7", alternative="get_bbox") def get_extent(self, renderer): """Return a tuple ``width, height, xdescent, ydescent`` of the box.""" - w, h, xd, yd, offsets = self.get_extent_offsets(renderer) - return w, h, xd, yd + bbox = self.get_bbox(renderer) + return bbox.width, bbox.height, -bbox.x0, -bbox.y0 - def get_window_extent(self, renderer): - """Return the bounding box (`.Bbox`) in display space.""" - w, h, xd, yd, offsets = self.get_extent_offsets(renderer) - px, py = self.get_offset(w, h, xd, yd, renderer) - return mtransforms.Bbox.from_bounds(px - xd, py - yd, w, h) + def get_window_extent(self, renderer=None): + # docstring inherited + if renderer is None: + renderer = self.figure._get_renderer() + bbox = self.get_bbox(renderer) + try: # Some subclasses redefine get_offset to take no args. + px, py = self.get_offset(bbox, renderer) + except TypeError: + px, py = self.get_offset() + return bbox.translated(px, py) def draw(self, renderer): """ Update the location of children if necessary and draw them to the given *renderer*. """ - width, height, xdescent, ydescent, offsets = self.get_extent_offsets( - renderer) - - px, py = self.get_offset(width, height, xdescent, ydescent, renderer) - + bbox, offsets = self._get_bbox_and_child_offsets(renderer) + px, py = self.get_offset(bbox, renderer) for c, (ox, oy) in zip(self.get_visible_children(), offsets): c.set_offset((px + ox, py + oy)) c.draw(renderer) - - bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) + _bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) self.stale = False class PackerBase(OffsetBox): - def __init__(self, pad=None, sep=None, width=None, height=None, + def __init__(self, pad=0., sep=0., width=None, height=None, align="baseline", mode="fixed", children=None): """ Parameters ---------- - pad : float, optional + pad : float, default: 0.0 The boundary padding in points. - sep : float, optional + sep : float, default: 0.0 The spacing between items in points. width, height : float, optional @@ -406,7 +453,7 @@ def __init__(self, pad=None, sep=None, width=None, height=None, Notes ----- *pad* and *sep* are in points and will be scaled with the renderer - dpi, while *width* and *height* are in in pixels. + dpi, while *width* and *height* are in pixels. """ super().__init__() self.height = height @@ -424,7 +471,7 @@ class VPacker(PackerBase): relative positions at draw time. """ - def get_extent_offsets(self, renderer): + def _get_bbox_and_child_offsets(self, renderer): # docstring inherited dpicor = renderer.points_to_pixels(1.) pad = self.pad * dpicor @@ -435,28 +482,19 @@ def get_extent_offsets(self, renderer): if isinstance(c, PackerBase) and c.mode == "expand": c.set_width(self.width) - whd_list = [c.get_extent(renderer) - for c in self.get_visible_children()] - whd_list = [(w, h, xd, (h - yd)) for w, h, xd, yd in whd_list] - - wd_list = [(w, xd) for w, h, xd, yd in whd_list] - width, xdescent, xoffsets = _get_aligned_offsets(wd_list, - self.width, - self.align) - - pack_list = [(h, yd) for w, h, xd, yd in whd_list] - height, yoffsets_ = _get_packed_offsets(pack_list, self.height, - sep, self.mode) - - yoffsets = yoffsets_ + [yd for w, h, xd, yd in whd_list] - ydescent = height - yoffsets[0] - yoffsets = height - yoffsets + bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()] + (x0, x1), xoffsets = _get_aligned_offsets( + [bbox.intervalx for bbox in bboxes], self.width, self.align) + height, yoffsets = _get_packed_offsets( + [bbox.height for bbox in bboxes], self.height, sep, self.mode) + yoffsets = height - (yoffsets + [bbox.y1 for bbox in bboxes]) + ydescent = yoffsets[0] yoffsets = yoffsets - ydescent - return (width + 2 * pad, height + 2 * pad, - xdescent + pad, ydescent + pad, - list(zip(xoffsets, yoffsets))) + return ( + Bbox.from_bounds(x0, -ydescent, x1 - x0, height).padded(pad), + [*zip(xoffsets, yoffsets)]) class HPacker(PackerBase): @@ -465,43 +503,26 @@ class HPacker(PackerBase): relative positions at draw time. """ - def get_extent_offsets(self, renderer): + def _get_bbox_and_child_offsets(self, renderer): # docstring inherited dpicor = renderer.points_to_pixels(1.) pad = self.pad * dpicor sep = self.sep * dpicor - whd_list = [c.get_extent(renderer) - for c in self.get_visible_children()] - - if not whd_list: - return 2 * pad, 2 * pad, pad, pad, [] - - if self.height is None: - height_descent = max(h - yd for w, h, xd, yd in whd_list) - ydescent = max(yd for w, h, xd, yd in whd_list) - height = height_descent + ydescent - else: - height = self.height - 2 * pad # width w/o pad - - hd_list = [(h, yd) for w, h, xd, yd in whd_list] - height, ydescent, yoffsets = _get_aligned_offsets(hd_list, - self.height, - self.align) - - pack_list = [(w, xd) for w, h, xd, yd in whd_list] + bboxes = [c.get_bbox(renderer) for c in self.get_visible_children()] + if not bboxes: + return Bbox.from_bounds(0, 0, 0, 0).padded(pad), [] - width, xoffsets_ = _get_packed_offsets(pack_list, self.width, - sep, self.mode) + (y0, y1), yoffsets = _get_aligned_offsets( + [bbox.intervaly for bbox in bboxes], self.height, self.align) + width, xoffsets = _get_packed_offsets( + [bbox.width for bbox in bboxes], self.width, sep, self.mode) - xoffsets = xoffsets_ + [xd for w, h, xd, yd in whd_list] + x0 = bboxes[0].x0 + xoffsets -= ([bbox.x0 for bbox in bboxes] - x0) - xdescent = whd_list[0][2] - xoffsets = xoffsets - xdescent - - return (width + 2 * pad, height + 2 * pad, - xdescent + pad, ydescent + pad, - list(zip(xoffsets, yoffsets))) + return (Bbox.from_bounds(x0, y0, width, y1 - y0).padded(pad), + [*zip(xoffsets, yoffsets)]) class PaddedBox(OffsetBox): @@ -511,15 +532,17 @@ class PaddedBox(OffsetBox): The `.PaddedBox` contains a `.FancyBboxPatch` that is used to visualize it when rendering. """ - def __init__(self, child, pad=None, draw_frame=False, patch_attrs=None): + + @_api.make_keyword_only("3.6", name="draw_frame") + def __init__(self, child, pad=0., draw_frame=False, patch_attrs=None): """ Parameters ---------- child : `~matplotlib.artist.Artist` The contained `.Artist`. - pad : float + pad : float, default: 0.0 The padding in points. This will be scaled with the renderer dpi. - In contrast *width* and *height* are in *pixels* and thus not + In contrast, *width* and *height* are in *pixels* and thus not scaled. draw_frame : bool Whether to draw the contained `.FancyBboxPatch`. @@ -540,21 +563,15 @@ def __init__(self, child, pad=None, draw_frame=False, patch_attrs=None): if patch_attrs is not None: self.patch.update(patch_attrs) - def get_extent_offsets(self, renderer): + def _get_bbox_and_child_offsets(self, renderer): # docstring inherited. - dpicor = renderer.points_to_pixels(1.) - pad = self.pad * dpicor - w, h, xd, yd = self._children[0].get_extent(renderer) - return (w + 2 * pad, h + 2 * pad, xd + pad, yd + pad, - [(0, 0)]) + pad = self.pad * renderer.points_to_pixels(1.) + return (self._children[0].get_bbox(renderer).padded(pad), [(0, 0)]) def draw(self, renderer): # docstring inherited - width, height, xdescent, ydescent, offsets = self.get_extent_offsets( - renderer) - - px, py = self.get_offset(width, height, xdescent, ydescent, renderer) - + bbox, offsets = self._get_bbox_and_child_offsets(renderer) + px, py = self.get_offset(bbox, renderer) for c, (ox, oy) in zip(self.get_visible_children(), offsets): c.set_offset((px + ox, py + oy)) @@ -563,11 +580,10 @@ def draw(self, renderer): for c in self.get_visible_children(): c.draw(renderer) - #bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) self.stale = False def update_frame(self, bbox, fontsize=None): - self.patch.set_bounds(bbox.x0, bbox.y0, bbox.width, bbox.height) + self.patch.set_bounds(bbox.bounds) if fontsize: self.patch.set_mutation_scale(fontsize) self.stale = True @@ -586,8 +602,7 @@ class DrawingArea(OffsetBox): boundaries of the parent. """ - def __init__(self, width, height, xdescent=0., - ydescent=0., clip=False): + def __init__(self, width, height, xdescent=0., ydescent=0., clip=False): """ Parameters ---------- @@ -649,18 +664,12 @@ def get_offset(self): """Return offset of the container.""" return self._offset - def get_window_extent(self, renderer): - """Return the bounding box in display space.""" - w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset() # w, h, xd, yd) - - return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) - - def get_extent(self, renderer): - """Return width, height, xdescent, ydescent of box.""" + def get_bbox(self, renderer): + # docstring inherited dpi_cor = renderer.points_to_pixels(1.) - return (self.width * dpi_cor, self.height * dpi_cor, - self.xdescent * dpi_cor, self.ydescent * dpi_cor) + return Bbox.from_bounds( + -self.xdescent * dpi_cor, -self.ydescent * dpi_cor, + self.width * dpi_cor, self.height * dpi_cor) def add_artist(self, a): """Add an `.Artist` to the container box.""" @@ -693,7 +702,7 @@ def draw(self, renderer): c.set_clip_path(tpath) c.draw(renderer) - bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) + _bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) self.stale = False @@ -706,11 +715,10 @@ class TextArea(OffsetBox): child text. """ - @_api.delete_parameter("3.4", "minimumdescent") + @_api.make_keyword_only("3.6", name="textprops") def __init__(self, s, textprops=None, multilinebaseline=False, - minimumdescent=True, ): """ Parameters @@ -723,9 +731,6 @@ def __init__(self, s, multilinebaseline : bool, default: False Whether the baseline for multiline text is adjusted so that it is (approximately) center-aligned with single-line text. - minimumdescent : bool, default: True - If `True`, the box has a minimum descent of "p". This is now - effectively always True. """ if textprops is None: textprops = {} @@ -737,7 +742,6 @@ def __init__(self, s, self._text.set_transform(self.offset_transform + self._baseline_transform) self._multilinebaseline = multilinebaseline - self._minimumdescent = minimumdescent def set_text(self, s): """Set the text of this area as a string.""" @@ -766,26 +770,6 @@ def get_multilinebaseline(self): """ return self._multilinebaseline - @_api.deprecated("3.4") - def set_minimumdescent(self, t): - """ - Set minimumdescent. - - If True, extent of the single line text is adjusted so that - its descent is at least the one of the glyph "p". - """ - # The current implementation of Text._get_layout always behaves as if - # this is True. - self._minimumdescent = t - self.stale = True - - @_api.deprecated("3.4") - def get_minimumdescent(self): - """ - Get minimumdescent. - """ - return self._minimumdescent - def set_transform(self, t): """ set_transform is ignored. @@ -809,19 +793,13 @@ def get_offset(self): """Return offset of the container.""" return self._offset - def get_window_extent(self, renderer): - """Return the bounding box in display space.""" - w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset() - return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) - - def get_extent(self, renderer): + def get_bbox(self, renderer): _, h_, d_ = renderer.get_text_width_height_descent( "lp", self._text._fontproperties, ismath="TeX" if self._text.get_usetex() else False) bbox, info, yd = self._text._get_layout(renderer) - w, h = bbox.width, bbox.height + w, h = bbox.size self._baseline_transform.clear() @@ -834,19 +812,14 @@ def get_extent(self, renderer): h = h_d + yd ha = self._text.get_horizontalalignment() - if ha == 'left': - xd = 0 - elif ha == 'center': - xd = w / 2 - elif ha == 'right': - xd = w + x0 = {"left": 0, "center": -w / 2, "right": -w}[ha] - return w, h, xd, yd + return Bbox.from_bounds(x0, -yd, w, h) def draw(self, renderer): # docstring inherited self._text.draw(renderer) - bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) + _bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) self.stale = False @@ -910,32 +883,25 @@ def get_offset(self): """Return offset of the container.""" return self._offset - def get_window_extent(self, renderer): - """Return the bounding box in display space.""" - w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset() # w, h, xd, yd) - return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) - - def get_extent(self, renderer): + def get_bbox(self, renderer): # clear the offset transforms _off = self.offset_transform.get_matrix() # to be restored later self.ref_offset_transform.clear() self.offset_transform.clear() # calculate the extent bboxes = [c.get_window_extent(renderer) for c in self._children] - ub = mtransforms.Bbox.union(bboxes) + ub = Bbox.union(bboxes) # adjust ref_offset_transform self.ref_offset_transform.translate(-ub.x0, -ub.y0) # restore offset transform self.offset_transform.set_matrix(_off) - - return ub.width, ub.height, 0., 0. + return Bbox.from_bounds(0, 0, ub.width, ub.height) def draw(self, renderer): # docstring inherited for c in self._children: c.draw(renderer) - bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) + _bbox_artist(self, renderer, fill=False, props=dict(pad=0.)) self.stale = False @@ -963,6 +929,7 @@ class AnchoredOffsetbox(OffsetBox): 'center': 10, } + @_api.make_keyword_only("3.6", name="pad") def __init__(self, loc, pad=0.4, borderpad=0.5, child=None, prop=None, frameon=True, @@ -973,43 +940,27 @@ def __init__(self, loc, Parameters ---------- loc : str - The box location. Supported values: - - - 'upper right' - - 'upper left' - - 'lower left' - - 'lower right' - - 'center left' - - 'center right' - - 'lower center' - - 'upper center' - - 'center' - + The box location. Valid locations are + 'upper left', 'upper center', 'upper right', + 'center left', 'center', 'center right', + 'lower left', 'lower center', 'lower right'. For backward compatibility, numeric values are accepted as well. See the parameter *loc* of `.Legend` for details. - pad : float, default: 0.4 Padding around the child as fraction of the fontsize. - borderpad : float, default: 0.5 Padding between the offsetbox frame and the *bbox_to_anchor*. - child : `.OffsetBox` The box that will be anchored. - prop : `.FontProperties` This is only used as a reference for paddings. If not given, :rc:`legend.fontsize` is used. - frameon : bool Whether to draw a frame around the box. - bbox_to_anchor : `.BboxBase`, 2-tuple, or 4-tuple of floats Box that is used to position the legend in conjunction with *loc*. - bbox_transform : None or :class:`matplotlib.transforms.Transform` The transform for the bounding box (*bbox_to_anchor*). - **kwargs All other parameters are passed on to `.OffsetBox`. @@ -1030,11 +981,11 @@ def __init__(self, loc, self.pad = pad if prop is None: - self.prop = FontProperties(size=rcParams["legend.fontsize"]) + self.prop = FontProperties(size=mpl.rcParams["legend.fontsize"]) else: self.prop = FontProperties._from_any(prop) if isinstance(prop, dict) and "size" not in prop: - self.prop.set_size(rcParams["legend.fontsize"]) + self.prop.set_size(mpl.rcParams["legend.fontsize"]) self.patch = FancyBboxPatch( xy=(0.0, 0.0), width=1., height=1., @@ -1060,17 +1011,11 @@ def get_children(self): """Return the list of children.""" return [self._child] - def get_extent(self, renderer): - """ - Return the extent of the box as (width, height, x, y). - - This is the extent of the child plus the padding. - """ - w, h, xd, yd = self.get_child().get_extent(renderer) + def get_bbox(self, renderer): + # docstring inherited fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) pad = self.pad * fontsize - - return w + 2 * pad, h + 2 * pad, xd + pad, yd + pad + return self.get_child().get_bbox(renderer).padded(pad) def get_bbox_to_anchor(self): """Return the bbox that the box is anchored to.""" @@ -1081,8 +1026,7 @@ def get_bbox_to_anchor(self): if transform is None: return self._bbox_to_anchor else: - return TransformedBbox(self._bbox_to_anchor, - transform) + return TransformedBbox(self._bbox_to_anchor, transform) def set_bbox_to_anchor(self, bbox, transform=None): """ @@ -1099,8 +1043,7 @@ def set_bbox_to_anchor(self, bbox, transform=None): try: l = len(bbox) except TypeError as err: - raise ValueError("Invalid argument for bbox : %s" % - str(bbox)) from err + raise ValueError(f"Invalid bbox: {bbox}") from err if l == 2: bbox = [bbox[0], bbox[1], 0, 0] @@ -1110,37 +1053,19 @@ def set_bbox_to_anchor(self, bbox, transform=None): self._bbox_to_anchor_transform = transform self.stale = True - def get_window_extent(self, renderer): - """Return the bounding box in display space.""" - self._update_offset_func(renderer) - w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset(w, h, xd, yd, renderer) - return Bbox.from_bounds(ox - xd, oy - yd, w, h) - - def _update_offset_func(self, renderer, fontsize=None): - """ - Update the offset func which depends on the dpi of the - renderer (because of the padding). - """ - if fontsize is None: - fontsize = renderer.points_to_pixels( - self.prop.get_size_in_points()) - - def _offset(w, h, xd, yd, renderer, fontsize=fontsize, self=self): - bbox = Bbox.from_bounds(0, 0, w, h) - borderpad = self.borderpad * fontsize - bbox_to_anchor = self.get_bbox_to_anchor() - - x0, y0 = self._get_anchored_bbox(self.loc, - bbox, - bbox_to_anchor, - borderpad) - return x0 + xd, y0 + yd - - self.set_offset(_offset) + @_compat_get_offset + def get_offset(self, bbox, renderer): + # docstring inherited + pad = (self.borderpad + * renderer.points_to_pixels(self.prop.get_size_in_points())) + bbox_to_anchor = self.get_bbox_to_anchor() + x0, y0 = _get_anchored_bbox( + self.loc, Bbox.from_bounds(0, 0, bbox.width, bbox.height), + bbox_to_anchor, pad) + return x0 - bbox.x0, y0 - bbox.y0 def update_frame(self, bbox, fontsize=None): - self.patch.set_bounds(bbox.x0, bbox.y0, bbox.width, bbox.height) + self.patch.set_bounds(bbox.bounds) if fontsize: self.patch.set_mutation_scale(fontsize) @@ -1149,47 +1074,28 @@ def draw(self, renderer): if not self.get_visible(): return - fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) - self._update_offset_func(renderer, fontsize) - # update the location and size of the legend bbox = self.get_window_extent(renderer) + fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) self.update_frame(bbox, fontsize) self.patch.draw(renderer) - width, height, xdescent, ydescent = self.get_extent(renderer) - - px, py = self.get_offset(width, height, xdescent, ydescent, renderer) - + px, py = self.get_offset(self.get_bbox(renderer), renderer) self.get_child().set_offset((px, py)) self.get_child().draw(renderer) self.stale = False - def _get_anchored_bbox(self, loc, bbox, parentbbox, borderpad): - """ - Return the position of the bbox anchored at the parentbbox - with the loc code, with the borderpad. - """ - assert loc in range(1, 11) # called only internally - - BEST, UR, UL, LL, LR, R, CL, CR, LC, UC, C = range(11) - - anchor_coefs = {UR: "NE", - UL: "NW", - LL: "SW", - LR: "SE", - R: "E", - CL: "W", - CR: "E", - LC: "S", - UC: "N", - C: "C"} - - c = anchor_coefs[loc] - container = parentbbox.padded(-borderpad) - anchored_box = bbox.anchored(c, container=container) - return anchored_box.x0, anchored_box.y0 +def _get_anchored_bbox(loc, bbox, parentbbox, borderpad): + """ + Return the (x, y) position of the *bbox* anchored at the *parentbbox* with + the *loc* code with the *borderpad*. + """ + # This is only called internally and *loc* should already have been + # validated. If 0 (None), we just let ``bbox.anchored`` raise. + c = [None, "NE", "NW", "SW", "SE", "E", "W", "E", "S", "N", "C"][loc] + container = parentbbox.padded(-borderpad) + return bbox.anchored(c, container=container).p0 class AnchoredText(AnchoredOffsetbox): @@ -1197,6 +1103,7 @@ class AnchoredText(AnchoredOffsetbox): AnchoredOffsetbox with Text. """ + @_api.make_keyword_only("3.6", name="pad") def __init__(self, s, loc, pad=0.4, borderpad=0.5, prop=None, **kwargs): """ Parameters @@ -1236,6 +1143,8 @@ def __init__(self, s, loc, pad=0.4, borderpad=0.5, prop=None, **kwargs): class OffsetImage(OffsetBox): + + @_api.make_keyword_only("3.6", name="zoom") def __init__(self, arr, zoom=1, cmap=None, @@ -1290,24 +1199,13 @@ def get_offset(self): def get_children(self): return [self.image] - def get_window_extent(self, renderer): - """Return the bounding box in display space.""" - w, h, xd, yd = self.get_extent(renderer) - ox, oy = self.get_offset() - return mtransforms.Bbox.from_bounds(ox - xd, oy - yd, w, h) - - def get_extent(self, renderer): - if self._dpi_cor: # True, do correction - dpi_cor = renderer.points_to_pixels(1.) - else: - dpi_cor = 1. - + def get_bbox(self, renderer): + dpi_cor = renderer.points_to_pixels(1.) if self._dpi_cor else 1. zoom = self.get_zoom() data = self.get_data() ny, nx = data.shape[:2] w, h = dpi_cor * nx * zoom, dpi_cor * ny * zoom - - return w, h, 0, 0 + return Bbox.from_bounds(0, 0, w, h) def draw(self, renderer): # docstring inherited @@ -1330,7 +1228,8 @@ class AnnotationBbox(martist.Artist, mtext._AnnotationBase): def __str__(self): return "AnnotationBbox(%g,%g)" % (self.xy[0], self.xy[1]) - @docstring.dedent_interpd + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="xycoords") def __init__(self, offsetbox, xy, xybox=None, xycoords='data', @@ -1355,18 +1254,30 @@ def __init__(self, offsetbox, xy, The position *(x, y)* to place the text at. The coordinate system is determined by *boxcoords*. - xycoords : str or `.Artist` or `.Transform` or callable or \ -(float, float), default: 'data' + xycoords : single or two-tuple of str or `.Artist` or `.Transform` or \ +callable, default: 'data' The coordinate system that *xy* is given in. See the parameter *xycoords* in `.Annotation` for a detailed description. - boxcoords : str or `.Artist` or `.Transform` or callable or \ -(float, float), default: value of *xycoords* + boxcoords : single or two-tuple of str or `.Artist` or `.Transform` \ +or callable, default: value of *xycoords* The coordinate system that *xybox* is given in. See the parameter *textcoords* in `.Annotation` for a detailed description. frameon : bool, default: True - Whether to draw a frame around the box. + By default, the text is surrounded by a white `.FancyBboxPatch` + (accessible as the ``patch`` attribute of the `.AnnotationBbox`). + If *frameon* is set to False, this patch is made invisible. + + annotation_clip: bool or None, default: None + Whether to clip (i.e. not draw) the annotation when the annotation + point *xy* is outside the axes area. + + - If *True*, the annotation will be clipped when *xy* is outside + the axes. + - If *False*, the annotation will always be drawn. + - If *None*, the annotation will be clipped when *xy* is outside + the axes and *xycoords* is 'data'. pad : float, default: 0.4 Padding around the offsetbox. @@ -1376,31 +1287,37 @@ def __init__(self, offsetbox, xy, the offset box w.r.t. the *boxcoords*. The lower-left corner is (0, 0) and upper-right corner is (1, 1). + bboxprops : dict, optional + A dictionary of properties to set for the annotation bounding box, + for example *boxstyle* and *alpha*. See `.FancyBboxPatch` for + details. + + arrowprops: dict, optional + Arrow properties, see `.Annotation` for description. + + fontsize: float or str, optional + Translated to points and passed as *mutation_scale* into + `.FancyBboxPatch` to scale attributes of the box style (e.g. pad + or rounding_size). The name is chosen in analogy to `.Text` where + *fontsize* defines the mutation scale as well. If not given, + :rc:`legend.fontsize` is used. See `.Text.set_fontsize` for valid + values. + **kwargs - Other parameters are identical to `.Annotation`. + Other `AnnotationBbox` properties. See `.AnnotationBbox.set` for + a list. """ martist.Artist.__init__(self) - mtext._AnnotationBase.__init__(self, - xy, - xycoords=xycoords, - annotation_clip=annotation_clip) + mtext._AnnotationBase.__init__( + self, xy, xycoords=xycoords, annotation_clip=annotation_clip) self.offsetbox = offsetbox - - self.arrowprops = arrowprops - + self.arrowprops = arrowprops.copy() if arrowprops is not None else None self.set_fontsize(fontsize) - - if xybox is None: - self.xybox = xy - else: - self.xybox = xybox - - if boxcoords is None: - self.boxcoords = xycoords - else: - self.boxcoords = boxcoords + self.xybox = xybox if xybox is not None else xy + self.boxcoords = boxcoords if boxcoords is not None else xycoords + self._box_alignment = box_alignment if arrowprops is not None: self._arrow_relpos = self.arrowprops.pop("relpos", (0.5, 0.5)) @@ -1410,10 +1327,7 @@ def __init__(self, offsetbox, xy, self._arrow_relpos = None self.arrow_patch = None - self._box_alignment = box_alignment - - # frame - self.patch = FancyBboxPatch( + self.patch = FancyBboxPatch( # frame xy=(0.0, 0.0), width=1., height=1., facecolor='w', edgecolor='k', mutation_scale=self.prop.get_size_in_points(), @@ -1424,7 +1338,7 @@ def __init__(self, offsetbox, xy, if bboxprops: self.patch.set(**bboxprops) - self.update(kwargs) + self._internal_update(kwargs) @property def xyann(self): @@ -1451,9 +1365,6 @@ def contains(self, mouseevent): if not self._check_xy(None): return False, {} return self.offsetbox.contains(mouseevent) - #if self.arrow_patch is not None: - # a, ainfo=self.arrow_patch.contains(event) - # t = t or a # self.arrow_patch is currently not checked as this can be a line - JJ def get_children(self): @@ -1475,50 +1386,30 @@ def set_fontsize(self, s=None): If *s* is not given, reset to :rc:`legend.fontsize`. """ if s is None: - s = rcParams["legend.fontsize"] + s = mpl.rcParams["legend.fontsize"] self.prop = FontProperties(size=s) self.stale = True - @_api.delete_parameter("3.3", "s") - def get_fontsize(self, s=None): + def get_fontsize(self): """Return the fontsize in points.""" return self.prop.get_size_in_points() - def get_window_extent(self, renderer): - """ - get the bounding box in display space. - """ - bboxes = [child.get_window_extent(renderer) - for child in self.get_children()] - - return Bbox.union(bboxes) - - def get_tightbbox(self, renderer): - """ - get tight bounding box in display space. - """ - bboxes = [child.get_tightbbox(renderer) - for child in self.get_children()] + def get_window_extent(self, renderer=None): + # docstring inherited + if renderer is None: + renderer = self.figure._get_renderer() + return Bbox.union([child.get_window_extent(renderer) + for child in self.get_children()]) - return Bbox.union(bboxes) + def get_tightbbox(self, renderer=None): + # docstring inherited + return Bbox.union([child.get_tightbbox(renderer) + for child in self.get_children()]) def update_positions(self, renderer): """ - Update the pixel positions of the annotated point and the text. - """ - xy_pixel = self._get_position_xy(renderer) - self._update_position_xybox(renderer, xy_pixel) - - mutation_scale = renderer.points_to_pixels(self.get_fontsize()) - self.patch.set_mutation_scale(mutation_scale) - - if self.arrow_patch: - self.arrow_patch.set_mutation_scale(mutation_scale) - - def _update_position_xybox(self, renderer, xy_pixel): - """ - Update the pixel positions of the annotation text and the arrow patch. + Update pixel positions for the annotated point, the text and the arrow. """ x, y = self.xybox @@ -1530,47 +1421,36 @@ def _update_position_xybox(self, renderer, xy_pixel): else: ox0, oy0 = self._get_xy(renderer, x, y, self.boxcoords) - w, h, xd, yd = self.offsetbox.get_extent(renderer) + bbox = self.offsetbox.get_bbox(renderer) + fw, fh = self._box_alignment + self.offsetbox.set_offset( + (ox0 - fw*bbox.width - bbox.x0, oy0 - fh*bbox.height - bbox.y0)) - _fw, _fh = self._box_alignment - self.offsetbox.set_offset((ox0 - _fw * w + xd, oy0 - _fh * h + yd)) - - # update patch position bbox = self.offsetbox.get_window_extent(renderer) - #self.offsetbox.set_offset((ox0-_fw*w, oy0-_fh*h)) - self.patch.set_bounds(bbox.x0, bbox.y0, - bbox.width, bbox.height) - - x, y = xy_pixel + self.patch.set_bounds(bbox.bounds) - ox1, oy1 = x, y + mutation_scale = renderer.points_to_pixels(self.get_fontsize()) + self.patch.set_mutation_scale(mutation_scale) if self.arrowprops: - d = self.arrowprops.copy() - # Use FancyArrowPatch if self.arrowprops has "arrowstyle" key. - # adjust the starting point of the arrow relative to - # the textbox. - # TODO : Rotation needs to be accounted. - relpos = self._arrow_relpos - - ox0 = bbox.x0 + bbox.width * relpos[0] - oy0 = bbox.y0 + bbox.height * relpos[1] - - # The arrow will be drawn from (ox0, oy0) to (ox1, - # oy1). It will be first clipped by patchA and patchB. - # Then it will be shrunk by shrinkA and shrinkB - # (in points). If patch A is not set, self.bbox_patch - # is used. - - self.arrow_patch.set_positions((ox0, oy0), (ox1, oy1)) - fs = self.prop.get_size_in_points() - mutation_scale = d.pop("mutation_scale", fs) - mutation_scale = renderer.points_to_pixels(mutation_scale) + # Adjust the starting point of the arrow relative to the textbox. + # TODO: Rotation needs to be accounted. + arrow_begin = bbox.p0 + bbox.size * self._arrow_relpos + arrow_end = self._get_position_xy(renderer) + # The arrow (from arrow_begin to arrow_end) will be first clipped + # by patchA and patchB, then shrunk by shrinkA and shrinkB (in + # points). If patch A is not set, self.bbox_patch is used. + self.arrow_patch.set_positions(arrow_begin, arrow_end) + + if "mutation_scale" in self.arrowprops: + mutation_scale = renderer.points_to_pixels( + self.arrowprops["mutation_scale"]) + # Else, use fontsize-based mutation_scale defined above. self.arrow_patch.set_mutation_scale(mutation_scale) - patchA = d.pop("patchA", self.patch) + patchA = self.arrowprops.get("patchA", self.patch) self.arrow_patch.set_patchA(patchA) def draw(self, renderer): @@ -1579,6 +1459,7 @@ def draw(self, renderer): self._renderer = renderer if not self.get_visible() or not self._check_xy(renderer): return + renderer.open_group(self.__class__.__name__, gid=self.get_gid()) self.update_positions(renderer) if self.arrow_patch is not None: if self.arrow_patch.figure is None and self.figure is not None: @@ -1586,6 +1467,7 @@ def draw(self, renderer): self.arrow_patch.draw(renderer) self.patch.draw(renderer) self.offsetbox.draw(renderer) + renderer.close_group(self.__class__.__name__) self.stale = False @@ -1620,22 +1502,19 @@ def finalize_offset(self): def __init__(self, ref_artist, use_blit=False): self.ref_artist = ref_artist + if not ref_artist.pickable(): + ref_artist.set_picker(True) self.got_artist = False - - self.canvas = self.ref_artist.figure.canvas self._use_blit = use_blit and self.canvas.supports_blit + self.cids = [ + self.canvas.callbacks._connect_picklable( + 'pick_event', self.on_pick), + self.canvas.callbacks._connect_picklable( + 'button_release_event', self.on_release), + ] - c2 = self.canvas.mpl_connect('pick_event', self.on_pick) - c3 = self.canvas.mpl_connect('button_release_event', self.on_release) - - if not ref_artist.pickable(): - ref_artist.set_picker(True) - overridden_picker = _api.deprecate_method_override( - __class__.artist_picker, self, since="3.3", - addendum="Directly set the artist's picker if desired.") - if overridden_picker is not None: - ref_artist.set_picker(overridden_picker) - self.cids = [c2, c3] + # A property, not an attribute, to maintain picklability. + canvas = property(lambda self: self.ref_artist.figure.canvas) def on_motion(self, evt): if self._check_still_parented() and self.got_artist: @@ -1644,21 +1523,12 @@ def on_motion(self, evt): self.update_offset(dx, dy) if self._use_blit: self.canvas.restore_region(self.background) - self.ref_artist.draw(self.ref_artist.figure._cachedRenderer) + self.ref_artist.draw( + self.ref_artist.figure._get_renderer()) self.canvas.blit() else: self.canvas.draw() - @_api.deprecated("3.3", alternative="self.on_motion") - def on_motion_blit(self, evt): - if self._check_still_parented() and self.got_artist: - dx = evt.x - self.mouse_x - dy = evt.y - self.mouse_y - self.update_offset(dx, dy) - self.canvas.restore_region(self.background) - self.ref_artist.draw(self.ref_artist.figure._cachedRenderer) - self.canvas.blit() - def on_pick(self, evt): if self._check_still_parented() and evt.artist == self.ref_artist: self.mouse_x = evt.mouseevent.x @@ -1669,9 +1539,10 @@ def on_pick(self, evt): self.canvas.draw() self.background = \ self.canvas.copy_from_bbox(self.ref_artist.figure.bbox) - self.ref_artist.draw(self.ref_artist.figure._cachedRenderer) + self.ref_artist.draw( + self.ref_artist.figure._get_renderer()) self.canvas.blit() - self._c1 = self.canvas.mpl_connect( + self._c1 = self.canvas.callbacks._connect_picklable( "motion_notify_event", self.on_motion) self.save_offset() @@ -1702,10 +1573,6 @@ def disconnect(self): else: self.canvas.mpl_disconnect(c1) - @_api.deprecated("3.3", alternative="self.ref_artist.contains") - def artist_picker(self, artist, evt): - return self.ref_artist.contains(evt) - def save_offset(self): pass @@ -1723,9 +1590,8 @@ def __init__(self, ref_artist, offsetbox, use_blit=False): def save_offset(self): offsetbox = self.offsetbox - renderer = offsetbox.figure._cachedRenderer - w, h, xd, yd = offsetbox.get_extent(renderer) - offset = offsetbox.get_offset(w, h, xd, yd, renderer) + renderer = offsetbox.figure._get_renderer() + offset = offsetbox.get_offset(offsetbox.get_bbox(renderer), renderer) self.offsetbox_x, self.offsetbox_y = offset self.offsetbox.set_offset(offset) @@ -1735,10 +1601,10 @@ def update_offset(self, dx, dy): def get_loc_in_canvas(self): offsetbox = self.offsetbox - renderer = offsetbox.figure._cachedRenderer - w, h, xd, yd = offsetbox.get_extent(renderer) + renderer = offsetbox.figure._get_renderer() + bbox = offsetbox.get_bbox(renderer) ox, oy = offsetbox._offset - loc_in_canvas = (ox - xd, oy - yd) + loc_in_canvas = (ox + bbox.x0, oy + bbox.y0) return loc_in_canvas diff --git a/lib/matplotlib/patches.py b/lib/matplotlib/patches.py index 07ee830be31d..2b4e0dc6e6a9 100644 --- a/lib/matplotlib/patches.py +++ b/lib/matplotlib/patches.py @@ -1,15 +1,20 @@ -import contextlib +r""" +Patches are `.Artist`\s with a face color and an edge color. +""" + import functools import inspect import math from numbers import Number import textwrap +from types import SimpleNamespace from collections import namedtuple +from matplotlib.transforms import Affine2D import numpy as np import matplotlib as mpl -from . import (_api, artist, cbook, colors, docstring, hatch as mhatch, +from . import (_api, artist, cbook, colors, _docstring, hatch as mhatch, lines as mlines, transforms) from .bezier import ( NonIntersectingPathException, get_cos_sin, get_intersection, @@ -19,7 +24,8 @@ from ._enums import JoinStyle, CapStyle -@cbook._define_aliases({ +@_docstring.interpd +@_api.define_aliases({ "antialiased": ["aa"], "edgecolor": ["ec"], "facecolor": ["fc"], @@ -35,22 +41,11 @@ class Patch(artist.Artist): """ zorder = 1 - @_api.deprecated("3.4") - @_api.classproperty - def validCap(cls): - with _api.suppress_matplotlib_deprecation_warning(): - return mlines.Line2D.validCap - - @_api.deprecated("3.4") - @_api.classproperty - def validJoin(cls): - with _api.suppress_matplotlib_deprecation_warning(): - return mlines.Line2D.validJoin - # Whether to draw an edge by default. Set on a # subclass-by-subclass basis. _edge_default = False + @_api.make_keyword_only("3.6", name="edgecolor") def __init__(self, edgecolor=None, facecolor=None, @@ -66,20 +61,16 @@ def __init__(self, """ The following kwarg properties are supported - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__() - if linewidth is None: - linewidth = mpl.rcParams['patch.linewidth'] if linestyle is None: linestyle = "solid" if capstyle is None: capstyle = CapStyle.butt if joinstyle is None: joinstyle = JoinStyle.miter - if antialiased is None: - antialiased = mpl.rcParams['patch.antialiased'] self._hatch_color = colors.to_rgba(mpl.rcParams['hatch.color']) self._fill = True # needed for set_facecolor call @@ -92,9 +83,10 @@ def __init__(self, else: self.set_edgecolor(edgecolor) self.set_facecolor(facecolor) - # unscaled dashes. Needed to scale dash patterns by lw - self._us_dashes = None + self._linewidth = 0 + self._unscaled_dash_pattern = (0, None) # offset, dash + self._dash_pattern = (0, None) # offset, dash (scaled by linewidth) self.set_fill(fill) self.set_linestyle(linestyle) @@ -105,13 +97,13 @@ def __init__(self, self.set_joinstyle(joinstyle) if len(kwargs): - self.update(kwargs) + self._internal_update(kwargs) def get_verts(self): """ Return a copy of the vertices used in this patch. - If the patch contains Bezier curves, the curves will be interpolated by + If the patch contains Bézier curves, the curves will be interpolated by line segments. To access the curves as curves, use `get_path`. """ trans = self.get_transform() @@ -174,7 +166,7 @@ def contains_point(self, point, radius=None): ``self.get_transform()``. These are display coordinates for patches that are added to a figure or axes. radius : float, optional - Add an additional margin on the patch in target coordinates of + Additional margin on the patch in target coordinates of ``self.get_transform()``. See `.Path.contains_point` for further details. @@ -224,7 +216,7 @@ def contains_points(self, points, radius=None): ``self.get_transform()``. These are display coordinates for patches that are added to a figure or axes. Columns contain x and y values. radius : float, optional - Add an additional margin on the patch in target coordinates of + Additional margin on the patch in target coordinates of ``self.get_transform()``. See `.Path.contains_point` for further details. @@ -254,9 +246,8 @@ def update_from(self, other): self._fill = other._fill self._hatch = other._hatch self._hatch_color = other._hatch_color - # copy the unscaled dash pattern - self._us_dashes = other._us_dashes - self.set_linewidth(other._linewidth) # also sets dash properties + self._unscaled_dash_pattern = other._unscaled_dash_pattern + self.set_linewidth(other._linewidth) # also sets scaled dashes self.set_transform(other.get_data_transform()) # If the transform of other needs further initialization, then it will # be the case for this artist too. @@ -316,7 +307,7 @@ def set_antialiased(self, aa): Parameters ---------- - b : bool or None + aa : bool or None """ if aa is None: aa = mpl.rcParams['patch.antialiased'] @@ -344,7 +335,7 @@ def set_edgecolor(self, color): Parameters ---------- - color : color or None or 'auto' + color : color or None """ self._original_edgecolor = color self._set_edgecolor(color) @@ -400,32 +391,24 @@ def set_linewidth(self, w): """ if w is None: w = mpl.rcParams['patch.linewidth'] - if w is None: - w = mpl.rcParams['axes.linewidth'] - self._linewidth = float(w) - # scale the dash pattern by the linewidth - offset, ls = self._us_dashes - self._dashoffset, self._dashes = mlines._scale_dashes( - offset, ls, self._linewidth) + self._dash_pattern = mlines._scale_dashes( + *self._unscaled_dash_pattern, w) self.stale = True def set_linestyle(self, ls): """ Set the patch linestyle. - =========================== ================= - linestyle description - =========================== ================= - ``'-'`` or ``'solid'`` solid line - ``'--'`` or ``'dashed'`` dashed line - ``'-.'`` or ``'dashdot'`` dash-dotted line - ``':'`` or ``'dotted'`` dotted line - ``'None'`` draw nothing - ``'none'`` draw nothing - ``' '`` draw nothing - ``''`` draw nothing - =========================== ================= + ========================================== ================= + linestyle description + ========================================== ================= + ``'-'`` or ``'solid'`` solid line + ``'--'`` or ``'dashed'`` dashed line + ``'-.'`` or ``'dashdot'`` dash-dotted line + ``':'`` or ``'dotted'`` dotted line + ``'none'``, ``'None'``, ``' '``, or ``''`` draw nothing + ========================================== ================= Alternatively a dash tuple of the following form can be provided:: @@ -443,11 +426,9 @@ def set_linestyle(self, ls): if ls in [' ', '', 'none']: ls = 'None' self._linestyle = ls - # get the unscaled dash pattern - offset, ls = self._us_dashes = mlines._get_dash_pattern(ls) - # scale the dash pattern by the linewidth - self._dashoffset, self._dashes = mlines._scale_dashes( - offset, ls, self._linewidth) + self._unscaled_dash_pattern = mlines._get_dash_pattern(ls) + self._dash_pattern = mlines._scale_dashes( + *self._unscaled_dash_pattern, self._linewidth) self.stale = True def set_fill(self, b): @@ -472,11 +453,14 @@ def get_fill(self): # attribute. fill = property(get_fill, set_fill) - @docstring.interpd + @_docstring.interpd def set_capstyle(self, s): """ Set the `.CapStyle`. + The default capstyle is 'round' for `.FancyArrowPatch` and 'butt' for + all other patches. + Parameters ---------- s : `.CapStyle` or %(CapStyle)s @@ -487,13 +471,16 @@ def set_capstyle(self, s): def get_capstyle(self): """Return the capstyle.""" - return self._capstyle + return self._capstyle.name - @docstring.interpd + @_docstring.interpd def set_joinstyle(self, s): """ Set the `.JoinStyle`. + The default joinstyle is 'round' for `.FancyArrowPatch` and 'miter' for + all other patches. + Parameters ---------- s : `.JoinStyle` or %(JoinStyle)s @@ -504,7 +491,7 @@ def set_joinstyle(self, s): def get_joinstyle(self): """Return the joinstyle.""" - return self._joinstyle + return self._joinstyle.name def set_hatch(self, hatch): r""" @@ -543,15 +530,15 @@ def get_hatch(self): """Return the hatching pattern.""" return self._hatch - @contextlib.contextmanager - def _bind_draw_path_function(self, renderer): + def _draw_paths_with_artist_properties( + self, renderer, draw_path_args_list): """ ``draw()`` helper factored out for sharing with `FancyArrowPatch`. - Yields a callable ``dp`` such that calling ``dp(*args, **kwargs)`` is - equivalent to calling ``renderer1.draw_path(gc, *args, **kwargs)`` - where ``renderer1`` and ``gc`` have been suitably set from ``renderer`` - and the artist's properties. + Configure *renderer* and the associated graphics context *gc* + from the artist properties, then repeatedly call + ``renderer.draw_path(gc, *draw_path_args)`` for each tuple + *draw_path_args* in *draw_path_args_list*. """ renderer.open_group('patch', self.get_gid()) @@ -563,7 +550,7 @@ def _bind_draw_path_function(self, renderer): if self._edgecolor[3] == 0 or self._linestyle == 'None': lw = 0 gc.set_linewidth(lw) - gc.set_dashes(self._dashoffset, self._dashes) + gc.set_dashes(*self._dash_pattern) gc.set_capstyle(self._capstyle) gc.set_joinstyle(self._joinstyle) @@ -585,11 +572,8 @@ def _bind_draw_path_function(self, renderer): from matplotlib.patheffects import PathEffectRenderer renderer = PathEffectRenderer(self.get_path_effects(), renderer) - # In `with _bind_draw_path_function(renderer) as draw_path: ...` - # (in the implementations of `draw()` below), calls to `draw_path(...)` - # will occur as if they took place here with `gc` inserted as - # additional first argument. - yield functools.partial(renderer.draw_path, gc) + for draw_path_args in draw_path_args_list: + renderer.draw_path(gc, *draw_path_args) gc.restore() renderer.close_group('patch') @@ -600,18 +584,17 @@ def draw(self, renderer): # docstring inherited if not self.get_visible(): return - # Patch has traditionally ignored the dashoffset. - with cbook._setattr_cm(self, _dashoffset=0), \ - self._bind_draw_path_function(renderer) as draw_path: - path = self.get_path() - transform = self.get_transform() - tpath = transform.transform_path_non_affine(path) - affine = transform.get_affine() - draw_path(tpath, affine, - # Work around a bug in the PDF and SVG renderers, which - # do not draw the hatches if the facecolor is fully - # transparent, but do if it is None. - self._facecolor if self._facecolor[3] else None) + path = self.get_path() + transform = self.get_transform() + tpath = transform.transform_path_non_affine(path) + affine = transform.get_affine() + self._draw_paths_with_artist_properties( + renderer, + [(tpath, affine, + # Work around a bug in the PDF and SVG renderers, which + # do not draw the hatches if the facecolor is fully + # transparent, but do if it is None. + self._facecolor if self._facecolor[3] else None)]) def get_path(self): """Return the path of this patch.""" @@ -627,23 +610,12 @@ def _convert_xy_units(self, xy): return x, y -_patch_kwdoc = artist.kwdoc(Patch) -for k in ['Rectangle', 'Circle', 'RegularPolygon', 'Polygon', 'Wedge', 'Arrow', - 'FancyArrow', 'CirclePolygon', 'Ellipse', 'Arc', 'FancyBboxPatch', - 'Patch']: - docstring.interpd.update({f'{k}_kwdoc': _patch_kwdoc}) - -# define Patch.__init__ docstring after the class has been added to interpd -docstring.dedent_interpd(Patch.__init__) - - class Shadow(Patch): def __str__(self): - return "Shadow(%s)" % (str(self.patch)) + return f"Shadow({self.patch})" - @_api.delete_parameter("3.3", "props") - @docstring.dedent_interpd - def __init__(self, patch, ox, oy, props=None, **kwargs): + @_docstring.dedent_interpd + def __init__(self, patch, ox, oy, **kwargs): """ Create a shadow of the given *patch*. @@ -657,38 +629,22 @@ def __init__(self, patch, ox, oy, props=None, **kwargs): ox, oy : float The shift of the shadow in data coordinates, scaled by a factor of dpi/72. - props : dict - *deprecated (use kwargs instead)* Properties of the shadow patch. **kwargs Properties of the shadow patch. Supported keys are: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__() self.patch = patch - # Note: when removing props, we can directly pass kwargs to _update() - # and remove self._props - if props is None: - color = .3 * np.asarray(colors.to_rgb(self.patch.get_facecolor())) - props = { - 'facecolor': color, - 'edgecolor': color, - 'alpha': 0.5, - } - self._props = {**props, **kwargs} self._ox, self._oy = ox, oy self._shadow_transform = transforms.Affine2D() - self._update() - - props = _api.deprecate_privatize_attribute("3.3") - def _update(self): self.update_from(self.patch) - - # Place the shadow patch directly behind the inherited patch. - self.set_zorder(np.nextafter(self.patch.zorder, -np.inf)) - - self.update(self._props) + color = .3 * np.asarray(colors.to_rgb(self.patch.get_facecolor())) + self.update({'facecolor': color, 'edgecolor': color, 'alpha': 0.5, + # Place shadow patch directly behind the inherited patch. + 'zorder': np.nextafter(self.patch.zorder, -np.inf), + **kwargs}) def _update_transform(self, renderer): ox = renderer.points_to_pixels(self._ox) @@ -720,7 +676,7 @@ class Rectangle(Patch): : (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* + actually depends on 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. """ @@ -730,8 +686,10 @@ def __str__(self): fmt = "Rectangle(xy=(%g, %g), width=%g, height=%g, angle=%g)" return fmt % pars - @docstring.dedent_interpd - def __init__(self, xy, width, height, angle=0.0, **kwargs): + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="angle") + def __init__(self, xy, width, height, angle=0.0, *, + rotation_point='xy', **kwargs): """ Parameters ---------- @@ -742,12 +700,16 @@ def __init__(self, xy, width, height, angle=0.0, **kwargs): height : float Rectangle height. angle : float, default: 0 - Rotation in degrees anti-clockwise about *xy*. + Rotation in degrees anti-clockwise about the rotation point. + rotation_point : {'xy', 'center', (number, number)}, default: 'xy' + If ``'xy'``, rotate around the anchor point. If ``'center'`` rotate + around the center. If 2-tuple of number, rotate around this + coordinate. Other Parameters ---------------- **kwargs : `.Patch` properties - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__(**kwargs) self._x0 = xy[0] @@ -755,6 +717,14 @@ def __init__(self, xy, width, height, angle=0.0, **kwargs): self._width = width self._height = height self.angle = float(angle) + self.rotation_point = rotation_point + # Required for RectangleSelector with axes aspect ratio != 1 + # The patch is defined in data coordinates and when changing the + # selector with square modifier and not in data coordinates, we need + # to correct for the aspect ratio difference between the data and + # display coordinate systems. Its value is typically provide by + # Axes._get_aspect_ratio() + self._aspect_ratio_correction = 1.0 self._convert_units() # Validate the inputs. def get_path(self): @@ -775,9 +745,36 @@ def get_patch_transform(self): # important to call the accessor method and not directly access the # transformation member variable. bbox = self.get_bbox() - return (transforms.BboxTransformTo(bbox) - + transforms.Affine2D().rotate_deg_around( - bbox.x0, bbox.y0, self.angle)) + if self.rotation_point == 'center': + width, height = bbox.x1 - bbox.x0, bbox.y1 - bbox.y0 + rotation_point = bbox.x0 + width / 2., bbox.y0 + height / 2. + elif self.rotation_point == 'xy': + rotation_point = bbox.x0, bbox.y0 + else: + rotation_point = self.rotation_point + return transforms.BboxTransformTo(bbox) \ + + transforms.Affine2D() \ + .translate(-rotation_point[0], -rotation_point[1]) \ + .scale(1, self._aspect_ratio_correction) \ + .rotate_deg(self.angle) \ + .scale(1, 1 / self._aspect_ratio_correction) \ + .translate(*rotation_point) + + @property + def rotation_point(self): + """The rotation point of the patch.""" + return self._rotation_point + + @rotation_point.setter + def rotation_point(self, value): + if value in ['center', 'xy'] or ( + isinstance(value, tuple) and len(value) == 2 and + isinstance(value[0], Number) and isinstance(value[1], Number) + ): + self._rotation_point = value + else: + raise ValueError("`rotation_point` must be one of " + "{'xy', 'center', (number, number)}.") def get_x(self): """Return the left coordinate of the rectangle.""" @@ -791,6 +788,18 @@ def get_xy(self): """Return the left and bottom coords of the rectangle as a tuple.""" return self._x0, self._y0 + def get_corners(self): + """ + Return the corners of the rectangle, moving anti-clockwise from + (x0, y0). + """ + return self.get_patch_transform().transform( + [(0, 0), (1, 0), (1, 1), (0, 1)]) + + def get_center(self): + """Return the centre of the rectangle.""" + return self.get_patch_transform().transform((0.5, 0.5)) + def get_width(self): """Return the width of the rectangle.""" return self._width @@ -799,6 +808,10 @@ def get_height(self): """Return the height of the rectangle.""" return self._height + def get_angle(self): + """Get the rotation angle in degrees.""" + return self.angle + def set_x(self, x): """Set the left coordinate of the rectangle.""" self._x0 = x @@ -809,6 +822,15 @@ def set_y(self, y): self._y0 = y self.stale = True + def set_angle(self, angle): + """ + Set the rotation angle in degrees. + + The rotation is performed anti-clockwise around *xy*. + """ + self.angle = angle + self.stale = True + def set_xy(self, xy): """ Set the left and bottom coordinates of the rectangle. @@ -867,7 +889,8 @@ def __str__(self): return s % (self.xy[0], self.xy[1], self.numvertices, self.radius, self.orientation) - @docstring.dedent_interpd + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="radius") def __init__(self, xy, numVertices, radius=5, orientation=0, **kwargs): """ @@ -888,7 +911,7 @@ def __init__(self, xy, numVertices, radius=5, orientation=0, **kwargs `Patch` properties: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ self.xy = xy self.numvertices = numVertices @@ -917,14 +940,14 @@ def __str__(self): s = "PathPatch%d((%g, %g) ...)" return s % (len(self._path.vertices), *tuple(self._path.vertices[0])) - @docstring.dedent_interpd + @_docstring.dedent_interpd def __init__(self, path, **kwargs): """ - *path* is a `~.path.Path` object. + *path* is a `.Path` object. Valid keyword arguments are: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__(**kwargs) self._path = path @@ -940,13 +963,13 @@ class StepPatch(PathPatch): """ A path patch describing a stepwise constant function. - By default the path is not closed and starts and stops at + By default, the path is not closed and starts and stops at baseline value. """ _edge_default = False - @docstring.dedent_interpd + @_docstring.dedent_interpd def __init__(self, values, edges, *, orientation='vertical', baseline=0, **kwargs): """ @@ -971,7 +994,7 @@ def __init__(self, values, edges, *, Other valid keyword arguments are: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ self.orientation = orientation self._edges = np.asarray(edges) @@ -1054,7 +1077,8 @@ def __str__(self): else: return "Polygon0()" - @docstring.dedent_interpd + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="closed") def __init__(self, xy, closed=True, **kwargs): """ *xy* is a numpy array with shape Nx2. @@ -1064,7 +1088,7 @@ def __init__(self, xy, closed=True, **kwargs): Valid keyword arguments are: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__(**kwargs) self._closed = closed @@ -1115,7 +1139,7 @@ def set_xy(self, xy): Notes ----- - Unlike `~.path.Path`, we do not ignore the last input vertex. If the + Unlike `.Path`, we do not ignore the last input vertex. If the polygon is meant to be closed, and the last point of the polygon is not equal to the first, we assume that the user has not explicitly passed a ``CLOSEPOLY`` vertex, and add it ourselves. @@ -1150,7 +1174,8 @@ def __str__(self): fmt = "Wedge(center=(%g, %g), r=%g, theta1=%g, theta2=%g, width=%s)" return fmt % pars - @docstring.dedent_interpd + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="width") def __init__(self, center, r, theta1, theta2, width=None, **kwargs): """ A wedge centered at *x*, *y* center with radius *r* that @@ -1160,7 +1185,7 @@ def __init__(self, center, r, theta1, theta2, width=None, **kwargs): Valid keyword arguments are: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__(**kwargs) self.center = center @@ -1186,21 +1211,15 @@ def _recompute_path(self): # followed by a reversed and scaled inner ring v1 = arc.vertices v2 = arc.vertices[::-1] * (self.r - self.width) / self.r - v = np.concatenate([v1, v2, [v1[0, :], (0, 0)]]) - c = np.concatenate([ - arc.codes, arc.codes, [connector, Path.CLOSEPOLY]]) - c[len(arc.codes)] = connector + v = np.concatenate([v1, v2, [(0, 0)]]) + c = [*arc.codes, connector, *arc.codes[1:], Path.CLOSEPOLY] else: # Wedge doesn't need an inner ring - v = np.concatenate([ - arc.vertices, [(0, 0), arc.vertices[0, :], (0, 0)]]) - c = np.concatenate([ - arc.codes, [connector, connector, Path.CLOSEPOLY]]) + v = np.concatenate([arc.vertices, [(0, 0), (0, 0)]]) + c = [*arc.codes, connector, Path.CLOSEPOLY] # Shift and scale the wedge to the final location. - v *= self.r - v += np.asarray(self.center) - self._path = Path(v, c) + self._path = Path(v * self.r + self.center, c) def set_center(self, center): self._path = None @@ -1240,13 +1259,12 @@ class Arrow(Patch): def __str__(self): return "Arrow()" - _path = Path([[0.0, 0.1], [0.0, -0.1], - [0.8, -0.1], [0.8, -0.3], - [1.0, 0.0], [0.8, 0.3], - [0.8, 0.1], [0.0, 0.1]], - closed=True) + _path = Path._create_closed([ + [0.0, 0.1], [0.0, -0.1], [0.8, -0.1], [0.8, -0.3], [1.0, 0.0], + [0.8, 0.3], [0.8, 0.1]]) - @docstring.dedent_interpd + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="width") def __init__(self, x, y, dx, dy, width=1.0, **kwargs): """ Draws an arrow from (*x*, *y*) to (*x* + *dx*, *y* + *dy*). @@ -1268,7 +1286,7 @@ def __init__(self, x, y, dx, dy, width=1.0, **kwargs): **kwargs Keyword arguments control the `Patch` properties: - %(Patch_kwdoc)s + %(Patch:kwdoc)s See Also -------- @@ -1301,13 +1319,20 @@ class FancyArrow(Polygon): def __str__(self): return "FancyArrow()" - @docstring.dedent_interpd + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="width") def __init__(self, x, y, dx, dy, width=0.001, length_includes_head=False, head_width=None, head_length=None, shape='full', overhang=0, head_starts_at_zero=False, **kwargs): """ Parameters ---------- + x, y : float + The x and y coordinates of the arrow base. + + dx, dy : float + The length of the arrow along x and y direction. + width : float, default: 0.001 Width of full arrow tail. @@ -1334,24 +1359,84 @@ def __init__(self, x, y, dx, dy, width=0.001, length_includes_head=False, **kwargs `.Patch` properties: - %(Patch_kwdoc)s + %(Patch:kwdoc)s + """ + self._x = x + self._y = y + self._dx = dx + self._dy = dy + self._width = width + self._length_includes_head = length_includes_head + self._head_width = head_width + self._head_length = head_length + self._shape = shape + self._overhang = overhang + self._head_starts_at_zero = head_starts_at_zero + self._make_verts() + super().__init__(self.verts, closed=True, **kwargs) + + def set_data(self, *, x=None, y=None, dx=None, dy=None, width=None, + head_width=None, head_length=None): + """ + Set `.FancyArrow` x, y, dx, dy, width, head_with, and head_length. + Values left as None will not be updated. + + Parameters + ---------- + x, y : float or None, default: None + The x and y coordinates of the arrow base. + + dx, dy : float or None, default: None + The length of the arrow along x and y direction. + + width : float or None, default: None + Width of full arrow tail. + + head_width : float or None, default: None + Total width of the full arrow head. + + head_length : float or None, default: None + Length of arrow head. """ - if head_width is None: - head_width = 3 * width - if head_length is None: + if x is not None: + self._x = x + if y is not None: + self._y = y + if dx is not None: + self._dx = dx + if dy is not None: + self._dy = dy + if width is not None: + self._width = width + if head_width is not None: + self._head_width = head_width + if head_length is not None: + self._head_length = head_length + self._make_verts() + self.set_xy(self.verts) + + def _make_verts(self): + if self._head_width is None: + head_width = 3 * self._width + else: + head_width = self._head_width + if self._head_length is None: head_length = 1.5 * head_width + else: + head_length = self._head_length - distance = np.hypot(dx, dy) + distance = np.hypot(self._dx, self._dy) - if length_includes_head: + if self._length_includes_head: length = distance else: length = distance + head_length if not length: - verts = np.empty([0, 2]) # display nothing if empty + self.verts = np.empty([0, 2]) # display nothing if empty else: # start by drawing horizontal arrow, point at (0, 0) - hw, hl, hs, lw = head_width, head_length, overhang, width + hw, hl = head_width, head_length + hs, lw = self._overhang, self._width left_half_arrow = np.array([ [0.0, 0.0], # tip [-hl, -hw / 2], # leftmost @@ -1360,40 +1445,42 @@ def __init__(self, x, y, dx, dy, width=0.001, length_includes_head=False, [-length, 0], ]) # if we're not including the head, shift up by head length - if not length_includes_head: + if not self._length_includes_head: left_half_arrow += [head_length, 0] # if the head starts at 0, shift up by another head length - if head_starts_at_zero: + if self._head_starts_at_zero: left_half_arrow += [head_length / 2, 0] # figure out the shape, and complete accordingly - if shape == 'left': + if self._shape == 'left': coords = left_half_arrow else: right_half_arrow = left_half_arrow * [1, -1] - if shape == 'right': + if self._shape == 'right': coords = right_half_arrow - elif shape == 'full': + elif self._shape == 'full': # The half-arrows contain the midpoint of the stem, # which we can omit from the full arrow. Including it # twice caused a problem with xpdf. coords = np.concatenate([left_half_arrow[:-1], right_half_arrow[-2::-1]]) else: - raise ValueError("Got unknown shape: %s" % shape) + raise ValueError(f"Got unknown shape: {self._shape!r}") if distance != 0: - cx = dx / distance - sx = dy / distance + cx = self._dx / distance + sx = self._dy / distance else: # Account for division by zero cx, sx = 0, 1 M = [[cx, sx], [-sx, cx]] - verts = np.dot(coords, M) + (x + dx, y + dy) - - super().__init__(verts, closed=True, **kwargs) + self.verts = np.dot(coords, M) + [ + self._x + self._dx, + self._y + self._dy, + ] -docstring.interpd.update( - FancyArrow="\n".join(inspect.getdoc(FancyArrow.__init__).splitlines()[2:])) +_docstring.interpd.update( + FancyArrow="\n".join( + (inspect.getdoc(FancyArrow.__init__) or "").splitlines()[2:])) class CirclePolygon(RegularPolygon): @@ -1403,7 +1490,8 @@ def __str__(self): s = "CirclePolygon((%g, %g), radius=%g, resolution=%d)" return s % (self.xy[0], self.xy[1], self.radius, self.numvertices) - @docstring.dedent_interpd + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="resolution") def __init__(self, xy, radius=5, resolution=20, # the number of vertices ** kwargs): @@ -1415,9 +1503,10 @@ def __init__(self, xy, radius=5, Valid keyword arguments are: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ - super().__init__(xy, resolution, radius, orientation=0, **kwargs) + super().__init__( + xy, resolution, radius=radius, orientation=0, **kwargs) class Ellipse(Patch): @@ -1429,7 +1518,8 @@ def __str__(self): fmt = "Ellipse(xy=(%s, %s), width=%s, height=%s, angle=%s)" return fmt % pars - @docstring.dedent_interpd + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="angle") def __init__(self, xy, width, height, angle=0, **kwargs): """ Parameters @@ -1447,7 +1537,7 @@ def __init__(self, xy, width, height, angle=0, **kwargs): ----- Valid keyword arguments are: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__(**kwargs) @@ -1455,6 +1545,12 @@ def __init__(self, xy, width, height, angle=0, **kwargs): self._width, self._height = width, height self._angle = angle self._path = Path.unit_circle() + # Required for EllipseSelector with axes aspect ratio != 1 + # The patch is defined in data coordinates and when changing the + # selector with square modifier and not in data coordinates, we need + # to correct for the aspect ratio difference between the data and + # display coordinate systems. + self._aspect_ratio_correction = 1.0 # Note: This cannot be calculated until this is added to an Axes self._patch_transform = transforms.IdentityTransform() @@ -1472,8 +1568,9 @@ def _recompute_transform(self): width = self.convert_xunits(self._width) height = self.convert_yunits(self._height) self._patch_transform = transforms.Affine2D() \ - .scale(width * 0.5, height * 0.5) \ + .scale(width * 0.5, height * 0.5 * self._aspect_ratio_correction) \ .rotate_deg(self.angle) \ + .scale(1, 1 / self._aspect_ratio_correction) \ .translate(*center) def get_path(self): @@ -1554,16 +1651,214 @@ def get_angle(self): angle = property(get_angle, set_angle) + def get_corners(self): + """ + Return the corners of the ellipse bounding box. -class Circle(Ellipse): - """A circle patch.""" + The bounding box orientation is moving anti-clockwise from the + lower left corner defined before rotation. + """ + return self.get_patch_transform().transform( + [(-1, -1), (1, -1), (1, 1), (-1, 1)]) + + +class Annulus(Patch): + """ + An elliptical annulus. + """ + + @_docstring.dedent_interpd + def __init__(self, xy, r, width, angle=0.0, **kwargs): + """ + Parameters + ---------- + xy : (float, float) + xy coordinates of annulus centre. + r : float or (float, float) + The radius, or semi-axes: + + - If float: radius of the outer circle. + - If two floats: semi-major and -minor axes of outer ellipse. + width : float + Width (thickness) of the annular ring. The width is measured inward + from the outer ellipse so that for the inner ellipse the semi-axes + are given by ``r - width``. *width* must be less than or equal to + the semi-minor axis. + angle : float, default: 0 + Rotation angle in degrees (anti-clockwise from the positive + x-axis). Ignored for circular annuli (i.e., if *r* is a scalar). + **kwargs + Keyword arguments control the `Patch` properties: + + %(Patch:kwdoc)s + """ + super().__init__(**kwargs) + + self.set_radii(r) + self.center = xy + self.width = width + self.angle = angle + self._path = None + + def __str__(self): + if self.a == self.b: + r = self.a + else: + r = (self.a, self.b) + + return "Annulus(xy=(%s, %s), r=%s, width=%s, angle=%s)" % \ + (*self.center, r, self.width, self.angle) + + def set_center(self, xy): + """ + Set the center of the annulus. + + Parameters + ---------- + xy : (float, float) + """ + self._center = xy + self._path = None + self.stale = True + + def get_center(self): + """Return the center of the annulus.""" + return self._center + + center = property(get_center, set_center) + + def set_width(self, width): + """ + Set the width (thickness) of the annulus ring. + + The width is measured inwards from the outer ellipse. + + Parameters + ---------- + width : float + """ + if min(self.a, self.b) <= width: + raise ValueError( + 'Width of annulus must be less than or equal semi-minor axis') + + self._width = width + self._path = None + self.stale = True + + def get_width(self): + """Return the width (thickness) of the annulus ring.""" + return self._width + + width = property(get_width, set_width) + + def set_angle(self, angle): + """ + Set the tilt angle of the annulus. + + Parameters + ---------- + angle : float + """ + self._angle = angle + self._path = None + self.stale = True + + def get_angle(self): + """Return the angle of the annulus.""" + return self._angle + + angle = property(get_angle, set_angle) + + def set_semimajor(self, a): + """ + Set the semi-major axis *a* of the annulus. + + Parameters + ---------- + a : float + """ + self.a = float(a) + self._path = None + self.stale = True + + def set_semiminor(self, b): + """ + Set the semi-minor axis *b* of the annulus. + + Parameters + ---------- + b : float + """ + self.b = float(b) + self._path = None + self.stale = True + + def set_radii(self, r): + """ + Set the semi-major (*a*) and semi-minor radii (*b*) of the annulus. + + Parameters + ---------- + r : float or (float, float) + The radius, or semi-axes: + + - If float: radius of the outer circle. + - If two floats: semi-major and -minor axes of outer ellipse. + """ + if np.shape(r) == (2,): + self.a, self.b = r + elif np.shape(r) == (): + self.a = self.b = float(r) + else: + raise ValueError("Parameter 'r' must be one or two floats.") + + self._path = None + self.stale = True + + def get_radii(self): + """Return the semi-major and semi-minor radii of the annulus.""" + return self.a, self.b + + radii = property(get_radii, set_radii) + def _transform_verts(self, verts, a, b): + return transforms.Affine2D() \ + .scale(*self._convert_xy_units((a, b))) \ + .rotate_deg(self.angle) \ + .translate(*self._convert_xy_units(self.center)) \ + .transform(verts) + + def _recompute_path(self): + # circular arc + arc = Path.arc(0, 360) + + # annulus needs to draw an outer ring + # followed by a reversed and scaled inner ring + a, b, w = self.a, self.b, self.width + v1 = self._transform_verts(arc.vertices, a, b) + v2 = self._transform_verts(arc.vertices[::-1], a - w, b - w) + v = np.vstack([v1, v2, v1[0, :], (0, 0)]) + c = np.hstack([arc.codes, Path.MOVETO, + arc.codes[1:], Path.MOVETO, + Path.CLOSEPOLY]) + self._path = Path(v, c) + + def get_path(self): + if self._path is None: + self._recompute_path() + return self._path + + +class Circle(Ellipse): + """ + A circle patch. + """ def __str__(self): pars = self.center[0], self.center[1], self.radius fmt = "Circle(xy=(%g, %g), radius=%g)" return fmt % pars - @docstring.dedent_interpd + @_docstring.dedent_interpd def __init__(self, xy, radius=5, **kwargs): """ Create a true circle at center *xy* = (*x*, *y*) with given *radius*. @@ -1573,7 +1868,7 @@ def __init__(self, xy, radius=5, **kwargs): Valid keyword arguments are: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__(xy, radius * 2, radius * 2, **kwargs) self.radius = radius @@ -1600,14 +1895,9 @@ class Arc(Ellipse): """ An elliptical arc, i.e. a segment of an ellipse. - Due to internal optimizations, there are certain restrictions on using Arc: - - - The arc cannot be filled. - - - The arc must be used in an `~.axes.Axes` instance. It can not be added - directly to a `.Figure` because it is optimized to only render the - segments that are inside the axes bounding box with high resolution. + Due to internal optimizations, the arc cannot be filled. """ + def __str__(self): pars = (self.center[0], self.center[1], self.width, self.height, self.angle, self.theta1, self.theta2) @@ -1615,7 +1905,8 @@ def __str__(self): "height=%g, angle=%g, theta1=%g, theta2=%g)") return fmt % pars - @docstring.dedent_interpd + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="angle") def __init__(self, xy, width, height, angle=0.0, theta1=0.0, theta2=360.0, **kwargs): """ @@ -1647,19 +1938,21 @@ def __init__(self, xy, width, height, angle=0.0, ---------------- **kwargs : `.Patch` properties Most `.Patch` properties are supported as keyword arguments, - with the exception of *fill* and *facecolor* because filling is - not supported. + except *fill* and *facecolor* because filling is not supported. - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ fill = kwargs.setdefault('fill', False) if fill: raise ValueError("Arc objects can not be filled") - super().__init__(xy, width, height, angle, **kwargs) + super().__init__(xy, width, height, angle=angle, **kwargs) self.theta1 = theta1 self.theta2 = theta2 + (self._theta1, self._theta2, self._stretched_width, + self._stretched_height) = self._theta_stretch() + self._path = Path.arc(self._theta1, self._theta2) @artist.allow_rasterization def draw(self, renderer): @@ -1689,12 +1982,11 @@ def draw(self, renderer): with each visible arc using a fixed number of spline segments (8). The algorithm proceeds as follows: - 1. The points where the ellipse intersects the axes bounding - box are located. (This is done be performing an inverse - transformation on the axes bbox such that it is relative - to the unit circle -- this makes the intersection - calculation much easier than doing rotated ellipse - intersection directly). + 1. The points where the ellipse intersects the axes (or figure) + bounding box are located. (This is done by performing an inverse + transformation on the bbox such that it is relative to the unit + circle -- this makes the intersection calculation much easier than + doing rotated ellipse intersection directly.) This uses the "line intersecting a circle" algorithm from: @@ -1708,43 +2000,12 @@ def draw(self, renderer): pairs of vertices are drawn using the Bezier arc approximation technique implemented in `.Path.arc`. """ - if not hasattr(self, 'axes'): - raise RuntimeError('Arcs can only be used in Axes instances') if not self.get_visible(): return self._recompute_transform() - width = self.convert_xunits(self.width) - height = self.convert_yunits(self.height) - - # If the width and height of ellipse are not equal, take into account - # stretching when calculating angles to draw between - def theta_stretch(theta, scale): - theta = np.deg2rad(theta) - x = np.cos(theta) - y = np.sin(theta) - stheta = np.rad2deg(np.arctan2(scale * y, x)) - # arctan2 has the range [-pi, pi], we expect [0, 2*pi] - return (stheta + 360) % 360 - - theta1 = self.theta1 - theta2 = self.theta2 - - if ( - # if we need to stretch the angles because we are distorted - width != height - # and we are not doing a full circle. - # - # 0 and 360 do not exactly round-trip through the angle - # stretching (due to both float precision limitations and - # the difference between the range of arctan2 [-pi, pi] and - # this method [0, 360]) so avoid doing it if we don't have to. - and not (theta1 != theta2 and theta1 % 360 == theta2 % 360) - ): - theta1 = theta_stretch(self.theta1, width / height) - theta2 = theta_stretch(self.theta2, width / height) - + self._update_path() # Get width and height in pixels we need to use # `self.get_data_transform` rather than `self.get_transform` # because we want the transform from dataspace to the @@ -1753,12 +2014,13 @@ def theta_stretch(theta, scale): # `self.get_transform()` goes from an idealized unit-radius # space to screen space). data_to_screen_trans = self.get_data_transform() - pwidth, pheight = (data_to_screen_trans.transform((width, height)) - - data_to_screen_trans.transform((0, 0))) + pwidth, pheight = ( + data_to_screen_trans.transform((self._stretched_width, + self._stretched_height)) - + data_to_screen_trans.transform((0, 0))) inv_error = (1.0 / 1.89818e-6) * 0.5 if pwidth < inv_error and pheight < inv_error: - self._path = Path.arc(theta1, theta2) return Patch.draw(self, renderer) def line_circle_intersect(x0, y0, x1, y1): @@ -1796,10 +2058,12 @@ def segment_circle_intersect(x0, y0, x1, y1): & (y0e - epsilon < ys) & (ys < y1e + epsilon) ] - # Transforms the axes box_path so that it is relative to the unit - # circle in the same way that it is relative to the desired ellipse. - box_path_transform = (transforms.BboxTransformTo(self.axes.bbox) - + self.get_transform().inverted()) + # Transform the axes (or figure) box_path so that it is relative to + # the unit circle in the same way that it is relative to the desired + # ellipse. + box_path_transform = ( + transforms.BboxTransformTo((self.axes or self.figure).bbox) + - self.get_transform()) box_path = Path.unit_rectangle().transformed(box_path_transform) thetas = set() @@ -1810,10 +2074,11 @@ def segment_circle_intersect(x0, y0, x1, y1): # arctan2 return [-pi, pi), the rest of our angles are in # [0, 360], adjust as needed. theta = (np.rad2deg(np.arctan2(y, x)) + 360) % 360 - thetas.update(theta[(theta1 < theta) & (theta < theta2)]) - thetas = sorted(thetas) + [theta2] - last_theta = theta1 - theta1_rad = np.deg2rad(theta1) + thetas.update( + theta[(self._theta1 < theta) & (theta < self._theta2)]) + thetas = sorted(thetas) + [self._theta2] + last_theta = self._theta1 + theta1_rad = np.deg2rad(self._theta1) inside = box_path.contains_point( (np.cos(theta1_rad), np.sin(theta1_rad)) ) @@ -1832,6 +2097,46 @@ def segment_circle_intersect(x0, y0, x1, y1): # restore original path self._path = path_original + def _update_path(self): + # Compute new values and update and set new _path if any value changed + stretched = self._theta_stretch() + if any(a != b for a, b in zip( + stretched, (self._theta1, self._theta2, self._stretched_width, + self._stretched_height))): + (self._theta1, self._theta2, self._stretched_width, + self._stretched_height) = stretched + self._path = Path.arc(self._theta1, self._theta2) + + def _theta_stretch(self): + # If the width and height of ellipse are not equal, take into account + # stretching when calculating angles to draw between + def theta_stretch(theta, scale): + theta = np.deg2rad(theta) + x = np.cos(theta) + y = np.sin(theta) + stheta = np.rad2deg(np.arctan2(scale * y, x)) + # arctan2 has the range [-pi, pi], we expect [0, 2*pi] + return (stheta + 360) % 360 + + width = self.convert_xunits(self.width) + height = self.convert_yunits(self.height) + if ( + # if we need to stretch the angles because we are distorted + width != height + # and we are not doing a full circle. + # + # 0 and 360 do not exactly round-trip through the angle + # stretching (due to both float precision limitations and + # the difference between the range of arctan2 [-pi, pi] and + # this method [0, 360]) so avoid doing it if we don't have to. + and not (self.theta1 != self.theta2 and + self.theta1 % 360 == self.theta2 % 360) + ): + theta1 = theta_stretch(self.theta1, width / height) + theta2 = theta_stretch(self.theta2, width / height) + return theta1, theta2, width, height + return self.theta1, self.theta2, width, height + def bbox_artist(artist, renderer, props=None, fill=True): """ @@ -1862,50 +2167,56 @@ def draw_bbox(bbox, renderer, color='k', trans=None): box returned by an artist's `.Artist.get_window_extent` to test whether the artist is returning the correct bbox. """ - r = Rectangle(xy=(bbox.x0, bbox.y0), width=bbox.width, height=bbox.height, + r = Rectangle(xy=bbox.p0, width=bbox.width, height=bbox.height, edgecolor=color, fill=False, clip_on=False) if trans is not None: r.set_transform(trans) r.draw(renderer) -def _simpleprint_styles(_styles): - """ - A helper function for the _Style class. Given the dictionary of - {stylename: styleclass}, return a string rep of the list of keys. - Used to update the documentation. - """ - return "[{}]".format("|".join(map(" '{}' ".format, sorted(_styles)))) - - class _Style: """ A base class for the Styles. It is meant to be a container class, where actual styles are declared as subclass of it, and it provides some helper functions. """ - def __new__(cls, stylename, **kw): - """Return the instance of the subclass with the given style name.""" + def __init_subclass__(cls): + # Automatically perform docstring interpolation on the subclasses: + # This allows listing the supported styles via + # - %(BoxStyle:table)s + # - %(ConnectionStyle:table)s + # - %(ArrowStyle:table)s + # and additionally adding .. ACCEPTS: blocks via + # - %(BoxStyle:table_and_accepts)s + # - %(ConnectionStyle:table_and_accepts)s + # - %(ArrowStyle:table_and_accepts)s + _docstring.interpd.update({ + f"{cls.__name__}:table": cls.pprint_styles(), + f"{cls.__name__}:table_and_accepts": ( + cls.pprint_styles() + + "\n\n .. ACCEPTS: [" + + "|".join(map(" '{}' ".format, cls._style_list)) + + "]") + }) + + def __new__(cls, stylename, **kwargs): + """Return the instance of the subclass with the given style name.""" # The "class" should have the _style_list attribute, which is a mapping # of style names to style classes. - _list = stylename.replace(" ", "").split(",") _name = _list[0].lower() try: _cls = cls._style_list[_name] except KeyError as err: - raise ValueError("Unknown style : %s" % stylename) from err - + raise ValueError(f"Unknown style: {stylename!r}") from err try: _args_pair = [cs.split("=") for cs in _list[1:]] _args = {k: float(v) for k, v in _args_pair} except ValueError as err: - raise ValueError("Incorrect style argument : %s" % - stylename) from err - _args.update(kw) - - return _cls(**_args) + raise ValueError( + f"Incorrect style argument: {stylename!r}") from err + return _cls(**{**_args, **kwargs}) @classmethod def get_styles(cls): @@ -1921,7 +2232,7 @@ def pprint_styles(cls): f'``{name}``', # [1:-1] drops the surrounding parentheses. str(inspect.signature(cls))[1:-1] or 'None') - for name, cls in sorted(cls._style_list.items())]] + for name, cls in cls._style_list.items()]] # Convert to rst table. col_len = [max(len(cell) for cell in column) for column in zip(*table)] table_formatstr = ' '.join('=' * cl for cl in col_len) @@ -1933,9 +2244,8 @@ def pprint_styles(cls): *[' '.join(cell.ljust(cl) for cell, cl in zip(row, col_len)) for row in table[1:]], table_formatstr, - '', ]) - return textwrap.indent(rst_table, prefix=' ' * 2) + return textwrap.indent(rst_table, prefix=' ' * 4) @classmethod def register(cls, name, style): @@ -1954,6 +2264,7 @@ def _register_style(style_list, cls=None, *, name=None): return cls +@_docstring.dedent_interpd class BoxStyle(_Style): """ `BoxStyle` is a container class which defines several @@ -1973,96 +2284,20 @@ class BoxStyle(_Style): The following boxstyle classes are defined. - %(AvailableBoxstyles)s + %(BoxStyle:table)s - An instance of any boxstyle class is an callable object, - whose call signature is:: + An instance of a boxstyle class is a callable object, with the signature :: - __call__(self, x0, y0, width, height, mutation_size) + __call__(self, x0, y0, width, height, mutation_size) -> Path - and returns a `.Path` instance. *x0*, *y0*, *width* and - *height* specify the location and size of the box to be - drawn. *mutation_scale* determines the overall size of the - mutation (by which I mean the transformation of the rectangle to - the fancy box). + *x0*, *y0*, *width* and *height* specify the location and size of the box + to be drawn; *mutation_size* scales the outline properties such as padding. """ _style_list = {} - @_api.deprecated("3.4") - class _Base: - """ - Abstract base class for styling of `.FancyBboxPatch`. - - This class is not an artist itself. The `__call__` method returns the - `~matplotlib.path.Path` for outlining the fancy box. The actual drawing - is handled in `.FancyBboxPatch`. - - Subclasses may only use parameters with default values in their - ``__init__`` method because they must be able to be initialized - without arguments. - - Subclasses must implement the `__call__` method. It receives the - enclosing rectangle *x0, y0, width, height* as well as the - *mutation_size*, which scales the outline properties such as padding. - It returns the outline of the fancy box as `.path.Path`. - """ - - @_api.deprecated("3.4") - def transmute(self, x0, y0, width, height, mutation_size): - """Return the `~.path.Path` outlining the given rectangle.""" - return self(self, x0, y0, width, height, mutation_size, 1) - - # This can go away once the deprecation period elapses, leaving _Base - # as a fully abstract base class just providing docstrings, no logic. - def __init_subclass__(cls): - transmute = _api.deprecate_method_override( - __class__.transmute, cls, since="3.4") - if transmute: - cls.__call__ = transmute - return - - __call__ = cls.__call__ - - @_api.delete_parameter("3.4", "mutation_aspect") - def call_wrapper( - self, x0, y0, width, height, mutation_size, - mutation_aspect=_api.deprecation._deprecated_parameter): - if mutation_aspect is _api.deprecation._deprecated_parameter: - # Don't trigger deprecation warning internally. - return __call__(self, x0, y0, width, height, mutation_size) - else: - # Squeeze the given height by the aspect_ratio. - y0, height = y0 / mutation_aspect, height / mutation_aspect - path = self(x0, y0, width, height, mutation_size, - mutation_aspect) - vertices, codes = path.vertices, path.codes - # Restore the height. - vertices[:, 1] = vertices[:, 1] * mutation_aspect - return Path(vertices, codes) - - cls.__call__ = call_wrapper - - def __call__(self, x0, y0, width, height, mutation_size): - """ - Given the location and size of the box, return the path of - the box around it. - - Parameters - ---------- - x0, y0, width, height : float - Location and size of the box. - mutation_size : float - A reference scale for the mutation. - - Returns - ------- - `~matplotlib.path.Path` - """ - raise NotImplementedError('Derived must override') - @_register_style(_style_list) - class Square(_Base): + class Square: """A square box.""" def __init__(self, pad=0.3): @@ -2081,11 +2316,11 @@ def __call__(self, x0, y0, width, height, mutation_size): # boundary of the padded box x0, y0 = x0 - pad, y0 - pad x1, y1 = x0 + width, y0 + height - return Path([(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)], - closed=True) + return Path._create_closed( + [(x0, y0), (x1, y0), (x1, y1), (x0, y1)]) @_register_style(_style_list) - class Circle(_Base): + class Circle: """A circular box.""" def __init__(self, pad=0.3): @@ -2103,11 +2338,39 @@ def __call__(self, x0, y0, width, height, mutation_size): # boundary of the padded box x0, y0 = x0 - pad, y0 - pad return Path.circle((x0 + width / 2, y0 + height / 2), - max(width, height) / 2) + max(width, height) / 2) @_register_style(_style_list) - class LArrow(_Base): - """A box in the shape of a left-pointing arrow.""" + class Ellipse: + """ + An elliptical box. + + .. versionadded:: 3.7 + """ + + def __init__(self, pad=0.3): + """ + Parameters + ---------- + pad : float, default: 0.3 + The amount of padding around the original box. + """ + self.pad = pad + + def __call__(self, x0, y0, width, height, mutation_size): + pad = mutation_size * self.pad + width, height = width + 2 * pad, height + 2 * pad + # boundary of the padded box + x0, y0 = x0 - pad, y0 - pad + a = width / math.sqrt(2) + b = height / math.sqrt(2) + trans = Affine2D().scale(a, b).translate(x0 + width / 2, + y0 + height / 2) + return trans.transform_path(Path.unit_circle()) + + @_register_style(_style_list) + class LArrow: + """A box in the shape of a left-pointing arrow.""" def __init__(self, pad=0.3): """ @@ -2131,11 +2394,11 @@ def __call__(self, x0, y0, width, height, mutation_size): dxx = dx / 2 x0 = x0 + pad / 1.4 # adjust by ~sqrt(2) - return Path([(x0 + dxx, y0), (x1, y0), (x1, y1), (x0 + dxx, y1), - (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx), - (x0 + dxx, y0 - dxx), # arrow - (x0 + dxx, y0), (x0 + dxx, y0)], - closed=True) + return Path._create_closed( + [(x0 + dxx, y0), (x1, y0), (x1, y1), (x0 + dxx, y1), + (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx), + (x0 + dxx, y0 - dxx), # arrow + (x0 + dxx, y0)]) @_register_style(_style_list) class RArrow(LArrow): @@ -2148,7 +2411,7 @@ def __call__(self, x0, y0, width, height, mutation_size): return p @_register_style(_style_list) - class DArrow(_Base): + class DArrow: """A box in the shape of a two-way arrow.""" # Modified from LArrow to add a right arrow to the bbox. @@ -2175,17 +2438,17 @@ def __call__(self, x0, y0, width, height, mutation_size): dxx = dx / 2 x0 = x0 + pad / 1.4 # adjust by ~sqrt(2) - return Path([(x0 + dxx, y0), (x1, y0), # bot-segment - (x1, y0 - dxx), (x1 + dx + dxx, y0 + dx), - (x1, y1 + dxx), # right-arrow - (x1, y1), (x0 + dxx, y1), # top-segment - (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx), - (x0 + dxx, y0 - dxx), # left-arrow - (x0 + dxx, y0), (x0 + dxx, y0)], # close-poly - closed=True) + return Path._create_closed([ + (x0 + dxx, y0), (x1, y0), # bot-segment + (x1, y0 - dxx), (x1 + dx + dxx, y0 + dx), + (x1, y1 + dxx), # right-arrow + (x1, y1), (x0 + dxx, y1), # top-segment + (x0 + dxx, y1 + dxx), (x0 - dx, y0 + dx), + (x0 + dxx, y0 - dxx), # left-arrow + (x0 + dxx, y0)]) @_register_style(_style_list) - class Round(_Base): + class Round: """A box with round corners.""" def __init__(self, pad=0.3, rounding_size=None): @@ -2240,12 +2503,10 @@ def __call__(self, x0, y0, width, height, mutation_size): Path.CURVE3, Path.CURVE3, Path.CLOSEPOLY] - path = Path(cp, com) - - return path + return Path(cp, com) @_register_style(_style_list) - class Round4(_Base): + class Round4: """A box with rounded edges.""" def __init__(self, pad=0.3, rounding_size=None): @@ -2291,12 +2552,10 @@ def __call__(self, x0, y0, width, height, mutation_size): Path.CURVE4, Path.CURVE4, Path.CURVE4, Path.CLOSEPOLY] - path = Path(cp, com) - - return path + return Path(cp, com) @_register_style(_style_list) - class Sawtooth(_Base): + class Sawtooth: """A box with a sawtooth outline.""" def __init__(self, pad=0.3, tooth_size=None): @@ -2322,74 +2581,39 @@ def _get_sawtooth_vertices(self, x0, y0, width, height, mutation_size): else: tooth_size = self.tooth_size * mutation_size - tooth_size2 = tooth_size / 2 + hsz = tooth_size / 2 width = width + 2 * pad - tooth_size height = height + 2 * pad - tooth_size # the sizes of the vertical and horizontal sawtooth are # separately adjusted to fit the given box size. - dsx_n = int(round((width - tooth_size) / (tooth_size * 2))) * 2 + dsx_n = round((width - tooth_size) / (tooth_size * 2)) * 2 dsx = (width - tooth_size) / dsx_n - dsy_n = int(round((height - tooth_size) / (tooth_size * 2))) * 2 + dsy_n = round((height - tooth_size) / (tooth_size * 2)) * 2 dsy = (height - tooth_size) / dsy_n - x0, y0 = x0 - pad + tooth_size2, y0 - pad + tooth_size2 + x0, y0 = x0 - pad + hsz, y0 - pad + hsz x1, y1 = x0 + width, y0 + height - bottom_saw_x = [ - x0, - *(x0 + tooth_size2 + dsx * .5 * np.arange(dsx_n * 2)), - x1 - tooth_size2, - ] - bottom_saw_y = [ - y0, - *([y0 - tooth_size2, y0, y0 + tooth_size2, y0] * dsx_n), - y0 - tooth_size2, - ] - right_saw_x = [ - x1, - *([x1 + tooth_size2, x1, x1 - tooth_size2, x1] * dsx_n), - x1 + tooth_size2, - ] - right_saw_y = [ - y0, - *(y0 + tooth_size2 + dsy * .5 * np.arange(dsy_n * 2)), - y1 - tooth_size2, + xs = [ + x0, *np.linspace(x0 + hsz, x1 - hsz, 2 * dsx_n + 1), # bottom + *([x1, x1 + hsz, x1, x1 - hsz] * dsy_n)[:2*dsy_n+2], # right + x1, *np.linspace(x1 - hsz, x0 + hsz, 2 * dsx_n + 1), # top + *([x0, x0 - hsz, x0, x0 + hsz] * dsy_n)[:2*dsy_n+2], # left ] - top_saw_x = [ - x1, - *(x1 - tooth_size2 - dsx * .5 * np.arange(dsx_n * 2)), - x0 + tooth_size2, - ] - top_saw_y = [ - y1, - *([y1 + tooth_size2, y1, y1 - tooth_size2, y1] * dsx_n), - y1 + tooth_size2, - ] - left_saw_x = [ - x0, - *([x0 - tooth_size2, x0, x0 + tooth_size2, x0] * dsy_n), - x0 - tooth_size2, - ] - left_saw_y = [ - y1, - *(y1 - tooth_size2 - dsy * .5 * np.arange(dsy_n * 2)), - y0 + tooth_size2, + ys = [ + *([y0, y0 - hsz, y0, y0 + hsz] * dsx_n)[:2*dsx_n+2], # bottom + y0, *np.linspace(y0 + hsz, y1 - hsz, 2 * dsy_n + 1), # right + *([y1, y1 + hsz, y1, y1 - hsz] * dsx_n)[:2*dsx_n+2], # top + y1, *np.linspace(y1 - hsz, y0 + hsz, 2 * dsy_n + 1), # left ] - saw_vertices = [*zip(bottom_saw_x, bottom_saw_y), - *zip(right_saw_x, right_saw_y), - *zip(top_saw_x, top_saw_y), - *zip(left_saw_x, left_saw_y), - (bottom_saw_x[0], bottom_saw_y[0])] - - return saw_vertices + return [*zip(xs, ys), (xs[0], ys[0])] def __call__(self, x0, y0, width, height, mutation_size): saw_vertices = self._get_sawtooth_vertices(x0, y0, width, height, mutation_size) - path = Path(saw_vertices, closed=True) - return path + return Path(saw_vertices, closed=True) @_register_style(_style_list) class Roundtooth(Sawtooth): @@ -2407,6 +2631,7 @@ def __call__(self, x0, y0, width, height, mutation_size): return Path(saw_vertices, codes) +@_docstring.dedent_interpd class ConnectionStyle(_Style): """ `ConnectionStyle` is a container class which defines @@ -2427,9 +2652,9 @@ class ConnectionStyle(_Style): The following classes are defined - %(AvailableConnectorstyles)s + %(ConnectionStyle:table)s - An instance of any connection style class is an callable object, + An instance of any connection style class is a callable object, whose call signature is:: __call__(self, posA, posB, @@ -2459,59 +2684,35 @@ class _Base: helper methods. """ + @_api.deprecated("3.7") class SimpleEvent: def __init__(self, xy): self.x, self.y = xy - def _clip(self, path, patchA, patchB): + def _in_patch(self, patch): """ - Clip the path to the boundary of the patchA and patchB. - The starting point of the path needed to be inside of the - patchA and the end point inside the patch B. The *contains* - methods of each patch object is utilized to test if the point - is inside the path. + Return a predicate function testing whether a point *xy* is + contained in *patch*. """ + return lambda xy: patch.contains( + SimpleNamespace(x=xy[0], y=xy[1]))[0] - if patchA: - def insideA(xy_display): - xy_event = ConnectionStyle._Base.SimpleEvent(xy_display) - return patchA.contains(xy_event)[0] - - try: - left, right = split_path_inout(path, insideA) - except ValueError: - right = path - - path = right - - if patchB: - def insideB(xy_display): - xy_event = ConnectionStyle._Base.SimpleEvent(xy_display) - return patchB.contains(xy_event)[0] - - try: - left, right = split_path_inout(path, insideB) - except ValueError: - left = path - - path = left - - return path - - def _shrink(self, path, shrinkA, shrinkB): + def _clip(self, path, in_start, in_stop): """ - Shrink the path by fixed size (in points) with shrinkA and shrinkB. + Clip *path* at its start by the region where *in_start* returns + True, and at its stop by the region where *in_stop* returns True. + + The original path is assumed to start in the *in_start* region and + to stop in the *in_stop* region. """ - if shrinkA: - insideA = inside_circle(*path.vertices[0], shrinkA) + if in_start: try: - left, path = split_path_inout(path, insideA) + _, path = split_path_inout(path, in_start) except ValueError: pass - if shrinkB: - insideB = inside_circle(*path.vertices[-1], shrinkB) + if in_stop: try: - path, right = split_path_inout(path, insideB) + path, _ = split_path_inout(path, in_stop) except ValueError: pass return path @@ -2523,14 +2724,22 @@ def __call__(self, posA, posB, *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 + path = self._clip( + path, + self._in_patch(patchA) if patchA else None, + self._in_patch(patchB) if patchB else None, + ) + path = self._clip( + path, + inside_circle(*path.vertices[0], shrinkA) if shrinkA else None, + inside_circle(*path.vertices[-1], shrinkB) if shrinkB else None + ) + return path @_register_style(_style_list) class Arc3(_Base): """ - Creates a simple quadratic Bezier curve between two + Creates a simple quadratic Bézier curve between two points. The curve is created so that the middle control point (C1) is located at the same distance from the start (C0) and end points(C2) and the distance of the C1 to the line @@ -2539,8 +2748,10 @@ class Arc3(_Base): def __init__(self, rad=0.): """ - *rad* - curvature of the curve. + Parameters + ---------- + rad : float + Curvature of the curve. """ self.rad = rad @@ -2566,19 +2777,21 @@ def connect(self, posA, posB): @_register_style(_style_list) class Angle3(_Base): """ - Creates a simple quadratic Bezier curve between two - points. The middle control points is placed at the - intersecting point of two lines which cross the start and - end point, and have a slope of angleA and angleB, respectively. + Creates a simple quadratic Bézier curve between two points. The middle + control point is placed at the intersecting point of two lines which + cross the start and end point, and have a slope of *angleA* and + *angleB*, respectively. """ def __init__(self, angleA=90, angleB=0): """ - *angleA* - starting angle of the path + Parameters + ---------- + angleA : float + Starting angle of the path. - *angleB* - ending angle of the path + angleB : float + Ending angle of the path. """ self.angleA = angleA @@ -2604,23 +2817,25 @@ def connect(self, posA, posB): @_register_style(_style_list) class Angle(_Base): """ - Creates a piecewise continuous quadratic Bezier path between - two points. The path has a one passing-through point placed at - the intersecting point of two lines which cross the start - and end point, and have a slope of angleA and angleB, respectively. + Creates a piecewise continuous quadratic Bézier path between two + points. The path has a one passing-through point placed at the + intersecting point of two lines which cross the start and end point, + and have a slope of *angleA* and *angleB*, respectively. The connecting edges are rounded with *rad*. """ def __init__(self, angleA=90, angleB=0, rad=0.): """ - *angleA* - starting angle of the path + Parameters + ---------- + angleA : float + Starting angle of the path. - *angleB* - ending angle of the path + angleB : float + Ending angle of the path. - *rad* - rounding radius of the edge + rad : float + Rounding radius of the edge. """ self.angleA = angleA @@ -2666,29 +2881,31 @@ def connect(self, posA, posB): @_register_style(_style_list) class Arc(_Base): """ - Creates a piecewise continuous quadratic Bezier path between - two points. The path can have two passing-through points, a - point placed at the distance of armA and angle of angleA from + Creates a piecewise continuous quadratic Bézier path between two + points. The path can have two passing-through points, a + point placed at the distance of *armA* and angle of *angleA* from point A, another point with respect to point B. The edges are rounded with *rad*. """ def __init__(self, angleA=0, angleB=0, armA=None, armB=None, rad=0.): """ - *angleA* : - starting angle of the path + Parameters + ---------- + angleA : float + Starting angle of the path. - *angleB* : - ending angle of the path + angleB : float + Ending angle of the path. - *armA* : - length of the starting arm + armA : float or None + Length of the starting arm. - *armB* : - length of the ending arm + armB : float or None + Length of the ending arm. - *rad* : - rounding radius of the edges + rad : float + Rounding radius of the edges. """ self.angleA = angleA @@ -2760,10 +2977,10 @@ def connect(self, posA, posB): @_register_style(_style_list) class Bar(_Base): """ - A line with *angle* between A and B with *armA* and - *armB*. One of the arms is extended so that they are connected in - a right angle. The length of armA is determined by (*armA* - + *fraction* x AB distance). Same for armB. + A line with *angle* between A and B with *armA* and *armB*. One of the + arms is extended so that they are connected in a right angle. The + length of *armA* is determined by (*armA* + *fraction* x AB distance). + Same for *armB*. """ def __init__(self, armA=0., armB=0., fraction=0.3, angle=None): @@ -2771,18 +2988,17 @@ def __init__(self, armA=0., armB=0., fraction=0.3, angle=None): Parameters ---------- armA : float - minimum length of armA + Minimum length of armA. armB : float - minimum length of armB + Minimum length of armB. fraction : float - a fraction of the distance between two points that - will be added to armA and armB. + A fraction of the distance between two points that will be + added to armA and armB. angle : float or None - angle of the connecting line (if None, parallel - to A and B) + Angle of the connecting line (if None, parallel to A and B). """ self.armA = armA self.armB = armB @@ -2843,13 +3059,14 @@ def _point_along_a_line(x0, y0, x1, y1, d): return x2, y2 +@_docstring.dedent_interpd class ArrowStyle(_Style): """ `ArrowStyle` is a container class which defines several arrowstyle classes, which is used to create an arrow path along a given path. These are mainly used with `FancyArrowPatch`. - A arrowstyle object can be either created as:: + An arrowstyle object can be either created as:: ArrowStyle.Fancy(head_length=.4, head_width=.4, tail_width=.4) @@ -2863,7 +3080,7 @@ class ArrowStyle(_Style): The following classes are defined - %(AvailableArrowstyles)s + %(ArrowStyle:table)s An instance of any arrow style class is a callable object, whose call signature is:: @@ -2877,6 +3094,14 @@ class ArrowStyle(_Style): stroked. This is meant to be used to correct the location of the head so that it does not overshoot the destination point, but not all classes support it. + + Notes + ----- + *angleA* and *angleB* specify the orientation of the bracket, as either a + clockwise or counterclockwise angle depending on the arrow type. 0 degrees + means perpendicular to the line connecting the arrow's head and tail. + + .. plot:: gallery/text_labels_and_annotations/angles_on_bracket_arrows.py """ _style_list = {} @@ -2891,7 +3116,6 @@ class _Base: value indicating the path is open therefore is not fillable. This class is not an artist and actual drawing of the fancy arrow is done by the FancyArrowPatch class. - """ # The derived classes are required to be able to be initialized @@ -2901,10 +3125,11 @@ class is not an artist and actual drawing of the fancy arrow is @staticmethod def ensure_quadratic_bezier(path): """ - Some ArrowStyle class only works with a simple quadratic Bezier - curve (created with Arc3Connection or Angle3Connector). This static - method is to check if the provided path is a simple quadratic - Bezier curve and returns its control points if true. + Some ArrowStyle classes only works with a simple quadratic + Bézier curve (created with `.ConnectionStyle.Arc3` or + `.ConnectionStyle.Angle3`). This static method checks if the + provided path is a simple quadratic Bézier curve and returns its + control points if true. """ segments = list(path.iter_segments()) if (len(segments) != 2 or segments[0][1] != Path.MOVETO or @@ -2916,14 +3141,14 @@ def ensure_quadratic_bezier(path): def transmute(self, path, mutation_size, linewidth): """ The transmute method is the very core of the ArrowStyle class and - must be overridden in the subclasses. It receives the path object - along which the arrow will be drawn, and the mutation_size, with - which the arrow head etc. will be scaled. The linewidth may be - used to adjust the path so that it does not pass beyond the given - points. It returns a tuple of a Path instance and a boolean. The - boolean value indicate whether the path can be filled or not. The - return value can also be a list of paths and list of booleans of a - same length. + must be overridden in the subclasses. It receives the *path* + object along which the arrow will be drawn, and the + *mutation_size*, with which the arrow head etc. will be scaled. + The *linewidth* may be used to adjust the path so that it does not + pass beyond the given points. It returns a tuple of a `.Path` + instance and a boolean. The boolean value indicate whether the + path can be filled or not. The return value can also be a list of + paths and list of booleans of the same length. """ raise NotImplementedError('Derived must override') @@ -2943,11 +3168,9 @@ def __call__(self, path, mutation_size, linewidth, mutation_size, linewidth) if np.iterable(fillable): - path_list = [] - for p in path_mutated: - # Restore the height - path_list.append( - Path(p.vertices * [1, aspect_ratio], p.codes)) + # Restore the height + path_list = [Path(p.vertices * [1, aspect_ratio], p.codes) + for p in path_mutated] return path_list, fillable else: return path_mutated, fillable @@ -2957,24 +3180,75 @@ def __call__(self, path, mutation_size, linewidth, class _Curve(_Base): """ A simple arrow which will work with any path instance. The - returned path is simply concatenation of the original path + at - most two paths representing the arrow head at the begin point and the - at the end point. The arrow heads can be either open or closed. + returned path is the concatenation of the original path, and at + most two paths representing the arrow head or bracket at the start + point and at the end point. The arrow heads can be either open + or closed. """ - def __init__(self, beginarrow=None, endarrow=None, - fillbegin=False, fillend=False, - head_length=.2, head_width=.1): + arrow = "-" + fillbegin = fillend = False # Whether arrows are filled. + + def __init__(self, head_length=.4, head_width=.2, widthA=1., widthB=1., + lengthA=0.2, lengthB=0.2, angleA=0, angleB=0, scaleA=None, + scaleB=None): """ - The arrows are drawn if *beginarrow* and/or *endarrow* are - true. *head_length* and *head_width* determines the size - of the arrow relative to the *mutation scale*. The - arrowhead at the begin (or end) is closed if fillbegin (or - fillend) is True. + Parameters + ---------- + head_length : float, default: 0.4 + Length of the arrow head, relative to *mutation_size*. + head_width : float, default: 0.2 + Width of the arrow head, relative to *mutation_size*. + widthA, widthB : float, default: 1.0 + Width of the bracket. + lengthA, lengthB : float, default: 0.2 + Length of the bracket. + angleA, angleB : float, default: 0 + Orientation of the bracket, as a counterclockwise angle. + 0 degrees means perpendicular to the line. + scaleA, scaleB : float, default: *mutation_size* + The scale of the brackets. """ - self.beginarrow, self.endarrow = beginarrow, endarrow + self.head_length, self.head_width = head_length, head_width - self.fillbegin, self.fillend = fillbegin, fillend + self.widthA, self.widthB = widthA, widthB + self.lengthA, self.lengthB = lengthA, lengthB + self.angleA, self.angleB = angleA, angleB + self.scaleA, self.scaleB = scaleA, scaleB + + self._beginarrow_head = False + self._beginarrow_bracket = False + self._endarrow_head = False + self._endarrow_bracket = False + + if "-" not in self.arrow: + raise ValueError("arrow must have the '-' between " + "the two heads") + + beginarrow, endarrow = self.arrow.split("-", 1) + + if beginarrow == "<": + self._beginarrow_head = True + self._beginarrow_bracket = False + elif beginarrow == "<|": + self._beginarrow_head = True + self._beginarrow_bracket = False + self.fillbegin = True + elif beginarrow in ("]", "|"): + self._beginarrow_head = False + self._beginarrow_bracket = True + + if endarrow == ">": + self._endarrow_head = True + self._endarrow_bracket = False + elif endarrow == "|>": + self._endarrow_head = True + self._endarrow_bracket = False + self.fillend = True + elif endarrow in ("[", "|"): + self._endarrow_head = False + self._endarrow_bracket = True + super().__init__() def _get_arrow_wedge(self, x0, y0, x1, y1, @@ -3019,19 +3293,49 @@ def _get_arrow_wedge(self, x0, y0, x1, y1, return vertices_arrow, codes_arrow, ddx, ddy + def _get_bracket(self, x0, y0, + x1, y1, width, length, angle): + + cos_t, sin_t = get_cos_sin(x1, y1, x0, y0) + + # arrow from x0, y0 to x1, y1 + from matplotlib.bezier import get_normal_points + x1, y1, x2, y2 = get_normal_points(x0, y0, cos_t, sin_t, width) + + dx, dy = length * cos_t, length * sin_t + + vertices_arrow = [(x1 + dx, y1 + dy), + (x1, y1), + (x2, y2), + (x2 + dx, y2 + dy)] + codes_arrow = [Path.MOVETO, + Path.LINETO, + Path.LINETO, + Path.LINETO] + + if angle: + trans = transforms.Affine2D().rotate_deg_around(x0, y0, angle) + vertices_arrow = trans.transform(vertices_arrow) + + return vertices_arrow, codes_arrow + def transmute(self, path, mutation_size, linewidth): + # docstring inherited + if self._beginarrow_head or self._endarrow_head: + head_length = self.head_length * mutation_size + head_width = self.head_width * mutation_size + head_dist = np.hypot(head_length, head_width) + cos_t, sin_t = head_length / head_dist, head_width / head_dist - head_length = self.head_length * mutation_size - head_width = self.head_width * mutation_size - head_dist = np.hypot(head_length, head_width) - cos_t, sin_t = head_length / head_dist, head_width / head_dist + scaleA = mutation_size if self.scaleA is None else self.scaleA + scaleB = mutation_size if self.scaleB is None else self.scaleB # begin arrow x0, y0 = path.vertices[0] x1, y1 = path.vertices[1] # If there is no room for an arrow and a line, then skip the arrow - has_begin_arrow = self.beginarrow and (x0, y0) != (x1, y1) + has_begin_arrow = self._beginarrow_head and (x0, y0) != (x1, y1) verticesA, codesA, ddxA, ddyA = ( self._get_arrow_wedge(x1, y1, x0, y0, head_dist, cos_t, sin_t, linewidth) @@ -3044,7 +3348,7 @@ def transmute(self, path, mutation_size, linewidth): x3, y3 = path.vertices[-1] # If there is no room for an arrow and a line, then skip the arrow - has_end_arrow = self.endarrow and (x2, y2) != (x3, y3) + has_end_arrow = self._endarrow_head and (x2, y2) != (x3, y3) verticesB, codesB, ddxB, ddyB = ( self._get_arrow_wedge(x2, y2, x3, y3, head_dist, cos_t, sin_t, linewidth) @@ -3054,342 +3358,216 @@ def transmute(self, path, mutation_size, linewidth): # This simple code will not work if ddx, ddy is greater than the # separation between vertices. - _path = [Path(np.concatenate([[(x0 + ddxA, y0 + ddyA)], + paths = [Path(np.concatenate([[(x0 + ddxA, y0 + ddyA)], path.vertices[1:-1], [(x3 + ddxB, y3 + ddyB)]]), path.codes)] - _fillable = [False] + fills = [False] if has_begin_arrow: if self.fillbegin: - p = np.concatenate([verticesA, [verticesA[0], - verticesA[0]], ]) - c = np.concatenate([codesA, [Path.LINETO, Path.CLOSEPOLY]]) - _path.append(Path(p, c)) - _fillable.append(True) + paths.append( + Path([*verticesA, (0, 0)], [*codesA, Path.CLOSEPOLY])) + fills.append(True) else: - _path.append(Path(verticesA, codesA)) - _fillable.append(False) + paths.append(Path(verticesA, codesA)) + fills.append(False) + elif self._beginarrow_bracket: + x0, y0 = path.vertices[0] + x1, y1 = path.vertices[1] + verticesA, codesA = self._get_bracket(x0, y0, x1, y1, + self.widthA * scaleA, + self.lengthA * scaleA, + self.angleA) + + paths.append(Path(verticesA, codesA)) + fills.append(False) if has_end_arrow: if self.fillend: - _fillable.append(True) - p = np.concatenate([verticesB, [verticesB[0], - verticesB[0]], ]) - c = np.concatenate([codesB, [Path.LINETO, Path.CLOSEPOLY]]) - _path.append(Path(p, c)) + fills.append(True) + paths.append( + Path([*verticesB, (0, 0)], [*codesB, Path.CLOSEPOLY])) else: - _fillable.append(False) - _path.append(Path(verticesB, codesB)) + fills.append(False) + paths.append(Path(verticesB, codesB)) + elif self._endarrow_bracket: + x0, y0 = path.vertices[-1] + x1, y1 = path.vertices[-2] + verticesB, codesB = self._get_bracket(x0, y0, x1, y1, + self.widthB * scaleB, + self.lengthB * scaleB, + self.angleB) + + paths.append(Path(verticesB, codesB)) + fills.append(False) - return _path, _fillable + return paths, fills @_register_style(_style_list, name="-") class Curve(_Curve): """A simple curve without any arrow head.""" - def __init__(self): - super().__init__(beginarrow=False, endarrow=False) + def __init__(self): # hide head_length, head_width + # These attributes (whose values come from backcompat) only matter + # if someone modifies beginarrow/etc. on an ArrowStyle instance. + super().__init__(head_length=.2, head_width=.1) @_register_style(_style_list, name="<-") class CurveA(_Curve): - """An arrow with a head at its begin point.""" - - def __init__(self, head_length=.4, head_width=.2): - """ - Parameters - ---------- - head_length : float, default: 0.4 - Length of the arrow head. - - head_width : float, default: 0.2 - Width of the arrow head. - """ - super().__init__(beginarrow=True, endarrow=False, - head_length=head_length, head_width=head_width) + """An arrow with a head at its start point.""" + arrow = "<-" @_register_style(_style_list, name="->") class CurveB(_Curve): """An arrow with a head at its end point.""" - - def __init__(self, head_length=.4, head_width=.2): - """ - Parameters - ---------- - head_length : float, default: 0.4 - Length of the arrow head. - - head_width : float, default: 0.2 - Width of the arrow head. - """ - super().__init__(beginarrow=False, endarrow=True, - head_length=head_length, head_width=head_width) + arrow = "->" @_register_style(_style_list, name="<->") class CurveAB(_Curve): - """An arrow with heads both at the begin and the end point.""" - - def __init__(self, head_length=.4, head_width=.2): - """ - Parameters - ---------- - head_length : float, default: 0.4 - Length of the arrow head. - - head_width : float, default: 0.2 - Width of the arrow head. - """ - super().__init__(beginarrow=True, endarrow=True, - head_length=head_length, head_width=head_width) + """An arrow with heads both at the start and the end point.""" + arrow = "<->" @_register_style(_style_list, name="<|-") class CurveFilledA(_Curve): - """An arrow with filled triangle head at the begin.""" - - def __init__(self, head_length=.4, head_width=.2): - """ - Parameters - ---------- - head_length : float, default: 0.4 - Length of the arrow head. - - head_width : float, default: 0.2 - Width of the arrow head. - """ - super().__init__(beginarrow=True, endarrow=False, - fillbegin=True, fillend=False, - head_length=head_length, head_width=head_width) + """An arrow with filled triangle head at the start.""" + arrow = "<|-" @_register_style(_style_list, name="-|>") class CurveFilledB(_Curve): """An arrow with filled triangle head at the end.""" - - def __init__(self, head_length=.4, head_width=.2): - """ - Parameters - ---------- - head_length : float, default: 0.4 - Length of the arrow head. - - head_width : float, default: 0.2 - Width of the arrow head. - """ - super().__init__(beginarrow=False, endarrow=True, - fillbegin=False, fillend=True, - head_length=head_length, head_width=head_width) + arrow = "-|>" @_register_style(_style_list, name="<|-|>") class CurveFilledAB(_Curve): """An arrow with filled triangle heads at both ends.""" + arrow = "<|-|>" - def __init__(self, head_length=.4, head_width=.2): - """ - Parameters - ---------- - head_length : float, default: 0.4 - Length of the arrow head. - - head_width : float, default: 0.2 - Width of the arrow head. - """ - super().__init__(beginarrow=True, endarrow=True, - fillbegin=True, fillend=True, - head_length=head_length, head_width=head_width) - - class _Bracket(_Base): - - def __init__(self, bracketA=None, bracketB=None, - widthA=1., widthB=1., - lengthA=0.2, lengthB=0.2, - angleA=None, angleB=None, - scaleA=None, scaleB=None): - self.bracketA, self.bracketB = bracketA, bracketB - self.widthA, self.widthB = widthA, widthB - self.lengthA, self.lengthB = lengthA, lengthB - self.angleA, self.angleB = angleA, angleB - self.scaleA, self.scaleB = scaleA, scaleB - - def _get_bracket(self, x0, y0, - cos_t, sin_t, width, length, angle): - - # arrow from x0, y0 to x1, y1 - from matplotlib.bezier import get_normal_points - x1, y1, x2, y2 = get_normal_points(x0, y0, cos_t, sin_t, width) - - dx, dy = length * cos_t, length * sin_t - - vertices_arrow = [(x1 + dx, y1 + dy), - (x1, y1), - (x2, y2), - (x2 + dx, y2 + dy)] - codes_arrow = [Path.MOVETO, - Path.LINETO, - Path.LINETO, - Path.LINETO] - - if angle is not None: - trans = transforms.Affine2D().rotate_deg_around(x0, y0, angle) - vertices_arrow = trans.transform(vertices_arrow) - - return vertices_arrow, codes_arrow - - def transmute(self, path, mutation_size, linewidth): - - if self.scaleA is None: - scaleA = mutation_size - else: - scaleA = self.scaleA - - if self.scaleB is None: - scaleB = mutation_size - else: - scaleB = self.scaleB - - vertices_list, codes_list = [], [] - - if self.bracketA: - x0, y0 = path.vertices[0] - x1, y1 = path.vertices[1] - cos_t, sin_t = get_cos_sin(x1, y1, x0, y0) - verticesA, codesA = self._get_bracket(x0, y0, cos_t, sin_t, - self.widthA * scaleA, - self.lengthA * scaleA, - self.angleA) - vertices_list.append(verticesA) - codes_list.append(codesA) - - vertices_list.append(path.vertices) - codes_list.append(path.codes) - - if self.bracketB: - x0, y0 = path.vertices[-1] - x1, y1 = path.vertices[-2] - cos_t, sin_t = get_cos_sin(x1, y1, x0, y0) - verticesB, codesB = self._get_bracket(x0, y0, cos_t, sin_t, - self.widthB * scaleB, - self.lengthB * scaleB, - self.angleB) - vertices_list.append(verticesB) - codes_list.append(codesB) - - vertices = np.concatenate(vertices_list) - codes = np.concatenate(codes_list) - - p = Path(vertices, codes) - - return p, False - - @_register_style(_style_list, name="]-[") - class BracketAB(_Bracket): - """An arrow with outward square brackets at both ends.""" + @_register_style(_style_list, name="]-") + class BracketA(_Curve): + """An arrow with an outward square bracket at its start.""" + arrow = "]-" - def __init__(self, - widthA=1., lengthA=0.2, angleA=None, - widthB=1., lengthB=0.2, angleB=None): + def __init__(self, widthA=1., lengthA=0.2, angleA=0): """ Parameters ---------- widthA : float, default: 1.0 Width of the bracket. - lengthA : float, default: 0.2 Length of the bracket. + angleA : float, default: 0 degrees + Orientation of the bracket, as a counterclockwise angle. + 0 degrees means perpendicular to the line. + """ + super().__init__(widthA=widthA, lengthA=lengthA, angleA=angleA) - angleA : float, default: None - Angle, in degrees, between the bracket and the line. Zero is - perpendicular to the line, and positive measures - counterclockwise. + @_register_style(_style_list, name="-[") + class BracketB(_Curve): + """An arrow with an outward square bracket at its end.""" + arrow = "-[" + def __init__(self, widthB=1., lengthB=0.2, angleB=0): + """ + Parameters + ---------- widthB : float, default: 1.0 Width of the bracket. - lengthB : float, default: 0.2 Length of the bracket. - - angleB : float, default: None - Angle, in degrees, between the bracket and the line. Zero is - perpendicular to the line, and positive measures - counterclockwise. + angleB : float, default: 0 degrees + Orientation of the bracket, as a counterclockwise angle. + 0 degrees means perpendicular to the line. """ - super().__init__(True, True, - widthA=widthA, lengthA=lengthA, angleA=angleA, - widthB=widthB, lengthB=lengthB, angleB=angleB) + super().__init__(widthB=widthB, lengthB=lengthB, angleB=angleB) - @_register_style(_style_list, name="]-") - class BracketA(_Bracket): - """An arrow with an outward square bracket at its start.""" + @_register_style(_style_list, name="]-[") + class BracketAB(_Curve): + """An arrow with outward square brackets at both ends.""" + arrow = "]-[" - def __init__(self, widthA=1., lengthA=0.2, angleA=None): + def __init__(self, + widthA=1., lengthA=0.2, angleA=0, + widthB=1., lengthB=0.2, angleB=0): """ Parameters ---------- - widthA : float, default: 1.0 + widthA, widthB : float, default: 1.0 Width of the bracket. - - lengthA : float, default: 0.2 + lengthA, lengthB : float, default: 0.2 Length of the bracket. - - angleA : float, default: None - Angle between the bracket and the line. + angleA, angleB : float, default: 0 degrees + Orientation of the bracket, as a counterclockwise angle. + 0 degrees means perpendicular to the line. """ - super().__init__(True, None, - widthA=widthA, lengthA=lengthA, angleA=angleA) + super().__init__(widthA=widthA, lengthA=lengthA, angleA=angleA, + widthB=widthB, lengthB=lengthB, angleB=angleB) - @_register_style(_style_list, name="-[") - class BracketB(_Bracket): - """An arrow with an outward square bracket at its end.""" + @_register_style(_style_list, name="|-|") + class BarAB(_Curve): + """An arrow with vertical bars ``|`` at both ends.""" + arrow = "|-|" - def __init__(self, widthB=1., lengthB=0.2, angleB=None): + def __init__(self, widthA=1., angleA=0, widthB=1., angleB=0): """ Parameters ---------- - widthB : float, default: 1.0 + widthA, widthB : float, default: 1.0 Width of the bracket. - - lengthB : float, default: 0.2 - Length of the bracket. - - angleB : float, default: None - Angle, in degrees, between the bracket and the line. Zero is - perpendicular to the line, and positive measures - counterclockwise. + angleA, angleB : float, default: 0 degrees + Orientation of the bracket, as a counterclockwise angle. + 0 degrees means perpendicular to the line. """ - super().__init__(None, True, - widthB=widthB, lengthB=lengthB, angleB=angleB) + super().__init__(widthA=widthA, lengthA=0, angleA=angleA, + widthB=widthB, lengthB=0, angleB=angleB) - @_register_style(_style_list, name="|-|") - class BarAB(_Bracket): - """An arrow with vertical bars ``|`` at both ends.""" + @_register_style(_style_list, name=']->') + class BracketCurve(_Curve): + """ + An arrow with an outward square bracket at its start and a head at + the end. + """ + arrow = "]->" - def __init__(self, - widthA=1., angleA=None, - widthB=1., angleB=None): + def __init__(self, widthA=1., lengthA=0.2, angleA=None): """ Parameters ---------- widthA : float, default: 1.0 Width of the bracket. + lengthA : float, default: 0.2 + Length of the bracket. + angleA : float, default: 0 degrees + Orientation of the bracket, as a counterclockwise angle. + 0 degrees means perpendicular to the line. + """ + super().__init__(widthA=widthA, lengthA=lengthA, angleA=angleA) - angleA : float, default: None - Angle, in degrees, between the bracket and the line. Zero is - perpendicular to the line, and positive measures - counterclockwise. + @_register_style(_style_list, name='<-[') + class CurveBracket(_Curve): + """ + An arrow with an outward square bracket at its end and a head at + the start. + """ + arrow = "<-[" + def __init__(self, widthB=1., lengthB=0.2, angleB=None): + """ + Parameters + ---------- widthB : float, default: 1.0 Width of the bracket. - - angleB : float, default: None - Angle, in degrees, between the bracket and the line. Zero is - perpendicular to the line, and positive measures - counterclockwise. + lengthB : float, default: 0.2 + Length of the bracket. + angleB : float, default: 0 degrees + Orientation of the bracket, as a counterclockwise angle. + 0 degrees means perpendicular to the line. """ - super().__init__(True, True, - widthA=widthA, lengthA=0, angleA=angleA, - widthB=widthB, lengthB=0, angleB=angleB) + super().__init__(widthB=widthB, lengthB=lengthB, angleB=angleB) @_register_style(_style_list) class Simple(_Base): - """A simple arrow. Only works with a quadratic Bezier curve.""" + """A simple arrow. Only works with a quadratic Bézier curve.""" def __init__(self, head_length=.5, head_width=.5, tail_width=.2): """ @@ -3409,7 +3587,7 @@ def __init__(self, head_length=.5, head_width=.5, tail_width=.2): super().__init__() def transmute(self, path, mutation_size, linewidth): - + # docstring inherited x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path) # divide the path into a head and a tail @@ -3419,8 +3597,7 @@ def transmute(self, path, mutation_size, linewidth): try: arrow_out, arrow_in = \ - split_bezier_intersecting_with_closedpath( - arrow_path, in_f, tolerance=0.01) + split_bezier_intersecting_with_closedpath(arrow_path, in_f) except NonIntersectingPathException: # if this happens, make a straight line of the head_length # long. @@ -3469,7 +3646,7 @@ def transmute(self, path, mutation_size, linewidth): @_register_style(_style_list) class Fancy(_Base): - """A fancy arrow. Only works with a quadratic Bezier curve.""" + """A fancy arrow. Only works with a quadratic Bézier curve.""" def __init__(self, head_length=.4, head_width=.4, tail_width=.4): """ @@ -3489,7 +3666,7 @@ def __init__(self, head_length=.4, head_width=.4, tail_width=.4): super().__init__() def transmute(self, path, mutation_size, linewidth): - + # docstring inherited x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path) # divide the path into a head and a tail @@ -3500,7 +3677,7 @@ def transmute(self, path, mutation_size, linewidth): in_f = inside_circle(x2, y2, head_length) try: path_out, path_in = split_bezier_intersecting_with_closedpath( - arrow_path, in_f, tolerance=0.01) + arrow_path, in_f) except NonIntersectingPathException: # if this happens, make a straight line of the head_length # long. @@ -3514,7 +3691,7 @@ def transmute(self, path, mutation_size, linewidth): # path for head in_f = inside_circle(x2, y2, head_length * .8) path_out, path_in = split_bezier_intersecting_with_closedpath( - arrow_path, in_f, tolerance=0.01) + arrow_path, in_f) path_tail = path_out # head @@ -3532,7 +3709,7 @@ def transmute(self, path, mutation_size, linewidth): # path for head in_f = inside_circle(x0, y0, tail_width * .3) path_in, path_out = split_bezier_intersecting_with_closedpath( - arrow_path, in_f, tolerance=0.01) + arrow_path, in_f) tail_start = path_in[-1] head_right, head_left = head_r, head_l @@ -3558,9 +3735,9 @@ def transmute(self, path, mutation_size, linewidth): @_register_style(_style_list) class Wedge(_Base): """ - Wedge(?) shape. Only works with a quadratic Bezier curve. The - begin point has a width of the tail_width and the end point has a - width of 0. At the middle, the width is shrink_factor*tail_width. + Wedge(?) shape. Only works with a quadratic Bézier curve. The + start point has a width of the *tail_width* and the end point has a + width of 0. At the middle, the width is *shrink_factor*x*tail_width*. """ def __init__(self, tail_width=.3, shrink_factor=0.5): @@ -3578,7 +3755,7 @@ def __init__(self, tail_width=.3, shrink_factor=0.5): super().__init__() def transmute(self, path, mutation_size, linewidth): - + # docstring inherited x0, y0, x1, y1, x2, y2 = self.ensure_quadratic_bezier(path) arrow_path = [(x0, y0), (x1, y1), (x2, y2)] @@ -3600,17 +3777,6 @@ def transmute(self, path, mutation_size, linewidth): return path, True -docstring.interpd.update( - AvailableBoxstyles=BoxStyle.pprint_styles(), - ListBoxstyles=_simpleprint_styles(BoxStyle._style_list), - AvailableArrowstyles=ArrowStyle.pprint_styles(), - AvailableConnectorstyles=ConnectionStyle.pprint_styles(), -) -docstring.dedent_interpd(BoxStyle) -docstring.dedent_interpd(ArrowStyle) -docstring.dedent_interpd(ConnectionStyle) - - class FancyBboxPatch(Patch): """ A fancy box around a rectangle with lower left at *xy* = (*x*, *y*) @@ -3627,12 +3793,9 @@ def __str__(self): s = self.__class__.__name__ + "((%g, %g), width=%g, height=%g)" return s % (self._x, self._y, self._width, self._height) - @docstring.dedent_interpd - @_api.delete_parameter("3.4", "bbox_transmuter", alternative="boxstyle") - def __init__(self, xy, width, height, - boxstyle="round", bbox_transmuter=None, - mutation_scale=1, mutation_aspect=1, - **kwargs): + @_docstring.dedent_interpd + def __init__(self, xy, width, height, boxstyle="round", *, + mutation_scale=1, mutation_aspect=1, **kwargs): """ Parameters ---------- @@ -3648,13 +3811,13 @@ def __init__(self, xy, width, height, boxstyle : str or `matplotlib.patches.BoxStyle` The style of the fancy box. This can either be a `.BoxStyle` instance or a string of the style name and optionally comma - seprarated attributes (e.g. "Round, pad=0.2"). This string is + separated attributes (e.g. "Round, pad=0.2"). This string is passed to `.BoxStyle` to construct a `.BoxStyle` object. See there for a full documentation. The following box styles are available: - %(AvailableBoxstyles)s + %(BoxStyle:table)s mutation_scale : float, default: 1 Scaling factor applied to the attributes of the box style @@ -3670,39 +3833,23 @@ def __init__(self, xy, width, height, ---------------- **kwargs : `.Patch` properties - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__(**kwargs) - - self._x = xy[0] - self._y = xy[1] + self._x, self._y = xy self._width = width self._height = height - - if boxstyle == "custom": - _api.warn_deprecated( - "3.4", message="Support for boxstyle='custom' is deprecated " - "since %(since)s and will be removed %(removal)s; directly " - "pass a boxstyle instance as the boxstyle parameter instead.") - if bbox_transmuter is None: - raise ValueError("bbox_transmuter argument is needed with " - "custom boxstyle") - self._bbox_transmuter = bbox_transmuter - else: - self.set_boxstyle(boxstyle) - + self.set_boxstyle(boxstyle) self._mutation_scale = mutation_scale self._mutation_aspect = mutation_aspect - self.stale = True - @docstring.dedent_interpd + @_docstring.dedent_interpd def set_boxstyle(self, boxstyle=None, **kwargs): """ - Set the box style. + Set the box style, possibly with further attributes. - Most box styles can be further configured using attributes. Attributes from the previous box style are not reused. Without argument (or with ``boxstyle=None``), the available box styles @@ -3711,17 +3858,14 @@ def set_boxstyle(self, boxstyle=None, **kwargs): Parameters ---------- boxstyle : str or `matplotlib.patches.BoxStyle` - The style of the fancy box. This can either be a `.BoxStyle` - instance or a string of the style name and optionally comma - seprarated attributes (e.g. "Round, pad=0.2"). This string is - passed to `.BoxStyle` to construct a `.BoxStyle` object. See - there for a full documentation. + The style of the box: either a `.BoxStyle` instance, or a string, + which is the style name and optionally comma separated attributes + (e.g. "Round,pad=0.2"). Such a string is used to construct a + `.BoxStyle` object, as documented in that class. The following box styles are available: - %(AvailableBoxstyles)s - - .. ACCEPTS: %(ListBoxstyles)s + %(BoxStyle:table_and_accepts)s **kwargs Additional attributes for the box style. See the table above for @@ -3731,19 +3875,20 @@ def set_boxstyle(self, boxstyle=None, **kwargs): -------- :: - set_boxstyle("round,pad=0.2") + set_boxstyle("Round,pad=0.2") set_boxstyle("round", pad=0.2) - """ if boxstyle is None: return BoxStyle.pprint_styles() - - if isinstance(boxstyle, BoxStyle._Base) or callable(boxstyle): - self._bbox_transmuter = boxstyle - else: - self._bbox_transmuter = BoxStyle(boxstyle, **kwargs) + self._bbox_transmuter = ( + BoxStyle(boxstyle, **kwargs) + if isinstance(boxstyle, str) else boxstyle) self.stale = True + def get_boxstyle(self): + """Return the boxstyle object.""" + return self._bbox_transmuter + def set_mutation_scale(self, scale): """ Set the mutation scale. @@ -3775,37 +3920,15 @@ def get_mutation_aspect(self): return (self._mutation_aspect if self._mutation_aspect is not None else 1) # backcompat. - def get_boxstyle(self): - """Return the boxstyle object.""" - return self._bbox_transmuter - def get_path(self): """Return the mutated path of the rectangle.""" boxstyle = self.get_boxstyle() - x = self._x - y = self._y - width = self._width - height = self._height - m_scale = self.get_mutation_scale() m_aspect = self.get_mutation_aspect() - # Squeeze the given height by the aspect_ratio. - y, height = y / m_aspect, height / m_aspect - # Call boxstyle with squeezed height. - try: - inspect.signature(boxstyle).bind(x, y, width, height, m_scale) - except TypeError: - # Don't apply aspect twice. - path = boxstyle(x, y, width, height, m_scale, 1) - _api.warn_deprecated( - "3.4", message="boxstyles must be callable without the " - "'mutation_aspect' parameter since %(since)s; support for the " - "old call signature will be removed %(removal)s.") - else: - path = boxstyle(x, y, width, height, m_scale) - vertices, codes = path.vertices, path.codes - # Restore the height. - vertices[:, 1] = vertices[:, 1] * m_aspect - return Path(vertices, codes) + # Call boxstyle with y, height squeezed by aspect_ratio. + path = boxstyle(self._x, self._y / m_aspect, + self._width, self._height / m_aspect, + self.get_mutation_scale()) + return Path(path.vertices * [1, m_aspect], path.codes) # Unsqueeze y. # Following methods are borrowed from the Rectangle class. @@ -3918,14 +4041,13 @@ def __str__(self): else: return f"{type(self).__name__}({self._path_original})" - @docstring.dedent_interpd - @_api.delete_parameter("3.4", "dpi_cor") + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="path") def __init__(self, posA=None, posB=None, path=None, arrowstyle="simple", connectionstyle="arc3", patchA=None, patchB=None, shrinkA=2, shrinkB=2, mutation_scale=1, mutation_aspect=1, - dpi_cor=1, **kwargs): """ There are two ways for defining an arrow: @@ -3955,7 +4077,7 @@ def __init__(self, posA=None, posB=None, path=None, meant to be scaled with the *mutation_scale*. The following arrow styles are available: - %(AvailableArrowstyles)s + %(ArrowStyle:table)s connectionstyle : str or `.ConnectionStyle` or None, optional, \ default: 'arc3' @@ -3964,7 +4086,7 @@ def __init__(self, posA=None, posB=None, path=None, names, with optional comma-separated attributes. The following connection styles are available: - %(AvailableConnectorstyles)s + %(ConnectionStyle:table)s patchA, patchB : `.Patch`, default: None Head and tail patches, respectively. @@ -3981,16 +4103,12 @@ def __init__(self, posA=None, posB=None, path=None, the mutation and the mutated box will be stretched by the inverse of it. - dpi_cor : float, default: 1 - dpi_cor is currently used for linewidth-related things and shrink - factor. Mutation scale is affected by this. Deprecated. - Other Parameters ---------------- **kwargs : `.Patch` properties, optional Here is a list of available `.Patch` properties: - %(Patch_kwdoc)s + %(Patch:kwdoc)s In contrast to other patches, the default ``capstyle`` and ``joinstyle`` for `FancyArrowPatch` are set to ``"round"``. @@ -4025,36 +4143,11 @@ def __init__(self, posA=None, posB=None, path=None, self._mutation_scale = mutation_scale self._mutation_aspect = mutation_aspect - self._dpi_cor = dpi_cor - - @_api.deprecated("3.4") - def set_dpi_cor(self, dpi_cor): - """ - dpi_cor is currently used for linewidth-related things and - shrink factor. Mutation scale is affected by this. - - Parameters - ---------- - dpi_cor : float - """ - self._dpi_cor = dpi_cor - self.stale = True - - @_api.deprecated("3.4") - def get_dpi_cor(self): - """ - dpi_cor is currently used for linewidth-related things and - shrink factor. Mutation scale is affected by this. - - Returns - ------- - scalar - """ - return self._dpi_cor + self._dpi_cor = 1.0 def set_positions(self, posA, posB): """ - Set the begin and end positions of the connecting path. + Set the start and end positions of the connecting path. Parameters ---------- @@ -4090,67 +4183,87 @@ def set_patchB(self, patchB): self.patchB = patchB self.stale = True - def set_connectionstyle(self, connectionstyle, **kw): + @_docstring.dedent_interpd + def set_connectionstyle(self, connectionstyle=None, **kwargs): """ - Set the connection style. Old attributes are forgotten. + Set the connection style, possibly with further attributes. + + Attributes from the previous connection style are not reused. + + Without argument (or with ``connectionstyle=None``), the available box + styles are returned as a human-readable string. Parameters ---------- - connectionstyle : str or `.ConnectionStyle` or None, optional - Can be a string with connectionstyle name with - optional comma-separated attributes, e.g.:: + connectionstyle : str or `matplotlib.patches.ConnectionStyle` + The style of the connection: either a `.ConnectionStyle` instance, + or a string, which is the style name and optionally comma separated + attributes (e.g. "Arc,armA=30,rad=10"). Such a string is used to + construct a `.ConnectionStyle` object, as documented in that class. - set_connectionstyle("arc,angleA=0,armA=30,rad=10") + The following connection styles are available: - Alternatively, the attributes can be provided as keywords, e.g.:: + %(ConnectionStyle:table_and_accepts)s - set_connectionstyle("arc", angleA=0,armA=30,rad=10) + **kwargs + Additional attributes for the connection style. See the table above + for supported parameters. - Without any arguments (or with ``connectionstyle=None``), return - available styles as a list of strings. - """ + Examples + -------- + :: + set_connectionstyle("Arc,armA=30,rad=10") + set_connectionstyle("arc", armA=30, rad=10) + """ if connectionstyle is None: return ConnectionStyle.pprint_styles() - - if (isinstance(connectionstyle, ConnectionStyle._Base) or - callable(connectionstyle)): - self._connector = connectionstyle - else: - self._connector = ConnectionStyle(connectionstyle, **kw) + self._connector = ( + ConnectionStyle(connectionstyle, **kwargs) + if isinstance(connectionstyle, str) else connectionstyle) self.stale = True def get_connectionstyle(self): """Return the `ConnectionStyle` used.""" return self._connector - def set_arrowstyle(self, arrowstyle=None, **kw): + def set_arrowstyle(self, arrowstyle=None, **kwargs): """ - Set the arrow style. Old attributes are forgotten. Without arguments - (or with ``arrowstyle=None``) returns available box styles as a list of - strings. + Set the arrow style, possibly with further attributes. + + Attributes from the previous arrow style are not reused. + + Without argument (or with ``arrowstyle=None``), the available box + styles are returned as a human-readable string. Parameters ---------- - arrowstyle : None or ArrowStyle or str, default: None - Can be a string with arrowstyle name with optional comma-separated - attributes, e.g.:: + arrowstyle : str or `matplotlib.patches.ArrowStyle` + The style of the arrow: either a `.ArrowStyle` instance, or a + string, which is the style name and optionally comma separated + attributes (e.g. "Fancy,head_length=0.2"). Such a string is used to + construct a `.ArrowStyle` object, as documented in that class. - set_arrowstyle("Fancy,head_length=0.2") + The following arrow styles are available: - Alternatively attributes can be provided as keywords, e.g.:: + %(ArrowStyle:table_and_accepts)s - set_arrowstyle("fancy", head_length=0.2) + **kwargs + Additional attributes for the arrow style. See the table above for + supported parameters. - """ + Examples + -------- + :: + set_arrowstyle("Fancy,head_length=0.2") + set_arrowstyle("fancy", head_length=0.2) + """ if arrowstyle is None: return ArrowStyle.pprint_styles() - - if isinstance(arrowstyle, ArrowStyle._Base): - self._arrow_transmuter = arrowstyle - else: - self._arrow_transmuter = ArrowStyle(arrowstyle, **kw) + self._arrow_transmuter = ( + ArrowStyle(arrowstyle, **kwargs) + if isinstance(arrowstyle, str) else arrowstyle) self.stale = True def get_arrowstyle(self): @@ -4195,17 +4308,15 @@ def get_mutation_aspect(self): else 1) # backcompat. def get_path(self): - """ - Return the path of the arrow in the data coordinates. Use - get_path_in_displaycoord() method to retrieve the arrow path - in display coordinates. - """ - _path, fillable = self.get_path_in_displaycoord() + """Return the path of the arrow in the data coordinates.""" + # The path is generated in display coordinates, then converted back to + # data coordinates. + _path, fillable = self._get_path_in_displaycoord() if np.iterable(fillable): _path = Path.make_compound_path(*_path) return self.get_transform().inverted().transform_path(_path) - def get_path_in_displaycoord(self): + def _get_path_in_displaycoord(self): """Return the mutated path of the arrow in display coordinates.""" dpi_cor = self._dpi_cor @@ -4234,25 +4345,22 @@ def draw(self, renderer): if not self.get_visible(): return - with self._bind_draw_path_function(renderer) as draw_path: - - # FIXME : dpi_cor is for the dpi-dependency of the linewidth. There - # could be room for improvement. Maybe get_path_in_displaycoord - # could take a renderer argument, but get_path should be adapted - # too. - self._dpi_cor = renderer.points_to_pixels(1.) - path, fillable = self.get_path_in_displaycoord() + # FIXME: dpi_cor is for the dpi-dependency of the linewidth. There + # could be room for improvement. Maybe _get_path_in_displaycoord could + # take a renderer argument, but get_path should be adapted too. + self._dpi_cor = renderer.points_to_pixels(1.) + path, fillable = self._get_path_in_displaycoord() - if not np.iterable(fillable): - path = [path] - fillable = [fillable] + if not np.iterable(fillable): + path = [path] + fillable = [fillable] - affine = transforms.IdentityTransform() + affine = transforms.IdentityTransform() - for p, f in zip(path, fillable): - draw_path( - p, affine, - self._facecolor if f and self._facecolor[3] else None) + self._draw_paths_with_artist_properties( + renderer, + [(p, affine, self._facecolor if f and self._facecolor[3] else None) + for p, f in zip(path, fillable)]) class ConnectionPatch(FancyArrowPatch): @@ -4262,8 +4370,8 @@ def __str__(self): return "ConnectionPatch((%g, %g), (%g, %g))" % \ (self.xy1[0], self.xy1[1], self.xy2[0], self.xy2[1]) - @docstring.dedent_interpd - @_api.delete_parameter("3.4", "dpi_cor") + @_docstring.dedent_interpd + @_api.make_keyword_only("3.6", name="axesA") def __init__(self, xyA, xyB, coordsA, coordsB=None, axesA=None, axesB=None, arrowstyle="-", @@ -4275,7 +4383,6 @@ def __init__(self, xyA, xyB, coordsA, coordsB=None, mutation_scale=10., mutation_aspect=None, clip_on=False, - dpi_cor=1., **kwargs): """ Connect point *xyA* in *coordsA* with point *xyB* in *coordsB*. @@ -4365,8 +4472,6 @@ def __init__(self, xyA, xyB, coordsA, coordsB=None, mutation_aspect=mutation_aspect, clip_on=clip_on, **kwargs) - self._dpi_cor = dpi_cor - # if True, draw annotation only if self.xy is inside the axes self._annotation_clip = None @@ -4429,18 +4534,16 @@ def _get_xy(self, xy, s, axes=None): def set_annotation_clip(self, b): """ - Set the clipping behavior. + Set the annotation's clipping behavior. Parameters ---------- b : bool or None - - - *False*: The annotation will always be drawn regardless of its - position. - - *True*: The annotation will only be drawn if ``self.xy`` is - inside the axes. - - *None*: The annotation will only be drawn if ``self.xy`` is - inside the axes and ``self.xycoords == "data"``. + - True: The annotation will be clipped when ``self.xy`` is + outside the axes. + - False: The annotation will always be drawn. + - None: The annotation will be clipped when ``self.xy`` is + outside the axes and ``self.xycoords == "data"``. """ self._annotation_clip = b self.stale = True @@ -4453,7 +4556,7 @@ def get_annotation_clip(self): """ return self._annotation_clip - def get_path_in_displaycoord(self): + def _get_path_in_displaycoord(self): """Return the mutated path of the arrow in display coordinates.""" dpi_cor = self._dpi_cor posA = self._get_xy(self.xy1, self.coords1, self.axesA) diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 475db80c42ef..1f65632c2d62 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -9,6 +9,7 @@ visualisation. """ +import copy from functools import lru_cache from weakref import WeakValueDictionary @@ -28,11 +29,11 @@ class Path: The underlying storage is made up of two parallel numpy arrays: - *vertices*: an Nx2 float array of vertices - - *codes*: an N-length uint8 array of vertex types, or None + - *codes*: an N-length uint8 array of path codes, or None These two arrays always have the same length in the first dimension. For example, to represent a cubic curve, you must - provide three vertices as well as three codes ``CURVE3``. + provide three vertices and three ``CURVE4`` codes. The code types are: @@ -47,11 +48,11 @@ class Path: Draw a line from the current position to the given vertex. - ``CURVE3`` : 1 control point, 1 endpoint - Draw a quadratic Bezier curve from the current position, with the given + Draw a quadratic Bézier curve from the current position, with the given control point, to the given end point. - ``CURVE4`` : 2 control points, 1 endpoint - Draw a cubic Bezier curve from the current position, with the given + Draw a cubic Bézier curve from the current position, with the given control points, to the given end point. - ``CLOSEPOLY`` : 1 vertex (ignored) @@ -108,7 +109,7 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, handled correctly by the Agg PathIterator and other consumers of path data, such as :meth:`iter_segments`. codes : array-like or None, optional - n-length array integers representing the codes of the path. + N-length array of integers representing the codes of the path. If not None, codes must be the same length as vertices. If None, *vertices* will be treated as a series of line segments. _interpolation_steps : int, optional @@ -161,7 +162,7 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, @classmethod def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None): """ - Creates a Path instance without the expense of calling the constructor. + Create a Path instance without the expense of calling the constructor. Parameters ---------- @@ -187,6 +188,17 @@ def _fast_from_codes_and_verts(cls, verts, codes, internals_from=None): pth._interpolation_steps = 1 return pth + @classmethod + def _create_closed(cls, vertices): + """ + Create a closed polygonal path going through *vertices*. + + Unlike ``Path(..., closed=True)``, *vertices* should **not** end with + an entry for the CLOSEPATH; this entry is added by `._create_closed`. + """ + v = _to_unmasked_float_array(vertices) + return cls(np.concatenate([v, v[:1]]), closed=True) + def _update_values(self): self._simplify_threshold = mpl.rcParams['path.simplify_threshold'] self._should_simplify = ( @@ -217,7 +229,7 @@ def codes(self): code is one of `STOP`, `MOVETO`, `LINETO`, `CURVE3`, `CURVE4` or `CLOSEPOLY`. For codes that correspond to more than one vertex (`CURVE3` and `CURVE4`), that code will be repeated so - that the length of `self.vertices` and `self.codes` is always + that the length of `vertices` and `codes` is always the same. """ return self._codes @@ -259,43 +271,37 @@ def readonly(self): """ return self._readonly - def __copy__(self): + def copy(self): """ Return a shallow copy of the `Path`, which will share the vertices and codes with the source `Path`. """ - import copy return copy.copy(self) - copy = __copy__ - def __deepcopy__(self, memo=None): """ Return a deepcopy of the `Path`. The `Path` will not be readonly, even if the source `Path` is. """ - try: - codes = self.codes.copy() - except AttributeError: - codes = None - return self.__class__( - self.vertices.copy(), codes, - _interpolation_steps=self._interpolation_steps) + # Deepcopying arrays (vertices, codes) strips the writeable=False flag. + p = copy.deepcopy(super(), memo) + p._readonly = False + return p deepcopy = __deepcopy__ @classmethod def make_compound_path_from_polys(cls, XY): """ - Make a compound path object to draw a number - of polygons with equal numbers of sides XY is a (numpolys x - numsides x 2) numpy array of vertices. Return object is a - :class:`Path` + Make a compound `Path` object to draw a number of polygons with equal + numbers of sides. .. plot:: gallery/misc/histogram_path.py + Parameters + ---------- + XY : (numpolys, numsides, 2) array """ - # for each poly: 1 for the MOVETO, (numsides-1) for the LINETO, 1 for # the CLOSEPOLY; the vert for the closepoly is ignored but we still # need it to keep the codes aligned with the vertices @@ -310,14 +316,13 @@ def make_compound_path_from_polys(cls, XY): codes[numsides::stride] = cls.CLOSEPOLY for i in range(numsides): verts[i::stride] = XY[:, i] - return cls(verts, codes) @classmethod def make_compound_path(cls, *args): """ - Make a compound path from a list of Path objects. Blindly removes all - Path.STOP control points. + Make a compound path from a list of `Path` objects. Blindly removes + all `Path.STOP` control points. """ # Handle an empty list in args (i.e. no args). if not args: @@ -413,7 +418,7 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None, def iter_bezier(self, **kwargs): """ - Iterate over each bezier curve (lines included) in a Path. + Iterate over each Bézier curve (lines included) in a Path. Parameters ---------- @@ -423,13 +428,13 @@ def iter_bezier(self, **kwargs): Yields ------ B : matplotlib.bezier.BezierSegment - The bezier curves that make up the current path. Note in particular - that freestanding points are bezier curves of order 0, and lines - are bezier curves of order 1 (with two control points). + The Bézier curves that make up the current path. Note in particular + that freestanding points are Bézier curves of order 0, and lines + are Bézier curves of order 1 (with two control points). code : Path.code_type The code describing what kind of curve is being returned. Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE4 correspond to - bezier curves with 1, 2, 3, and 4 control points (respectively). + Bézier curves with 1, 2, 3, and 4 control points (respectively). Path.CLOSEPOLY is a Path.LINETO with the control points correctly chosen based on the start/end points of the current stroke. """ @@ -455,12 +460,11 @@ def iter_bezier(self, **kwargs): elif code == Path.STOP: return else: - raise ValueError("Invalid Path.code_type: " + str(code)) + raise ValueError(f"Invalid Path.code_type: {code}") prev_vert = verts[-2:] - @_api.delete_parameter("3.3", "quantize") def cleaned(self, transform=None, remove_nans=False, clip=None, - quantize=False, simplify=False, curves=False, + *, simplify=False, curves=False, stroke_width=1.0, snap=False, sketch=None): """ Return a new Path with vertices and codes cleaned according to the @@ -508,7 +512,7 @@ def contains_point(self, point, transform=None, radius=0.0): by *transform*; i.e. for a correct check, *transform* should transform the path into the coordinate system of *point*. radius : float, default: 0 - Add an additional margin on the path in coordinates of *point*. + Additional margin on the path in coordinates of *point*. The path is extended tangentially by *radius/2*; i.e. if you would draw the path with a linewidth of *radius*, all points on the line would still be considered to be contained in the area. Conversely, @@ -558,7 +562,7 @@ def contains_points(self, points, transform=None, radius=0.0): by *transform*; i.e. for a correct check, *transform* should transform the path into the coordinate system of *points*. radius : float, default: 0 - Add an additional margin on the path in coordinates of *points*. + Additional margin on the path in coordinates of *points*. The path is extended tangentially by *radius/2*; i.e. if you would draw the path with a linewidth of *radius*, all points on the line would still be considered to be contained in the area. Conversely, @@ -683,7 +687,7 @@ def to_polygons(self, transform=None, width=0, height=0, closed_only=True): polygon/polyline is an Nx2 array of vertices. In other words, each polygon has no ``MOVETO`` instructions or curves. This is useful for displaying in backends that do not support - compound paths or Bezier curves. + compound paths or Bézier curves. If *width* and *height* are both non-zero then the lines will be simplified so that vertices outside of (0, 0), (width, @@ -822,7 +826,7 @@ def circle(cls, center=(0., 0.), radius=1., readonly=False): Notes ----- - The circle is approximated using 8 cubic Bezier curves, as described in + The circle is approximated using 8 cubic Bézier curves, as described in Lancaster, Don. `Approximating a Circle or an Ellipse Using Four Bezier Cubic Splines `_. @@ -920,8 +924,8 @@ def unit_circle_righthalf(cls): @classmethod def arc(cls, theta1, theta2, n=None, is_wedge=False): """ - Return the unit circle arc from angles *theta1* to *theta2* (in - degrees). + Return a `Path` for the unit circle arc from angles *theta1* to + *theta2* (in degrees). *theta2* is unwrapped to produce the shortest arc within 360 degrees. That is, if *theta2* > *theta1* + 360, the arc will be from *theta1* to @@ -933,7 +937,7 @@ def arc(cls, theta1, theta2, n=None, is_wedge=False): Masionobe, L. 2003. `Drawing an elliptical arc using polylines, quadratic or cubic Bezier curves - `_. + `_. """ halfpi = np.pi * 0.5 @@ -999,8 +1003,8 @@ def arc(cls, theta1, theta2, n=None, is_wedge=False): @classmethod def wedge(cls, theta1, theta2, n=None): """ - Return the unit circle wedge from angles *theta1* to *theta2* (in - degrees). + Return a `Path` for the unit circle wedge from angles *theta1* to + *theta2* (in degrees). *theta2* is unwrapped to produce the shortest wedge within 360 degrees. That is, if *theta2* > *theta1* + 360, the wedge will be from *theta1* @@ -1036,7 +1040,6 @@ def clip_to_bbox(self, bbox, inside=True): If *inside* is `True`, clip to the inside of the box, otherwise to the outside of the box. """ - # Use make_compound_path_from_polys verts = _path.clip_path_to_rect(self, bbox, inside) paths = [Path(poly) for poly in verts] return self.make_compound_path(*paths) @@ -1045,18 +1048,18 @@ def clip_to_bbox(self, bbox, inside=True): def get_path_collection_extents( master_transform, paths, transforms, offsets, offset_transform): r""" - Given a sequence of `Path`\s, `~.Transform`\s objects, and offsets, as - found in a `~.PathCollection`, returns the bounding box that encapsulates + Given a sequence of `Path`\s, `.Transform`\s objects, and offsets, as + found in a `.PathCollection`, returns the bounding box that encapsulates all of them. Parameters ---------- - master_transform : `~.Transform` + master_transform : `.Transform` Global transformation applied to all paths. paths : list of `Path` - transforms : list of `~.Affine2D` + transforms : list of `.Affine2D` offsets : (N, 2) array-like - offset_transform : `~.Affine2D` + offset_transform : `.Affine2D` Transform applied to the offsets before offsetting the path. Notes diff --git a/lib/matplotlib/patheffects.py b/lib/matplotlib/patheffects.py index fb66df43f0fb..191f62b8d2be 100644 --- a/lib/matplotlib/patheffects.py +++ b/lib/matplotlib/patheffects.py @@ -1,6 +1,6 @@ """ -Defines classes for path effects. The path effects are supported in `~.Text`, -`~.Line2D` and `~.Patch`. +Defines classes for path effects. The path effects are supported in `.Text`, +`.Line2D` and `.Patch`. .. seealso:: :doc:`/tutorials/advanced/patheffects_guide` @@ -232,7 +232,6 @@ def __init__(self, offset=(2, -2), The shadow color. alpha : float, default: 0.3 The alpha transparency of the created shadow patch. - http://matplotlib.1069221.n5.nabble.com/path-effects-question-td27630.html rho : float, default: 0.3 A scale factor to apply to the rgbFace color if *shadow_rgbFace* is not specified. diff --git a/lib/matplotlib/projections/__init__.py b/lib/matplotlib/projections/__init__.py index 556273803665..16a5651da1d1 100644 --- a/lib/matplotlib/projections/__init__.py +++ b/lib/matplotlib/projections/__init__.py @@ -1,4 +1,58 @@ -from .. import axes, docstring +""" +Non-separable transforms that map from data space to screen space. + +Projections are defined as `~.axes.Axes` subclasses. They include the +following elements: + +- A transformation from data coordinates into display coordinates. + +- An inverse of that transformation. This is used, for example, to convert + mouse positions from screen space back into data space. + +- Transformations for the gridlines, ticks and ticklabels. Custom projections + will often need to place these elements in special locations, and Matplotlib + has a facility to help with doing so. + +- Setting up default values (overriding `~.axes.Axes.cla`), since the defaults + for a rectilinear axes may not be appropriate. + +- Defining the shape of the axes, for example, an elliptical axes, that will be + used to draw the background of the plot and for clipping any data elements. + +- Defining custom locators and formatters for the projection. For example, in + a geographic projection, it may be more convenient to display the grid in + degrees, even if the data is in radians. + +- Set up interactive panning and zooming. This is left as an "advanced" + feature left to the reader, but there is an example of this for polar plots + in `matplotlib.projections.polar`. + +- Any additional methods for additional convenience or features. + +Once the projection axes is defined, it can be used in one of two ways: + +- By defining the class attribute ``name``, the projection axes can be + registered with `matplotlib.projections.register_projection` and subsequently + simply invoked by name:: + + fig.add_subplot(projection="my_proj_name") + +- For more complex, parameterisable projections, a generic "projection" object + may be defined which includes the method ``_as_mpl_axes``. ``_as_mpl_axes`` + should take no arguments and return the projection's axes subclass and a + dictionary of additional arguments to pass to the subclass' ``__init__`` + method. Subsequently a parameterised projection can be initialised with:: + + fig.add_subplot(projection=MyProjection(param1=param1_value)) + + where MyProjection is an object which implements a ``_as_mpl_axes`` method. + +A full-fledged and heavily annotated example is in +:doc:`/gallery/misc/custom_projection`. The polar plot functionality in +`matplotlib.projections.polar` may also be of interest. +""" + +from .. import axes, _docstring from .geo import AitoffAxes, HammerAxes, LambertAxes, MollweideAxes from .polar import PolarAxes from mpl_toolkits.mplot3d import Axes3D @@ -57,4 +111,4 @@ def get_projection_class(projection=None): get_projection_names = projection_registry.get_projection_names -docstring.interpd.update(projection_names=get_projection_names()) +_docstring.interpd.update(projection_names=get_projection_names()) diff --git a/lib/matplotlib/projections/geo.py b/lib/matplotlib/projections/geo.py index 4aa374c5d75d..75e582e81028 100644 --- a/lib/matplotlib/projections/geo.py +++ b/lib/matplotlib/projections/geo.py @@ -1,6 +1,7 @@ import numpy as np -from matplotlib import _api, rcParams +import matplotlib as mpl +from matplotlib import _api from matplotlib.axes import Axes import matplotlib.axis as maxis from matplotlib.patches import Circle @@ -34,10 +35,10 @@ def _init_axis(self): # Do not register xaxis or yaxis with spines -- as done in # Axes._init_axis() -- until GeoAxes.xaxis.clear() works. # self.spines['geo'].register_axis(self.yaxis) - self._update_transScale() - def cla(self): - super().cla() + def clear(self): + # docstring inherited + super().clear() self.set_longitude_grid(30) self.set_latitude_grid(15) @@ -50,7 +51,7 @@ def cla(self): # Why do we need to turn on yaxis tick labels, but # xaxis tick labels are already on? - self.grid(rcParams['axes.grid']) + self.grid(mpl.rcParams['axes.grid']) Axes.set_xlim(self, -np.pi, np.pi) Axes.set_ylim(self, -np.pi / 2.0, np.pi / 2.0) @@ -147,6 +148,7 @@ def set_yscale(self, *args, **kwargs): set_xscale = set_yscale def set_xlim(self, *args, **kwargs): + """Not supported. Please consider using Cartopy.""" raise TypeError("Changing axes limits of a geographic projection is " "not supported. Please consider using Cartopy.") @@ -155,14 +157,8 @@ def set_xlim(self, *args, **kwargs): def format_coord(self, lon, lat): """Return a format string formatting the coordinate.""" lon, lat = np.rad2deg([lon, lat]) - if lat >= 0.0: - ns = 'N' - else: - ns = 'S' - if lon >= 0.0: - ew = 'E' - else: - ew = 'W' + ns = 'N' if lat >= 0.0 else 'S' + ew = 'E' if lon >= 0.0 else 'W' return ('%f\N{DEGREE SIGN}%s, %f\N{DEGREE SIGN}%s' % (abs(lat), ns, abs(lon), ew)) @@ -202,17 +198,17 @@ def get_data_ratio(self): def can_zoom(self): """ - Return whether this axes supports the zoom box button functionality. + Return whether this Axes supports the zoom box button functionality. - This axes object does not support interactive zoom box. + This Axes object does not support interactive zoom box. """ return False def can_pan(self): """ - Return whether this axes supports the pan/zoom button functionality. + Return whether this Axes supports the pan/zoom button functionality. - This axes object does not support interactive pan/zoom. + This Axes object does not support interactive pan/zoom. """ return False @@ -289,7 +285,7 @@ def __init__(self, *args, **kwargs): self._longitude_cap = np.pi / 2.0 super().__init__(*args, **kwargs) self.set_aspect(0.5, adjustable='box', anchor='C') - self.cla() + self.clear() def _get_core_transform(self, resolution): return self.AitoffTransform(resolution) @@ -334,7 +330,7 @@ def __init__(self, *args, **kwargs): self._longitude_cap = np.pi / 2.0 super().__init__(*args, **kwargs) self.set_aspect(0.5, adjustable='box', anchor='C') - self.cla() + self.clear() def _get_core_transform(self, resolution): return self.HammerTransform(resolution) @@ -404,7 +400,7 @@ def __init__(self, *args, **kwargs): self._longitude_cap = np.pi / 2.0 super().__init__(*args, **kwargs) self.set_aspect(0.5, adjustable='box', anchor='C') - self.cla() + self.clear() def _get_core_transform(self, resolution): return self.MollweideTransform(resolution) @@ -489,10 +485,11 @@ def __init__(self, *args, center_longitude=0, center_latitude=0, **kwargs): self._center_latitude = center_latitude super().__init__(*args, **kwargs) self.set_aspect('equal', adjustable='box', anchor='C') - self.cla() + self.clear() - def cla(self): - super().cla() + def clear(self): + # docstring inherited + super().clear() self.yaxis.set_major_formatter(NullFormatter()) def _get_core_transform(self, resolution): diff --git a/lib/matplotlib/projections/polar.py b/lib/matplotlib/projections/polar.py index 867e12307ff7..3243c76d8661 100644 --- a/lib/matplotlib/projections/polar.py +++ b/lib/matplotlib/projections/polar.py @@ -1,9 +1,10 @@ -from collections import OrderedDict +import math import types import numpy as np -from matplotlib import _api, cbook, rcParams +import matplotlib as mpl +from matplotlib import _api, cbook from matplotlib.axes import Axes import matplotlib.axis as maxis import matplotlib.markers as mmarkers @@ -11,42 +12,69 @@ from matplotlib.path import Path import matplotlib.ticker as mticker import matplotlib.transforms as mtransforms -import matplotlib.spines as mspines +from matplotlib.spines import Spine class PolarTransform(mtransforms.Transform): + r""" + The base polar transform. + + This transform maps polar coordinates :math:`\theta, r` into Cartesian + coordinates :math:`x, y = r \cos(\theta), r \sin(\theta)` + (but does not fully transform into Axes coordinates or + handle positioning in screen space). + + This transformation is designed to be applied to data after any scaling + along the radial axis (e.g. log-scaling) has been applied to the input + data. + + Path segments at a fixed radius are automatically transformed to circular + arcs as long as ``path._interpolation_steps > 1``. """ - The base polar transform. This handles projection *theta* and - *r* into Cartesian coordinate space *x* and *y*, but does not - perform the ultimate affine transformation into the correct - position. - """ + input_dims = output_dims = 2 def __init__(self, axis=None, use_rmin=True, - _apply_theta_transforms=True): + _apply_theta_transforms=True, *, scale_transform=None): + """ + Parameters + ---------- + axis : `~matplotlib.axis.Axis`, optional + Axis associated with this transform. This is used to get the + minimum radial limit. + use_rmin : `bool`, optional + If ``True``, subtract the minimum radial axis limit before + transforming to Cartesian coordinates. *axis* must also be + specified for this to take effect. + """ super().__init__() self._axis = axis self._use_rmin = use_rmin self._apply_theta_transforms = _apply_theta_transforms + self._scale_transform = scale_transform __str__ = mtransforms._make_str_method( "_axis", use_rmin="_use_rmin", _apply_theta_transforms="_apply_theta_transforms") + def _get_rorigin(self): + # Get lower r limit after being scaled by the radial scale transform + return self._scale_transform.transform( + (0, self._axis.get_rorigin()))[1] + def transform_non_affine(self, tr): # docstring inherited - t, r = np.transpose(tr) + theta, r = np.transpose(tr) # PolarAxes does not use the theta transforms here, but apply them for # backwards-compatibility if not being used by it. if self._apply_theta_transforms and self._axis is not None: - t *= self._axis.get_theta_direction() - t += self._axis.get_theta_offset() + theta *= self._axis.get_theta_direction() + theta += self._axis.get_theta_offset() if self._use_rmin and self._axis is not None: - r = (r - self._axis.get_rorigin()) * self._axis.get_rsign() + r = (r - self._get_rorigin()) * self._axis.get_rsign() r = np.where(r >= 0, r, np.nan) - return np.column_stack([r * np.cos(t), r * np.sin(t)]) + return np.column_stack([r * np.cos(theta), r * np.sin(theta)]) def transform_path_non_affine(self, path): # docstring inherited @@ -68,7 +96,7 @@ def transform_path_non_affine(self, path): # that behavior here. last_td, td = np.rad2deg([last_t, t]) if self._use_rmin and self._axis is not None: - r = ((r - self._axis.get_rorigin()) + r = ((r - self._get_rorigin()) * self._axis.get_rsign()) if last_td <= td: while td - last_td > 360: @@ -109,15 +137,30 @@ def inverted(self): class PolarAffine(mtransforms.Affine2DBase): - """ - The affine part of the polar projection. Scales the output so - that maximum radius rests on the edge of the axes circle. + r""" + The affine part of the polar projection. + + Scales the output so that maximum radius rests on the edge of the axes + circle and the origin is mapped to (0.5, 0.5). The transform applied is + the same to x and y components and given by: + + .. math:: + + x_{1} = 0.5 \left [ \frac{x_{0}}{(r_{\max} - r_{\min})} + 1 \right ] + + :math:`r_{\min}, r_{\max}` are the minimum and maximum radial limits after + any scaling (e.g. log scaling) has been removed. """ def __init__(self, scale_transform, limits): """ - *limits* is the view limit of the data. The only part of - its bounds that is used is the y limits (for the radius limits). - The theta range is handled by the non-affine transform. + Parameters + ---------- + scale_transform : `~matplotlib.transforms.Transform` + Scaling transform for the data. This is used to remove any scaling + from the radial view limits. + limits : `~matplotlib.transforms.BboxBase` + View limits of the data. The only part of its bounds that is used + is the y limits (for the radius limits). """ super().__init__() self._scale_transform = scale_transform @@ -150,6 +193,17 @@ class InvertedPolarTransform(mtransforms.Transform): def __init__(self, axis=None, use_rmin=True, _apply_theta_transforms=True): + """ + Parameters + ---------- + axis : `~matplotlib.axis.Axis`, optional + Axis associated with this transform. This is used to get the + minimum radial limit. + use_rmin : `bool`, optional + If ``True`` add the minimum radial axis limit after + transforming from Cartesian coordinates. *axis* must also be + specified for this to take effect. + """ super().__init__() self._axis = axis self._use_rmin = use_rmin @@ -187,11 +241,12 @@ class ThetaFormatter(mticker.Formatter): Used to format the *theta* tick labels. Converts the native unit of radians into degrees and adds a degree symbol. """ + def __call__(self, x, pos=None): vmin, vmax = self.axis.get_view_interval() d = np.rad2deg(abs(vmax - vmin)) digits = max(-int(np.log10(d) - 1.5), 0) - # Use unicode rather than mathtext with \circ, so that it will work + # Use Unicode rather than mathtext with \circ, so that it will work # correctly with any arbitrary font (assuming it has a degree sign), # whereas $5\circ$ will only work correctly with one of the supported # math fonts (Computer Modern and STIX). @@ -246,22 +301,10 @@ def __call__(self): else: return np.deg2rad(self.base()) - @_api.deprecated("3.3") - def pan(self, numsteps): - return self.base.pan(numsteps) - - def refresh(self): - # docstring inherited - return self.base.refresh() - def view_limits(self, vmin, vmax): vmin, vmax = np.rad2deg((vmin, vmax)) return np.deg2rad(self.base.view_limits(vmin, vmax)) - @_api.deprecated("3.3") - def zoom(self, direction): - return self.base.zoom(direction) - class ThetaTick(maxis.XTick): """ @@ -290,9 +333,8 @@ def __init__(self, axes, *args, **kwargs): rotation_mode='anchor', transform=self.label2.get_transform() + self._text2_translate) - def _apply_params(self, **kw): - super()._apply_params(**kw) - + def _apply_params(self, **kwargs): + super()._apply_params(**kwargs) # Ensure transform is correct; sometimes this gets reset. trans = self.label1.get_transform() if not trans.contains_branch(self._text1_translate): @@ -365,13 +407,7 @@ class ThetaAxis(maxis.XAxis): """ __name__ = 'thetaaxis' axis_name = 'theta' #: Read-only name identifying the axis. - - def _get_tick(self, major): - if major: - tick_kw = self._major_tick_kw - else: - tick_kw = self._minor_tick_kw - return ThetaTick(self.axes, 0, major=major, **tick_kw) + _tick_class = ThetaTick def _wrap_locator_formatter(self): self.set_major_locator(ThetaLocator(self.get_major_locator())) @@ -380,16 +416,20 @@ def _wrap_locator_formatter(self): self.isDefault_majfmt = True def clear(self): + # docstring inherited super().clear() self.set_ticks_position('none') self._wrap_locator_formatter() - @_api.deprecated("3.4", alternative="ThetaAxis.clear()") - def cla(self): - self.clear() - def _set_scale(self, value, **kwargs): + if value != 'linear': + raise NotImplementedError( + "The xscale cannot be set on a polar plot") super()._set_scale(value, **kwargs) + # LinearScale.set_default_locators_and_formatters just set the major + # locator to be an AutoLocator, so we customize it here to have ticks + # at sensible degree multiples. + self.get_major_locator().set_params(steps=[1, 1.5, 3, 4.5, 9, 10]) self._wrap_locator_formatter() def _copy_tick_props(self, src, dest): @@ -418,40 +458,37 @@ def __init__(self, base, axes=None): self.base = base self._axes = axes + def set_axis(self, axis): + self.base.set_axis(axis) + def __call__(self): - show_all = True # Ensure previous behaviour with full circle non-annular views. if self._axes: if _is_full_circle_rad(*self._axes.viewLim.intervalx): rorigin = self._axes.get_rorigin() * self._axes.get_rsign() if self._axes.get_rmin() <= rorigin: - show_all = False - if show_all: - return self.base() - else: - return [tick for tick in self.base() if tick > rorigin] - - @_api.deprecated("3.3") - def pan(self, numsteps): - return self.base.pan(numsteps) - - @_api.deprecated("3.3") - def zoom(self, direction): - return self.base.zoom(direction) + return [tick for tick in self.base() if tick > rorigin] + return self.base() - @_api.deprecated("3.3") - def refresh(self): - # docstring inherited - return self.base.refresh() + def _zero_in_bounds(self): + """ + Return True if zero is within the valid values for the + scale of the radial axis. + """ + vmin, vmax = self._axes.yaxis._scale.limit_range_for_scale(0, 1, 1e-5) + return vmin == 0 def nonsingular(self, vmin, vmax): # docstring inherited - return ((0, 1) if (vmin, vmax) == (-np.inf, np.inf) # Init. limits. - else self.base.nonsingular(vmin, vmax)) + if self._zero_in_bounds() and (vmin, vmax) == (-np.inf, np.inf): + # Initial view limits + return (0, 1) + else: + return self.base.nonsingular(vmin, vmax) def view_limits(self, vmin, vmax): vmin, vmax = self.base.view_limits(vmin, vmax) - if vmax > vmin: + if self._zero_in_bounds() and vmax > vmin: # this allows inverted r/y-lims vmin = min(0, vmin) return mtransforms.nonsingular(vmin, vmax) @@ -667,32 +704,23 @@ class RadialAxis(maxis.YAxis): """ __name__ = 'radialaxis' axis_name = 'radius' #: Read-only name identifying the axis. + _tick_class = RadialTick def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.sticky_edges.y.append(0) - def _get_tick(self, major): - if major: - tick_kw = self._major_tick_kw - else: - tick_kw = self._minor_tick_kw - return RadialTick(self.axes, 0, major=major, **tick_kw) - def _wrap_locator_formatter(self): self.set_major_locator(RadialLocator(self.get_major_locator(), self.axes)) self.isDefault_majloc = True def clear(self): + # docstring inherited super().clear() self.set_ticks_position('none') self._wrap_locator_formatter() - @_api.deprecated("3.4", alternative="RadialAxis.clear()") - def cla(self): - self.clear() - def _set_scale(self, value, **kwargs): super()._set_scale(value, **kwargs) self._wrap_locator_formatter() @@ -790,10 +818,11 @@ def __init__(self, *args, super().__init__(*args, **kwargs) self.use_sticky_edges = True self.set_aspect('equal', adjustable='box', anchor='C') - self.cla() + self.clear() - def cla(self): - super().cla() + def clear(self): + # docstring inherited + super().clear() self.title.set_y(1.05) @@ -805,7 +834,7 @@ def cla(self): end.set_visible(False) self.set_xlim(0.0, 2 * np.pi) - self.grid(rcParams['polaraxes.grid']) + self.grid(mpl.rcParams['polaraxes.grid']) inner = self.spines.get('inner', None) if inner: inner.set_visible(False) @@ -818,11 +847,9 @@ def _init_axis(self): # This is moved out of __init__ because non-separable axes don't use it self.xaxis = ThetaAxis(self) self.yaxis = RadialAxis(self) - # Calling polar_axes.xaxis.clear() or polar_axes.xaxis.clear() - # results in weird artifacts. Therefore we disable this for - # now. + # Calling polar_axes.xaxis.clear() or polar_axes.yaxis.clear() + # results in weird artifacts. Therefore we disable this for now. # self.spines['polar'].register_axis(self.yaxis) - self._update_transScale() def _set_lim_and_transforms(self): # A view limit where the minimum radius can be locked if the user @@ -861,7 +888,9 @@ def _set_lim_and_transforms(self): # data. This one is aware of rmin self.transProjection = self.PolarTransform( self, - _apply_theta_transforms=False) + _apply_theta_transforms=False, + scale_transform=self.transScale + ) # Add dependency on rorigin. self.transProjection.set_children(self._originViewLim) @@ -872,9 +901,25 @@ def _set_lim_and_transforms(self): # The complete data transformation stack -- from data all the # way to display coordinates + # + # 1. Remove any radial axis scaling (e.g. log scaling) + # 2. Shift data in the theta direction + # 3. Project the data from polar to cartesian values + # (with the origin in the same place) + # 4. Scale and translate the cartesian values to Axes coordinates + # (here the origin is moved to the lower left of the Axes) + # 5. Move and scale to fill the Axes + # 6. Convert from Axes coordinates to Figure coordinates self.transData = ( - self.transScale + self.transShift + self.transProjection + - (self.transProjectionAffine + self.transWedge + self.transAxes)) + self.transScale + + self.transShift + + self.transProjection + + ( + self.transProjectionAffine + + self.transWedge + + self.transAxes + ) + ) # This is the transform for theta-axis ticks. It is # equivalent to transData, except it always puts r == 0.0 and r == 1.0 @@ -945,9 +990,7 @@ def get_yaxis_text2_transform(self, pad): pad_shift = _ThetaShift(self, pad, 'min') return self._yaxis_text_transform + pad_shift, 'center', halign - @_api.delete_parameter("3.3", "args") - @_api.delete_parameter("3.3", "kwargs") - def draw(self, renderer, *args, **kwargs): + def draw(self, renderer): self._unstale_viewLim() thetamin, thetamax = np.rad2deg(self._realViewLim.intervalx) if thetamin > thetamax: @@ -991,20 +1034,18 @@ def draw(self, renderer, *args, **kwargs): self.yaxis.reset_ticks() self.yaxis.set_clip_path(self.patch) - super().draw(renderer, *args, **kwargs) + super().draw(renderer) def _gen_axes_patch(self): return mpatches.Wedge((0.5, 0.5), 0.5, 0.0, 360.0) def _gen_axes_spines(self): - spines = OrderedDict([ - ('polar', mspines.Spine.arc_spine(self, 'top', - (0.5, 0.5), 0.5, 0.0, 360.0)), - ('start', mspines.Spine.linear_spine(self, 'left')), - ('end', mspines.Spine.linear_spine(self, 'right')), - ('inner', mspines.Spine.arc_spine(self, 'bottom', - (0.5, 0.5), 0.0, 0.0, 360.0)) - ]) + spines = { + 'polar': Spine.arc_spine(self, 'top', (0.5, 0.5), 0.5, 0, 360), + 'start': Spine.linear_spine(self, 'left'), + 'end': Spine.linear_spine(self, 'right'), + 'inner': Spine.arc_spine(self, 'bottom', (0.5, 0.5), 0.0, 0, 360), + } spines['polar'].set_transform(self.transWedge + self.transAxes) spines['inner'].set_transform(self.transWedge + self.transAxes) spines['start'].set_transform(self._yaxis_transform) @@ -1040,7 +1081,7 @@ def set_thetalim(self, *args, **kwargs): where minval and maxval are the minimum and maximum limits. Values are wrapped in to the range :math:`[0, 2\pi]` (in radians), so for example it is possible to do ``set_thetalim(-np.pi / 2, np.pi / 2)`` to have - an axes symmetric around 0. A ValueError is raised if the absolute + an axis symmetric around 0. A ValueError is raised if the absolute angle difference is larger than a full circle. """ orig_lim = self.get_xlim() # in radians @@ -1188,9 +1229,17 @@ def get_rorigin(self): def get_rsign(self): return np.sign(self._originViewLim.y1 - self._originViewLim.y0) + @_api.make_keyword_only("3.6", "emit") def set_rlim(self, bottom=None, top=None, emit=True, auto=False, **kwargs): """ - See `~.polar.PolarAxes.set_ylim`. + Set the radial axis view limits. + + This function behaves like `.Axes.set_ylim`, but additionally supports + *rmin* and *rmax* as aliases for *bottom* and *top*. + + See Also + -------- + .Axes.set_ylim """ if 'rmin' in kwargs: if bottom is None: @@ -1207,58 +1256,6 @@ def set_rlim(self, bottom=None, top=None, emit=True, auto=False, **kwargs): return self.set_ylim(bottom=bottom, top=top, emit=emit, auto=auto, **kwargs) - def set_ylim(self, bottom=None, top=None, emit=True, auto=False, - *, ymin=None, ymax=None): - """ - Set the data limits for the radial axis. - - Parameters - ---------- - bottom : float, optional - The bottom limit (default: None, which leaves the bottom - limit unchanged). - The bottom and top ylims may be passed as the tuple - (*bottom*, *top*) as the first positional argument (or as - the *bottom* keyword argument). - - top : float, optional - The top limit (default: None, which leaves the top limit - unchanged). - - emit : bool, default: True - Whether to notify observers of limit change. - - auto : bool or None, default: False - Whether to turn on autoscaling of the y-axis. True turns on, - False turns off, None leaves unchanged. - - ymin, ymax : float, optional - These arguments are deprecated and will be removed in a future - version. They are equivalent to *bottom* and *top* respectively, - and it is an error to pass both *ymin* and *bottom* or - *ymax* and *top*. - - Returns - ------- - bottom, top : (float, float) - The new y-axis limits in data coordinates. - """ - if ymin is not None: - if bottom is not None: - raise ValueError('Cannot supply both positional "bottom" ' - 'argument and kwarg "ymin"') - else: - bottom = ymin - if ymax is not None: - if top is not None: - raise ValueError('Cannot supply both positional "top" ' - 'argument and kwarg "ymax"') - else: - top = ymax - if top is None and np.iterable(bottom): - bottom, top = bottom[0], bottom[1] - return super().set_ylim(bottom=bottom, top=top, emit=emit, auto=auto) - def get_rlabel_position(self): """ Returns @@ -1319,7 +1316,7 @@ def set_thetagrids(self, angles, labels=None, fmt=None, **kwargs): Other Parameters ---------------- **kwargs - *kwargs* are optional `~.Text` properties for the labels. + *kwargs* are optional `.Text` properties for the labels. See Also -------- @@ -1337,7 +1334,7 @@ def set_thetagrids(self, angles, labels=None, fmt=None, **kwargs): elif fmt is not None: self.xaxis.set_major_formatter(mticker.FormatStrFormatter(fmt)) for t in self.xaxis.get_ticklabels(): - t.update(kwargs) + t._internal_update(kwargs) return self.xaxis.get_ticklines(), self.xaxis.get_ticklabels() def set_rgrids(self, radii, labels=None, angle=None, fmt=None, **kwargs): @@ -1371,7 +1368,7 @@ def set_rgrids(self, radii, labels=None, angle=None, fmt=None, **kwargs): Other Parameters ---------------- **kwargs - *kwargs* are optional `~.Text` properties for the labels. + *kwargs* are optional `.Text` properties for the labels. See Also -------- @@ -1392,21 +1389,39 @@ def set_rgrids(self, radii, labels=None, angle=None, fmt=None, **kwargs): angle = self.get_rlabel_position() self.set_rlabel_position(angle) for t in self.yaxis.get_ticklabels(): - t.update(kwargs) + t._internal_update(kwargs) return self.yaxis.get_gridlines(), self.yaxis.get_ticklabels() - def set_xscale(self, scale, *args, **kwargs): - if scale != 'linear': - raise NotImplementedError( - "You can not set the xscale on a polar plot.") - def format_coord(self, theta, r): # docstring inherited + screen_xy = self.transData.transform((theta, r)) + screen_xys = screen_xy + np.stack( + np.meshgrid([-1, 0, 1], [-1, 0, 1])).reshape((2, -1)).T + ts, rs = self.transData.inverted().transform(screen_xys).T + delta_t = abs((ts - theta + np.pi) % (2 * np.pi) - np.pi).max() + delta_t_halfturns = delta_t / np.pi + delta_t_degrees = delta_t_halfturns * 180 + delta_r = abs(rs - r).max() if theta < 0: theta += 2 * np.pi - theta /= np.pi - return ('\N{GREEK SMALL LETTER THETA}=%0.3f\N{GREEK SMALL LETTER PI} ' - '(%0.3f\N{DEGREE SIGN}), r=%0.3f') % (theta, theta * 180.0, r) + theta_halfturns = theta / np.pi + theta_degrees = theta_halfturns * 180 + + # See ScalarFormatter.format_data_short. For r, use #g-formatting + # (as for linear axes), but for theta, use f-formatting as scientific + # notation doesn't make sense and the trailing dot is ugly. + def format_sig(value, delta, opt, fmt): + # For "f", only count digits after decimal point. + prec = (max(0, -math.floor(math.log10(delta))) if fmt == "f" else + cbook._g_sig_digits(value, delta)) + return f"{value:-{opt}.{prec}{fmt}}" + + return ('\N{GREEK SMALL LETTER THETA}={}\N{GREEK SMALL LETTER PI} ' + '({}\N{DEGREE SIGN}), r={}').format( + format_sig(theta_halfturns, delta_t_halfturns, "", "f"), + format_sig(theta_degrees, delta_t_degrees, "", "f"), + format_sig(r, delta_r, "#", "g"), + ) def get_data_ratio(self): """ @@ -1419,17 +1434,17 @@ def get_data_ratio(self): def can_zoom(self): """ - Return whether this axes supports the zoom box button functionality. + Return whether this Axes supports the zoom box button functionality. - Polar axes do not support zoom boxes. + A polar Axes does not support zoom boxes. """ return False def can_pan(self): """ - Return whether this axes supports the pan/zoom button functionality. + Return whether this Axes supports the pan/zoom button functionality. - For polar axes, this is slightly misleading. Both panning and + For a polar Axes, this is slightly misleading. Both panning and zooming are performed by the same button. Panning is performed in azimuth while zooming is done along the radial. """ @@ -1486,9 +1501,12 @@ def drag_pan(self, button, key, x, y): self.set_rmax(p.rmax / scale) -# to keep things all self contained, we can put aliases to the Polar classes +# To keep things all self-contained, we can put aliases to the Polar classes # defined above. This isn't strictly necessary, but it makes some of the -# code more readable (and provides a backwards compatible Polar API) +# code more readable, and provides a backwards compatible Polar API. In +# particular, this is used by the :doc:`/gallery/specialty_plots/radar_chart` +# example to override PolarTransform on a PolarAxes subclass, so make sure that +# that example is unaffected before changing this. PolarAxes.PolarTransform = PolarTransform PolarAxes.PolarAffine = PolarAffine PolarAxes.InvertedPolarTransform = InvertedPolarTransform diff --git a/lib/matplotlib/pylab.py b/lib/matplotlib/pylab.py index 6b9f89d29ead..289aa9050e0c 100644 --- a/lib/matplotlib/pylab.py +++ b/lib/matplotlib/pylab.py @@ -16,8 +16,7 @@ import matplotlib as mpl from matplotlib.dates import ( - date2num, num2date, datestr2num, drange, epoch2num, - num2epoch, DateFormatter, IndexDateFormatter, DateLocator, + date2num, num2date, datestr2num, drange, DateFormatter, DateLocator, RRuleLocator, YearLocator, MonthLocator, WeekdayLocator, DayLocator, HourLocator, MinuteLocator, SecondLocator, rrule, MO, TU, WE, TH, FR, SA, SU, YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, SECONDLY, diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 030ae51e7f27..9a4cb27e30dc 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -3,7 +3,8 @@ """ `matplotlib.pyplot` is a state-based interface to matplotlib. It provides -a MATLAB-like way of plotting. +an implicit, MATLAB-like, way of plotting. It also opens figures on your +screen, and acts as the figure GUI manager. pyplot is mainly intended for interactive plots and simple cases of programmatic plot generation:: @@ -15,9 +16,27 @@ y = np.sin(x) plt.plot(x, y) -The object-oriented API is recommended for more complex plots. +The explicit object-oriented API is recommended for complex plots, though +pyplot is still usually used to create the figure and often the axes in the +figure. See `.pyplot.figure`, `.pyplot.subplots`, and +`.pyplot.subplot_mosaic` to create figures, and +:doc:`Axes API ` for the plotting methods on an Axes:: + + import numpy as np + import matplotlib.pyplot as plt + + x = np.arange(0, 5, 0.1) + y = np.sin(x) + fig, ax = plt.subplots() + ax.plot(x, y) + + +See :ref:`api_interfaces` for an explanation of the tradeoffs between the +implicit and explicit interfaces. """ +from contextlib import ExitStack +from enum import Enum import functools import importlib import inspect @@ -25,11 +44,8 @@ from numbers import Number import re import sys +import threading import time -try: - import threading -except ImportError: - import dummy_threading as threading from cycler import cycler import matplotlib @@ -39,9 +55,10 @@ from matplotlib import rcsetup, style from matplotlib import _pylab_helpers, interactive from matplotlib import cbook -from matplotlib import docstring -from matplotlib.backend_bases import FigureCanvasBase, MouseButton -from matplotlib.figure import Figure, figaspect +from matplotlib import _docstring +from matplotlib.backend_bases import ( + FigureCanvasBase, FigureManagerBase, MouseButton) +from matplotlib.figure import Figure, FigureBase, figaspect from matplotlib.gridspec import GridSpec, SubplotSpec from matplotlib import rcParams, rcParamsDefault, get_backend, rcParamsOrig from matplotlib.rcsetup import interactive_bk as _interactive_bk @@ -52,7 +69,8 @@ from matplotlib.scale import get_scale_names from matplotlib import cm -from matplotlib.cm import get_cmap, register_cmap +from matplotlib.cm import _colormaps as colormaps, register_cmap +from matplotlib.colors import _color_sequences as color_sequences import numpy as np @@ -61,7 +79,7 @@ from matplotlib.lines import Line2D from matplotlib.text import Text, Annotation from matplotlib.patches import Polygon, Rectangle, Circle, Arrow -from matplotlib.widgets import SubplotTool, Button, Slider, Widget +from matplotlib.widgets import Button, Slider, Widget from .ticker import ( TickHelper, Formatter, FixedFormatter, NullFormatter, FuncFormatter, @@ -72,30 +90,17 @@ _log = logging.getLogger(__name__) -_code_objs = { - _api.rename_parameter: - _api.rename_parameter("", "old", "new", lambda new: None).__code__, - _api.make_keyword_only: - _api.make_keyword_only("", "p", lambda p: None).__code__, -} - - def _copy_docstring_and_deprecators(method, func=None): if func is None: return functools.partial(_copy_docstring_and_deprecators, method) - decorators = [docstring.copy(method)] + decorators = [_docstring.copy(method)] # Check whether the definition of *method* includes @_api.rename_parameter # or @_api.make_keyword_only decorators; if so, propagate them to the # pyplot wrapper as well. while getattr(method, "__wrapped__", None) is not None: - for decorator_maker, code in _code_objs.items(): - if method.__code__ is code: - kwargs = { - k: v.cell_contents - for k, v in zip(code.co_freevars, method.__closure__)} - assert kwargs["func"] is method.__wrapped__ - kwargs.pop("func") - decorators.append(decorator_maker(**kwargs)) + decorator = _api.deprecation.DECORATORS.get(method) + if decorator: + decorators.append(decorator) method = method.__wrapped__ for decorator in decorators[::-1]: func = decorator(func) @@ -105,99 +110,67 @@ def _copy_docstring_and_deprecators(method, func=None): ## Global ## -_IP_REGISTERED = None -_INSTALL_FIG_OBSERVER = False +# The state controlled by {,un}install_repl_displayhook(). +_ReplDisplayHook = Enum("_ReplDisplayHook", ["NONE", "PLAIN", "IPYTHON"]) +_REPL_DISPLAYHOOK = _ReplDisplayHook.NONE + + +def _draw_all_if_interactive(): + if matplotlib.is_interactive(): + draw_all() def install_repl_displayhook(): """ - Install a repl display hook so that any stale figure are automatically - redrawn when control is returned to the repl. + Connect to the display hook of the current shell. + + The display hook gets called when the read-evaluate-print-loop (REPL) of + the shell has finished the execution of a command. We use this callback + to be able to automatically update a figure in interactive mode. This works both with IPython and with vanilla python shells. """ - global _IP_REGISTERED - global _INSTALL_FIG_OBSERVER - - class _NotIPython(Exception): - pass - - # see if we have IPython hooks around, if use them - - try: - if 'IPython' in sys.modules: - from IPython import get_ipython - ip = get_ipython() - if ip is None: - raise _NotIPython() - - if _IP_REGISTERED: - return - - def post_execute(): - if matplotlib.is_interactive(): - draw_all() + global _REPL_DISPLAYHOOK - # IPython >= 2 - try: - ip.events.register('post_execute', post_execute) - except AttributeError: - # IPython 1.x - ip.register_post_execute(post_execute) + if _REPL_DISPLAYHOOK is _ReplDisplayHook.IPYTHON: + return - _IP_REGISTERED = post_execute - _INSTALL_FIG_OBSERVER = False + # See if we have IPython hooks around, if so use them. + # Use ``sys.modules.get(name)`` rather than ``name in sys.modules`` as + # entries can also have been explicitly set to None. + mod_ipython = sys.modules.get("IPython") + if not mod_ipython: + _REPL_DISPLAYHOOK = _ReplDisplayHook.PLAIN + return + ip = mod_ipython.get_ipython() + if not ip: + _REPL_DISPLAYHOOK = _ReplDisplayHook.PLAIN + return - # trigger IPython's eventloop integration, if available - from IPython.core.pylabtools import backend2gui - - ipython_gui_name = backend2gui.get(get_backend()) - if ipython_gui_name: - ip.enable_gui(ipython_gui_name) - else: - _INSTALL_FIG_OBSERVER = True + ip.events.register("post_execute", _draw_all_if_interactive) + _REPL_DISPLAYHOOK = _ReplDisplayHook.IPYTHON - # import failed or ipython is not running - except (ImportError, _NotIPython): - _INSTALL_FIG_OBSERVER = True + from IPython.core.pylabtools import backend2gui + # trigger IPython's eventloop integration, if available + ipython_gui_name = backend2gui.get(get_backend()) + if ipython_gui_name: + ip.enable_gui(ipython_gui_name) def uninstall_repl_displayhook(): - """ - Uninstall the matplotlib display hook. - - .. warning:: - - Need IPython >= 2 for this to work. For IPython < 2 will raise a - ``NotImplementedError`` - - .. warning:: - - If you are using vanilla python and have installed another - display hook this will reset ``sys.displayhook`` to what ever - function was there when matplotlib installed it's displayhook, - possibly discarding your changes. - """ - global _IP_REGISTERED - global _INSTALL_FIG_OBSERVER - if _IP_REGISTERED: + """Disconnect from the display hook of the current shell.""" + global _REPL_DISPLAYHOOK + if _REPL_DISPLAYHOOK is _ReplDisplayHook.IPYTHON: from IPython import get_ipython ip = get_ipython() - try: - ip.events.unregister('post_execute', _IP_REGISTERED) - except AttributeError as err: - raise NotImplementedError("Can not unregister events " - "in IPython < 2.0") from err - _IP_REGISTERED = None - - if _INSTALL_FIG_OBSERVER: - _INSTALL_FIG_OBSERVER = False + ip.events.unregister("post_execute", _draw_all_if_interactive) + _REPL_DISPLAYHOOK = _ReplDisplayHook.NONE draw_all = _pylab_helpers.Gcf.draw_all -@functools.wraps(matplotlib.set_loglevel) +@_copy_docstring_and_deprecators(matplotlib.set_loglevel) def set_loglevel(*args, **kwargs): # Ensure this appears in the pyplot docs. return matplotlib.set_loglevel(*args, **kwargs) @@ -210,33 +183,58 @@ def findobj(o=None, match=None, include_self=True): def _get_required_interactive_framework(backend_mod): - return getattr( - backend_mod.FigureCanvas, "required_interactive_framework", None) + if not hasattr(getattr(backend_mod, "FigureCanvas", None), + "required_interactive_framework"): + _api.warn_deprecated( + "3.6", name="Support for FigureCanvases without a " + "required_interactive_framework attribute") + return None + # Inline this once the deprecation elapses. + return backend_mod.FigureCanvas.required_interactive_framework + +_backend_mod = None + + +def _get_backend_mod(): + """ + Ensure that a backend is selected and return it. + + This is currently private, but may be made public in the future. + """ + if _backend_mod is None: + # Use rcParams._get("backend") to avoid going through the fallback + # logic (which will (re)import pyplot and then call switch_backend if + # we need to resolve the auto sentinel) + switch_backend(rcParams._get("backend")) + return _backend_mod def switch_backend(newbackend): """ - Close all open figures and set the Matplotlib backend. + Set the pyplot backend. + + Switching to an interactive backend is possible only if no event loop for + another interactive backend has started. Switching to and from + non-interactive backends is always possible. - The argument is case-insensitive. Switching to an interactive backend is - possible only if no event loop for another interactive backend has started. - Switching to and from non-interactive backends is always possible. + If the new backend is different than the current backend then all open + Figures will be closed via ``plt.close('all')``. Parameters ---------- newbackend : str - The name of the backend to use. + The case-insensitive name of the backend to use. + """ global _backend_mod # make sure the init is pulled up so we can assign to it later import matplotlib.backends - close("all") if newbackend is rcsetup._auto_backend_sentinel: current_framework = cbook._get_running_interactive_framework() - mapping = {'qt5': 'qt5agg', - 'qt4': 'qt4agg', + mapping = {'qt': 'qtagg', 'gtk3': 'gtk3agg', + 'gtk4': 'gtk4agg', 'wx': 'wxagg', 'tk': 'tkagg', 'macosx': 'macosx', @@ -247,7 +245,8 @@ def switch_backend(newbackend): candidates = [best_guess] else: candidates = [] - candidates += ["macosx", "qt5agg", "gtk3agg", "tkagg", "wxagg"] + candidates += [ + "macosx", "qtagg", "gtk4agg", "gtk3agg", "tkagg", "wxagg"] # Don't try to fallback on the cairo-based backends as they each have # an additional dependency (pycairo) over the agg-based backend, and @@ -266,16 +265,11 @@ def switch_backend(newbackend): switch_backend("agg") rcParamsOrig["backend"] = "agg" return + # have to escape the switch on access logic + old_backend = dict.__getitem__(rcParams, 'backend') - # Backends are implemented as modules, but "inherit" default method - # implementations from backend_bases._Backend. This is achieved by - # creating a "class" that inherits from backend_bases._Backend and whose - # body is filled with the module's globals. - - backend_name = cbook._backend_module_name(newbackend) - - class backend_mod(matplotlib.backend_bases._Backend): - locals().update(vars(importlib.import_module(backend_name))) + backend_mod = importlib.import_module( + cbook._backend_module_name(newbackend)) required_framework = _get_required_interactive_framework(backend_mod) if required_framework is not None: @@ -287,6 +281,61 @@ class backend_mod(matplotlib.backend_bases._Backend): "framework, as {!r} is currently running".format( newbackend, required_framework, current_framework)) + # Load the new_figure_manager() and show() functions from the backend. + + # Classically, backends can directly export these functions. This should + # keep working for backcompat. + new_figure_manager = getattr(backend_mod, "new_figure_manager", None) + show = getattr(backend_mod, "show", None) + + # In that classical approach, backends are implemented as modules, but + # "inherit" default method implementations from backend_bases._Backend. + # This is achieved by creating a "class" that inherits from + # backend_bases._Backend and whose body is filled with the module globals. + class backend_mod(matplotlib.backend_bases._Backend): + locals().update(vars(backend_mod)) + + # However, the newer approach for defining new_figure_manager and + # show is to derive them from canvas methods. In that case, also + # update backend_mod accordingly; also, per-backend customization of + # draw_if_interactive is disabled. + if new_figure_manager is None: + # Only try to get the canvas class if have opted into the new scheme. + canvas_class = backend_mod.FigureCanvas + + def new_figure_manager_given_figure(num, figure): + return canvas_class.new_manager(figure, num) + + def new_figure_manager(num, *args, FigureClass=Figure, **kwargs): + fig = FigureClass(*args, **kwargs) + return new_figure_manager_given_figure(num, fig) + + def draw_if_interactive(): + if matplotlib.is_interactive(): + manager = _pylab_helpers.Gcf.get_active() + if manager: + manager.canvas.draw_idle() + + backend_mod.new_figure_manager_given_figure = \ + new_figure_manager_given_figure + backend_mod.new_figure_manager = new_figure_manager + backend_mod.draw_if_interactive = draw_if_interactive + + # If the manager explicitly overrides pyplot_show, use it even if a global + # show is already present, as the latter may be here for backcompat. + manager_class = getattr(getattr(backend_mod, "FigureCanvas", None), + "manager_class", None) + # We can't compare directly manager_class.pyplot_show and FMB.pyplot_show because + # pyplot_show is a classmethod so the above constructs are bound classmethods, and + # thus always different (being bound to different classes). We also have to use + # getattr_static instead of vars as manager_class could have no __dict__. + manager_pyplot_show = inspect.getattr_static(manager_class, "pyplot_show", None) + base_pyplot_show = inspect.getattr_static(FigureManagerBase, "pyplot_show", None) + if (show is None + or (manager_pyplot_show is not None + and manager_pyplot_show != base_pyplot_show)): + backend_mod.show = manager_class.pyplot_show + _log.debug("Loaded backend %s version %s.", newbackend, backend_mod.backend_version) @@ -299,11 +348,30 @@ class backend_mod(matplotlib.backend_bases._Backend): # Need to keep a global reference to the backend for compatibility reasons. # See https://github.com/matplotlib/matplotlib/issues/6092 matplotlib.backends.backend = newbackend + if not cbook._str_equal(old_backend, newbackend): + close("all") + + # make sure the repl display hook is installed in case we become + # interactive + install_repl_displayhook() def _warn_if_gui_out_of_main_thread(): - if (_get_required_interactive_framework(_backend_mod) - and threading.current_thread() is not threading.main_thread()): + warn = False + if _get_required_interactive_framework(_get_backend_mod()): + if hasattr(threading, 'get_native_id'): + # This compares native thread ids because even if Python-level + # Thread objects match, the underlying OS thread (which is what + # really matters) may be different on Python implementations with + # green threads. + if threading.get_native_id() != threading.main_thread().native_id: + warn = True + else: + # Fall back to Python-level Thread if native IDs are unavailable, + # mainly for PyPy. + if threading.current_thread() is not threading.main_thread(): + warn = True + if warn: _api.warn_external( "Starting a Matplotlib GUI outside of the main thread will likely " "fail.") @@ -313,7 +381,7 @@ def _warn_if_gui_out_of_main_thread(): def new_figure_manager(*args, **kwargs): """Create a new figure manager instance.""" _warn_if_gui_out_of_main_thread() - return _backend_mod.new_figure_manager(*args, **kwargs) + return _get_backend_mod().new_figure_manager(*args, **kwargs) # This function's signature is rewritten upon backend-load by switch_backend. @@ -326,7 +394,7 @@ def draw_if_interactive(*args, **kwargs): End users will typically not have to call this function because the the interactive mode takes care of this. """ - return _backend_mod.draw_if_interactive(*args, **kwargs) + return _get_backend_mod().draw_if_interactive(*args, **kwargs) # This function's signature is rewritten upon backend-load by switch_backend. @@ -375,7 +443,7 @@ def show(*args, **kwargs): explicitly there. """ _warn_if_gui_out_of_main_thread() - return _backend_mod.show(*args, **kwargs) + return _get_backend_mod().show(*args, **kwargs) def isinteractive(): @@ -408,58 +476,6 @@ def isinteractive(): return matplotlib.is_interactive() -class _IoffContext: - """ - Context manager for `.ioff`. - - The state is changed in ``__init__()`` instead of ``__enter__()``. The - latter is a no-op. This allows using `.ioff` both as a function and - as a context. - """ - - def __init__(self): - self.wasinteractive = isinteractive() - matplotlib.interactive(False) - uninstall_repl_displayhook() - - def __enter__(self): - pass - - def __exit__(self, exc_type, exc_value, traceback): - if self.wasinteractive: - matplotlib.interactive(True) - install_repl_displayhook() - else: - matplotlib.interactive(False) - uninstall_repl_displayhook() - - -class _IonContext: - """ - Context manager for `.ion`. - - The state is changed in ``__init__()`` instead of ``__enter__()``. The - latter is a no-op. This allows using `.ion` both as a function and - as a context. - """ - - def __init__(self): - self.wasinteractive = isinteractive() - matplotlib.interactive(True) - install_repl_displayhook() - - def __enter__(self): - pass - - def __exit__(self, exc_type, exc_value, traceback): - if not self.wasinteractive: - matplotlib.interactive(False) - uninstall_repl_displayhook() - else: - matplotlib.interactive(True) - install_repl_displayhook() - - def ioff(): """ Disable interactive mode. @@ -489,11 +505,15 @@ def ioff(): fig2 = plt.figure() # ... - To enable usage as a context manager, this function returns an - ``_IoffContext`` object. The return value is not intended to be stored - or accessed by the user. + To enable optional usage as a context manager, this function returns a + `~contextlib.ExitStack` object, which is not intended to be stored or + accessed by the user. """ - return _IoffContext() + stack = ExitStack() + stack.callback(ion if isinteractive() else ioff) + matplotlib.interactive(False) + uninstall_repl_displayhook() + return stack def ion(): @@ -525,11 +545,15 @@ def ion(): fig2 = plt.figure() # ... - To enable usage as a context manager, this function returns an - ``_IonContext`` object. The return value is not intended to be stored - or accessed by the user. + To enable optional usage as a context manager, this function returns a + `~contextlib.ExitStack` object, which is not intended to be stored or + accessed by the user. """ - return _IonContext() + stack = ExitStack() + stack.callback(ion if isinteractive() else ioff) + matplotlib.interactive(True) + install_repl_displayhook() + return stack def pause(interval): @@ -628,50 +652,43 @@ def xkcd(scale=1, length=100, randomness=2): # This figure will be in regular style fig2 = plt.figure() """ - return _xkcd(scale, length, randomness) - - -class _xkcd: - # This cannot be implemented in terms of rc_context() because this needs to - # work as a non-contextmanager too. - - def __init__(self, scale, length, randomness): - self._orig = rcParams.copy() - - if rcParams['text.usetex']: - raise RuntimeError( - "xkcd mode is not compatible with text.usetex = True") - - from matplotlib import patheffects - rcParams.update({ - 'font.family': ['xkcd', 'xkcd Script', 'Humor Sans', 'Comic Neue', - 'Comic Sans MS'], - 'font.size': 14.0, - 'path.sketch': (scale, length, randomness), - 'path.effects': [ - patheffects.withStroke(linewidth=4, foreground="w")], - 'axes.linewidth': 1.5, - 'lines.linewidth': 2.0, - 'figure.facecolor': 'white', - 'grid.linewidth': 0.0, - 'axes.grid': False, - 'axes.unicode_minus': False, - 'axes.edgecolor': 'black', - 'xtick.major.size': 8, - 'xtick.major.width': 3, - 'ytick.major.size': 8, - 'ytick.major.width': 3, - }) - - def __enter__(self): - return self - - def __exit__(self, *args): - dict.update(rcParams, self._orig) + # This cannot be implemented in terms of contextmanager() or rc_context() + # because this needs to work as a non-contextmanager too. + + if rcParams['text.usetex']: + raise RuntimeError( + "xkcd mode is not compatible with text.usetex = True") + + stack = ExitStack() + stack.callback(dict.update, rcParams, rcParams.copy()) + + from matplotlib import patheffects + rcParams.update({ + 'font.family': ['xkcd', 'xkcd Script', 'Humor Sans', 'Comic Neue', + 'Comic Sans MS'], + 'font.size': 14.0, + 'path.sketch': (scale, length, randomness), + 'path.effects': [ + patheffects.withStroke(linewidth=4, foreground="w")], + 'axes.linewidth': 1.5, + 'lines.linewidth': 2.0, + 'figure.facecolor': 'white', + 'grid.linewidth': 0.0, + 'axes.grid': False, + 'axes.unicode_minus': False, + 'axes.edgecolor': 'black', + 'xtick.major.size': 8, + 'xtick.major.width': 3, + 'ytick.major.size': 8, + 'ytick.major.width': 3, + }) + + return stack ## Figures ## +@_api.make_keyword_only("3.6", "facecolor") def figure(num=None, # autoincrement if None, else integer from 1-N figsize=None, # defaults to rc figure.figsize dpi=None, # defaults to rc figure.dpi @@ -687,7 +704,7 @@ def figure(num=None, # autoincrement if None, else integer from 1-N Parameters ---------- - num : int or str or `.Figure`, optional + num : int or str or `.Figure` or `.SubFigure`, optional A unique identifier for the figure. If a figure with that identifier already exists, this figure is made @@ -699,7 +716,8 @@ def figure(num=None, # autoincrement if None, else integer from 1-N will be used for the ``Figure.number`` attribute, otherwise, an auto-generated integer value is used (starting at 1 and incremented for each new figure). If *num* is a string, the figure label and the - window title is set to this value. + window title is set to this value. If num is a ``SubFigure``, its + parent ``Figure`` is activated. figsize : (float, float), default: :rc:`figure.figsize` Width, height in inches. @@ -717,40 +735,64 @@ def figure(num=None, # autoincrement if None, else integer from 1-N If False, suppress drawing the figure frame. FigureClass : subclass of `~matplotlib.figure.Figure` - Optionally use a custom `.Figure` instance. + If set, an instance of this subclass will be created, rather than a + plain `.Figure`. clear : bool, default: False If True and the figure already exists, then it is cleared. - tight_layout : bool or dict, default: :rc:`figure.autolayout` - If ``False`` use *subplotpars*. If ``True`` adjust subplot - parameters using `.tight_layout` with default padding. - When providing a dict containing the keys ``pad``, ``w_pad``, - ``h_pad``, and ``rect``, the default `.tight_layout` paddings - will be overridden. + layout : {'constrained', 'compressed', 'tight', 'none', `.LayoutEngine`, None}, \ +default: None + The layout mechanism for positioning of plot elements to avoid + overlapping Axes decorations (labels, ticks, etc). Note that layout + managers can measurably slow down figure display. + + - 'constrained': The constrained layout solver adjusts axes sizes + to avoid overlapping axes decorations. Can handle complex plot + layouts and colorbars, and is thus recommended. - constrained_layout : bool, default: :rc:`figure.constrained_layout.use` - If ``True`` use constrained layout to adjust positioning of plot - elements. Like ``tight_layout``, but designed to be more - flexible. See - :doc:`/tutorials/intermediate/constrainedlayout_guide` - for examples. (Note: does not work with `add_subplot` or - `~.pyplot.subplot2grid`.) + See :doc:`/tutorials/intermediate/constrainedlayout_guide` + for examples. + - 'compressed': uses the same algorithm as 'constrained', but + removes extra space between fixed-aspect-ratio Axes. Best for + simple grids of axes. - **kwargs : optional - See `~.matplotlib.figure.Figure` for other possible arguments. + - 'tight': Use the tight layout mechanism. This is a relatively + simple algorithm that adjusts the subplot parameters so that + decorations do not overlap. See `.Figure.set_tight_layout` for + further details. + + - 'none': Do not use a layout engine. + + - A `.LayoutEngine` instance. Builtin layout classes are + `.ConstrainedLayoutEngine` and `.TightLayoutEngine`, more easily + accessible by 'constrained' and 'tight'. Passing an instance + allows third parties to provide their own layout engine. + + If not given, fall back to using the parameters *tight_layout* and + *constrained_layout*, including their config defaults + :rc:`figure.autolayout` and :rc:`figure.constrained_layout.use`. + + **kwargs + Additional keyword arguments are passed to the `.Figure` constructor. Returns ------- `~matplotlib.figure.Figure` - The `.Figure` instance returned will also be passed to - new_figure_manager in the backends, which allows to hook custom - `.Figure` classes into the pyplot interface. Additional kwargs will be - passed to the `.Figure` init function. Notes ----- + A newly created figure is passed to the `~.FigureCanvasBase.new_manager` + method or the `new_figure_manager` function provided by the current + backend, which install a canvas and a manager on the figure. + + Once this is done, :rc:`figure.hooks` are called, one at a time, on the + figure; these hooks allow arbitrary customization of the figure (e.g., + attaching callbacks) or of associated elements (e.g., modifying the + toolbar). See :doc:`/gallery/user_interfaces/mplcvd` for an example of + toolbar customization. + If you are creating many figures, make sure you explicitly call `.pyplot.close` on the figures you are not using, because this will enable pyplot to properly clean up the memory. @@ -758,11 +800,11 @@ def figure(num=None, # autoincrement if None, else integer from 1-N `~matplotlib.rcParams` defines the default values, which can be modified in the matplotlibrc file. """ - if isinstance(num, Figure): + if isinstance(num, FigureBase): if num.canvas.manager is None: raise ValueError("The passed figure is not managed by pyplot") _pylab_helpers.Gcf.set_active(num.canvas.manager) - return num + return num.figure allnums = get_fignums() next_num = max(allnums) + 1 if allnums else 1 @@ -791,7 +833,8 @@ def figure(num=None, # autoincrement if None, else integer from 1-N f"Figures created through the pyplot interface " f"(`matplotlib.pyplot.figure`) are retained until explicitly " f"closed and may consume too much memory. (To control this " - f"warning, see the rcParam `figure.max_open_warning`).", + f"warning, see the rcParam `figure.max_open_warning`). " + f"Consider using `matplotlib.pyplot.close()`.", RuntimeWarning) manager = new_figure_manager( @@ -802,6 +845,13 @@ def figure(num=None, # autoincrement if None, else integer from 1-N if fig_label: fig.set_label(fig_label) + for hookspecs in rcParams["figure.hooks"]: + module_name, dotted_name = hookspecs.split(":") + obj = importlib.import_module(module_name) + for part in dotted_name.split("."): + obj = getattr(obj, part) + obj(fig) + _pylab_helpers.Gcf._set_new_active_manager(manager) # make sure backends (inline) that we don't ship that expect this @@ -810,7 +860,7 @@ def figure(num=None, # autoincrement if None, else integer from 1-N # FigureManager base class. draw_if_interactive() - if _INSTALL_FIG_OBSERVER: + if _REPL_DISPLAYHOOK is _ReplDisplayHook.PLAIN: fig.stale_callback = _auto_draw_if_interactive if clear: @@ -844,8 +894,10 @@ def gcf(): """ Get the current figure. - If no current figure exists, a new one is created using - `~.pyplot.figure()`. + If there is currently no figure on the pyplot figure stack, a new one is + created using `~.pyplot.figure()`. (To test whether there is currently a + figure on the pyplot figure stack, check whether `~.pyplot.get_fignums()` + is empty.) """ manager = _pylab_helpers.Gcf.get_active() if manager is not None: @@ -942,7 +994,7 @@ def close(fig=None): def clf(): """Clear the current figure.""" - gcf().clf() + gcf().clear() def draw(): @@ -956,6 +1008,11 @@ def draw(): This is equivalent to calling ``fig.canvas.draw_idle()``, where ``fig`` is the current figure. + + See Also + -------- + .FigureCanvasBase.draw_idle + .FigureCanvasBase.draw """ gcf().canvas.draw_idle() @@ -964,7 +1021,7 @@ def draw(): def savefig(*args, **kwargs): fig = gcf() res = fig.savefig(*args, **kwargs) - fig.canvas.draw_idle() # need this if 'transparent=True' to reset colors + fig.canvas.draw_idle() # Need this if 'transparent=True', to reset colors. return res @@ -974,15 +1031,18 @@ def savefig(*args, **kwargs): def figlegend(*args, **kwargs): return gcf().legend(*args, **kwargs) if Figure.legend.__doc__: - figlegend.__doc__ = Figure.legend.__doc__.replace("legend(", "figlegend(") + figlegend.__doc__ = Figure.legend.__doc__ \ + .replace(" legend(", " figlegend(") \ + .replace("fig.legend(", "plt.figlegend(") \ + .replace("ax.plot(", "plt.plot(") ## Axes ## -@docstring.dedent_interpd +@_docstring.dedent_interpd def axes(arg=None, **kwargs): """ - Add an axes to the current figure and make it the current axes. + Add an Axes to the current figure and make it the current Axes. Call signatures:: @@ -995,10 +1055,10 @@ def axes(arg=None, **kwargs): arg : None or 4-tuple The exact behavior of this function depends on the type: - - *None*: A new full window axes is added using + - *None*: A new full window Axes is added using ``subplot(**kwargs)``. - 4-tuple of floats *rect* = ``[left, bottom, width, height]``. - A new axes is added with dimensions *rect* in normalized + A new Axes is added with dimensions *rect* in normalized (0, 1) units using `~.Figure.add_axes` on the current figure. projection : {None, 'aitoff', 'hammer', 'lambert', 'mollweide', \ @@ -1013,10 +1073,10 @@ def axes(arg=None, **kwargs): sharex, sharey : `~.axes.Axes`, optional Share the x or y `~matplotlib.axis` with sharex and/or sharey. The axis will have the same limits, ticks, and scale as the axis - of the shared axes. + of the shared Axes. label : str - A label for the returned axes. + A label for the returned Axes. Returns ------- @@ -1029,24 +1089,13 @@ def axes(arg=None, **kwargs): ---------------- **kwargs This method also takes the keyword arguments for - the returned axes class. The keyword arguments for the - rectilinear axes class `~.axes.Axes` can be found in + the returned Axes class. The keyword arguments for the + rectilinear Axes class `~.axes.Axes` can be found in the following table but there might also be other keyword - arguments if another projection is used, see the actual axes + arguments if another projection is used, see the actual Axes class. - %(Axes_kwdoc)s - - Notes - ----- - If the figure already has a axes with key (*args*, - *kwargs*) then it will simply make that axes current and - return it. This behavior is deprecated. Meanwhile, if you do - not want this behavior (i.e., you want to force the creation of a - new axes), you must use a unique set of args and kwargs. The axes - *label* attribute has been exposed for this purpose: if you want - two axes that are otherwise identical to be added to the figure, - make sure you give them unique labels. + %(Axes:kwdoc)s See Also -------- @@ -1060,15 +1109,19 @@ def axes(arg=None, **kwargs): -------- :: - # Creating a new full window axes + # Creating a new full window Axes plt.axes() - # Creating a new axes with specified dimensions and some kwargs - plt.axes((left, bottom, width, height), facecolor='w') + # Creating a new Axes with specified dimensions and a grey background + plt.axes((left, bottom, width, height), facecolor='grey') """ fig = gcf() + pos = kwargs.pop('position', None) if arg is None: - return fig.add_subplot(**kwargs) + if pos is None: + return fig.add_subplot(**kwargs) + else: + return fig.add_axes(pos, **kwargs) else: return fig.add_axes(arg, **kwargs) @@ -1098,7 +1151,7 @@ def cla(): ## More ways of creating axes ## -@docstring.dedent_interpd +@_docstring.dedent_interpd def subplot(*args, **kwargs): """ Add an Axes to the current figure or retrieve an existing Axes. @@ -1150,13 +1203,11 @@ def subplot(*args, **kwargs): Returns ------- - `.axes.SubplotBase`, or another subclass of `~.axes.Axes` + `~.axes.Axes` - The axes of the subplot. The returned axes base class depends on - the projection used. It is `~.axes.Axes` if rectilinear projection - is used and `.projections.polar.PolarAxes` if polar projection - is used. The returned axes is then a subplot subclass of the - base class. + The Axes of the subplot. The returned Axes can actually be an instance + of a subclass, such as `.projections.polar.PolarAxes` for polar + projections. Other Parameters ---------------- @@ -1167,11 +1218,11 @@ def subplot(*args, **kwargs): the following table but there might also be other keyword arguments if another projection is used. - %(Axes_kwdoc)s + %(Axes:kwdoc)s Notes ----- - Creating a new Axes will delete any pre-existing Axes that + Creating a new Axes will delete any preexisting Axes that overlaps with it beyond sharing a boundary:: import matplotlib.pyplot as plt @@ -1272,8 +1323,8 @@ def subplot(*args, **kwargs): key = SubplotSpec._from_subplot_args(fig, args) for ax in fig.axes: - # if we found an axes at the position sort out if we can re-use it - if hasattr(ax, 'get_subplotspec') and ax.get_subplotspec() == key: + # if we found an Axes at the position sort out if we can re-use it + if ax.get_subplotspec() == key: # if the user passed no kwargs, re-use if kwargs == {}: break @@ -1288,21 +1339,21 @@ def subplot(*args, **kwargs): fig.sca(ax) - bbox = ax.bbox - axes_to_delete = [] - for other_ax in fig.axes: - if other_ax == ax: - continue - if bbox.fully_overlaps(other_ax.bbox): - axes_to_delete.append(other_ax) + axes_to_delete = [other for other in fig.axes + if other != ax and ax.bbox.fully_overlaps(other.bbox)] + if axes_to_delete: + _api.warn_deprecated( + "3.6", message="Auto-removal of overlapping axes is deprecated " + "since %(since)s and will be removed %(removal)s; explicitly call " + "ax.remove() as needed.") for ax_to_del in axes_to_delete: delaxes(ax_to_del) return ax -@_api.make_keyword_only("3.3", "sharex") -def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, +def subplots(nrows=1, ncols=1, *, sharex=False, sharey=False, squeeze=True, + width_ratios=None, height_ratios=None, subplot_kw=None, gridspec_kw=None, **fig_kw): """ Create a figure and a set of subplots. @@ -1348,6 +1399,18 @@ def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, always a 2D array containing Axes instances, even if it ends up being 1x1. + width_ratios : array-like of length *ncols*, optional + Defines the relative widths of the columns. Each column gets a + relative width of ``width_ratios[i] / sum(width_ratios)``. + If not given, all columns will have the same width. Equivalent + to ``gridspec_kw={'width_ratios': [...]}``. + + height_ratios : array-like of length *nrows*, optional + Defines the relative heights of the rows. Each row gets a + relative height of ``height_ratios[i] / sum(height_ratios)``. + If not given, all rows will have the same height. Convenience + for ``gridspec_kw={'height_ratios': [...]}``. + subplot_kw : dict, optional Dict with keywords passed to the `~matplotlib.figure.Figure.add_subplot` call used to create each @@ -1363,13 +1426,12 @@ def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, Returns ------- - fig : `~.figure.Figure` + fig : `.Figure` - ax : `.axes.Axes` or array of Axes - *ax* can be either a single `~matplotlib.axes.Axes` object or an - array of Axes objects if more than one subplot was created. The - dimensions of the resulting array can be controlled with the squeeze - keyword, see above. + ax : `~.axes.Axes` or array of Axes + *ax* can be either a single `~.axes.Axes` object, or an array of Axes + objects if more than one subplot was created. The dimensions of the + resulting array can be controlled with the squeeze keyword, see above. Typical idioms for handling the return value are:: @@ -1439,22 +1501,22 @@ def subplots(nrows=1, ncols=1, sharex=False, sharey=False, squeeze=True, fig = figure(**fig_kw) axs = fig.subplots(nrows=nrows, ncols=ncols, sharex=sharex, sharey=sharey, squeeze=squeeze, subplot_kw=subplot_kw, - gridspec_kw=gridspec_kw) + gridspec_kw=gridspec_kw, height_ratios=height_ratios, + width_ratios=width_ratios) return fig, axs -def subplot_mosaic(mosaic, *, subplot_kw=None, gridspec_kw=None, - empty_sentinel='.', **fig_kw): +def subplot_mosaic(mosaic, *, sharex=False, sharey=False, + width_ratios=None, height_ratios=None, empty_sentinel='.', + subplot_kw=None, gridspec_kw=None, + per_subplot_kw=None, **fig_kw): """ Build a layout of Axes based on ASCII art or nested lists. This is a helper function to build complex GridSpec layouts visually. - .. note :: - - This API is provisional and may be revised in the future based on - early user feedback. - + See :doc:`/gallery/subplots_axes_and_figures/mosaic` + for an example and full API documentation Parameters ---------- @@ -1466,7 +1528,7 @@ def subplot_mosaic(mosaic, *, subplot_kw=None, gridspec_kw=None, x = [['A panel', 'A panel', 'edge'], ['C panel', '.', 'edge']] - Produces 4 axes: + produces 4 axes: - 'A panel' which is 1 row high and spans the first two columns - 'edge' which is 2 rows high and is on the right edge @@ -1487,13 +1549,23 @@ def subplot_mosaic(mosaic, *, subplot_kw=None, gridspec_kw=None, This only allows only single character Axes labels and does not allow nesting but is very terse. - subplot_kw : dict, optional - Dictionary with keywords passed to the `.Figure.add_subplot` call - used to create each subplot. + sharex, sharey : bool, default: False + If True, the x-axis (*sharex*) or y-axis (*sharey*) will be shared + among all subplots. In that case, tick label visibility and axis units + behave as for `subplots`. If False, each subplot's x- or y-axis will + be independent. - gridspec_kw : dict, optional - Dictionary with keywords passed to the `.GridSpec` constructor used - to create the grid the subplots are placed on. + width_ratios : array-like of length *ncols*, optional + Defines the relative widths of the columns. Each column gets a + relative width of ``width_ratios[i] / sum(width_ratios)``. + If not given, all columns will have the same width. Convenience + for ``gridspec_kw={'width_ratios': [...]}``. + + height_ratios : array-like of length *nrows*, optional + Defines the relative heights of the rows. Each row gets a + relative height of ``height_ratios[i] / sum(height_ratios)``. + If not given, all rows will have the same height. Convenience + for ``gridspec_kw={'height_ratios': [...]}``. empty_sentinel : object, optional Entry in the layout to mean "leave this space empty". Defaults @@ -1501,13 +1573,35 @@ def subplot_mosaic(mosaic, *, subplot_kw=None, gridspec_kw=None, `inspect.cleandoc` to remove leading white space, which may interfere with using white-space as the empty sentinel. + subplot_kw : dict, optional + Dictionary with keywords passed to the `.Figure.add_subplot` call + used to create each subplot. These values may be overridden by + values in *per_subplot_kw*. + + per_subplot_kw : dict, optional + A dictionary mapping the Axes identifiers or tuples of identifiers + to a dictionary of keyword arguments to be passed to the + `.Figure.add_subplot` call used to create each subplot. The values + in these dictionaries have precedence over the values in + *subplot_kw*. + + If *mosaic* is a string, and thus all keys are single characters, + it is possible to use a single string instead of a tuple as keys; + i.e. ``"AB"`` is equivalent to ``("A", "B")``. + + .. versionadded:: 3.7 + + gridspec_kw : dict, optional + Dictionary with keywords passed to the `.GridSpec` constructor used + to create the grid the subplots are placed on. + **fig_kw All additional keyword arguments are passed to the `.pyplot.figure` call. Returns ------- - fig : `~.figure.Figure` + fig : `.Figure` The new figure dict[label, Axes] @@ -1518,10 +1612,11 @@ def subplot_mosaic(mosaic, *, subplot_kw=None, gridspec_kw=None, """ fig = figure(**fig_kw) ax_dict = fig.subplot_mosaic( - mosaic, - subplot_kw=subplot_kw, - gridspec_kw=gridspec_kw, - empty_sentinel=empty_sentinel + mosaic, sharex=sharex, sharey=sharey, + height_ratios=height_ratios, width_ratios=width_ratios, + subplot_kw=subplot_kw, gridspec_kw=gridspec_kw, + empty_sentinel=empty_sentinel, + per_subplot_kw=per_subplot_kw, ) return fig, ax_dict @@ -1547,12 +1642,11 @@ def subplot2grid(shape, loc, rowspan=1, colspan=1, fig=None, **kwargs): Returns ------- - `.axes.SubplotBase`, or another subclass of `~.axes.Axes` + `~.axes.Axes` - The axes of the subplot. The returned axes base class depends on the - projection used. It is `~.axes.Axes` if rectilinear projection is used - and `.projections.polar.PolarAxes` if polar projection is used. The - returned axes is then a subplot subclass of the base class. + The Axes of the subplot. The returned Axes can actually be an instance + of a subclass, such as `.projections.polar.PolarAxes` for polar + projections. Notes ----- @@ -1575,13 +1669,14 @@ def subplot2grid(shape, loc, rowspan=1, colspan=1, fig=None, **kwargs): subplotspec = gs.new_subplotspec(loc, rowspan=rowspan, colspan=colspan) ax = fig.add_subplot(subplotspec, **kwargs) - bbox = ax.bbox - axes_to_delete = [] - for other_ax in fig.axes: - if other_ax == ax: - continue - if bbox.fully_overlaps(other_ax.bbox): - axes_to_delete.append(other_ax) + + axes_to_delete = [other for other in fig.axes + if other != ax and ax.bbox.fully_overlaps(other.bbox)] + if axes_to_delete: + _api.warn_deprecated( + "3.6", message="Auto-removal of overlapping axes is deprecated " + "since %(since)s and will be removed %(removal)s; explicitly call " + "ax.remove() as needed.") for ax_to_del in axes_to_delete: delaxes(ax_to_del) @@ -1624,41 +1719,20 @@ def subplot_tool(targetfig=None): """ Launch a subplot tool window for a figure. - A `matplotlib.widgets.SubplotTool` instance is returned. You must maintain - a reference to the instance to keep the associated callbacks alive. + Returns + ------- + `matplotlib.widgets.SubplotTool` """ if targetfig is None: targetfig = gcf() - with rc_context({"toolbar": "none"}): # No navbar for the toolfig. - # Use new_figure_manager() instead of figure() so that the figure - # doesn't get registered with pyplot. - manager = new_figure_manager(-1, (6, 3)) - manager.set_window_title("Subplot configuration tool") - tool_fig = manager.canvas.figure - tool_fig.subplots_adjust(top=0.9) - manager.show() - return SubplotTool(targetfig, tool_fig) - - -# After deprecation elapses, this can be autogenerated by boilerplate.py. -@_api.make_keyword_only("3.3", "pad") -def tight_layout(pad=1.08, h_pad=None, w_pad=None, rect=None): - """ - Adjust the padding between and around subplots. - - Parameters - ---------- - pad : float, default: 1.08 - Padding between the figure edge and the edges of subplots, - as a fraction of the font size. - h_pad, w_pad : float, default: *pad* - Padding (height/width) between edges of adjacent subplots, - as a fraction of the font size. - rect : tuple (left, bottom, right, top), default: (0, 0, 1, 1) - A rectangle in normalized figure coordinates into which the whole - subplots area (including labels) will fit. - """ - gcf().tight_layout(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) + tb = targetfig.canvas.manager.toolbar + if hasattr(tb, "configure_subplots"): # toolbar2 + return tb.configure_subplots() + elif hasattr(tb, "trigger_tool"): # toolmanager + return tb.trigger_tool("subplots") + else: + raise ValueError("subplot_tool can only be launched for figures with " + "an associated toolbar") def box(on=None): @@ -1758,7 +1832,7 @@ def ylim(*args, **kwargs): return ret -def xticks(ticks=None, labels=None, **kwargs): +def xticks(ticks=None, labels=None, *, minor=False, **kwargs): """ Get or set the current tick locations and labels of the x-axis. @@ -1771,6 +1845,9 @@ def xticks(ticks=None, labels=None, **kwargs): labels : array-like, optional The labels to place at the given *ticks* locations. This argument can only be passed if *ticks* is passed as well. + minor : bool, default: False + If ``False``, get/set the major ticks/labels; if ``True``, the minor + ticks/labels. **kwargs `.Text` properties can be used to control the appearance of the labels. @@ -1801,24 +1878,24 @@ def xticks(ticks=None, labels=None, **kwargs): ax = gca() if ticks is None: - locs = ax.get_xticks() + locs = ax.get_xticks(minor=minor) if labels is not None: raise TypeError("xticks(): Parameter 'labels' can't be set " "without setting 'ticks'") else: - locs = ax.set_xticks(ticks) + locs = ax.set_xticks(ticks, minor=minor) if labels is None: - labels = ax.get_xticklabels() + labels = ax.get_xticklabels(minor=minor) + for l in labels: + l._internal_update(kwargs) else: - labels = ax.set_xticklabels(labels, **kwargs) - for l in labels: - l.update(kwargs) + labels = ax.set_xticklabels(labels, minor=minor, **kwargs) return locs, labels -def yticks(ticks=None, labels=None, **kwargs): +def yticks(ticks=None, labels=None, *, minor=False, **kwargs): """ Get or set the current tick locations and labels of the y-axis. @@ -1831,6 +1908,9 @@ def yticks(ticks=None, labels=None, **kwargs): labels : array-like, optional The labels to place at the given *ticks* locations. This argument can only be passed if *ticks* is passed as well. + minor : bool, default: False + If ``False``, get/set the major ticks/labels; if ``True``, the minor + ticks/labels. **kwargs `.Text` properties can be used to control the appearance of the labels. @@ -1861,19 +1941,19 @@ def yticks(ticks=None, labels=None, **kwargs): ax = gca() if ticks is None: - locs = ax.get_yticks() + locs = ax.get_yticks(minor=minor) if labels is not None: raise TypeError("yticks(): Parameter 'labels' can't be set " "without setting 'ticks'") else: - locs = ax.set_yticks(ticks) + locs = ax.set_yticks(ticks, minor=minor) if labels is None: - labels = ax.get_yticklabels() + labels = ax.get_yticklabels(minor=minor) + for l in labels: + l._internal_update(kwargs) else: - labels = ax.set_yticklabels(labels, **kwargs) - for l in labels: - l.update(kwargs) + labels = ax.set_yticklabels(labels, minor=minor, **kwargs) return locs, labels @@ -1918,7 +1998,7 @@ def rgrids(radii=None, labels=None, angle=None, fmt=None, **kwargs): Other Parameters ---------------- **kwargs - *kwargs* are optional `~.Text` properties for the labels. + *kwargs* are optional `.Text` properties for the labels. See Also -------- @@ -1986,7 +2066,7 @@ def thetagrids(angles=None, labels=None, fmt=None, **kwargs): Other Parameters ---------------- **kwargs - *kwargs* are optional `~.Text` properties for the labels. + *kwargs* are optional `.Text` properties for the labels. See Also -------- @@ -2017,24 +2097,23 @@ def thetagrids(angles=None, labels=None, fmt=None, **kwargs): return lines, labels -## Plotting Info ## - - -def plotting(): - pass - - +@_api.deprecated("3.7", pending=True) def get_plot_commands(): """ Get a sorted list of all of the plotting commands. """ + NON_PLOT_COMMANDS = { + 'connect', 'disconnect', 'get_current_fig_manager', 'ginput', + 'new_figure_manager', 'waitforbuttonpress'} + return (name for name in _get_pyplot_commands() + if name not in NON_PLOT_COMMANDS) + + +def _get_pyplot_commands(): # This works by searching for all functions in this module and removing # a few hard-coded exclusions, as well as all of the colormap-setting # 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'} - exclude |= set(colormaps()) + exclude = {'colormaps', 'colors', 'get_plot_commands', *colormaps} this_module = inspect.getmodule(get_plot_commands) return sorted( name for name, obj in globals().items() @@ -2043,309 +2122,11 @@ def get_plot_commands(): and inspect.getmodule(obj) is this_module) -def colormaps(): - """ - Matplotlib provides a number of colormaps, and others can be added using - :func:`~matplotlib.cm.register_cmap`. This function documents the built-in - colormaps, and will also return a list of all registered colormaps if - called. - - You can set the colormap for an image, pcolor, scatter, etc, - using a keyword argument:: - - imshow(X, cmap=cm.hot) - - or using the :func:`set_cmap` function:: - - imshow(X) - pyplot.set_cmap('hot') - pyplot.set_cmap('jet') - - In interactive mode, :func:`set_cmap` will update the colormap post-hoc, - allowing you to see which one works best for your data. - - All built-in colormaps can be reversed by appending ``_r``: For instance, - ``gray_r`` is the reverse of ``gray``. - - There are several common color schemes used in visualization: - - Sequential schemes - for unipolar data that progresses from low to high - Diverging schemes - for bipolar data that emphasizes positive or negative deviations from a - central value - Cyclic schemes - for plotting values that wrap around at the endpoints, such as phase - angle, wind direction, or time of day - Qualitative schemes - for nominal data that has no inherent ordering, where color is used - only to distinguish categories - - Matplotlib ships with 4 perceptually uniform colormaps which are - the recommended colormaps for sequential data: - - ========= =================================================== - Colormap Description - ========= =================================================== - inferno perceptually uniform shades of black-red-yellow - magma perceptually uniform shades of black-red-white - plasma perceptually uniform shades of blue-red-yellow - viridis perceptually uniform shades of blue-green-yellow - ========= =================================================== - - The following colormaps are based on the `ColorBrewer - `_ color specifications and designs developed by - Cynthia Brewer: - - ColorBrewer Diverging (luminance is highest at the midpoint, and - decreases towards differently-colored endpoints): - - ======== =================================== - Colormap Description - ======== =================================== - BrBG brown, white, blue-green - PiYG pink, white, yellow-green - PRGn purple, white, green - PuOr orange, white, purple - RdBu red, white, blue - RdGy red, white, gray - RdYlBu red, yellow, blue - RdYlGn red, yellow, green - Spectral red, orange, yellow, green, blue - ======== =================================== - - ColorBrewer Sequential (luminance decreases monotonically): - - ======== ==================================== - Colormap Description - ======== ==================================== - Blues white to dark blue - BuGn white, light blue, dark green - BuPu white, light blue, dark purple - GnBu white, light green, dark blue - Greens white to dark green - Greys white to black (not linear) - Oranges white, orange, dark brown - OrRd white, orange, dark red - PuBu white, light purple, dark blue - PuBuGn white, light purple, dark green - PuRd white, light purple, dark red - Purples white to dark purple - RdPu white, pink, dark purple - Reds white to dark red - YlGn light yellow, dark green - YlGnBu light yellow, light green, dark blue - YlOrBr light yellow, orange, dark brown - YlOrRd light yellow, orange, dark red - ======== ==================================== - - ColorBrewer Qualitative: - - (For plotting nominal data, `.ListedColormap` is used, - not `.LinearSegmentedColormap`. Different sets of colors are - recommended for different numbers of categories.) - - * Accent - * Dark2 - * Paired - * Pastel1 - * Pastel2 - * Set1 - * Set2 - * Set3 - - A set of colormaps derived from those of the same name provided - with Matlab are also included: - - ========= ======================================================= - Colormap Description - ========= ======================================================= - autumn sequential linearly-increasing shades of red-orange-yellow - bone sequential increasing black-white colormap with - a tinge of blue, to emulate X-ray film - cool linearly-decreasing shades of cyan-magenta - copper sequential increasing shades of black-copper - flag repetitive red-white-blue-black pattern (not cyclic at - endpoints) - gray sequential linearly-increasing black-to-white - grayscale - hot sequential black-red-yellow-white, to emulate blackbody - radiation from an object at increasing temperatures - jet a spectral map with dark endpoints, blue-cyan-yellow-red; - based on a fluid-jet simulation by NCSA [#]_ - pink sequential increasing pastel black-pink-white, meant - for sepia tone colorization of photographs - prism repetitive red-yellow-green-blue-purple-...-green pattern - (not cyclic at endpoints) - spring linearly-increasing shades of magenta-yellow - summer sequential linearly-increasing shades of green-yellow - winter linearly-increasing shades of blue-green - ========= ======================================================= - - A set of palettes from the `Yorick scientific visualisation - package `_, an evolution of - the GIST package, both by David H. Munro are included: - - ============ ======================================================= - Colormap Description - ============ ======================================================= - gist_earth mapmaker's colors from dark blue deep ocean to green - lowlands to brown highlands to white mountains - gist_heat sequential increasing black-red-orange-white, to emulate - blackbody radiation from an iron bar as it grows hotter - gist_ncar pseudo-spectral black-blue-green-yellow-red-purple-white - colormap from National Center for Atmospheric - Research [#]_ - gist_rainbow runs through the colors in spectral order from red to - violet at full saturation (like *hsv* but not cyclic) - gist_stern "Stern special" color table from Interactive Data - Language software - ============ ======================================================= - - A set of cyclic colormaps: - - ================ ================================================= - Colormap Description - ================ ================================================= - hsv red-yellow-green-cyan-blue-magenta-red, formed by - changing the hue component in the HSV color space - twilight perceptually uniform shades of - white-blue-black-red-white - twilight_shifted perceptually uniform shades of - black-blue-white-red-black - ================ ================================================= - - Other miscellaneous schemes: - - ============= ======================================================= - Colormap Description - ============= ======================================================= - afmhot sequential black-orange-yellow-white blackbody - spectrum, commonly used in atomic force microscopy - brg blue-red-green - bwr diverging blue-white-red - coolwarm diverging blue-gray-red, meant to avoid issues with 3D - shading, color blindness, and ordering of colors [#]_ - CMRmap "Default colormaps on color images often reproduce to - confusing grayscale images. The proposed colormap - maintains an aesthetically pleasing color image that - automatically reproduces to a monotonic grayscale with - discrete, quantifiable saturation levels." [#]_ - cubehelix Unlike most other color schemes cubehelix was designed - by D.A. Green to be monotonically increasing in terms - of perceived brightness. Also, when printed on a black - and white postscript printer, the scheme results in a - greyscale with monotonically increasing brightness. - This color scheme is named cubehelix because the (r, g, b) - values produced can be visualised as a squashed helix - around the diagonal in the (r, g, b) color cube. - gnuplot gnuplot's traditional pm3d scheme - (black-blue-red-yellow) - gnuplot2 sequential color printable as gray - (black-blue-violet-yellow-white) - ocean green-blue-white - rainbow spectral purple-blue-green-yellow-orange-red colormap - with diverging luminance - seismic diverging blue-white-red - nipy_spectral black-purple-blue-green-yellow-red-white spectrum, - originally from the Neuroimaging in Python project - terrain mapmaker's colors, blue-green-yellow-brown-white, - originally from IGOR Pro - turbo Spectral map (purple-blue-green-yellow-orange-red) with - a bright center and darker endpoints. A smoother - alternative to jet. - ============= ======================================================= - - The following colormaps are redundant and may be removed in future - versions. It's recommended to use the names in the descriptions - instead, which produce identical output: - - ========= ======================================================= - Colormap Description - ========= ======================================================= - gist_gray identical to *gray* - gist_yarg identical to *gray_r* - binary identical to *gray_r* - ========= ======================================================= - - .. rubric:: Footnotes - - .. [#] 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 - `_ - - .. [#] See `Diverging Color Maps for Scientific Visualization - `_ by Kenneth Moreland. - - .. [#] See `A Color Map for Effective Black-and-White Rendering of - Color-Scale Images - `_ - by Carey Rappaport - """ - return sorted(cm._cmap_registry) - - -def _setup_pyplot_info_docstrings(): - """ - Setup the docstring of `plotting` and of the colormap-setting functions. - - These must be done after the entire module is imported, so it is called - from the end of this module, which is generated by boilerplate.py. - """ - commands = get_plot_commands() - - first_sentence = re.compile(r"(?:\s*).+?\.(?:\s+|$)", flags=re.DOTALL) - - # Collect the first sentence of the docstring for all of the - # plotting commands. - rows = [] - max_name = len("Function") - max_summary = len("Description") - for name in commands: - doc = globals()[name].__doc__ - summary = '' - if doc is not None: - match = first_sentence.match(doc) - if match is not None: - summary = inspect.cleandoc(match.group(0)).replace('\n', ' ') - name = '`%s`' % name - rows.append([name, summary]) - max_name = max(max_name, len(name)) - max_summary = max(max_summary, len(summary)) - - separator = '=' * max_name + ' ' + '=' * max_summary - lines = [ - separator, - '{:{}} {:{}}'.format('Function', max_name, 'Description', max_summary), - separator, - ] + [ - '{:{}} {:{}}'.format(name, max_name, summary, max_summary) - for name, summary in rows - ] + [ - separator, - ] - plotting.__doc__ = '\n'.join(lines) - - for cm_name in colormaps(): - if cm_name in globals(): - globals()[cm_name].__doc__ = f""" - Set the colormap to {cm_name!r}. - - This changes the default colormap as well as the colormap of the current - image if there is one. See ``help(colormaps)`` for more information. - """ - - ## Plotting part 1: manually generated functions and wrappers ## @_copy_docstring_and_deprecators(Figure.colorbar) -def colorbar(mappable=None, cax=None, ax=None, **kw): +def colorbar(mappable=None, cax=None, ax=None, **kwargs): if mappable is None: mappable = gci() if mappable is None: @@ -2353,7 +2134,7 @@ def colorbar(mappable=None, cax=None, ax=None, **kw): 'creation. First define a mappable such as ' 'an image (with imshow) or a contour set (' 'with contourf).') - ret = gcf().colorbar(mappable, cax=cax, ax=ax, **kw) + ret = gcf().colorbar(mappable, cax=cax, ax=ax, **kwargs) return ret @@ -2378,6 +2159,13 @@ def clim(vmin=None, vmax=None): im.set_clim(vmin, vmax) +# eventually this implementation should move here, use indirection for now to +# avoid having two copies of the code floating around. +def get_cmap(name=None, lut=None): + return cm._get_cmap(name=name, lut=lut) +get_cmap.__doc__ = cm._get_cmap.__doc__ + + def set_cmap(cmap): """ Set the default colormap, and applies it to the current image if any. @@ -2393,7 +2181,7 @@ def set_cmap(cmap): matplotlib.cm.register_cmap matplotlib.cm.get_cmap """ - cmap = cm.get_cmap(cmap) + cmap = get_cmap(cmap) rc('image', cmap=cmap.name) im = gci() @@ -2478,33 +2266,22 @@ def polar(*args, **kwargs): # If an axis already exists, check if it has a polar projection if gcf().get_axes(): ax = gca() - if isinstance(ax, PolarAxes): - return ax - else: + if not isinstance(ax, PolarAxes): _api.warn_external('Trying to create polar plot on an Axes ' 'that does not have a polar projection.') - ax = axes(projection="polar") - ret = ax.plot(*args, **kwargs) - return ret + else: + ax = axes(projection="polar") + return ax.plot(*args, **kwargs) # If rcParams['backend_fallback'] is true, and an interactive backend is # requested, ignore rcParams['backend'] and force selection of a backend that # is compatible with the current running interactive framework. if (rcParams["backend_fallback"] - and dict.__getitem__(rcParams, "backend") in ( + and rcParams._get_backend_or_none() in ( set(_interactive_bk) - {'WebAgg', 'nbAgg'}) and cbook._get_running_interactive_framework()): - dict.__setitem__(rcParams, "backend", rcsetup._auto_backend_sentinel) -# Set up the backend. -switch_backend(rcParams["backend"]) - -# Just to be safe. Interactive mode can be turned on without -# calling `plt.ion()` so register it again here. -# This is safe because multiple calls to `install_repl_displayhook` -# are no-ops and the registered function respect `mpl.is_interactive()` -# to determine if they should trigger a draw. -install_repl_displayhook() + rcParams._set("backend", rcsetup._auto_backend_sentinel) ################# REMAINING CONTENT GENERATED BY boilerplate.py ############## @@ -2528,8 +2305,8 @@ def figtext(x, y, s, fontdict=None, **kwargs): # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Figure.gca) -def gca(**kwargs): - return gcf().gca(**kwargs) +def gca(): + return gcf().gca() # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @@ -2566,6 +2343,12 @@ def suptitle(t, **kwargs): return gcf().suptitle(t, **kwargs) +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +@_copy_docstring_and_deprecators(Figure.tight_layout) +def tight_layout(*, pad=1.08, h_pad=None, w_pad=None, rect=None): + return gcf().tight_layout(pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect) + + # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Figure.waitforbuttonpress) def waitforbuttonpress(timeout=-1): @@ -2591,8 +2374,13 @@ def angle_spectrum( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.annotate) -def annotate(text, xy, *args, **kwargs): - return gca().annotate(text, xy, *args, **kwargs) +def annotate( + text, xy, xytext=None, xycoords='data', textcoords=None, + arrowprops=None, annotation_clip=None, **kwargs): + return gca().annotate( + text, xy, xytext=xytext, xycoords=xycoords, + textcoords=textcoords, arrowprops=arrowprops, + annotation_clip=annotation_clip, **kwargs) # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @@ -2621,8 +2409,8 @@ def axhspan(ymin, ymax, xmin=0, xmax=1, **kwargs): # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.axis) -def axis(*args, emit=True, **kwargs): - return gca().axis(*args, emit=emit, **kwargs) +def axis(arg=None, /, *, emit=True, **kwargs): + return gca().axis(arg, emit=emit, **kwargs) # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @@ -2655,16 +2443,20 @@ def bar( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.barbs) -def barbs(*args, data=None, **kw): +def barbs(*args, data=None, **kwargs): return gca().barbs( - *args, **({"data": data} if data is not None else {}), **kw) + *args, **({"data": data} if data is not None else {}), + **kwargs) # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.barh) -def barh(y, width, height=0.8, left=None, *, align='center', **kwargs): +def barh( + y, width, height=0.8, left=None, *, align='center', + data=None, **kwargs): return gca().barh( - y, width, height=height, left=left, align=align, **kwargs) + y, width, height=height, left=left, align=align, + **({"data": data} if data is not None else {}), **kwargs) # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @@ -2687,7 +2479,7 @@ def boxplot( showfliers=None, boxprops=None, labels=None, flierprops=None, medianprops=None, meanprops=None, capprops=None, whiskerprops=None, manage_ticks=True, autorange=False, - zorder=None, *, data=None): + zorder=None, capwidths=None, *, data=None): return gca().boxplot( x, notch=notch, sym=sym, vert=vert, whis=whis, positions=positions, widths=widths, patch_artist=patch_artist, @@ -2698,7 +2490,7 @@ def boxplot( flierprops=flierprops, medianprops=medianprops, meanprops=meanprops, capprops=capprops, whiskerprops=whiskerprops, manage_ticks=manage_ticks, - autorange=autorange, zorder=zorder, + autorange=autorange, zorder=zorder, capwidths=capwidths, **({"data": data} if data is not None else {})) @@ -2781,12 +2573,12 @@ def errorbar( @_copy_docstring_and_deprecators(Axes.eventplot) def eventplot( positions, orientation='horizontal', lineoffsets=1, - linelengths=1, linewidths=None, colors=None, + linelengths=1, linewidths=None, colors=None, alpha=None, linestyles='solid', *, data=None, **kwargs): return gca().eventplot( positions, orientation=orientation, lineoffsets=lineoffsets, linelengths=linelengths, linewidths=linewidths, colors=colors, - linestyles=linestyles, + alpha=alpha, linestyles=linestyles, **({"data": data} if data is not None else {}), **kwargs) @@ -2820,8 +2612,8 @@ def fill_betweenx( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.grid) -def grid(b=None, which='major', axis='both', **kwargs): - return gca().grid(b=b, which=which, axis=axis, **kwargs) +def grid(visible=None, which='major', axis='both', **kwargs): + return gca().grid(visible=visible, which=which, axis=axis, **kwargs) # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @@ -2896,14 +2688,15 @@ def hlines( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.imshow) def imshow( - X, cmap=None, norm=None, aspect=None, interpolation=None, - alpha=None, vmin=None, vmax=None, origin=None, extent=None, *, - filternorm=True, filterrad=4.0, resample=None, url=None, - data=None, **kwargs): + X, cmap=None, norm=None, *, aspect=None, interpolation=None, + alpha=None, vmin=None, vmax=None, origin=None, extent=None, + interpolation_stage=None, filternorm=True, filterrad=4.0, + resample=None, url=None, data=None, **kwargs): __ret = gca().imshow( X, cmap=cmap, norm=norm, aspect=aspect, interpolation=interpolation, alpha=alpha, vmin=vmin, vmax=vmax, origin=origin, extent=extent, + interpolation_stage=interpolation_stage, filternorm=filternorm, filterrad=filterrad, resample=resample, url=url, **({"data": data} if data is not None else {}), **kwargs) @@ -3002,7 +2795,7 @@ def pie( pctdistance=0.6, shadow=False, labeldistance=1.1, startangle=0, radius=1, counterclock=True, wedgeprops=None, textprops=None, center=(0, 0), frame=False, - rotatelabels=False, *, normalize=None, data=None): + rotatelabels=False, *, normalize=True, hatch=None, data=None): return gca().pie( x, explode=explode, labels=labels, colors=colors, autopct=autopct, pctdistance=pctdistance, shadow=shadow, @@ -3010,7 +2803,7 @@ def pie( radius=radius, counterclock=counterclock, wedgeprops=wedgeprops, textprops=textprops, center=center, frame=frame, rotatelabels=rotatelabels, normalize=normalize, - **({"data": data} if data is not None else {})) + hatch=hatch, **({"data": data} if data is not None else {})) # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @@ -3046,17 +2839,18 @@ def psd( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.quiver) -def quiver(*args, data=None, **kw): +def quiver(*args, data=None, **kwargs): __ret = gca().quiver( - *args, **({"data": data} if data is not None else {}), **kw) + *args, **({"data": data} if data is not None else {}), + **kwargs) sci(__ret) return __ret # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.quiverkey) -def quiverkey(Q, X, Y, U, label, **kw): - return gca().quiverkey(Q, X, Y, U, label, **kw) +def quiverkey(Q, X, Y, U, label, **kwargs): + return gca().quiverkey(Q, X, Y, U, label, **kwargs) # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @@ -3129,8 +2923,9 @@ def stackplot( @_copy_docstring_and_deprecators(Axes.stem) def stem( *args, linefmt=None, markerfmt=None, basefmt=None, bottom=0, - label=None, use_line_collection=True, orientation='vertical', - data=None): + label=None, + use_line_collection=_api.deprecation._deprecated_parameter, + orientation='vertical', data=None): return gca().stem( *args, linefmt=linefmt, markerfmt=markerfmt, basefmt=basefmt, bottom=bottom, label=label, @@ -3153,7 +2948,8 @@ def streamplot( x, y, u, v, density=1, linewidth=None, color=None, cmap=None, norm=None, arrowsize=1, arrowstyle='-|>', minlength=0.1, transform=None, zorder=None, start_points=None, maxlength=4.0, - integration_direction='both', *, data=None): + integration_direction='both', broken_streamlines=True, *, + data=None): __ret = gca().streamplot( x, y, u, v, density=density, linewidth=linewidth, color=color, cmap=cmap, norm=norm, arrowsize=arrowsize, @@ -3161,6 +2957,7 @@ def streamplot( transform=transform, zorder=zorder, start_points=start_points, maxlength=maxlength, integration_direction=integration_direction, + broken_streamlines=broken_streamlines, **({"data": data} if data is not None else {})) sci(__ret.lines) return __ret @@ -3317,25 +3114,209 @@ def yscale(value, **kwargs): # Autogenerated by boilerplate.py. Do not edit as changes will be lost. -def autumn(): set_cmap('autumn') -def bone(): set_cmap('bone') -def cool(): set_cmap('cool') -def copper(): set_cmap('copper') -def flag(): set_cmap('flag') -def gray(): set_cmap('gray') -def hot(): set_cmap('hot') -def hsv(): set_cmap('hsv') -def jet(): set_cmap('jet') -def pink(): set_cmap('pink') -def prism(): set_cmap('prism') -def spring(): set_cmap('spring') -def summer(): set_cmap('summer') -def winter(): set_cmap('winter') -def magma(): set_cmap('magma') -def inferno(): set_cmap('inferno') -def plasma(): set_cmap('plasma') -def viridis(): set_cmap('viridis') -def nipy_spectral(): set_cmap('nipy_spectral') - - -_setup_pyplot_info_docstrings() +def autumn(): + """ + Set the colormap to 'autumn'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('autumn') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def bone(): + """ + Set the colormap to 'bone'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('bone') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def cool(): + """ + Set the colormap to 'cool'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('cool') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def copper(): + """ + Set the colormap to 'copper'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('copper') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def flag(): + """ + Set the colormap to 'flag'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('flag') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def gray(): + """ + Set the colormap to 'gray'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('gray') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def hot(): + """ + Set the colormap to 'hot'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('hot') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def hsv(): + """ + Set the colormap to 'hsv'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('hsv') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def jet(): + """ + Set the colormap to 'jet'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('jet') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def pink(): + """ + Set the colormap to 'pink'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('pink') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def prism(): + """ + Set the colormap to 'prism'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('prism') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def spring(): + """ + Set the colormap to 'spring'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('spring') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def summer(): + """ + Set the colormap to 'summer'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('summer') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def winter(): + """ + Set the colormap to 'winter'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('winter') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def magma(): + """ + Set the colormap to 'magma'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('magma') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def inferno(): + """ + Set the colormap to 'inferno'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('inferno') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def plasma(): + """ + Set the colormap to 'plasma'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('plasma') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def viridis(): + """ + Set the colormap to 'viridis'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('viridis') + + +# Autogenerated by boilerplate.py. Do not edit as changes will be lost. +def nipy_spectral(): + """ + Set the colormap to 'nipy_spectral'. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap('nipy_spectral') diff --git a/lib/matplotlib/quiver.py b/lib/matplotlib/quiver.py index ec124c4b1334..1d80ed5343ac 100644 --- a/lib/matplotlib/quiver.py +++ b/lib/matplotlib/quiver.py @@ -15,12 +15,11 @@ """ import math -import weakref import numpy as np from numpy import ma -from matplotlib import _api, cbook, docstring, font_manager +from matplotlib import _api, cbook, _docstring import matplotlib.artist as martist import matplotlib.collections as mcollections from matplotlib.patches import CirclePolygon @@ -33,29 +32,28 @@ Call signature:: - quiver([X, Y], U, V, [C], **kw) + quiver([X, Y], U, V, [C], **kwargs) *X*, *Y* define the arrow locations, *U*, *V* define the arrow directions, and *C* optionally sets the color. -**Arrow size** +**Arrow length** The default settings auto-scales the length of the arrows to a reasonable size. To change this behavior see the *scale* and *scale_units* parameters. **Arrow shape** -The defaults give a slightly swept-back arrow; to make the head a -triangle, make *headaxislength* the same as *headlength*. To make the -arrow more pointed, reduce *headwidth* or increase *headlength* and -*headaxislength*. To make the head smaller relative to the shaft, -scale down all the head parameters. You will probably do best to leave -minshaft alone. +The arrow shape is determined by *width*, *headwidth*, *headlength* and +*headaxislength*. See the notes below. -**Arrow outline** +**Arrow styling** + +Each arrow is internally represented by a filled polygon with a default edge +linewidth of 0. As a result, an arrow is rather a filled area, not a line with +a head, and `.PolyCollection` properties like *linewidth*, *edgecolor*, +*facecolor*, etc. act accordingly. -*linewidths* and *edgecolors* can be used to customize the arrow -outlines. Parameters ---------- @@ -70,11 +68,12 @@ must match the column and row dimensions of *U* and *V*. U, V : 1D or 2D array-like - The x and y direction components of the arrow vectors. + The x and y direction components of the arrow vectors. The interpretation + of these components (in data or in screen space) depends on *angles*. - They must have the same number of elements, matching the number of arrow - locations. *U* and *V* may be masked. Only locations unmasked in - *U*, *V*, and *C* will be drawn. + *U* and *V* must have the same number of elements, matching the number of + arrow locations in *X*, *Y*. *U* and *V* may be masked. Locations masked + in any of *U*, *V*, and *C* will not be drawn. C : 1D or 2D array-like, optional Numeric data that defines the arrow colors by colormapping via *norm* and @@ -84,38 +83,20 @@ use *color* instead. The size of *C* must match the number of arrow locations. -units : {'width', 'height', 'dots', 'inches', 'x', 'y', 'xy'}, default: 'width' - The arrow dimensions (except for *length*) are measured in multiples of - this unit. - - The following values are supported: - - - 'width', 'height': The width or height of the axis. - - 'dots', 'inches': Pixels or inches based on the figure dpi. - - 'x', 'y', 'xy': *X*, *Y* or :math:`\\sqrt{X^2 + Y^2}` in data units. - - The arrows scale differently depending on the units. For - 'x' or 'y', the arrows get larger as one zooms in; for other - units, the arrow size is independent of the zoom state. For - 'width or 'height', the arrow size increases with the width and - height of the axes, respectively, when the window is resized; - for 'dots' or 'inches', resizing does not change the arrows. - angles : {'uv', 'xy'} or array-like, default: 'uv' Method for determining the angle of the arrows. - - 'uv': The arrow axis aspect ratio is 1 so that - if *U* == *V* the orientation of the arrow on the plot is 45 degrees - counter-clockwise from the horizontal axis (positive to the right). + - 'uv': Arrow direction in screen coordinates. Use this if the arrows + symbolize a quantity that is not based on *X*, *Y* data coordinates. - Use this if the arrows symbolize a quantity that is not based on - *X*, *Y* data coordinates. + If *U* == *V* the orientation of the arrow on the plot is 45 degrees + counter-clockwise from the horizontal axis (positive to the right). - - 'xy': Arrows point from (x, y) to (x+u, y+v). - Use this for plotting a gradient field, for example. + - 'xy': Arrow direction in data coordinates, i.e. the arrows point from + (x, y) to (x+u, y+v). Use this e.g. for plotting a gradient field. - - Alternatively, arbitrary angles may be specified explicitly as an array - of values in degrees, counter-clockwise from the horizontal axis. + - Arbitrary angles may be specified explicitly as an array of values + in degrees, counter-clockwise from the horizontal axis. In this case *U*, *V* is only used to determine the length of the arrows. @@ -123,7 +104,15 @@ Note: inverting a data axis will correspondingly invert the arrows only with ``angles='xy'``. +pivot : {'tail', 'mid', 'middle', 'tip'}, default: 'tail' + The part of the arrow that is anchored to the *X*, *Y* grid. The arrow + rotates about this point. + + 'mid' is a synonym for 'middle'. + scale : float, optional + Scales the length of the arrow inversely. + Number of data units per arrow length unit, e.g., m/s per plot width; a smaller scale parameter makes the arrow longer. Default is *None*. @@ -145,19 +134,42 @@ the same units as x and y, use ``angles='xy', scale_units='xy', scale=1``. +units : {'width', 'height', 'dots', 'inches', 'x', 'y', 'xy'}, default: 'width' + Affects the arrow size (except for the length). In particular, the shaft + *width* is measured in multiples of this unit. + + Supported values are: + + - 'width', 'height': The width or height of the Axes. + - 'dots', 'inches': Pixels or inches based on the figure dpi. + - 'x', 'y', 'xy': *X*, *Y* or :math:`\\sqrt{X^2 + Y^2}` in data units. + + The following table summarizes how these values affect the visible arrow + size under zooming and figure size changes: + + ================= ================= ================== + units zoom figure size change + ================= ================= ================== + 'x', 'y', 'xy' arrow size scales — + 'width', 'height' — arrow size scales + 'dots', 'inches' — — + ================= ================= ================== + width : float, optional - Shaft width in arrow units; default depends on choice of units, - above, and number of vectors; a typical starting value is about - 0.005 times the width of the plot. + Shaft width in arrow units. All head parameters are relative to *width*. + + The default depends on choice of *units* above, and number of vectors; + a typical starting value is about 0.005 times the width of the plot. headwidth : float, default: 3 - Head width as multiple of shaft width. + Head width as multiple of shaft *width*. See the notes below. headlength : float, default: 5 - Head length as multiple of shaft width. + Head length as multiple of shaft *width*. See the notes below. headaxislength : float, default: 4.5 - Head length at shaft intersection. + Head length at shaft intersection as multiple of shaft *width*. + See the notes below. minshaft : float, default: 1 Length below which arrow scales, in units of head length. Do not @@ -167,29 +179,57 @@ Minimum length as a multiple of shaft width; if an arrow length is less than this, plot a dot (hexagon) of this diameter instead. -pivot : {'tail', 'mid', 'middle', 'tip'}, default: 'tail' - The part of the arrow that is anchored to the *X*, *Y* grid. The arrow - rotates about this point. - - 'mid' is a synonym for 'middle'. - color : color or color sequence, optional Explicit color(s) for the arrows. If *C* has been set, *color* has no effect. - This is a synonym for the `~.PolyCollection` *facecolor* parameter. + This is a synonym for the `.PolyCollection` *facecolor* parameter. Other Parameters ---------------- +data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs : `~matplotlib.collections.PolyCollection` properties, optional All other keyword arguments are passed on to `.PolyCollection`: - %(PolyCollection_kwdoc)s + %(PolyCollection:kwdoc)s + +Returns +------- +`~matplotlib.quiver.Quiver` See Also -------- .Axes.quiverkey : Add a key to a quiver plot. -""" % docstring.interpd.params + +Notes +----- + +**Arrow shape** + +The arrow is drawn as a polygon using the nodes as shown below. The values +*headwidth*, *headlength*, and *headaxislength* are in units of *width*. + +.. image:: /_static/quiver_sizes.svg + :width: 500px + +The defaults give a slightly swept-back arrow. Here are some guidelines how to +get other head shapes: + +- To make the head a triangle, make *headaxislength* the same as *headlength*. +- To make the arrow more pointed, reduce *headwidth* or increase *headlength* + and *headaxislength*. +- To make the head smaller relative to the shaft, scale down all the head + parameters proportionally. +- To remove the head completely, set all *head* parameters to 0. +- To get a diamond-shaped head, make *headaxislength* larger than *headlength*. +- Warning: For *headaxislength* < (*headlength* / *headwidth*), the "headaxis" + nodes (i.e. the ones connecting the head with the shaft) will protrude out + of the head in forward direction so that the arrow head looks broken. +""" % _docstring.interpd.params + +_docstring.interpd.update(quiver_doc=_quiver_doc) class QuiverKey(martist.Artist): @@ -200,8 +240,7 @@ class QuiverKey(martist.Artist): def __init__(self, Q, X, Y, U, label, *, angle=0, coordinates='axes', color=None, labelsep=0.1, - labelpos='N', labelcolor=None, fontproperties=None, - **kw): + labelpos='N', labelcolor=None, fontproperties=None, **kwargs): """ Add a key to a quiver plot. @@ -259,48 +298,28 @@ def __init__(self, Q, X, Y, U, label, self.color = color self.label = label self._labelsep_inches = labelsep - self.labelsep = (self._labelsep_inches * Q.axes.figure.dpi) - - # try to prevent closure over the real self - weak_self = weakref.ref(self) - - def on_dpi_change(fig): - self_weakref = weak_self() - if self_weakref is not None: - self_weakref.labelsep = self_weakref._labelsep_inches * fig.dpi - # simple brute force update works because _init is called at - # the start of draw. - self_weakref._initialized = False - - self._cid = Q.axes.figure.callbacks.connect( - 'dpi_changed', on_dpi_change) self.labelpos = labelpos self.labelcolor = labelcolor self.fontproperties = fontproperties or dict() - self.kw = kw - _fp = self.fontproperties - # boxprops = dict(facecolor='red') + self.kw = kwargs self.text = mtext.Text( - text=label, # bbox=boxprops, + text=label, horizontalalignment=self.halign[self.labelpos], verticalalignment=self.valign[self.labelpos], - fontproperties=font_manager.FontProperties._from_any(_fp)) - + fontproperties=self.fontproperties) if self.labelcolor is not None: self.text.set_color(self.labelcolor) - self._initialized = False + self._dpi_at_last_init = None self.zorder = Q.zorder + 0.1 - def remove(self): - # docstring inherited - self.Q.axes.figure.callbacks.disconnect(self._cid) - self._cid = None - super().remove() # pass the remove call up the stack + @property + def labelsep(self): + return self._labelsep_inches * self.Q.axes.figure.dpi def _init(self): - if True: # not self._initialized: - if not self.Q._initialized: + if True: # self._dpi_at_last_init != self.axes.figure.dpi + if self.Q._dpi_at_last_init != self.Q.axes.figure.dpi: self.Q._init() self._set_transform() with cbook._setattr_cm(self.Q, pivot=self.pivot[self.labelpos], @@ -312,42 +331,33 @@ def _init(self): else 'uv') self.verts = self.Q._make_verts( np.array([u]), np.array([v]), angle) - kw = self.Q.polykw - kw.update(self.kw) + kwargs = self.Q.polykw + kwargs.update(self.kw) self.vector = mcollections.PolyCollection( - self.verts, - offsets=[(self.X, self.Y)], - transOffset=self.get_transform(), - **kw) + self.verts, + offsets=[(self.X, self.Y)], + offset_transform=self.get_transform(), + **kwargs) if self.color is not None: self.vector.set_color(self.color) self.vector.set_transform(self.Q.get_transform()) self.vector.set_figure(self.get_figure()) - self._initialized = True + self._dpi_at_last_init = self.Q.axes.figure.dpi - def _text_x(self, x): - if self.labelpos == 'E': - return x + self.labelsep - elif self.labelpos == 'W': - return x - self.labelsep - else: - return x - - def _text_y(self, y): - if self.labelpos == 'N': - return y + self.labelsep - elif self.labelpos == 'S': - return y - self.labelsep - else: - return y + def _text_shift(self): + return { + "N": (0, +self.labelsep), + "S": (0, -self.labelsep), + "E": (+self.labelsep, 0), + "W": (-self.labelsep, 0), + }[self.labelpos] @martist.allow_rasterization def draw(self, renderer): self._init() self.vector.draw(renderer) - x, y = self.get_transform().transform((self.X, self.Y)) - self.text.set_x(self._text_x(x)) - self.text.set_y(self._text_y(y)) + pos = self.get_transform().transform((self.X, self.Y)) + self.text.set_position(pos + self._text_shift()) self.text.draw(renderer) self.stale = False @@ -396,20 +406,19 @@ def _parse_args(*args, caller_name='function'): """ X = Y = C = None - len_args = len(args) - if len_args == 2: + nargs = len(args) + if nargs == 2: # The use of atleast_1d allows for handling scalar arguments while also # keeping masked arrays U, V = np.atleast_1d(*args) - elif len_args == 3: + elif nargs == 3: U, V, C = np.atleast_1d(*args) - elif len_args == 4: + elif nargs == 4: X, Y, U, V = np.atleast_1d(*args) - elif len_args == 5: + elif nargs == 5: X, Y, U, V, C = np.atleast_1d(*args) else: - raise TypeError(f'{caller_name} takes 2-5 positional arguments but ' - f'{len_args} were given') + raise _api.nargs_error(caller_name, takes="from 2 to 5", given=nargs) nr, nc = (1, U.shape[0]) if U.ndim == 1 else U.shape @@ -454,11 +463,11 @@ class Quiver(mcollections.PolyCollection): _PIVOT_VALS = ('tail', 'middle', 'tip') - @docstring.Substitution(_quiver_doc) + @_docstring.Substitution(_quiver_doc) def __init__(self, ax, *args, scale=None, headwidth=3, headlength=5, headaxislength=4.5, minshaft=1, minlength=1, units='width', scale_units=None, - angles='uv', width=None, color='k', pivot='tail', **kw): + angles='uv', width=None, color='k', pivot='tail', **kwargs): """ The constructor takes one required argument, an Axes instance, followed by the args and kwargs described @@ -466,7 +475,7 @@ def __init__(self, ax, *args, %s """ self._axes = ax # The attr actually set by the Artist.axes property. - X, Y, U, V, C = _parse_args(*args, caller_name='quiver()') + X, Y, U, V, C = _parse_args(*args, caller_name='quiver') self.X = X self.Y = Y self.XY = np.column_stack((X, Y)) @@ -487,37 +496,14 @@ def __init__(self, ax, *args, self.pivot = pivot.lower() _api.check_in_list(self._PIVOT_VALS, pivot=self.pivot) - self.transform = kw.pop('transform', ax.transData) - kw.setdefault('facecolors', color) - kw.setdefault('linewidths', (0,)) - super().__init__([], offsets=self.XY, transOffset=self.transform, - closed=False, **kw) - self.polykw = kw + self.transform = kwargs.pop('transform', ax.transData) + kwargs.setdefault('facecolors', color) + kwargs.setdefault('linewidths', (0,)) + super().__init__([], offsets=self.XY, offset_transform=self.transform, + closed=False, **kwargs) + self.polykw = kwargs self.set_UVC(U, V, C) - self._initialized = False - - weak_self = weakref.ref(self) # Prevent closure over the real self. - - def on_dpi_change(fig): - self_weakref = weak_self() - if self_weakref is not None: - # vertices depend on width, span which in turn depend on dpi - self_weakref._new_UV = True - # simple brute force update works because _init is called at - # the start of draw. - self_weakref._initialized = False - - self._cid = ax.figure.callbacks.connect('dpi_changed', on_dpi_change) - - @_api.deprecated("3.3", alternative="axes") - def ax(self): - return self.axes - - def remove(self): - # docstring inherited - self.axes.figure.callbacks.disconnect(self._cid) - self._cid = None - super().remove() # pass the remove call up the stack + self._dpi_at_last_init = None def _init(self): """ @@ -526,7 +512,7 @@ def _init(self): """ # It seems that there are not enough event notifications # available to have this work on an as-needed basis at present. - if True: # not self._initialized: + if True: # self._dpi_at_last_init != self.axes.figure.dpi trans = self._set_transform() self.span = trans.inverted().transform_bbox(self.axes.bbox).width if self.width is None: @@ -534,15 +520,16 @@ def _init(self): self.width = 0.06 * self.span / sn # _make_verts sets self.scale if not already specified - if not self._initialized and self.scale is None: + if (self._dpi_at_last_init != self.axes.figure.dpi + and self.scale is None): self._make_verts(self.U, self.V, self.angles) - self._initialized = True + self._dpi_at_last_init = self.axes.figure.dpi def get_datalim(self, transData): trans = self.get_transform() - transOffset = self.get_offset_transform() - full_transform = (trans - transData) + (transOffset - transData) + offset_trf = self.get_offset_transform() + full_transform = (trans - transData) + (offset_trf - transData) XY = full_transform.transform(self.XY) bbox = transforms.Bbox.null() bbox.update_from_data_xy(XY, ignore=True) @@ -553,7 +540,6 @@ def draw(self, renderer): self._init() verts = self._make_verts(self.U, self.V, self.angles) self.set_verts(verts, closed=False) - self._new_UV = False super().draw(renderer) self.stale = False @@ -582,40 +568,21 @@ def set_UVC(self, U, V, C=None): self.Umask = mask if C is not None: self.set_array(C) - self._new_UV = True self.stale = True def _dots_per_unit(self, units): - """ - Return a scale factor for converting from units to pixels - """ - if units in ('x', 'y', 'xy'): - if units == 'x': - dx0 = self.axes.viewLim.width - dx1 = self.axes.bbox.width - elif units == 'y': - dx0 = self.axes.viewLim.height - dx1 = self.axes.bbox.height - else: # 'xy' is assumed - 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 = self.axes.bbox.width - elif units == 'height': - dx = self.axes.bbox.height - elif units == 'dots': - dx = 1.0 - elif units == 'inches': - dx = self.axes.figure.dpi - else: - raise ValueError('unrecognized units') - return dx + """Return a scale factor for converting from units to pixels.""" + bb = self.axes.bbox + vl = self.axes.viewLim + return _api.check_getitem({ + 'x': bb.width / vl.width, + 'y': bb.height / vl.height, + 'xy': np.hypot(*bb.size) / np.hypot(*vl.size), + 'width': bb.width, + 'height': bb.height, + 'dots': 1., + 'inches': self.axes.figure.dpi, + }, units=units) def _set_transform(self): """ @@ -755,7 +722,7 @@ def _h_arrows(self, length): # Mask handling is deferred to the caller, _make_verts. return X, Y - quiver_doc = _quiver_doc + quiver_doc = _api.deprecated("3.7")(property(lambda self: _quiver_doc)) _barbs_doc = r""" @@ -763,7 +730,7 @@ def _h_arrows(self, length): Call signature:: - barbs([X, Y], U, V, [C], **kw) + barbs([X, Y], U, V, [C], **kwargs) Where *X*, *Y* define the barb locations, *U*, *V* define the barb directions, and *C* optionally sets the color. @@ -884,14 +851,17 @@ def _h_arrows(self, length): Other Parameters ---------------- +data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs The barbs can further be customized using `.PolyCollection` keyword arguments: - %(PolyCollection_kwdoc)s -""" % docstring.interpd.params + %(PolyCollection:kwdoc)s +""" % _docstring.interpd.params -docstring.interpd.update(barbs_doc=_barbs_doc) +_docstring.interpd.update(barbs_doc=_barbs_doc) class Barbs(mcollections.PolyCollection): @@ -908,14 +878,16 @@ class Barbs(mcollections.PolyCollection): From there :meth:`_make_barbs` is used to find the vertices of the polygon to represent the barb based on this information. """ + # This may be an abuse of polygons here to render what is essentially maybe # 1 triangle and a series of lines. It works fine as far as I can tell # however. - @docstring.interpd + + @_docstring.interpd def __init__(self, ax, *args, pivot='tip', length=7, barbcolor=None, flagcolor=None, sizes=None, fill_empty=False, barb_increments=None, - rounding=True, flip_barb=False, **kw): + rounding=True, flip_barb=False, **kwargs): """ The constructor takes one required argument, an Axes instance, followed by the args and kwargs described @@ -927,11 +899,9 @@ def __init__(self, ax, *args, self.barb_increments = barb_increments or dict() self.rounding = rounding self.flip = np.atleast_1d(flip_barb) - transform = kw.pop('transform', ax.transData) + transform = kwargs.pop('transform', ax.transData) self._pivot = pivot self._length = length - barbcolor = barbcolor - flagcolor = flagcolor # Flagcolor and barbcolor provide convenience parameters for # setting the facecolor and edgecolor, respectively, of the barb @@ -939,68 +909,68 @@ def __init__(self, ax, *args, # rest of the barb by default if None in (barbcolor, flagcolor): - kw['edgecolors'] = 'face' + kwargs['edgecolors'] = 'face' if flagcolor: - kw['facecolors'] = flagcolor + kwargs['facecolors'] = flagcolor elif barbcolor: - kw['facecolors'] = barbcolor + kwargs['facecolors'] = barbcolor else: # Set to facecolor passed in or default to black - kw.setdefault('facecolors', 'k') + kwargs.setdefault('facecolors', 'k') else: - kw['edgecolors'] = barbcolor - kw['facecolors'] = flagcolor + kwargs['edgecolors'] = barbcolor + kwargs['facecolors'] = flagcolor # Explicitly set a line width if we're not given one, otherwise # polygons are not outlined and we get no barbs - if 'linewidth' not in kw and 'lw' not in kw: - kw['linewidth'] = 1 + if 'linewidth' not in kwargs and 'lw' not in kwargs: + kwargs['linewidth'] = 1 # Parse out the data arrays from the various configurations supported - x, y, u, v, c = _parse_args(*args, caller_name='barbs()') + x, y, u, v, c = _parse_args(*args, caller_name='barbs') self.x = x self.y = y xy = np.column_stack((x, y)) # Make a collection barb_size = self._length ** 2 / 4 # Empirically determined - super().__init__([], (barb_size,), offsets=xy, transOffset=transform, - **kw) + super().__init__( + [], (barb_size,), offsets=xy, offset_transform=transform, **kwargs) self.set_transform(transforms.IdentityTransform()) self.set_UVC(u, v, c) def _find_tails(self, mag, rounding=True, half=5, full=10, flag=50): """ - Find how many of each of the tail pieces is necessary. Flag - specifies the increment for a flag, barb for a full barb, and half for - half a barb. Mag should be the magnitude of a vector (i.e., >= 0). + Find how many of each of the tail pieces is necessary. - This returns a tuple of: - - (*number of flags*, *number of barbs*, *half_flag*, *empty_flag*) + Parameters + ---------- + mag : `~numpy.ndarray` + Vector magnitudes; must be non-negative (and an actual ndarray). + rounding : bool, default: True + Whether to round or to truncate to the nearest half-barb. + half, full, flag : float, defaults: 5, 10, 50 + Increments for a half-barb, a barb, and a flag. - The bool *half_flag* indicates whether half of a barb is needed, - since there should only ever be one half on a given - barb. *empty_flag* flag is an array of flags to easily tell if - a barb is empty (too low to plot any barbs/flags. + Returns + ------- + n_flags, n_barbs : int array + For each entry in *mag*, the number of flags and barbs. + half_flag : bool array + For each entry in *mag*, whether a half-barb is needed. + empty_flag : bool array + For each entry in *mag*, whether nothing is drawn. """ - # If rounding, round to the nearest multiple of half, the smallest # increment if rounding: - mag = half * (mag / half + 0.5).astype(int) - - num_flags = np.floor(mag / flag).astype(int) - mag = mag % flag - - num_barb = np.floor(mag / full).astype(int) - mag = mag % full - + mag = half * np.around(mag / half) + n_flags, mag = divmod(mag, flag) + n_barb, mag = divmod(mag, full) half_flag = mag >= half - empty_flag = ~(half_flag | (num_flags > 0) | (num_barb > 0)) - - return num_flags, num_barb, half_flag, empty_flag + empty_flag = ~(half_flag | (n_flags > 0) | (n_barb > 0)) + return n_flags.astype(int), n_barb.astype(int), half_flag, empty_flag def _make_barbs(self, u, v, nflags, nbarbs, half_barb, empty_flag, length, pivot, sizes, fill_empty, flip): @@ -1172,9 +1142,8 @@ def set_UVC(self, U, V, C=None): _check_consistent_shapes(x, y, u, v, flip) magnitude = np.hypot(u, v) - flags, barbs, halves, empty = self._find_tails(magnitude, - self.rounding, - **self.barb_increments) + flags, barbs, halves, empty = self._find_tails( + magnitude, self.rounding, **self.barb_increments) # Get the vertices for each of the barbs @@ -1210,4 +1179,4 @@ def set_offsets(self, xy): super().set_offsets(xy) self.stale = True - barbs_doc = _barbs_doc + barbs_doc = _api.deprecated("3.7")(property(lambda self: _barbs_doc)) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index e53dc2c27eb1..22b11f44e8b5 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -17,14 +17,15 @@ from functools import lru_cache, reduce from numbers import Number import operator +import os import re import numpy as np -from matplotlib import _api, animation, cbook +from matplotlib import _api, cbook from matplotlib.cbook import ls_mapper from matplotlib.colors import Colormap, is_color_like -from matplotlib.fontconfig_pattern import parse_fontconfig_pattern +from matplotlib._fontconfig_pattern import parse_fontconfig_pattern from matplotlib._enums import JoinStyle, CapStyle # Don't let the original cycler collide with our validating cycler @@ -33,13 +34,15 @@ # The capitalized forms are needed for ipython at present; this may # change for later versions. -interactive_bk = ['GTK3Agg', 'GTK3Cairo', - 'MacOSX', - 'nbAgg', - 'Qt4Agg', 'Qt4Cairo', 'Qt5Agg', 'Qt5Cairo', - 'TkAgg', 'TkCairo', - 'WebAgg', - 'WX', 'WXAgg', 'WXCairo'] +interactive_bk = [ + 'GTK3Agg', 'GTK3Cairo', 'GTK4Agg', 'GTK4Cairo', + 'MacOSX', + 'nbAgg', + 'QtAgg', 'QtCairo', 'Qt5Agg', 'Qt5Cairo', + 'TkAgg', 'TkCairo', + 'WebAgg', + 'WX', 'WXAgg', 'WXCairo', +] non_interactive_bk = ['agg', 'cairo', 'pdf', 'pgf', 'ps', 'svg', 'template'] all_backends = interactive_bk + non_interactive_bk @@ -65,7 +68,7 @@ def __call__(self, s): name, = (k for k, v in globals().items() if v is self) _api.warn_deprecated( self._deprecated_since, name=name, obj_type="function") - if self.ignorecase: + if self.ignorecase and isinstance(s, str): s = s.lower() if s in self.valid: return self.valid[s] @@ -143,65 +146,7 @@ def validate_bool(b): elif b in ('f', 'n', 'no', 'off', 'false', '0', 0, False): return False else: - raise ValueError('Could not convert "%s" to bool' % b) - - -@_api.deprecated("3.3") -def validate_bool_maybe_none(b): - """Convert b to ``bool`` or raise, passing through *None*.""" - if isinstance(b, str): - b = b.lower() - if b is None or b == 'none': - return None - if b in ('t', 'y', 'yes', 'on', 'true', '1', 1, True): - return True - elif b in ('f', 'n', 'no', 'off', 'false', '0', 0, False): - return False - else: - raise ValueError('Could not convert "%s" to bool' % b) - - -def _validate_date_converter(s): - if s is None: - return - s = validate_string(s) - if s not in ['auto', 'concise']: - _api.warn_external(f'date.converter string must be "auto" or ' - f'"concise", not "{s}". Check your matplotlibrc') - return - import matplotlib.dates as mdates - mdates._rcParam_helper.set_converter(s) - - -def _validate_date_int_mult(s): - if s is None: - return - s = validate_bool(s) - import matplotlib.dates as mdates - mdates._rcParam_helper.set_int_mult(s) - - -def _validate_tex_preamble(s): - if s is None or s == 'None': - _api.warn_deprecated( - "3.3", message="Support for setting the 'text.latex.preamble' or " - "'pgf.preamble' rcParam to None is deprecated since %(since)s and " - "will be removed %(removal)s; set it to an empty string instead.") - return "" - try: - if isinstance(s, str): - return s - elif np.iterable(s): - _api.warn_deprecated( - "3.3", message="Support for setting the 'text.latex.preamble' " - "or 'pgf.preamble' rcParam to a list of strings is deprecated " - "since %(since)s and will be removed %(removal)s; set it to a " - "single string instead.") - return '\n'.join(s) - else: - raise TypeError - except TypeError as e: - raise ValueError('Could not convert "%s" to string' % s) from e + raise ValueError(f'Cannot convert {b!r} to bool') def validate_axisbelow(s): @@ -211,14 +156,8 @@ def validate_axisbelow(s): if isinstance(s, str): if s == 'line': return 'line' - if s.lower().startswith('line'): - _api.warn_deprecated( - "3.3", message=f"Support for setting axes.axisbelow to " - f"{s!r} to mean 'line' is deprecated since %(since)s and " - f"will be removed %(removal)s; set it to 'line' instead.") - return 'line' - raise ValueError('%s cannot be interpreted as' - ' True, False, or "line"' % s) + raise ValueError(f'{s!r} cannot be interpreted as' + ' True, False, or "line"') def validate_dpi(s): @@ -242,6 +181,8 @@ def validator(s): if (allow_none and (s is None or isinstance(s, str) and s.lower() == "none")): return None + if cls is str and not isinstance(s, str): + raise ValueError(f'Could not convert {s!r} to str') try: return cls(s) except (TypeError, ValueError) as e: @@ -268,6 +209,15 @@ def validator(s): validate_float, doc='return a list of floats') +def _validate_pathlike(s): + if isinstance(s, (str, os.PathLike)): + # Store value as str because savefig.directory needs to distinguish + # between "" (cwd) and "." (cwd, but gets updated by user selections). + return os.fsdecode(s) + else: + return validate_string(s) + + def validate_fonttype(s): """ Confirm that this is a Postscript or PDF font type that we know how to @@ -303,11 +253,6 @@ def validate_backend(s): return backend -validate_toolbar = ValidateInStrings( - 'toolbar', ['None', 'toolbar2', 'toolmanager'], ignorecase=True, - _deprecated_since="3.3") - - def _validate_toolbar(s): s = ValidateInStrings( 'toolbar', ['None', 'toolbar2', 'toolmanager'], ignorecase=True)(s) @@ -318,42 +263,6 @@ def _validate_toolbar(s): return s -@_api.deprecated("3.3") -def _make_nseq_validator(cls, n=None, allow_none=False): - - def validator(s): - """Convert *n* objects using ``cls``, or raise.""" - if isinstance(s, str): - s = [x.strip() for x in s.split(',')] - if n is not None and len(s) != n: - raise ValueError( - f'Expected exactly {n} comma-separated values, ' - f'but got {len(s)} comma-separated values: {s}') - else: - if n is not None and len(s) != n: - raise ValueError( - f'Expected exactly {n} values, ' - f'but got {len(s)} values: {s}') - try: - return [cls(val) if not allow_none or val is not None else val - for val in s] - except ValueError as e: - raise ValueError( - f'Could not convert all entries to {cls.__name__}s') from e - - return validator - - -@_api.deprecated("3.3") -def validate_nseq_float(n): - return _make_nseq_validator(float, n) - - -@_api.deprecated("3.3") -def validate_nseq_int(n): - return _make_nseq_validator(int, n) - - def validate_color_or_inherit(s): """Return a valid color arg.""" if cbook._str_equal(s, 'inherit'): @@ -374,6 +283,27 @@ def validate_color_for_prop_cycle(s): return validate_color(s) +def _validate_color_or_linecolor(s): + if cbook._str_equal(s, 'linecolor'): + return s + elif cbook._str_equal(s, 'mfc') or cbook._str_equal(s, 'markerfacecolor'): + return 'markerfacecolor' + elif cbook._str_equal(s, 'mec') or cbook._str_equal(s, 'markeredgecolor'): + return 'markeredgecolor' + elif s is None: + return None + elif isinstance(s, str) and len(s) == 6 or len(s) == 8: + stmp = '#' + s + if is_color_like(stmp): + return stmp + if s.lower() == 'none': + return None + elif is_color_like(s): + return s + + raise ValueError(f'{s!r} does not look like a color arg') + + def validate_color(s): """Return a valid color arg.""" if isinstance(s, str): @@ -408,10 +338,6 @@ def _validate_cmap(s): return s -validate_orientation = ValidateInStrings( - 'orientation', ['landscape', 'portrait'], _deprecated_since="3.3") - - def validate_aspect(s): if s in ('auto', 'equal'): return s @@ -458,28 +384,25 @@ def validate_fontweight(s): raise ValueError(f'{s} is not a valid font weight.') from e +def validate_fontstretch(s): + stretchvalues = [ + 'ultra-condensed', 'extra-condensed', 'condensed', 'semi-condensed', + 'normal', 'semi-expanded', 'expanded', 'extra-expanded', + 'ultra-expanded'] + # Note: Historically, stretchvalues have been case-sensitive in Matplotlib + if s in stretchvalues: + return s + try: + return int(s) + except (ValueError, TypeError) as e: + raise ValueError(f'{s} is not a valid font stretch.') from e + + def validate_font_properties(s): parse_fontconfig_pattern(s) return s -def _validate_mathtext_fallback_to_cm(b): - """ - Temporary validate for fallback_to_cm, while deprecated - - """ - if isinstance(b, str): - b = b.lower() - if b is None or b == 'none': - return None - else: - _api.warn_deprecated( - "3.3", message="Support for setting the 'mathtext.fallback_to_cm' " - "rcParam is deprecated since %(since)s and will be removed " - "%(removal)s; use 'mathtext.fallback : 'cm' instead.") - return validate_bool_maybe_none(b) - - def _validate_mathtext_fallback(s): _fallback_fonts = ['cm', 'stix', 'stixsans'] if isinstance(s, str): @@ -495,19 +418,6 @@ def _validate_mathtext_fallback(s): "fallback off.") -validate_fontset = ValidateInStrings( - 'fontset', - ['dejavusans', 'dejavuserif', 'cm', 'stix', 'stixsans', 'custom'], - _deprecated_since="3.3") -validate_mathtext_default = ValidateInStrings( - 'default', "rm cal it tt sf bf default bb frak scr regular".split(), - _deprecated_since="3.3") -_validate_alignment = ValidateInStrings( - 'alignment', - ['center', 'top', 'bottom', 'baseline', 'center_baseline'], - _deprecated_since="3.3") - - def validate_whiskers(s): try: return _listify_validator(validate_float, n=2)(s) @@ -515,18 +425,10 @@ def validate_whiskers(s): try: return float(s) except ValueError as e: - raise ValueError("Not a valid whisker value ['range', float, " + raise ValueError("Not a valid whisker value [float, " "(float, float)]") from e -validate_ps_papersize = ValidateInStrings( - 'ps_papersize', - ['auto', 'letter', 'legal', 'ledger', - 'a0', 'a1', 'a2', 'a3', 'a4', 'a5', 'a6', 'a7', 'a8', 'a9', 'a10', - 'b0', 'b1', 'b2', 'b3', 'b4', 'b5', 'b6', 'b7', 'b8', 'b9', 'b10', - ], ignorecase=True, _deprecated_since="3.3") - - def validate_ps_distiller(s): if isinstance(s, str): s = s.lower() @@ -564,25 +466,20 @@ def _is_iterable_not_string_like(x): # nonsensically interpreted as sequences of numbers (codepoints). return np.iterable(x) and not isinstance(x, (str, bytes, bytearray)) - # (offset, (on, off, on, off, ...)) - if (_is_iterable_not_string_like(ls) - and len(ls) == 2 - and isinstance(ls[0], (type(None), Number)) - and _is_iterable_not_string_like(ls[1]) - and len(ls[1]) % 2 == 0 - and all(isinstance(elem, Number) for elem in ls[1])): - if ls[0] is None: - _api.warn_deprecated( - "3.3", message="Passing the dash offset as None is deprecated " - "since %(since)s and support for it will be removed " - "%(removal)s; pass it as zero instead.") - ls = (0, ls[1]) - return ls - # For backcompat: (on, off, on, off, ...); the offset is implicitly None. - if (_is_iterable_not_string_like(ls) - and len(ls) % 2 == 0 - and all(isinstance(elem, Number) for elem in ls)): - return (0, ls) + if _is_iterable_not_string_like(ls): + if len(ls) == 2 and _is_iterable_not_string_like(ls[1]): + # (offset, (on, off, on, off, ...)) + offset, onoff = ls + else: + # For backcompat: (on, off, on, off, ...); the offset is implicit. + offset = 0 + onoff = ls + + if (isinstance(offset, Number) + and len(onoff) % 2 == 0 + and all(isinstance(elem, Number) for elem in onoff)): + return (offset, onoff) + raise ValueError(f"linestyle {ls!r} is not a valid on-off ink sequence.") @@ -599,14 +496,11 @@ def validate_markevery(s): Parameters ---------- - s : None, int, float, slice, length-2 tuple of ints, - length-2 tuple of floats, list of ints + s : None, int, (int, int), slice, float, (float, float), or list[int] Returns ------- - None, int, float, slice, length-2 tuple of ints, - length-2 tuple of floats, list of ints - + None, int, (int, int), slice, float, (float, float), or list[int] """ # Validate s against type slice float int and None if isinstance(s, (slice, float, int, type(None))): @@ -632,62 +526,6 @@ def validate_markevery(s): validate_markeverylist = _listify_validator(validate_markevery) -validate_legend_loc = ValidateInStrings( - 'legend_loc', - ['best', - 'upper right', - 'upper left', - 'lower left', - 'lower right', - 'right', - 'center left', - 'center right', - 'lower center', - 'upper center', - 'center'], ignorecase=True, _deprecated_since="3.3") - -validate_svg_fonttype = ValidateInStrings( - 'svg.fonttype', ['none', 'path'], _deprecated_since="3.3") - - -@_api.deprecated("3.3") -def validate_hinting(s): - return _validate_hinting(s) - - -# Replace by plain list in _prop_validators after deprecation period. -_validate_hinting = ValidateInStrings( - 'text.hinting', - ['default', 'no_autohint', 'force_autohint', 'no_hinting', - 'auto', 'native', 'either', 'none'], - ignorecase=True) - - -validate_pgf_texsystem = ValidateInStrings( - 'pgf.texsystem', ['xelatex', 'lualatex', 'pdflatex'], - _deprecated_since="3.3") - - -@_api.deprecated("3.3") -def validate_movie_writer(s): - # writers.list() would only list actually available writers, but - # FFMpeg.isAvailable is slow and not worth paying for at every import. - if s in animation.writers._registered: - return s - else: - raise ValueError(f"Supported animation writers are " - f"{sorted(animation.writers._registered)}") - - -validate_movie_frame_fmt = ValidateInStrings( - 'animation.frame_format', ['png', 'jpeg', 'tiff', 'raw', 'rgba', 'ppm', - 'sgi', 'bmp', 'pbm', 'svg'], - _deprecated_since="3.3") -validate_axis_locator = ValidateInStrings( - 'major', ['minor', 'both', 'major'], _deprecated_since="3.3") -validate_movie_html_fmt = ValidateInStrings( - 'animation.html', ['html5', 'jshtml', 'none'], _deprecated_since="3.3") - def validate_bbox(s): if isinstance(s, str): @@ -736,10 +574,6 @@ def _validate_greaterequal0_lessequal1(s): } -validate_grid_axis = ValidateInStrings( - 'axes.grid.axis', ['x', 'y', 'both'], _deprecated_since="3.3") - - def validate_hatch(s): r""" Validate a hatch pattern. @@ -877,6 +711,13 @@ def cycler(*args, **kwargs): return reduce(operator.add, (ccycler(k, v) for k, v in validated)) +class _DunderChecker(ast.NodeVisitor): + def visit_Attribute(self, node): + if node.attr.startswith("__") and node.attr.endswith("__"): + raise ValueError("cycler strings with dunders are forbidden") + self.generic_visit(node) + + def validate_cycler(s): """Return a Cycler object from a string repr or the object itself.""" if isinstance(s, str): @@ -888,23 +729,21 @@ def validate_cycler(s): # I locked it down by only having the 'cycler()' function available. # UPDATE: Partly plugging a security hole. # I really should have read this: - # http://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html + # https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html # We should replace this eval with a combo of PyParsing and # ast.literal_eval() try: - if '.__' in s.replace(' ', ''): - raise ValueError("'%s' seems to have dunder methods. Raising" - " an exception for your safety") + _DunderChecker().visit(ast.parse(s)) s = eval(s, {'cycler': cycler, '__builtins__': {}}) except BaseException as e: - raise ValueError("'%s' is not a valid cycler construction: %s" % - (s, e)) from e + raise ValueError(f"{s!r} is not a valid cycler construction: {e}" + ) from e # Should make sure what comes from the above eval() # is a Cycler object. if isinstance(s, Cycler): cycler_inst = s else: - raise ValueError("object was not a string or Cycler instance: %s" % s) + raise ValueError(f"Object is not a string or Cycler instance: {s!r}") unknowns = cycler_inst.keys - (set(_prop_validators) | set(_prop_aliases)) if unknowns: @@ -916,12 +755,11 @@ def validate_cycler(s): for prop in cycler_inst.keys: norm_prop = _prop_aliases.get(prop, prop) if norm_prop != prop and norm_prop in cycler_inst.keys: - raise ValueError("Cannot specify both '{0}' and alias '{1}'" - " in the same prop_cycle".format(norm_prop, prop)) + raise ValueError(f"Cannot specify both {norm_prop!r} and alias " + f"{prop!r} in the same prop_cycle") if norm_prop in checker: - raise ValueError("Another property was already aliased to '{0}'." - " Collision normalizing '{1}'.".format(norm_prop, - prop)) + raise ValueError(f"Another property was already aliased to " + f"{norm_prop!r}. Collision normalizing {prop!r}.") checker.update([norm_prop]) # This is just an extra-careful check, just in case there is some @@ -955,23 +793,6 @@ def validate_hist_bins(s): " a sequence of floats".format(valid_strs)) -@_api.deprecated("3.3") -def validate_webagg_address(s): - if s is not None: - import socket - try: - socket.inet_aton(s) - except socket.error as e: - raise ValueError( - "'webagg.address' is not a valid IP address") from e - return s - raise ValueError("'webagg.address' is not a valid IP address") - - -validate_axes_titlelocation = ValidateInStrings( - 'axes.titlelocation', ['left', 'center', 'right'], _deprecated_since="3.3") - - class _ignorecase(list): """A marker class indicating that a list-of-str is case-insensitive.""" @@ -992,6 +813,7 @@ def _convert_validator_spec(key, conv): _validators = { "backend": validate_backend, "backend_fallback": validate_bool, + "figure.hooks": validate_stringlist, "toolbar": _validate_toolbar, "interactive": validate_bool, "timezone": validate_string, @@ -1090,7 +912,7 @@ def _convert_validator_spec(key, conv): "font.family": validate_stringlist, # used by text object "font.style": validate_string, "font.variant": validate_string, - "font.stretch": validate_string, + "font.stretch": validate_fontstretch, "font.weight": validate_fontweight, "font.size": validate_float, # Base font size in points "font.serif": validate_stringlist, @@ -1102,12 +924,13 @@ def _convert_validator_spec(key, conv): # text props "text.color": validate_color, "text.usetex": validate_bool, - "text.latex.preamble": _validate_tex_preamble, - "text.latex.preview": validate_bool, - "text.hinting": _validate_hinting, + "text.latex.preamble": validate_string, + "text.hinting": ["default", "no_autohint", "force_autohint", + "no_hinting", "auto", "native", "either", "none"], "text.hinting_factor": validate_int, "text.kerning_factor": validate_int, "text.antialiased": validate_bool, + "text.parse_math": validate_bool, "mathtext.cal": validate_font_properties, "mathtext.rm": validate_font_properties, @@ -1119,7 +942,6 @@ def _convert_validator_spec(key, conv): "stixsans", "custom"], "mathtext.default": ["rm", "cal", "it", "tt", "sf", "bf", "default", "bb", "frak", "scr", "regular"], - "mathtext.fallback_to_cm": _validate_mathtext_fallback_to_cm, "mathtext.fallback": _validate_mathtext_fallback, "image.aspect": validate_aspect, # equal, auto, a number @@ -1136,6 +958,7 @@ def _convert_validator_spec(key, conv): "contour.negative_linestyle": _validate_linestyle, "contour.corner_mask": validate_bool, "contour.linewidth": validate_float_or_None, + "contour.algorithm": ["mpl2005", "mpl2014", "serial", "threaded"], # errorbar props "errorbar.capsize": validate_float, @@ -1195,6 +1018,10 @@ def _convert_validator_spec(key, conv): "polaraxes.grid": validate_bool, # display polar grid or not "axes3d.grid": validate_bool, # display 3d grid + "axes3d.xaxis.panecolor": validate_color, # 3d background pane + "axes3d.yaxis.panecolor": validate_color, # 3d background pane + "axes3d.zaxis.panecolor": validate_color, # 3d background pane + # scatter props "scatter.marker": validate_string, "scatter.edgecolors": validate_string, @@ -1208,10 +1035,9 @@ def _convert_validator_spec(key, conv): "date.autoformatter.second": validate_string, "date.autoformatter.microsecond": validate_string, - # 'auto', 'concise', 'auto-noninterval' - 'date.converter': _validate_date_converter, + 'date.converter': ['auto', 'concise'], # for auto date locator, choose interval_multiples - 'date.interval_multiples': _validate_date_int_mult, + 'date.interval_multiples': validate_bool, # legend properties "legend.fancybox": validate_bool, @@ -1227,12 +1053,14 @@ def _convert_validator_spec(key, conv): "legend.scatterpoints": validate_int, "legend.fontsize": validate_fontsize, "legend.title_fontsize": validate_fontsize_None, - # the relative size of legend markers vs. original + # color of the legend + "legend.labelcolor": _validate_color_or_linecolor, + # the relative size of legend markers vs. original "legend.markerscale": validate_float, "legend.shadow": validate_bool, - # whether or not to draw a frame around legend + # whether or not to draw a frame around legend "legend.frameon": validate_bool, - # alpha value of the legend frame + # alpha value of the legend frame "legend.framealpha": validate_float_or_None, ## the following dimensions are in fraction of the font size @@ -1306,6 +1134,10 @@ def _convert_validator_spec(key, conv): "figure.titlesize": validate_fontsize, "figure.titleweight": validate_fontweight, + # figure labels + "figure.labelsize": validate_fontsize, + "figure.labelweight": validate_fontweight, + # figure size in inches: width by height "figure.figsize": _listify_validator(validate_float, n=2), "figure.dpi": validate_float, @@ -1337,12 +1169,11 @@ def _convert_validator_spec(key, conv): 'savefig.facecolor': validate_color_or_auto, 'savefig.edgecolor': validate_color_or_auto, 'savefig.orientation': ['landscape', 'portrait'], - 'savefig.jpeg_quality': validate_int, "savefig.format": validate_string, "savefig.bbox": validate_bbox, # "tight", or "standard" (= None) "savefig.pad_inches": validate_float, # default directory in savefig dialog box - "savefig.directory": validate_string, + "savefig.directory": _validate_pathlike, "savefig.transparent": validate_bool, "tk.window_focus": validate_bool, # Maintain shell focus for TkAgg @@ -1364,7 +1195,7 @@ def _convert_validator_spec(key, conv): "pgf.texsystem": ["xelatex", "lualatex", "pdflatex"], # latex variant used "pgf.rcfonts": validate_bool, # use mpl's rc settings for font config - "pgf.preamble": _validate_tex_preamble, # custom LaTeX preamble + "pgf.preamble": validate_string, # custom LaTeX preamble # write raster image data into the svg file "svg.image_inline": validate_bool, @@ -1395,7 +1226,6 @@ def _convert_validator_spec(key, conv): "keymap.grid_minor": validate_stringlist, "keymap.yscale": validate_stringlist, "keymap.xscale": validate_stringlist, - "keymap.all_axes": validate_stringlist, "keymap.help": validate_stringlist, "keymap.copy": validate_stringlist, @@ -1410,18 +1240,12 @@ def _convert_validator_spec(key, conv): # Controls image format when frames are written to disk "animation.frame_format": ["png", "jpeg", "tiff", "raw", "rgba", "ppm", "sgi", "bmp", "pbm", "svg"], - # Additional arguments for HTML writer - "animation.html_args": validate_stringlist, # Path to ffmpeg binary. If just binary name, subprocess uses $PATH. - "animation.ffmpeg_path": validate_string, + "animation.ffmpeg_path": _validate_pathlike, # Additional arguments for ffmpeg movie writer (using pipes) "animation.ffmpeg_args": validate_stringlist, - # Path to AVConv binary. If just binary name, subprocess uses $PATH. - "animation.avconv_path": validate_string, - # Additional arguments for avconv movie writer (using pipes) - "animation.avconv_args": validate_stringlist, # Path to convert binary. If just binary name, subprocess uses $PATH. - "animation.convert_path": validate_string, + "animation.convert_path": _validate_pathlike, # Additional arguments for convert movie writer (using pipes) "animation.convert_args": validate_stringlist, @@ -1432,18 +1256,11 @@ def _convert_validator_spec(key, conv): "_internal.classic_mode": validate_bool } _hardcoded_defaults = { # Defaults not inferred from matplotlibrc.template... - # ... because it can"t be: - "backend": _auto_backend_sentinel, # ... because they are private: "_internal.classic_mode": False, # ... because they are deprecated: - "animation.avconv_path": "avconv", - "animation.avconv_args": [], - "animation.html_args": [], - "mathtext.fallback_to_cm": None, - "keymap.all_axes": ["a"], - "savefig.jpeg_quality": 95, - "text.latex.preview": False, + # No current deprecations. + # backend is handled separately when constructing rcParamsDefault. } _validators = {k: _convert_validator_spec(k, conv) for k, conv in _validators.items()} diff --git a/lib/matplotlib/sankey.py b/lib/matplotlib/sankey.py index 8032001561bb..39e8fce98fbb 100644 --- a/lib/matplotlib/sankey.py +++ b/lib/matplotlib/sankey.py @@ -11,7 +11,7 @@ from matplotlib.path import Path from matplotlib.patches import PathPatch from matplotlib.transforms import Affine2D -from matplotlib import docstring +from matplotlib import _docstring _log = logging.getLogger(__name__) @@ -204,12 +204,12 @@ def _arc(self, quadrant=0, cw=True, radius=1, center=(0, 0)): # Insignificant # [6.12303177e-17, 1.00000000e+00]]) [0.00000000e+00, 1.00000000e+00]]) - if quadrant == 0 or quadrant == 2: + if quadrant in (0, 2): if cw: vertices = ARC_VERTICES else: vertices = ARC_VERTICES[:, ::-1] # Swap x and y. - elif quadrant == 1 or quadrant == 3: + else: # 1, 3 # Negate x. if cw: # Swap x and y. @@ -299,15 +299,11 @@ def _add_output(self, path, angle, flow, length): else: # Vertical x += self.gap if angle == UP: - sign = 1 + sign, quadrant = 1, 3 else: - sign = -1 + sign, quadrant = -1, 0 tip = [x - flow / 2.0, y + sign * (length + tipheight)] - if angle == UP: - quadrant = 3 - else: - quadrant = 0 # Inner arc isn't needed if inner radius is zero if self.radius: path.extend(self._arc(quadrant=quadrant, @@ -351,7 +347,7 @@ def _revert(self, path, first_action=Path.LINETO): # path[2] = path[2][::-1] # return path - @docstring.dedent_interpd + @_docstring.dedent_interpd def add(self, patchlabel='', flows=None, orientations=None, labels='', trunklength=1.0, pathlengths=0.25, prior=None, connect=(0, 0), rotation=0, **kwargs): @@ -375,7 +371,7 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', the outside in. If the sum of the inputs and outputs is - nonzero, the discrepancy will appear as a cubic Bezier curve along + nonzero, the discrepancy will appear as a cubic Bézier curve along the top and bottom edges of the trunk. orientations : list of {-1, 0, 1} @@ -431,17 +427,14 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', properties, listed below. For example, one may want to use ``fill=False`` or ``label="A legend entry"``. - %(Patch_kwdoc)s + %(Patch:kwdoc)s See Also -------- Sankey.finish """ # Check and preprocess the arguments. - if flows is None: - flows = np.array([1.0, -1.0]) - else: - flows = np.array(flows) + flows = np.array([1.0, -1.0]) if flows is None else np.array(flows) n = flows.shape[0] # Number of flows if rotation is None: rotation = 0 @@ -525,7 +518,7 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', if orient == 1: if is_input: angles[i] = DOWN - elif not is_input: + elif is_input is False: # Be specific since is_input can be None. angles[i] = UP elif orient == 0: @@ -538,7 +531,7 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', f"but it must be -1, 0, or 1") if is_input: angles[i] = UP - elif not is_input: + elif is_input is False: angles[i] = DOWN # Justify the lengths of the paths. @@ -561,7 +554,7 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', if angle == DOWN and is_input: pathlengths[i] = ullength ullength += flow - elif angle == UP and not is_input: + elif angle == UP and is_input is False: pathlengths[i] = urlength urlength -= flow # Flow is negative for outputs. # Determine the lengths of the bottom-side arrows @@ -571,7 +564,7 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', if angle == UP and is_input: pathlengths[n - i - 1] = lllength lllength += flow - elif angle == DOWN and not is_input: + elif angle == DOWN and is_input is False: pathlengths[n - i - 1] = lrlength lrlength -= flow # Determine the lengths of the left-side arrows @@ -591,7 +584,7 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', for i, (angle, is_input, spec) in enumerate(zip( angles, are_inputs, list(zip(scaled_flows, pathlengths)))): if angle == RIGHT: - if not is_input: + if is_input is False: if has_right_output: pathlengths[i] = 0 else: @@ -637,7 +630,7 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', if angle == DOWN and is_input: tips[i, :], label_locations[i, :] = self._add_input( ulpath, angle, *spec) - elif angle == UP and not is_input: + elif angle == UP and is_input is False: tips[i, :], label_locations[i, :] = self._add_output( urpath, angle, *spec) # Add the bottom-side inputs and outputs from the middle outwards. @@ -647,7 +640,7 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', tip, label_location = self._add_input(llpath, angle, *spec) tips[n - i - 1, :] = tip label_locations[n - i - 1, :] = label_location - elif angle == DOWN and not is_input: + elif angle == DOWN and is_input is False: tip, label_location = self._add_output(lrpath, angle, *spec) tips[n - i - 1, :] = tip label_locations[n - i - 1, :] = label_location @@ -670,7 +663,7 @@ def add(self, patchlabel='', flows=None, orientations=None, labels='', has_right_output = False for i, (angle, is_input, spec) in enumerate(zip( angles, are_inputs, list(zip(scaled_flows, pathlengths)))): - if angle == RIGHT and not is_input: + if angle == RIGHT and is_input is False: if not has_right_output: # Make sure the upper path extends # at least as far as the lower one. diff --git a/lib/matplotlib/scale.py b/lib/matplotlib/scale.py index 7c90369250d0..01e09f11b444 100644 --- a/lib/matplotlib/scale.py +++ b/lib/matplotlib/scale.py @@ -1,24 +1,28 @@ """ Scales define the distribution of data values on an axis, e.g. a log scaling. - -They are attached to an `~.axis.Axis` and hold a `.Transform`, which is -responsible for the actual data transformation. +They are defined as subclasses of `ScaleBase`. See also `.axes.Axes.set_xscale` and the scales examples in the documentation. + +See :doc:`/gallery/scales/custom_scale` for a full example of defining a custom +scale. + +Matplotlib also supports non-separable transformations that operate on both +`~.axis.Axis` at the same time. They are known as projections, and defined in +`matplotlib.projections`. """ import inspect import textwrap import numpy as np -from numpy import ma import matplotlib as mpl -from matplotlib import _api, docstring +from matplotlib import _api, _docstring from matplotlib.ticker import ( NullFormatter, ScalarFormatter, LogFormatterSciNotation, LogitFormatter, NullLocator, LogLocator, AutoLocator, AutoMinorLocator, - SymmetricalLogLocator, LogitLocator) + SymmetricalLogLocator, AsinhLocator, LogitLocator) from matplotlib.transforms import Transform, IdentityTransform @@ -28,16 +32,20 @@ class ScaleBase: Scales are separable transformations, working on a single dimension. - Any subclasses will want to override: - - - :attr:`name` - - :meth:`get_transform` - - :meth:`set_default_locators_and_formatters` - - And optionally: - - - :meth:`limit_range_for_scale` - + Subclasses should override + + :attr:`name` + The scale's name. + :meth:`get_transform` + A method returning a `.Transform`, which converts data coordinates to + scaled coordinates. This transform should be invertible, so that e.g. + mouse positions can be converted back to data coordinates. + :meth:`set_default_locators_and_formatters` + A method that sets default locators and formatters for an `~.axis.Axis` + that uses this scale. + :meth:`limit_range_for_scale` + An optional method that "fixes" the axis range to acceptable values, + e.g. restricting log-scaled axes to positive values. """ def __init__(self, axis): @@ -56,8 +64,7 @@ def __init__(self, axis): def get_transform(self): """ - Return the :class:`~matplotlib.transforms.Transform` object - associated with this scale. + Return the `.Transform` object associated with this scale. """ raise NotImplementedError() @@ -194,7 +201,6 @@ def set_default_locators_and_formatters(self, axis): class LogTransform(Transform): input_dims = output_dims = 1 - @_api.rename_parameter("3.3", "nonpos", "nonpositive") def __init__(self, base, nonpositive='clip'): super().__init__() if base <= 0 or base == 1: @@ -244,7 +250,7 @@ def __str__(self): return "{}(base={})".format(type(self).__name__, self.base) def transform_non_affine(self, a): - return ma.power(self.base, a) + return np.power(self.base, a) def inverted(self): return LogTransform(self.base) @@ -256,17 +262,7 @@ class LogScale(ScaleBase): """ name = 'log' - @_api.deprecated("3.3", alternative="scale.LogTransform") - @property - def LogTransform(self): - return LogTransform - - @_api.deprecated("3.3", alternative="scale.InvertedLogTransform") - @property - def InvertedLogTransform(self): - return InvertedLogTransform - - def __init__(self, axis, **kwargs): + def __init__(self, axis, *, base=10, subs=None, nonpositive="clip"): """ Parameters ---------- @@ -282,18 +278,6 @@ def __init__(self, axis, **kwargs): in a log10 scale, ``[2, 3, 4, 5, 6, 7, 8, 9]`` will place 8 logarithmically spaced minor ticks between each major tick. """ - # After the deprecation, the whole (outer) __init__ can be replaced by - # def __init__(self, axis, *, base=10, subs=None, nonpositive="clip") - # The following is to emit the right warnings depending on the axis - # used, as the *old* kwarg names depended on the axis. - axis_name = getattr(axis, "axis_name", "x") - @_api.rename_parameter("3.3", f"base{axis_name}", "base") - @_api.rename_parameter("3.3", f"subs{axis_name}", "subs") - @_api.rename_parameter("3.3", f"nonpos{axis_name}", "nonpositive") - def __init__(*, base=10, subs=None, nonpositive="clip"): - return base, subs, nonpositive - - base, subs, nonpositive = __init__(**kwargs) self._transform = LogTransform(base, nonpositive) self.subs = subs @@ -405,6 +389,11 @@ def __init__(self, base, linthresh, linscale): def transform_non_affine(self, a): abs_a = np.abs(a) + if (abs_a < self.linthresh).all(): + _api.warn_external( + "All values for SymLogScale are below linthresh, making " + "it effectively linear. You likely should lower the value " + "of linthresh. ") with np.errstate(divide="ignore", invalid="ignore"): out = np.sign(a) * self.linthresh * ( np.power(self.base, @@ -452,28 +441,7 @@ class SymmetricalLogScale(ScaleBase): """ name = 'symlog' - @_api.deprecated("3.3", alternative="scale.SymmetricalLogTransform") - @property - def SymmetricalLogTransform(self): - return SymmetricalLogTransform - - @_api.deprecated( - "3.3", alternative="scale.InvertedSymmetricalLogTransform") - @property - def InvertedSymmetricalLogTransform(self): - return InvertedSymmetricalLogTransform - - def __init__(self, axis, **kwargs): - axis_name = getattr(axis, "axis_name", "x") - # See explanation in LogScale.__init__. - @_api.rename_parameter("3.3", f"base{axis_name}", "base") - @_api.rename_parameter("3.3", f"linthresh{axis_name}", "linthresh") - @_api.rename_parameter("3.3", f"subs{axis_name}", "subs") - @_api.rename_parameter("3.3", f"linscale{axis_name}", "linscale") - def __init__(*, base=10, linthresh=2, subs=None, linscale=1): - return base, linthresh, subs, linscale - - base, linthresh, subs, linscale = __init__(**kwargs) + def __init__(self, axis, *, base=10, linthresh=2, subs=None, linscale=1): self._transform = SymmetricalLogTransform(base, linthresh, linscale) self.subs = subs @@ -494,10 +462,126 @@ def get_transform(self): return self._transform +class AsinhTransform(Transform): + """Inverse hyperbolic-sine transformation used by `.AsinhScale`""" + input_dims = output_dims = 1 + + def __init__(self, linear_width): + super().__init__() + if linear_width <= 0.0: + raise ValueError("Scale parameter 'linear_width' " + + "must be strictly positive") + self.linear_width = linear_width + + def transform_non_affine(self, a): + return self.linear_width * np.arcsinh(a / self.linear_width) + + def inverted(self): + return InvertedAsinhTransform(self.linear_width) + + +class InvertedAsinhTransform(Transform): + """Hyperbolic sine transformation used by `.AsinhScale`""" + input_dims = output_dims = 1 + + def __init__(self, linear_width): + super().__init__() + self.linear_width = linear_width + + def transform_non_affine(self, a): + return self.linear_width * np.sinh(a / self.linear_width) + + def inverted(self): + return AsinhTransform(self.linear_width) + + +class AsinhScale(ScaleBase): + """ + A quasi-logarithmic scale based on the inverse hyperbolic sine (asinh) + + For values close to zero, this is essentially a linear scale, + but for large magnitude values (either positive or negative) + it is asymptotically logarithmic. The transition between these + linear and logarithmic regimes is smooth, and has no discontinuities + in the function gradient in contrast to + the `.SymmetricalLogScale` ("symlog") scale. + + Specifically, the transformation of an axis coordinate :math:`a` is + :math:`a \\rightarrow a_0 \\sinh^{-1} (a / a_0)` where :math:`a_0` + is the effective width of the linear region of the transformation. + In that region, the transformation is + :math:`a \\rightarrow a + \\mathcal{O}(a^3)`. + For large values of :math:`a` the transformation behaves as + :math:`a \\rightarrow a_0 \\, \\mathrm{sgn}(a) \\ln |a| + \\mathcal{O}(1)`. + + .. note:: + + This API is provisional and may be revised in the future + based on early user feedback. + """ + + name = 'asinh' + + auto_tick_multipliers = { + 3: (2, ), + 4: (2, ), + 5: (2, ), + 8: (2, 4), + 10: (2, 5), + 16: (2, 4, 8), + 64: (4, 16), + 1024: (256, 512) + } + + def __init__(self, axis, *, linear_width=1.0, + base=10, subs='auto', **kwargs): + """ + Parameters + ---------- + linear_width : float, default: 1 + The scale parameter (elsewhere referred to as :math:`a_0`) + defining the extent of the quasi-linear region, + and the coordinate values beyond which the transformation + becomes asymptotically logarithmic. + base : int, default: 10 + The number base used for rounding tick locations + on a logarithmic scale. If this is less than one, + then rounding is to the nearest integer multiple + of powers of ten. + subs : sequence of int + Multiples of the number base used for minor ticks. + If set to 'auto', this will use built-in defaults, + e.g. (2, 5) for base=10. + """ + super().__init__(axis) + self._transform = AsinhTransform(linear_width) + self._base = int(base) + if subs == 'auto': + self._subs = self.auto_tick_multipliers.get(self._base) + else: + self._subs = subs + + linear_width = property(lambda self: self._transform.linear_width) + + def get_transform(self): + return self._transform + + def set_default_locators_and_formatters(self, axis): + axis.set(major_locator=AsinhLocator(self.linear_width, + base=self._base), + minor_locator=AsinhLocator(self.linear_width, + base=self._base, + subs=self._subs), + minor_formatter=NullFormatter()) + if self._base > 1: + axis.set_major_formatter(LogFormatterSciNotation(self._base)) + else: + axis.set_major_formatter('{x:.3g}'), + + class LogitTransform(Transform): input_dims = output_dims = 1 - @_api.rename_parameter("3.3", "nonpos", "nonpositive") def __init__(self, nonpositive='mask'): super().__init__() _api.check_in_list(['mask', 'clip'], nonpositive=nonpositive) @@ -523,7 +607,6 @@ def __str__(self): class LogisticTransform(Transform): input_dims = output_dims = 1 - @_api.rename_parameter("3.3", "nonpos", "nonpositive") def __init__(self, nonpositive='mask'): super().__init__() self._nonpositive = nonpositive @@ -548,7 +631,6 @@ class LogitScale(ScaleBase): """ name = 'logit' - @_api.rename_parameter("3.3", "nonpos", "nonpositive") def __init__(self, axis, nonpositive='mask', *, one_half=r"\frac{1}{2}", use_overline=False): r""" @@ -607,6 +689,7 @@ def limit_range_for_scale(self, vmin, vmax, minpos): 'linear': LinearScale, 'log': LogScale, 'symlog': SymmetricalLogScale, + 'asinh': AsinhScale, 'logit': LogitScale, 'function': FuncScale, 'functionlog': FuncScaleLog, @@ -627,9 +710,8 @@ def scale_factory(scale, axis, **kwargs): scale : {%(names)s} axis : `matplotlib.axis.Axis` """ - scale = scale.lower() - _api.check_in_list(_scale_mapping, scale=scale) - return _scale_mapping[scale](axis, **kwargs) + scale_cls = _api.check_getitem(_scale_mapping, scale=scale) + return scale_cls(axis, **kwargs) if scale_factory.__doc__: @@ -655,16 +737,17 @@ def _get_scale_docs(): """ docs = [] for name, scale_class in _scale_mapping.items(): + docstring = inspect.getdoc(scale_class.__init__) or "" docs.extend([ f" {name!r}", "", - textwrap.indent(inspect.getdoc(scale_class.__init__), " " * 8), + textwrap.indent(docstring, " " * 8), "" ]) return "\n".join(docs) -docstring.interpd.update( +_docstring.interpd.update( scale_type='{%s}' % ', '.join([repr(x) for x in get_scale_names()]), scale_docs=_get_scale_docs().rstrip(), ) diff --git a/lib/matplotlib/sphinxext/mathmpl.py b/lib/matplotlib/sphinxext/mathmpl.py index b86d0e841c53..dd30f34a8e66 100644 --- a/lib/matplotlib/sphinxext/mathmpl.py +++ b/lib/matplotlib/sphinxext/mathmpl.py @@ -1,12 +1,79 @@ +r""" +A role and directive to display mathtext in Sphinx +================================================== + +.. warning:: + In most cases, you will likely want to use one of `Sphinx's builtin Math + extensions + `__ + instead of this one. + +Mathtext may be included in two ways: + +1. Inline, using the role:: + + This text uses inline math: :mathmpl:`\alpha > \beta`. + + which produces: + + This text uses inline math: :mathmpl:`\alpha > \beta`. + +2. Standalone, using the directive:: + + Here is some standalone math: + + .. mathmpl:: + + \alpha > \beta + + which produces: + + Here is some standalone math: + + .. mathmpl:: + + \alpha > \beta + +Options +------- + +The ``mathmpl`` role and directive both support the following options: + + fontset : str, default: 'cm' + The font set to use when displaying math. See :rc:`mathtext.fontset`. + + fontsize : float + The font size, in points. Defaults to the value from the extension + configuration option defined below. + +Configuration options +--------------------- + +The mathtext extension has the following configuration options: + + mathmpl_fontsize : float, default: 10.0 + Default font size, in points. + + mathmpl_srcset : list of str, default: [] + Additional image sizes to generate when embedding in HTML, to support + `responsive resolution images + `__. + The list should contain additional x-descriptors (``'1.5x'``, ``'2x'``, + etc.) to generate (1x is the default and always included.) + +""" + import hashlib from pathlib import Path from docutils import nodes from docutils.parsers.rst import Directive, directives import sphinx +from sphinx.errors import ConfigError, ExtensionError import matplotlib as mpl from matplotlib import _api, mathtext +from matplotlib.rcsetup import validate_float_or_None # Define LaTeX math node: @@ -25,32 +92,40 @@ def math_role(role, rawtext, text, lineno, inliner, node = latex_math(rawtext) node['latex'] = latex node['fontset'] = options.get('fontset', 'cm') + node['fontsize'] = options.get('fontsize', + setup.app.config.mathmpl_fontsize) return [node], [] -math_role.options = {'fontset': fontset_choice} +math_role.options = {'fontset': fontset_choice, + 'fontsize': validate_float_or_None} class MathDirective(Directive): + """ + The ``.. mathmpl::`` directive, as documented in the module's docstring. + """ has_content = True required_arguments = 0 optional_arguments = 0 final_argument_whitespace = False - option_spec = {'fontset': fontset_choice} + option_spec = {'fontset': fontset_choice, + 'fontsize': validate_float_or_None} def run(self): latex = ''.join(self.content) node = latex_math(self.block_text) node['latex'] = latex node['fontset'] = self.options.get('fontset', 'cm') + node['fontsize'] = self.options.get('fontsize', + setup.app.config.mathmpl_fontsize) return [node] # This uses mathtext to render the expression -def latex2png(latex, filename, fontset='cm'): - latex = "$%s$" % latex - with mpl.rc_context({'mathtext.fontset': fontset}): +def latex2png(latex, filename, fontset='cm', fontsize=10, dpi=100): + with mpl.rc_context({'mathtext.fontset': fontset, 'font.size': fontsize}): try: depth = mathtext.math_to_image( - latex, filename, dpi=100, format="png") + f"${latex}$", filename, dpi=dpi, format="png") except Exception: _api.warn_external(f"Could not render math expression {latex}") depth = 0 @@ -62,14 +137,26 @@ def latex2html(node, source): inline = isinstance(node.parent, nodes.TextElement) latex = node['latex'] fontset = node['fontset'] + fontsize = node['fontsize'] name = 'math-{}'.format( - hashlib.md5((latex + fontset).encode()).hexdigest()[-10:]) + hashlib.md5(f'{latex}{fontset}{fontsize}'.encode()).hexdigest()[-10:]) destdir = Path(setup.app.builder.outdir, '_images', 'mathmpl') destdir.mkdir(parents=True, exist_ok=True) - dest = destdir / f'{name}.png' - depth = latex2png(latex, dest, fontset) + dest = destdir / f'{name}.png' + depth = latex2png(latex, dest, fontset, fontsize=fontsize) + + srcset = [] + for size in setup.app.config.mathmpl_srcset: + filename = f'{name}-{size.replace(".", "_")}.png' + latex2png(latex, destdir / filename, fontset, fontsize=fontsize, + dpi=100 * float(size[:-1])) + srcset.append( + f'{setup.app.builder.imgpath}/mathmpl/{filename} {size}') + if srcset: + srcset = (f'srcset="{setup.app.builder.imgpath}/mathmpl/{name}.png, ' + + ', '.join(srcset) + '" ') if inline: cls = '' @@ -81,11 +168,35 @@ def latex2html(node, source): style = '' return (f'') + f' {srcset}{cls}{style}/>') + + +def _config_inited(app, config): + # Check for srcset hidpi images + for i, size in enumerate(app.config.mathmpl_srcset): + if size[-1] == 'x': # "2x" = "2.0" + try: + float(size[:-1]) + except ValueError: + raise ConfigError( + f'Invalid value for mathmpl_srcset parameter: {size!r}. ' + 'Must be a list of strings with the multiplicative ' + 'factor followed by an "x". e.g. ["2.0x", "1.5x"]') + else: + raise ConfigError( + f'Invalid value for mathmpl_srcset parameter: {size!r}. ' + 'Must be a list of strings with the multiplicative ' + 'factor followed by an "x". e.g. ["2.0x", "1.5x"]') def setup(app): setup.app = app + app.add_config_value('mathmpl_fontsize', 10.0, True) + app.add_config_value('mathmpl_srcset', [], True) + try: + app.connect('config-inited', _config_inited) # Sphinx 1.8+ + except ExtensionError: + app.connect('env-updated', lambda app, env: _config_inited(app, None)) # Add visit/depart methods to HTML-Translator: def visit_latex_math_html(self, node): diff --git a/lib/matplotlib/sphinxext/plot_directive.py b/lib/matplotlib/sphinxext/plot_directive.py index 9433393b6dd7..c942085e2159 100644 --- a/lib/matplotlib/sphinxext/plot_directive.py +++ b/lib/matplotlib/sphinxext/plot_directive.py @@ -2,10 +2,13 @@ A directive for including a Matplotlib plot in a Sphinx document ================================================================ -By default, in HTML output, `plot` will include a .png file with a link to a -high-res .png and .pdf. In LaTeX output, it will include a .pdf. +This is a Sphinx extension providing a reStructuredText directive +``.. plot::`` for including a plot in a Sphinx document. -The source code for the plot may be included in one of three ways: +In HTML output, ``.. plot::`` will include a .png file with a link +to a high-res .png and .pdf. In LaTeX output, it will include a .pdf. + +The plot content may be defined in one of three ways: 1. **A path to a source file** as the argument to the directive:: @@ -16,7 +19,7 @@ .. plot:: path/to/plot.py - The plot's caption. + The plot caption. Additionally, one may specify the name of a function to call (with no arguments) immediately after importing the module:: @@ -28,10 +31,8 @@ .. plot:: import matplotlib.pyplot as plt - import matplotlib.image as mpimg - import numpy as np - img = mpimg.imread('_static/stinkbug.png') - imgplot = plt.imshow(img) + plt.plot([1, 2, 3], [4, 5, 6]) + plt.title("A plotting exammple") 3. Using **doctest** syntax:: @@ -44,21 +45,22 @@ Options ------- -The ``plot`` directive supports the following options: +The ``.. plot::`` directive supports the following options: - format : {'python', 'doctest'} - The format of the input. + ``:format:`` : {'python', 'doctest'} + The format of the input. If unset, the format is auto-detected. - include-source : bool - Whether to display the source code. The default can be changed - using the `plot_include_source` variable in :file:`conf.py`. + ``:include-source:`` : bool + Whether to display the source code. The default can be changed using + the ``plot_include_source`` variable in :file:`conf.py` (which itself + defaults to False). - encoding : str - If this source file is in a non-UTF8 or non-ASCII encoding, the - encoding must be specified using the ``:encoding:`` option. The - encoding will not be inferred using the ``-*- coding -*-`` metacomment. + ``:show-source-link:`` : bool + Whether to show a link to the source in HTML. The default can be + changed using the ``plot_html_show_source_link`` variable in + :file:`conf.py` (which itself defaults to True). - context : bool or str + ``:context:`` : bool or str If provided, the code will be run in the context of all previous plot directives for which the ``:context:`` option was specified. This only applies to inline code plot directives, not those run from files. If @@ -67,18 +69,19 @@ running the code. ``:context: close-figs`` keeps the context but closes previous figures before running the code. - nofigs : bool + ``:nofigs:`` : bool If specified, the code block will be run, but no figures will be inserted. This is usually useful with the ``:context:`` option. - caption : str + ``:caption:`` : str If specified, the option's argument will be used as a caption for the figure. This overwrites the caption given in the content, when the plot is generated from a file. -Additionally, this directive supports all of the options of the `image` -directive, except for *target* (since plot will add its own target). These -include *alt*, *height*, *width*, *scale*, *align* and *class*. +Additionally, this directive supports all the options of the `image directive +`_, +except for ``:target:`` (since plot will add its own target). These include +``:alt:``, ``:height:``, ``:width:``, ``:scale:``, ``:align:`` and ``:class:``. Configuration options --------------------- @@ -86,25 +89,26 @@ The plot directive has the following configuration options: plot_include_source - Default value for the include-source option + Default value for the include-source option (default: False). plot_html_show_source_link - Whether to show a link to the source in HTML. + Whether to show a link to the source in HTML (default: True). plot_pre_code - Code that should be executed before each plot. If not specified or None + Code that should be executed before each plot. If None (the default), it will default to a string containing:: import numpy as np from matplotlib import pyplot as plt plot_basedir - Base directory, to which ``plot::`` file names are relative - to. (If None or empty, file names are relative to the - directory where the file containing the directive is.) + Base directory, to which ``plot::`` file names are relative to. + If None or empty (the default), file names are relative to the + directory where the file containing the directive is. plot_formats - File formats to generate. List of tuples or strings:: + File formats to generate (default: ['png', 'hires.png', 'pdf']). + List of tuples or strings:: [(suffix, dpi), suffix, ...] @@ -114,16 +118,16 @@ suffix:dpi,suffix:dpi, ... plot_html_show_formats - Whether to show links to the files in HTML. + Whether to show links to the files in HTML (default: True). plot_rcparams A dictionary containing any non-standard rcParams that should - be applied before each plot. + be applied before each plot (default: {}). plot_apply_rcparams By default, rcParams are applied when ``:context:`` option is not used - in a plot directive. This configuration option overrides this behavior - and applies rcParams before each plot. + in a plot directive. If set, this configuration option overrides this + behavior and applies rcParams before each plot. plot_working_directory By default, the working directory will be changed to the directory of @@ -138,6 +142,7 @@ """ import contextlib +import doctest from io import StringIO import itertools import os @@ -156,12 +161,9 @@ import matplotlib from matplotlib.backend_bases import FigureManagerBase import matplotlib.pyplot as plt -from matplotlib import _api, _pylab_helpers, cbook +from matplotlib import _pylab_helpers, cbook matplotlib.use("agg") -align = _api.deprecated( - "3.4", alternative="docutils.parsers.rst.directives.images.Image.align")( - Image.align) __version__ = 2 @@ -180,7 +182,7 @@ def _option_boolean(arg): elif arg.strip().lower() in ('yes', '1', 'true'): return True else: - raise ValueError('"%s" unknown boolean' % arg) + raise ValueError(f'{arg!r} unknown boolean') def _option_context(arg): @@ -238,10 +240,10 @@ class PlotDirective(Directive): 'align': Image.align, 'class': directives.class_option, 'include-source': _option_boolean, + 'show-source-link': _option_boolean, 'format': _option_format, 'context': _option_context, 'nofigs': directives.flag, - 'encoding': directives.encoding, 'caption': directives.unchanged, } @@ -259,7 +261,8 @@ def _copy_css_file(app, exc): src = cbook._get_data_path('plot_directive/plot_directive.css') dst = app.outdir / Path('_static') dst.mkdir(exist_ok=True) - shutil.copy(src, dst) + # Use copyfile because we do not want to copy src's permissions. + shutil.copyfile(src, dst / Path('plot_directive.css')) def setup(app): @@ -302,43 +305,26 @@ def contains_doctest(text): return bool(m) -def unescape_doctest(text): - """ - Extract code from a piece of text, which contains either Python code - or doctests. - """ - if not contains_doctest(text): - return text - - code = "" - for line in text.split("\n"): - m = re.match(r'^\s*(>>>|\.\.\.) (.*)$', line) - if m: - code += m.group(2) + "\n" - elif line.strip(): - code += "# " + line.strip() + "\n" - else: - code += "\n" - return code - - -def split_code_at_show(text): +def _split_code_at_show(text, function_name): """Split code at plt.show().""" - parts = [] - is_doctest = contains_doctest(text) - part = [] - for line in text.split("\n"): - if (not is_doctest and line.strip() == 'plt.show()') or \ - (is_doctest and line.strip() == '>>> plt.show()'): - part.append(line) + is_doctest = contains_doctest(text) + if function_name is None: + parts = [] + part = [] + for line in text.split("\n"): + if ((not is_doctest and line.startswith('plt.show(')) or + (is_doctest and line.strip() == '>>> plt.show()')): + part.append(line) + parts.append("\n".join(part)) + part = [] + else: + part.append(line) + if "\n".join(part).strip(): parts.append("\n".join(part)) - part = [] - else: - part.append(line) - if "\n".join(part).strip(): - parts.append("\n".join(part)) - return parts + else: + parts = [text] + return is_doctest, parts # ----------------------------------------------------------------------------- @@ -350,16 +336,16 @@ def split_code_at_show(text): .. only:: html - {% if source_link or (html_show_formats and not multi_image) %} + {% if src_name or (html_show_formats and not multi_image) %} ( - {%- if source_link -%} - `Source code <{{ source_link }}>`__ + {%- if src_name -%} + :download:`Source code <{{ build_dir }}/{{ src_name }}>` {%- endif -%} {%- if html_show_formats and not multi_image -%} {%- for img in images -%} {%- for fmt in img.formats -%} - {%- if source_link or not loop.first -%}, {% endif -%} - `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + {%- if src_name or not loop.first -%}, {% endif -%} + :download:`{{ fmt }} <{{ build_dir }}/{{ img.basename }}.{{ fmt }}>` {%- endfor -%} {%- endfor -%} {%- endif -%} @@ -376,7 +362,7 @@ def split_code_at_show(text): ( {%- for fmt in img.formats -%} {%- if not loop.first -%}, {% endif -%} - `{{ fmt }} <{{ dest_dir }}/{{ img.basename }}.{{ fmt }}>`__ + :download:`{{ fmt }} <{{ build_dir }}/{{ img.basename }}.{{ fmt }}>` {%- endfor -%} ) {%- endif -%} @@ -424,21 +410,33 @@ def filenames(self): return [self.filename(fmt) for fmt in self.formats] -def out_of_date(original, derived): +def out_of_date(original, derived, includes=None): """ - Return whether *derived* is out-of-date relative to *original*, both of - which are full file paths. + Return whether *derived* is out-of-date relative to *original* or any of + the RST files included in it using the RST include directive (*includes*). + *derived* and *original* are full paths, and *includes* is optionally a + list of full paths which may have been included in the *original*. """ - return (not os.path.exists(derived) or - (os.path.exists(original) and - os.stat(derived).st_mtime < os.stat(original).st_mtime)) + if not os.path.exists(derived): + return True + + if includes is None: + includes = [] + files_to_check = [original, *includes] + + def out_of_date_one(original, derived_mtime): + return (os.path.exists(original) and + derived_mtime < os.stat(original).st_mtime) + + derived_mtime = os.stat(derived).st_mtime + return any(out_of_date_one(f, derived_mtime) for f in files_to_check) class PlotError(RuntimeError): pass -def run_code(code, code_path, ns=None, function_name=None): +def _run_code(code, code_path, ns=None, function_name=None): """ Import a Python module from a path, and run the function given by name, if function_name is not None. @@ -452,13 +450,13 @@ def run_code(code, code_path, ns=None, function_name=None): try: os.chdir(setup.config.plot_working_directory) except OSError as err: - raise OSError(str(err) + '\n`plot_working_directory` option in' - 'Sphinx configuration file must be a valid ' - 'directory path') from err + raise OSError(f'{err}\n`plot_working_directory` option in ' + f'Sphinx configuration file must be a valid ' + f'directory path') from err except TypeError as err: - raise TypeError(str(err) + '\n`plot_working_directory` option in ' - 'Sphinx configuration file must be a string or ' - 'None') from err + raise TypeError(f'{err}\n`plot_working_directory` option in ' + f'Sphinx configuration file must be a string or ' + f'None') from err elif code_path is not None: dirname = os.path.abspath(os.path.dirname(code_path)) os.chdir(dirname) @@ -467,7 +465,6 @@ def run_code(code, code_path, ns=None, function_name=None): sys, argv=[code_path], path=[os.getcwd(), *sys.path]), \ contextlib.redirect_stdout(StringIO()): try: - code = unescape_doctest(code) if ns is None: ns = {} if not ns: @@ -519,34 +516,38 @@ def get_plot_formats(config): def render_figures(code, code_path, output_dir, output_base, context, function_name, config, context_reset=False, - close_figs=False): + close_figs=False, + code_includes=None): """ Run a pyplot script and save the images in *output_dir*. Save the images under *output_dir* with file names derived from *output_base* """ + if function_name is not None: + output_base = f'{output_base}_{function_name}' formats = get_plot_formats(config) # Try to determine if all images already exist - code_pieces = split_code_at_show(code) + is_doctest, code_pieces = _split_code_at_show(code, function_name) # Look for single-figure output files first - all_exists = True img = ImageFile(output_base, output_dir) for format, dpi in formats: - if out_of_date(code_path, img.filename(format)): + if context or out_of_date(code_path, img.filename(format), + includes=code_includes): all_exists = False break img.formats.append(format) + else: + all_exists = True if all_exists: return [(code, [img])] # Then look for multi-figure output files results = [] - all_exists = True for i, code_piece in enumerate(code_pieces): images = [] for j in itertools.count(): @@ -556,7 +557,8 @@ def render_figures(code, code_path, output_dir, output_base, context, else: img = ImageFile('%s_%02d' % (output_base, j), output_dir) for fmt, dpi in formats: - if out_of_date(code_path, img.filename(fmt)): + if context or out_of_date(code_path, img.filename(fmt), + includes=code_includes): all_exists = False break img.formats.append(fmt) @@ -569,6 +571,8 @@ def render_figures(code, code_path, output_dir, output_base, context, if not all_exists: break results.append((code_piece, images)) + else: + all_exists = True if all_exists: return results @@ -576,10 +580,7 @@ def render_figures(code, code_path, output_dir, output_base, context, # We didn't find the files, so build them results = [] - if context: - ns = plot_context - else: - ns = {} + ns = plot_context if context else {} if context_reset: clear_state(config.plot_rcparams) @@ -594,7 +595,9 @@ def render_figures(code, code_path, output_dir, output_base, context, elif close_figs: plt.close('all') - run_code(code_piece, code_path, ns, function_name) + _run_code(doctest.script_from_examples(code_piece) if is_doctest + else code_piece, + code_path, ns, function_name) images = [] fig_managers = _pylab_helpers.Gcf.get_all_fig_managers() @@ -631,6 +634,7 @@ def run(arguments, content, options, state_machine, state, lineno): default_fmt = formats[0][0] options.setdefault('include-source', config.plot_include_source) + options.setdefault('show-source-link', config.plot_html_show_source_link) if 'class' in options: # classes are parsed into a list of string, and output by simply # printing the list, abusing the fact that RST guarantees to strip @@ -702,9 +706,7 @@ def run(arguments, content, options, state_machine, state, lineno): # determine output directory name fragment source_rel_name = relpath(source_file_name, setup.confdir) - source_rel_dir = os.path.dirname(source_rel_name) - while source_rel_dir.startswith(os.path.sep): - source_rel_dir = source_rel_dir[1:] + source_rel_dir = os.path.dirname(source_rel_name).lstrip(os.path.sep) # build_dir: where to place output files (temporarily) build_dir = os.path.join(os.path.dirname(setup.app.doctreedir), @@ -714,38 +716,55 @@ def run(arguments, content, options, state_machine, state, lineno): # see note in Python docs for warning about symbolic links on Windows. # need to compare source and dest paths at end build_dir = os.path.normpath(build_dir) - - if not os.path.exists(build_dir): - os.makedirs(build_dir) - - # output_dir: final location in the builder's directory - dest_dir = os.path.abspath(os.path.join(setup.app.builder.outdir, - source_rel_dir)) - if not os.path.exists(dest_dir): - os.makedirs(dest_dir) # no problem here for me, but just use built-ins + os.makedirs(build_dir, exist_ok=True) # how to link to files from the RST file - dest_dir_link = os.path.join(relpath(setup.confdir, rst_dir), - source_rel_dir).replace(os.path.sep, '/') try: build_dir_link = relpath(build_dir, rst_dir).replace(os.path.sep, '/') except ValueError: # on Windows, relpath raises ValueError when path and start are on # different mounts/drives build_dir_link = build_dir - source_link = dest_dir_link + '/' + output_base + source_ext + + # get list of included rst files so that the output is updated when any + # plots in the included files change. These attributes are modified by the + # include directive (see the docutils.parsers.rst.directives.misc module). + try: + source_file_includes = [os.path.join(os.getcwd(), t[0]) + for t in state.document.include_log] + except AttributeError: + # the document.include_log attribute only exists in docutils >=0.17, + # before that we need to inspect the state machine + possible_sources = {os.path.join(setup.confdir, t[0]) + for t in state_machine.input_lines.items} + source_file_includes = [f for f in possible_sources + if os.path.isfile(f)] + # remove the source file itself from the includes + try: + source_file_includes.remove(source_file_name) + except ValueError: + pass + + # save script (if necessary) + if options['show-source-link']: + Path(build_dir, output_base + source_ext).write_text( + doctest.script_from_examples(code) + if source_file_name == rst_file and is_doctest + else code, + encoding='utf-8') # make figures try: - results = render_figures(code, - source_file_name, - build_dir, - output_base, - keep_context, - function_name, - config, + results = render_figures(code=code, + code_path=source_file_name, + output_dir=build_dir, + output_base=output_base, + context=keep_context, + function_name=function_name, + config=config, context_reset=context_opt == 'reset', - close_figs=context_opt == 'close-figs') + close_figs=context_opt == 'close-figs', + code_includes=source_file_includes) errors = [] except PlotError as err: reporter = state.memo.reporter @@ -780,18 +799,17 @@ def run(arguments, content, options, state_machine, state, lineno): ':%s: %s' % (key, val) for key, val in options.items() if key in ('alt', 'height', 'width', 'scale', 'align', 'class')] - # Not-None src_link signals the need for a source link in the generated - # html - if j == 0 and config.plot_html_show_source_link: - src_link = source_link + # Not-None src_name signals the need for a source download in the + # generated html + if j == 0 and options['show-source-link']: + src_name = output_base + source_ext else: - src_link = None + src_name = None result = jinja2.Template(config.plot_template or TEMPLATE).render( default_fmt=default_fmt, - dest_dir=dest_dir_link, build_dir=build_dir_link, - source_link=src_link, + src_name=src_name, multi_image=len(images) > 1, options=opts, images=images, @@ -805,19 +823,4 @@ def run(arguments, content, options, state_machine, state, lineno): if total_lines: state_machine.insert_input(total_lines, source=source_file_name) - # copy image files to builder's output directory, if necessary - Path(dest_dir).mkdir(parents=True, exist_ok=True) - - for code_piece, images in results: - for img in images: - for fn in img.filenames(): - destimg = os.path.join(dest_dir, os.path.basename(fn)) - if fn != destimg: - shutil.copyfile(fn, destimg) - - # copy script (if necessary) - Path(dest_dir, output_base + source_ext).write_text( - unescape_doctest(code) if source_file_name == rst_file else code, - encoding='utf-8') - return errors diff --git a/lib/matplotlib/spines.py b/lib/matplotlib/spines.py index 40ceb157fd6e..674ae3e97067 100644 --- a/lib/matplotlib/spines.py +++ b/lib/matplotlib/spines.py @@ -3,8 +3,8 @@ import numpy as np -import matplotlib -from matplotlib import _api, docstring, rcParams +import matplotlib as mpl +from matplotlib import _api, _docstring from matplotlib.artist import allow_rasterization import matplotlib.transforms as mtransforms import matplotlib.patches as mpatches @@ -23,15 +23,16 @@ class Spine(mpatches.Patch): Spines are subclasses of `.Patch`, and inherit much of their behavior. - Spines draw a line, a circle, or an arc depending if + Spines draw a line, a circle, or an arc depending on if `~.Spine.set_patch_line`, `~.Spine.set_patch_circle`, or `~.Spine.set_patch_arc` has been called. Line-like is the default. + For examples see :ref:`spines_examples`. """ def __str__(self): return "Spine" - @docstring.dedent_interpd + @_docstring.dedent_interpd def __init__(self, axes, spine_type, path, **kwargs): """ Parameters @@ -48,15 +49,15 @@ def __init__(self, axes, spine_type, path, **kwargs): **kwargs Valid keyword arguments are: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ super().__init__(**kwargs) self.axes = axes self.set_figure(self.axes.figure) self.spine_type = spine_type self.set_facecolor('none') - self.set_edgecolor(rcParams['axes.edgecolor']) - self.set_linewidth(rcParams['axes.linewidth']) + self.set_edgecolor(mpl.rcParams['axes.edgecolor']) + self.set_linewidth(mpl.rcParams['axes.linewidth']) self.set_capstyle('projecting') self.axis = None @@ -69,7 +70,7 @@ def __init__(self, axes, spine_type, path, **kwargs): # non-rectangular axes is currently implemented, and this lets # them pass through the spines machinery without errors.) self._position = None - _api.check_isinstance(matplotlib.path.Path, path=path) + _api.check_isinstance(mpath.Path, path=path) self._path = path # To support drawing both linear and circular spines, this @@ -151,15 +152,16 @@ def get_window_extent(self, renderer=None): # make sure the location is updated so that transforms etc are correct: self._adjust_location() bb = super().get_window_extent(renderer=renderer) - if self.axis is None: + if self.axis is None or not self.axis.get_visible(): return bb bboxes = [bb] - tickstocheck = [self.axis.majorTicks[0]] - if len(self.axis.minorTicks) > 1: - # only pad for minor ticks if there are more than one - # of them. There is always one... - tickstocheck.append(self.axis.minorTicks[1]) - for tick in tickstocheck: + drawn_ticks = self.axis._update_ticks() + + major_tick = next(iter({*drawn_ticks} & {*self.axis.majorTicks}), None) + minor_tick = next(iter({*drawn_ticks} & {*self.axis.minorTicks}), None) + for tick in [major_tick, minor_tick]: + if tick is None: + continue bb0 = bb.frozen() tickl = tick._size tickdir = tick._tickdir @@ -222,10 +224,6 @@ def clear(self): if self.axis is not None: self.axis.clear() - @_api.deprecated("3.4", alternative="Spine.clear()") - def cla(self): - self.clear() - def _adjust_location(self): """Automatically set spine bounds to the view interval.""" @@ -305,8 +303,12 @@ def set_position(self, position): Additionally, shorthand notations define a special positions: - * 'center' -> ('axes', 0.5) - * 'zero' -> ('data', 0.0) + * 'center' -> ``('axes', 0.5)`` + * 'zero' -> ``('data', 0.0)`` + + Examples + -------- + :doc:`/gallery/spines/spine_placement_demo` """ if position in ('center', 'zero'): # special positions pass @@ -435,7 +437,7 @@ def linear_spine(cls, axes, spine_type, **kwargs): else: raise ValueError('unable to make path for spine "%s"' % spine_type) result = cls(axes, spine_type, path, **kwargs) - result.set_visible(rcParams['axes.spines.{0}'.format(spine_type)]) + result.set_visible(mpl.rcParams['axes.spines.{0}'.format(spine_type)]) return result @@ -515,7 +517,7 @@ class Spines(MutableMapping): The container of all `.Spine`\s in an Axes. The interface is dict-like mapping names (e.g. 'left') to `.Spine` objects. - Additionally it implements some pandas.Series-like features like accessing + Additionally, it implements some pandas.Series-like features like accessing elements by attribute:: spines['top'].set_visible(False) @@ -550,7 +552,7 @@ def __getattr__(self, name): try: return self._dict[name] except KeyError: - raise ValueError( + raise AttributeError( f"'Spines' object does not contain a '{name}' spine") def __getitem__(self, key): diff --git a/lib/matplotlib/stackplot.py b/lib/matplotlib/stackplot.py index a2e269875346..c97a21e029f9 100644 --- a/lib/matplotlib/stackplot.py +++ b/lib/matplotlib/stackplot.py @@ -1,11 +1,13 @@ """ Stacked area plot for 1D arrays inspired by Douglas Y'barbo's stackoverflow answer: -http://stackoverflow.com/questions/2225995/how-can-i-create-stacked-line-graph-with-matplotlib +https://stackoverflow.com/q/2225995/ -(http://stackoverflow.com/users/66549/doug) +(https://stackoverflow.com/users/66549/doug) """ +import itertools + import numpy as np from matplotlib import _api @@ -53,6 +55,9 @@ def stackplot(axes, x, *args, If not specified, the colors from the Axes property cycle will be used. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs All other keyword arguments are passed to `.Axes.fill_between`. @@ -67,7 +72,9 @@ def stackplot(axes, x, *args, labels = iter(labels) if colors is not None: - axes.set_prop_cycle(color=colors) + colors = itertools.cycle(colors) + else: + colors = (axes._get_lines.get_next_color() for _ in y) # Assume data passed has not been 'stacked', so stack it here. # We'll need a float buffer for the upcoming calculations. @@ -105,17 +112,16 @@ def stackplot(axes, x, *args, stack += first_line # Color between x = 0 and the first array. - color = axes._get_lines.get_next_color() coll = axes.fill_between(x, first_line, stack[0, :], - facecolor=color, label=next(labels, None), + facecolor=next(colors), label=next(labels, None), **kwargs) coll.sticky_edges.y[:] = [0] r = [coll] # Color between array i-1 and array i for i in range(len(y) - 1): - color = axes._get_lines.get_next_color() r.append(axes.fill_between(x, stack[i, :], stack[i + 1, :], - facecolor=color, label=next(labels, None), + facecolor=next(colors), + label=next(labels, None), **kwargs)) return r diff --git a/lib/matplotlib/streamplot.py b/lib/matplotlib/streamplot.py index 4e75ee19cb85..293fd6791213 100644 --- a/lib/matplotlib/streamplot.py +++ b/lib/matplotlib/streamplot.py @@ -5,12 +5,11 @@ import numpy as np -import matplotlib -from matplotlib import _api, cm +import matplotlib as mpl +from matplotlib import _api, cm, patches import matplotlib.colors as mcolors import matplotlib.collections as mcollections import matplotlib.lines as mlines -import matplotlib.patches as patches __all__ = ['streamplot'] @@ -19,14 +18,17 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, cmap=None, norm=None, arrowsize=1, arrowstyle='-|>', minlength=0.1, transform=None, zorder=None, start_points=None, - maxlength=4.0, integration_direction='both'): + maxlength=4.0, integration_direction='both', + broken_streamlines=True): """ Draw streamlines of a vector flow. Parameters ---------- x, y : 1D/2D arrays - Evenly spaced strictly increasing arrays to make a grid. + Evenly spaced strictly increasing arrays to make a grid. If 2D, all + rows of *x* must be equal and all columns of *y* must be equal; i.e., + they must be as if generated by ``np.meshgrid(x_1d, y_1d)``. u, v : 2D arrays *x* and *y*-velocities. The number of rows and columns must match the length of *y* and *x*, respectively. @@ -37,19 +39,17 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, For different densities in each direction, use a tuple (density_x, density_y). linewidth : float or 2D array - The width of the stream lines. With a 2D array the line width can be + The width of the streamlines. With a 2D array the line width can be varied across the grid. The array must have the same shape as *u* and *v*. color : color or 2D array The streamline color. If given an array, its values are converted to colors using *cmap* and *norm*. The array must have the same shape as *u* and *v*. - cmap : `~matplotlib.colors.Colormap` - Colormap used to plot streamlines and arrows. This is only used if - *color* is an array. - norm : `~matplotlib.colors.Normalize` - Normalize object used to scale luminance data to 0, 1. If ``None``, - stretch (min, max) to (0, 1). This is only used if *color* is an array. + cmap, norm + Data normalization and colormapping parameters for *color*; only used + if *color* is an array of floats. See `~.Axes.imshow` for a detailed + description. arrowsize : float Scaling factor for the arrow size. arrowstyle : str @@ -60,13 +60,19 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, start_points : Nx2 array Coordinates of starting points for the streamlines in data coordinates (the same coordinates as the *x* and *y* arrays). - zorder : int - The zorder of the stream lines and arrows. + zorder : float + The zorder of the streamlines and arrows. Artists with lower zorder values are drawn first. maxlength : float Maximum length of streamline in axes coordinates. integration_direction : {'forward', 'backward', 'both'}, default: 'both' Integrate the streamline in forward, backward or both directions. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + broken_streamlines : boolean, default: True + If False, forces streamlines to continue until they + leave the plot domain. If True, they may be terminated if they + come too close to another streamline. Returns ------- @@ -76,7 +82,7 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, - ``lines``: `.LineCollection` of streamlines - ``arrows``: `.PatchCollection` containing `.FancyArrowPatch` - objects representing the arrows half-way along stream lines. + objects representing the arrows half-way along streamlines. This container will probably change in the future to allow changes to the colormap, alpha, etc. for both lines and arrows, but these @@ -97,7 +103,7 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, color = axes._get_lines.get_next_color() if linewidth is None: - linewidth = matplotlib.rcParams['lines.linewidth'] + linewidth = mpl.rcParams['lines.linewidth'] line_kw = {} arrow_kw = dict(arrowstyle=arrowstyle, mutation_scale=10 * arrowsize) @@ -112,8 +118,8 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, if use_multicolor_lines: if color.shape != grid.shape: raise ValueError("If 'color' is given, it must match the shape of " - "'Grid(x, y)'") - line_colors = [] + "the (x, y) grid") + line_colors = [[]] # Empty entry allows concatenation of zero arrays. color = np.ma.masked_invalid(color) else: line_kw['color'] = color @@ -122,7 +128,7 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, if isinstance(linewidth, np.ndarray): if linewidth.shape != grid.shape: raise ValueError("If 'linewidth' is given, it must match the " - "shape of 'Grid(x, y)'") + "shape of the (x, y) grid") line_kw['linewidth'] = [] else: line_kw['linewidth'] = linewidth @@ -133,20 +139,20 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, # Sanity checks. if u.shape != grid.shape or v.shape != grid.shape: - raise ValueError("'u' and 'v' must match the shape of 'Grid(x, y)'") + raise ValueError("'u' and 'v' must match the shape of the (x, y) grid") u = np.ma.masked_invalid(u) v = np.ma.masked_invalid(v) - integrate = get_integrator(u, v, dmap, minlength, maxlength, - integration_direction) + integrate = _get_integrator(u, v, dmap, minlength, maxlength, + integration_direction) trajectories = [] if start_points is None: for xm, ym in _gen_starting_points(mask.shape): if mask[ym, xm] == 0: xg, yg = dmap.mask2grid(xm, ym) - t = integrate(xg, yg) + t = integrate(xg, yg, broken_streamlines) if t is not None: trajectories.append(t) else: @@ -167,32 +173,35 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, for xs, ys in sp2: xg, yg = dmap.data2grid(xs, ys) - t = integrate(xg, yg) + # Floating point issues can cause xg, yg to be slightly out of + # bounds for xs, ys on the upper boundaries. Because we have + # already checked that the starting points are within the original + # grid, clip the xg, yg to the grid to work around this issue + xg = np.clip(xg, 0, grid.nx - 1) + yg = np.clip(yg, 0, grid.ny - 1) + + t = integrate(xg, yg, broken_streamlines) if t is not None: trajectories.append(t) if use_multicolor_lines: if norm is None: norm = mcolors.Normalize(color.min(), color.max()) - if cmap is None: - cmap = cm.get_cmap(matplotlib.rcParams['image.cmap']) - else: - cmap = cm.get_cmap(cmap) + cmap = cm._ensure_cmap(cmap) streamlines = [] arrows = [] for t in trajectories: - tgx = np.array(t[0]) - tgy = np.array(t[1]) + tgx, tgy = t.T # Rescale from grid-coordinates to data-coordinates. - tx, ty = dmap.grid2data(*np.array(t)) + tx, ty = dmap.grid2data(tgx, tgy) tx += grid.x_origin ty += grid.y_origin points = np.transpose([tx, ty]).reshape(-1, 1, 2) streamlines.extend(np.hstack([points[:-1], points[1:]])) - # Add arrows half way along each trajectory. + # Add arrows halfway along each trajectory. s = np.cumsum(np.hypot(np.diff(tx), np.diff(ty))) n = np.searchsorted(s, s[-1] / 2.) arrow_tail = (tx[n], ty[n]) @@ -210,7 +219,6 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, p = patches.FancyArrowPatch( arrow_tail, arrow_head, transform=transform, **arrow_kw) - axes.add_patch(p) arrows.append(p) lc = mcollections.LineCollection( @@ -222,22 +230,20 @@ def streamplot(axes, x, y, u, v, density=1, linewidth=None, color=None, lc.set_cmap(cmap) lc.set_norm(norm) axes.add_collection(lc) - axes.autoscale_view() - ac = matplotlib.collections.PatchCollection(arrows) + ac = mcollections.PatchCollection(arrows) + # Adding the collection itself is broken; see #2341. + for p in arrows: + axes.add_patch(p) + + axes.autoscale_view() stream_container = StreamplotSet(lc, ac) return stream_container class StreamplotSet: - def __init__(self, lines, arrows, **kwargs): - if kwargs: - _api.warn_deprecated( - "3.3", - message="Passing arbitrary keyword arguments to StreamplotSet " - "is deprecated since %(since) and will become an " - "error %(removal)s.") + def __init__(self, lines, arrows): self.lines = lines self.arrows = arrows @@ -279,8 +285,7 @@ def __init__(self, grid, mask): def grid2mask(self, xi, yi): """Return nearest space in mask-coords from given grid-coords.""" - return (int(xi * self.x_grid2mask + 0.5), - int(yi * self.y_grid2mask + 0.5)) + return round(xi * self.x_grid2mask), round(yi * self.y_grid2mask) def mask2grid(self, xm, ym): return xm * self.x_mask2grid, ym * self.y_mask2grid @@ -291,19 +296,19 @@ def data2grid(self, xd, yd): def grid2data(self, xg, yg): return xg / self.x_data2grid, yg / self.y_data2grid - def start_trajectory(self, xg, yg): + def start_trajectory(self, xg, yg, broken_streamlines=True): xm, ym = self.grid2mask(xg, yg) - self.mask._start_trajectory(xm, ym) + self.mask._start_trajectory(xm, ym, broken_streamlines) def reset_start_point(self, xg, yg): xm, ym = self.grid2mask(xg, yg) self.mask._current_xy = (xm, ym) - def update_trajectory(self, xg, yg): + def update_trajectory(self, xg, yg, broken_streamlines=True): if not self.grid.within_grid(xg, yg): raise InvalidIndexError xm, ym = self.grid2mask(xg, yg) - self.mask._update_trajectory(xm, ym) + self.mask._update_trajectory(xm, ym, broken_streamlines) def undo_trajectory(self): self.mask._undo_trajectory() @@ -313,21 +318,22 @@ class Grid: """Grid of data.""" def __init__(self, x, y): - if x.ndim == 1: + if np.ndim(x) == 1: pass - elif x.ndim == 2: - x_row = x[0, :] + elif np.ndim(x) == 2: + x_row = x[0] if not np.allclose(x_row, x): raise ValueError("The rows of 'x' must be equal") x = x_row else: raise ValueError("'x' can have at maximum 2 dimensions") - if y.ndim == 1: + if np.ndim(y) == 1: pass - elif y.ndim == 2: - y_col = y[:, 0] - if not np.allclose(y_col, y.T): + elif np.ndim(y) == 2: + yt = np.transpose(y) # Also works for nested lists. + y_col = yt[0] + if not np.allclose(y_col, yt): raise ValueError("The columns of 'y' must be equal") y = y_col else: @@ -392,17 +398,17 @@ def __init__(self, density): def __getitem__(self, args): return self._mask[args] - def _start_trajectory(self, xm, ym): + def _start_trajectory(self, xm, ym, broken_streamlines=True): """Start recording streamline trajectory""" self._traj = [] - self._update_trajectory(xm, ym) + self._update_trajectory(xm, ym, broken_streamlines) def _undo_trajectory(self): """Remove current trajectory from mask""" for t in self._traj: self._mask[t] = 0 - def _update_trajectory(self, xm, ym): + def _update_trajectory(self, xm, ym, broken_streamlines=True): """ Update current trajectory position in mask. @@ -414,7 +420,10 @@ def _update_trajectory(self, xm, ym): self._mask[ym, xm] = 1 self._current_xy = (xm, ym) else: - raise InvalidIndexError + if broken_streamlines: + raise InvalidIndexError + else: + pass class InvalidIndexError(Exception): @@ -428,7 +437,7 @@ class TerminateTrajectory(Exception): # Integrator definitions # ======================= -def get_integrator(u, v, dmap, minlength, maxlength, integration_direction): +def _get_integrator(u, v, dmap, minlength, maxlength, integration_direction): # rescale velocity onto grid-coordinates for integrations. u, v = dmap.data2grid(u, v) @@ -453,7 +462,7 @@ def backward_time(xi, yi): dxi, dyi = forward_time(xi, yi) return -dxi, -dyi - def integrate(x0, y0): + def integrate(x0, y0, broken_streamlines=True): """ Return x, y grid-coordinates of trajectory based on starting point. @@ -465,30 +474,27 @@ def integrate(x0, y0): resulting trajectory is None if it is shorter than `minlength`. """ - stotal, x_traj, y_traj = 0., [], [] + stotal, xy_traj = 0., [] try: - dmap.start_trajectory(x0, y0) + dmap.start_trajectory(x0, y0, broken_streamlines) except InvalidIndexError: return None if integration_direction in ['both', 'backward']: - s, xt, yt = _integrate_rk12(x0, y0, dmap, backward_time, maxlength) + s, xyt = _integrate_rk12(x0, y0, dmap, backward_time, maxlength, + broken_streamlines) stotal += s - x_traj += xt[::-1] - y_traj += yt[::-1] + xy_traj += xyt[::-1] if integration_direction in ['both', 'forward']: dmap.reset_start_point(x0, y0) - s, xt, yt = _integrate_rk12(x0, y0, dmap, forward_time, maxlength) - if len(x_traj) > 0: - xt = xt[1:] - yt = yt[1:] + s, xyt = _integrate_rk12(x0, y0, dmap, forward_time, maxlength, + broken_streamlines) stotal += s - x_traj += xt - y_traj += yt + xy_traj += xyt[1:] if stotal > minlength: - return x_traj, y_traj + return np.broadcast_arrays(xy_traj, np.empty((1, 2)))[0] else: # reject short trajectories dmap.undo_trajectory() return None @@ -500,7 +506,7 @@ class OutOfBounds(IndexError): pass -def _integrate_rk12(x0, y0, dmap, f, maxlength): +def _integrate_rk12(x0, y0, dmap, f, maxlength, broken_streamlines=True): """ 2nd-order Runge-Kutta algorithm with adaptive step size. @@ -520,9 +526,8 @@ def _integrate_rk12(x0, y0, dmap, f, maxlength): timestep is more suited to the problem as this would be very hard to judge automatically otherwise. - This integrator is about 1.5 - 2x as fast as both the RK4 and RK45 - solvers in most setups on my machine. I would recommend removing the - other two to keep things simple. + This integrator is about 1.5 - 2x as fast as RK4 and RK45 solvers (using + similar Python implementations) in most setups. """ # This error is below that needed to match the RK4 integrator. It # is set for visual reasons -- too low and corners start @@ -541,14 +546,12 @@ def _integrate_rk12(x0, y0, dmap, f, maxlength): stotal = 0 xi = x0 yi = y0 - xf_traj = [] - yf_traj = [] + xyf_traj = [] while True: try: if dmap.grid.within_grid(xi, yi): - xf_traj.append(xi) - yf_traj.append(yi) + xyf_traj.append((xi, yi)) else: raise OutOfBounds @@ -562,9 +565,8 @@ def _integrate_rk12(x0, y0, dmap, f, maxlength): # Out of the domain during this step. # Take an Euler step to the boundary to improve neatness # unless the trajectory is currently empty. - if xf_traj: - ds, xf_traj, yf_traj = _euler_step(xf_traj, yf_traj, - dmap, f) + if xyf_traj: + ds, xyf_traj = _euler_step(xyf_traj, dmap, f) stotal += ds break except TerminateTrajectory: @@ -575,7 +577,7 @@ def _integrate_rk12(x0, y0, dmap, f, maxlength): dx2 = ds * 0.5 * (k1x + k2x) dy2 = ds * 0.5 * (k1y + k2y) - nx, ny = dmap.grid.shape + ny, nx = dmap.grid.shape # Error is normalized to the axes coordinates error = np.hypot((dx2 - dx1) / (nx - 1), (dy2 - dy1) / (ny - 1)) @@ -584,7 +586,7 @@ def _integrate_rk12(x0, y0, dmap, f, maxlength): xi += dx2 yi += dy2 try: - dmap.update_trajectory(xi, yi) + dmap.update_trajectory(xi, yi, broken_streamlines) except InvalidIndexError: break if stotal + ds > maxlength: @@ -597,14 +599,13 @@ def _integrate_rk12(x0, y0, dmap, f, maxlength): else: ds = min(maxds, 0.85 * ds * (maxerror / error) ** 0.5) - return stotal, xf_traj, yf_traj + return stotal, xyf_traj -def _euler_step(xf_traj, yf_traj, dmap, f): +def _euler_step(xyf_traj, dmap, f): """Simple Euler integration step that extends streamline to boundary.""" ny, nx = dmap.grid.shape - xi = xf_traj[-1] - yi = yf_traj[-1] + xi, yi = xyf_traj[-1] cx, cy = f(xi, yi) if cx == 0: dsx = np.inf @@ -619,9 +620,8 @@ def _euler_step(xf_traj, yf_traj, dmap, f): else: dsy = (ny - 1 - yi) / cy ds = min(dsx, dsy) - xf_traj.append(xi + cx * ds) - yf_traj.append(yi + cy * ds) - return ds, xf_traj, yf_traj + xyf_traj.append((xi + cx * ds, yi + cy * ds)) + return ds, xyf_traj # Utility functions diff --git a/lib/matplotlib/style/__init__.py b/lib/matplotlib/style/__init__.py index 42d050d22cd0..488c6d6ae1ec 100644 --- a/lib/matplotlib/style/__init__.py +++ b/lib/matplotlib/style/__init__.py @@ -1 +1,4 @@ -from .core import use, context, available, library, reload_library +from .core import available, context, library, reload_library, use + + +__all__ = ["available", "context", "library", "reload_library", "use"] diff --git a/lib/matplotlib/style/core.py b/lib/matplotlib/style/core.py index 0c2f632e016a..4ff4618ca6a3 100644 --- a/lib/matplotlib/style/core.py +++ b/lib/matplotlib/style/core.py @@ -15,11 +15,18 @@ import logging import os from pathlib import Path -import re +import sys import warnings +if sys.version_info >= (3, 10): + import importlib.resources as importlib_resources +else: + # Even though Py3.9 has importlib.resources, it doesn't properly handle + # modules added in sys.path. + import importlib_resources + import matplotlib as mpl -from matplotlib import _api, rc_params_from_file, rcParamsDefault +from matplotlib import _api, _docstring, _rc_params_in_file, rcParamsDefault _log = logging.getLogger(__name__) @@ -30,35 +37,44 @@ # Users may want multiple library paths, so store a list of paths. USER_LIBRARY_PATHS = [os.path.join(mpl.get_configdir(), 'stylelib')] STYLE_EXTENSION = 'mplstyle' -STYLE_FILE_PATTERN = re.compile(r'([\S]+).%s$' % STYLE_EXTENSION) - - # A list of rcParams that should not be applied from styles STYLE_BLACKLIST = { - 'interactive', 'backend', 'backend.qt4', 'webagg.port', 'webagg.address', + 'interactive', 'backend', 'webagg.port', 'webagg.address', 'webagg.port_retries', 'webagg.open_in_browser', 'backend_fallback', - 'toolbar', 'timezone', 'datapath', 'figure.max_open_warning', + 'toolbar', 'timezone', 'figure.max_open_warning', 'figure.raise_window', 'savefig.directory', 'tk.window_focus', 'docstring.hardcopy', 'date.epoch'} - - -def _remove_blacklisted_style_params(d, warn=True): - o = {} - for key in d: # prevent triggering RcParams.__getitem__('backend') - if key in STYLE_BLACKLIST: - if warn: - _api.warn_external( - "Style includes a parameter, '{0}', that is not related " - "to style. Ignoring".format(key)) - else: - o[key] = d[key] - return o - - -def _apply_style(d, warn=True): - mpl.rcParams.update(_remove_blacklisted_style_params(d, warn=warn)) - - +_DEPRECATED_SEABORN_STYLES = { + s: s.replace("seaborn", "seaborn-v0_8") + for s in [ + "seaborn", + "seaborn-bright", + "seaborn-colorblind", + "seaborn-dark", + "seaborn-darkgrid", + "seaborn-dark-palette", + "seaborn-deep", + "seaborn-muted", + "seaborn-notebook", + "seaborn-paper", + "seaborn-pastel", + "seaborn-poster", + "seaborn-talk", + "seaborn-ticks", + "seaborn-white", + "seaborn-whitegrid", + ] +} +_DEPRECATED_SEABORN_MSG = ( + "The seaborn styles shipped by Matplotlib are deprecated since %(since)s, " + "as they no longer correspond to the styles shipped by seaborn. However, " + "they will remain available as 'seaborn-v0_8- + @@ -15,7 +26,7 @@ L 460.8 345.6 L 460.8 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 343.296 307.584 L 343.296 41.472 L 57.6 41.472 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #67001f"/> - - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #67001f"/> - - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #67001f"/> - - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #67001f"/> - - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #67001f"/> - - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #67001f"/> - - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #67001f"/> - - + - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #a51429"/> - - + - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #e48066"/> - - + - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #fcdfcf"/> - - + +" clip-path="url(#pd7e07e7879)" style="fill: #d7e8f1"/> - - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #6bacd1"/> - - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #1c5c9f"/> - - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #053061"/> - - + - + +" clip-path="url(#pd7e07e7879)" style="fill: #053061"/> - - + +" clip-path="url(#pd7e07e7879)" style="fill: #053061"/> - - + +" clip-path="url(#pd7e07e7879)" style="fill: #053061"/> - +" clip-path="url(#pd7e07e7879)" style="fill: #053061"/> - +" clip-path="url(#pd7e07e7879)" style="fill: #053061"/> - +" clip-path="url(#pd7e07e7879)" style="fill: #053061"/> - +" style="stroke: #000000; stroke-width: 0.8"/> - + - - + + - + - - - - +" transform="scale(0.015625)"/> + + + - + - - - - - + + + + + - + - - - - - + + + + + - + - - + + - - - +" transform="scale(0.015625)"/> + + - + - + - + - + - + - + - + - - + + - - - +" transform="scale(0.015625)"/> + + - + - - - - + + + + @@ -14677,133 +14693,133 @@ z - +" style="stroke: #000000; stroke-width: 0.8"/> - + - - + + - + - - + + - + - - + + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + + + - + - + - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #bfbf00; stroke-width: 2"/> - - + - + - + - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #bfbf00; stroke-width: 2"/> - - + - + - + - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #bfbf00; stroke-width: 2"/> - - + - + - + - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #bfbf00; stroke-width: 2"/> - - + - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #bfbf00; stroke-width: 2"/> - - + - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #bfbf00; stroke-width: 2"/> - - + - + - + - + - + +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #bfbf00; stroke-width: 2"/> - - + - + - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #00bfbf; stroke-width: 2"/> - - + - + - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #00bfbf; stroke-width: 2"/> - - + - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #00bfbf; stroke-width: 2"/> - - + - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #00bfbf; stroke-width: 2"/> - - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #00bfbf; stroke-width: 2"/> - - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #00bfbf; stroke-width: 2"/> - - + +z +" clip-path="url(#pd7e07e7879)" style="fill: none; stroke: #00bfbf; stroke-width: 2"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="fill: #ffffff"/> - - + + + + - + + + + + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - +" clip-path="url(#p1451b0d78d)" style="fill: #053061"/> - - + - +" style="stroke: #000000; stroke-width: 0.8"/> - + - - + + - + - - - - - - +" transform="scale(0.015625)"/> + + + + + - + - - - - - + + + + + - + - - + + - - - - - - +" transform="scale(0.015625)"/> + + + + + - + - - - - - + + + + + - + - - - - + + + + - + - - - - + + + + - + - - - - + + + + - + - - - - + + + + - + - - - - + + + + - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - - + + - - + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.pdf b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.pdf index f4da53e4890f..c145cbbeb74d 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.png b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.png index 91d5a7e89bca..9f3ff4488a99 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.png and b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg index 7302d4e74273..b4d97d7d0e9f 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/errorbar_mixed.svg @@ -1,12 +1,23 @@ - - + + + + + + 2022-01-07T01:42:44.033823 + image/svg+xml + + + Matplotlib v3.6.0.dev1138+gd48fca95df.d20220107, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 576 432 L 576 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,66 +35,66 @@ L 274.909091 200.290909 L 274.909091 43.2 L 72 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#p2bb8a737e6)" style="fill: none; stroke: #0000ff"/> - +" style="stroke: #0000ff; stroke-width: 0.5"/> - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - +" style="stroke: #000000; stroke-width: 0.5"/> - - - - - - - - - + + + + + + + + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - + - + - + - + - + - + @@ -202,388 +213,399 @@ L 0 4 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - + - + - + - - +" transform="scale(0.015625)"/> + - - - + + + - + - + - - + + - + - + - - + + - + - + - - - + + + - - + + - + - + - - + + - - + + - + - + - + - - + + - + - + - + - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -594,66 +616,66 @@ L 518.4 200.290909 L 518.4 43.2 L 315.490909 43.2 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#p06e990243b)" style="fill: none; stroke: #0000ff; stroke-opacity: 0.4"/> - +" style="stroke: #0000ff; stroke-opacity: 0.4; stroke-width: 0.5"/> - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - +" style="stroke: #000000; stroke-opacity: 0.4; stroke-width: 0.5"/> - - - - - - - - - + + + + + + + + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - + - + - + - + - + - + - + @@ -762,409 +784,416 @@ L 518.4 43.2 - + - + - - + + - + - + - - - + + + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - - + + - - + + - + - + - + - + - + - + - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + @@ -1175,115 +1204,115 @@ L 274.909091 388.8 L 274.909091 231.709091 L 72 231.709091 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#p2c9259b7f3)" style="fill: none; stroke: #0000ff"/> - - + - + - + - + - + - + - + +" clip-path="url(#p2c9259b7f3)" style="fill: none; stroke: #0000ff"/> - +" style="stroke: #0000ff; stroke-width: 0.5"/> - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - +" clip-path="url(#p2c9259b7f3)" style="fill: none; stroke-dasharray: 6,6; stroke-dashoffset: 0; stroke: #0000ff"/> + + + + + + + + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - + - + @@ -1364,12 +1393,12 @@ L 274.909091 231.709091 - + - + @@ -1382,12 +1411,12 @@ L 274.909091 231.709091 - + - + @@ -1400,12 +1429,12 @@ L 274.909091 231.709091 - + - + @@ -1420,133 +1449,134 @@ L 274.909091 231.709091 - + - + - - - + + + - + - + - - + + - + - + - - + + - + - + - - + + - + - + - - + + - - - + + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + @@ -1557,179 +1587,179 @@ L 518.4 388.8 L 518.4 231.709091 L 315.490909 231.709091 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + - + - + - + - + - + - + +" clip-path="url(#pc7f973f0cd)" style="fill: none; stroke: #008000"/> - - + - + - + - + - + - + - + +" clip-path="url(#pc7f973f0cd)" style="fill: none; stroke: #008000"/> - +" style="stroke: #008000; stroke-width: 2"/> - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - +" style="stroke: #008000; stroke-width: 2"/> - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - + + + + + + + + + +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - + - + - + - + - + @@ -1742,12 +1772,12 @@ L 518.4 231.709091 - + - + @@ -1760,12 +1790,12 @@ L 518.4 231.709091 - + - + @@ -1778,12 +1808,12 @@ L 518.4 231.709091 - + - + @@ -1798,568 +1828,564 @@ L 518.4 231.709091 - + - + - - - - - - - - - + + + + + + - + - + - - - - - - + + + + + + - + - + - - - + + + - + - + - - - + + + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - + + - + - + - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/extent_units.png b/lib/matplotlib/tests/baseline_images/test_axes/extent_units.png new file mode 100644 index 000000000000..28bde8bf76ec Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/extent_units.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.pdf b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.pdf index eeb8969fa702..6a624dea46c8 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.png b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.png index 59c32a9084d7..007007ec6ee8 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.png and b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.svg b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.svg index 35a003c97c21..2e77acfd7601 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/fill_between_interpolate.svg @@ -1,12 +1,23 @@ - + + + + + 2021-02-17T21:57:55.184111 + image/svg+xml + + + Matplotlib v3.3.4.post2378.dev0+g01d3149b6, https://matplotlib.org/ + + + + + - + @@ -27,7 +38,7 @@ z " style="fill:#ffffff;"/> - - + - + - + +" style="fill:url(#ha0fdf7a1f0);stroke:#000000;"/> - - - - - - - +" id="m58dade9745" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="mc946a5ae82" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + @@ -572,92 +583,92 @@ L 0 4 +" id="m8b2567c4af" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m580a4f3bfe" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -673,7 +684,7 @@ z " style="fill:#ffffff;"/> - - - - - - - - - - - - - - - + - + - + - + - + - + - + - + - + - + @@ -1153,72 +1164,72 @@ L 518.4 231.709091 - + - + - + - + - + - + - + - + - + - + - + - + @@ -1226,15 +1237,15 @@ L 518.4 231.709091 - + - + - + + + + + + + + 2021-02-17T21:51:47.989640 + image/svg+xml + + + Matplotlib v3.3.4.post2378.dev0+g01d3149b6, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/hexbin_linear.png b/lib/matplotlib/tests/baseline_images/test_axes/hexbin_linear.png new file mode 100644 index 000000000000..824ea49b0599 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/hexbin_linear.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/hexbin_log.png b/lib/matplotlib/tests/baseline_images/test_axes/hexbin_log.png index febefb870918..466519461aac 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/hexbin_log.png and b/lib/matplotlib/tests/baseline_images/test_axes/hexbin_log.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.pdf b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.pdf index c33c7c4e29ca..5e2fd6190682 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.pdf and b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.png b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.png index e68b7c3c0baf..cde64b03c7f6 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.png and b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg index b5ebff4427b2..d1169e860808 100644 --- a/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg +++ b/lib/matplotlib/tests/baseline_images/test_axes/imshow_clip.svg @@ -1,12 +1,23 @@ - + + + + + 2021-03-02T20:09:49.859581 + image/svg+xml + + + Matplotlib v3.3.4.post2495+g8432e3164, https://matplotlib.org/ + + + + + - + @@ -26,9 +37,9 @@ L 103.104 41.472 z " style="fill:#ffffff;"/> - - + + @@ -36,38 +47,38 @@ iVBORw0KGgoAAAANSUhEUgAAAXIAAAFyCAYAAADoJFEJAAAABHNCSVQICAgIfAhkiAAAIABJREFUeJzt +" id="m0bf9183b9d" style="stroke:#000000;stroke-width:0.8;"/> - + - - + + - - +" id="DejaVuSans-30" transform="scale(0.015625)"/> + @@ -75,38 +86,38 @@ z - + - - - + + + @@ -115,31 +126,33 @@ z - + - - + + - - +" id="DejaVuSans-34" transform="scale(0.015625)"/> + @@ -148,44 +161,44 @@ z - + - - + + - - +" id="DejaVuSans-36" transform="scale(0.015625)"/> + @@ -194,53 +207,53 @@ z - + - - + + - - +" id="DejaVuSans-38" transform="scale(0.015625)"/> + @@ -253,10 +266,10 @@ z +" id="m09e66e4baf" style="stroke:#000000;stroke-width:0.8;"/> - + @@ -269,7 +282,7 @@ L -3.5 0 - + @@ -283,7 +296,7 @@ L -3.5 0 - + @@ -297,7 +310,7 @@ L -3.5 0 - + @@ -311,7 +324,7 @@ L -3.5 0 - + @@ -323,17 +336,23 @@ L -3.5 0 - - + @@ -455,7 +585,7 @@ L 369.216 41.472 - + - + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/inset_polar.png b/lib/matplotlib/tests/baseline_images/test_axes/inset_polar.png new file mode 100644 index 000000000000..b7b7faf198ec Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/inset_polar.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf deleted file mode 100644 index c76b653c33fb..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png deleted file mode 100644 index e305de1d9ac7..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg b/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg deleted file mode 100644 index 0a29c9d0af21..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_axes/log_scales.svg +++ /dev/null @@ -1,670 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_axes/markevery_linear_scales_nans.pdf b/lib/matplotlib/tests/baseline_images/test_axes/markevery_linear_scales_nans.pdf new file mode 100644 index 000000000000..bd4feafd6aa0 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/markevery_linear_scales_nans.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/markevery_linear_scales_nans.png b/lib/matplotlib/tests/baseline_images/test_axes/markevery_linear_scales_nans.png new file mode 100644 index 000000000000..2eb087944ec4 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/markevery_linear_scales_nans.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/markevery_linear_scales_nans.svg b/lib/matplotlib/tests/baseline_images/test_axes/markevery_linear_scales_nans.svg new file mode 100644 index 000000000000..c52aaf9de094 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_axes/markevery_linear_scales_nans.svg @@ -0,0 +1,3581 @@ + + + + + + + + 2022-03-28T18:57:12.789026 + image/svg+xml + + + Matplotlib v3.6.0.dev1706+g252085fd25, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_axes/mixed_errorbar_polar_caps.png b/lib/matplotlib/tests/baseline_images/test_axes/mixed_errorbar_polar_caps.png new file mode 100644 index 000000000000..bbe879779df4 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_axes/mixed_errorbar_polar_caps.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/pcolormesh_small.eps b/lib/matplotlib/tests/baseline_images/test_axes/pcolormesh_small.eps new file mode 100644 index 000000000000..c96cc35aea08 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_axes/pcolormesh_small.eps @@ -0,0 +1,312 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%Title: pcolormesh_small.eps +%%Creator: Matplotlib v3.6.0.dev2473+ga4ddf81873.d20220618, https://matplotlib.org/ +%%CreationDate: Sat Jun 18 21:17:11 2022 +%%Orientation: portrait +%%BoundingBox: 18 180 594 612 +%%HiResBoundingBox: 18.000000 180.000000 594.000000 612.000000 +%%EndComments +%%BeginProlog +/mpldict 10 dict def +mpldict begin +/_d { bind def } bind def +/m { moveto } _d +/l { lineto } _d +/r { rlineto } _d +/c { curveto } _d +/cl { closepath } _d +/ce { closepath eofill } _d +/box { + m + 1 index 0 r + 0 exch r + neg 0 r + cl + } _d +/clipbox { + box + clip + newpath + } _d +/sc { setcachedevice } _d +end +%%EndProlog +mpldict begin +18 180 translate +576 432 0 0 clipbox +gsave +0 0 m +576 0 l +576 432 l +0 432 l +cl +1.000 setgray +fill +grestore +0.500 setlinewidth +1 setlinejoin +0 setlinecap +[] 0 setdash +0.000 setgray +gsave +131.294 345.6 72 43.2 clipbox +144.211765 43.632866 m +83.208395 129.816433 l +119.371572 148.320555 l +180.374942 62.136988 l +144.211765 43.632866 l +gsave +0.500 0.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +131.294 345.6 72 43.2 clipbox +83.208395 129.816433 m +144.211765 216 l +180.374942 234.504122 l +119.371572 148.320555 l +83.208395 129.816433 l +gsave +0.000 0.000 0.500 setrgbcolor +fill +grestore +stroke +grestore +gsave +131.294 345.6 72 43.2 clipbox +180.374942 62.136988 m +119.371572 148.320555 l +141.279737 190.467054 l +202.283106 104.283487 l +180.374942 62.136988 l +gsave +0.161 1.000 0.806 setrgbcolor +fill +grestore +stroke +grestore +gsave +131.294 345.6 72 43.2 clipbox +119.371572 148.320555 m +180.374942 234.504122 l +202.283106 276.650621 l +141.279737 190.467054 l +119.371572 148.320555 l +stroke +grestore +gsave +131.294 345.6 72 43.2 clipbox +202.283106 104.283487 m +141.279737 190.467054 l +141.279737 241.532946 l +202.283106 155.349379 l +202.283106 104.283487 l +gsave +0.000 0.000 0.714 setrgbcolor +fill +grestore +stroke +grestore +gsave +131.294 345.6 72 43.2 clipbox +141.279737 190.467054 m +202.283106 276.650621 l +202.283106 327.716513 l +141.279737 241.532946 l +141.279737 190.467054 l +stroke +grestore +gsave +131.294 345.6 72 43.2 clipbox +202.283106 155.349379 m +141.279737 241.532946 l +119.371572 283.679445 l +180.374942 197.495878 l +202.283106 155.349379 l +stroke +grestore +gsave +131.294 345.6 72 43.2 clipbox +141.279737 241.532946 m +202.283106 327.716513 l +180.374942 369.863012 l +119.371572 283.679445 l +141.279737 241.532946 l +stroke +grestore +gsave +131.294 345.6 72 43.2 clipbox +180.374942 197.495878 m +119.371572 283.679445 l +83.208395 302.183567 l +144.211765 216 l +180.374942 197.495878 l +stroke +grestore +gsave +131.294 345.6 72 43.2 clipbox +119.371572 283.679445 m +180.374942 369.863012 l +144.211765 388.367134 l +83.208395 302.183567 l +119.371572 283.679445 l +stroke +grestore +2.000 setlinewidth +0.000 0.000 1.000 setrgbcolor +gsave +131.294 345.6 229.553 43.2 clipbox +301.764706 43.632866 m +240.761336 129.816433 l +276.924513 148.320555 l +337.927883 62.136988 l +301.764706 43.632866 l +gsave +0.500 0.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +1.000 setgray +gsave +131.294 345.6 229.553 43.2 clipbox +240.761336 129.816433 m +301.764706 216 l +337.927883 234.504122 l +276.924513 148.320555 l +240.761336 129.816433 l +gsave +0.000 0.000 0.500 setrgbcolor +fill +grestore +stroke +grestore +0.000 0.000 1.000 setrgbcolor +gsave +131.294 345.6 229.553 43.2 clipbox +337.927883 62.136988 m +276.924513 148.320555 l +298.832678 190.467054 l +359.836047 104.283487 l +337.927883 62.136988 l +gsave +0.161 1.000 0.806 setrgbcolor +fill +grestore +stroke +grestore +1.000 setgray +gsave +131.294 345.6 229.553 43.2 clipbox +276.924513 148.320555 m +337.927883 234.504122 l +359.836047 276.650621 l +298.832678 190.467054 l +276.924513 148.320555 l +stroke +grestore +0.000 0.000 1.000 setrgbcolor +gsave +131.294 345.6 229.553 43.2 clipbox +359.836047 104.283487 m +298.832678 190.467054 l +298.832678 241.532946 l +359.836047 155.349379 l +359.836047 104.283487 l +gsave +0.000 0.000 0.714 setrgbcolor +fill +grestore +stroke +grestore +1.000 setgray +gsave +131.294 345.6 229.553 43.2 clipbox +298.832678 190.467054 m +359.836047 276.650621 l +359.836047 327.716513 l +298.832678 241.532946 l +298.832678 190.467054 l +stroke +grestore +0.000 0.000 1.000 setrgbcolor +gsave +131.294 345.6 229.553 43.2 clipbox +359.836047 155.349379 m +298.832678 241.532946 l +276.924513 283.679445 l +337.927883 197.495878 l +359.836047 155.349379 l +stroke +grestore +1.000 setgray +gsave +131.294 345.6 229.553 43.2 clipbox +298.832678 241.532946 m +359.836047 327.716513 l +337.927883 369.863012 l +276.924513 283.679445 l +298.832678 241.532946 l +stroke +grestore +0.000 0.000 1.000 setrgbcolor +gsave +131.294 345.6 229.553 43.2 clipbox +337.927883 197.495878 m +276.924513 283.679445 l +240.761336 302.183567 l +301.764706 216 l +337.927883 197.495878 l +stroke +grestore +1.000 setgray +gsave +131.294 345.6 229.553 43.2 clipbox +276.924513 283.679445 m +337.927883 369.863012 l +301.764706 388.367134 l +240.761336 302.183567 l +276.924513 283.679445 l +stroke +grestore +gsave +<< /ShadingType 4 + /ColorSpace [/DeviceRGB] + /BitsPerCoordinate 32 + /BitsPerComponent 8 + /BitsPerFlag 8 + /AntiAlias true + /Decode [ -3697.69 4613.39 -4052.37 4484.37 0 1 0 1 0 1 ] + /DataSource < +00800b99127ad4c0007f0000007e2a90007d6a604b00007f007fa9a91e7c6697312a3f53007e2a90007d6a604b00007f007f47b92a7df86e63000000007fa9a9 +1e7c6697312a3f53007f47b92a7df86e63000000008128c23c7b62ce1729ffcd007fa9a91e7c6697312a3f53008128c23c7b62ce1729ffcd00800b99127ad4c0 +007f0000007fa9a91e7c6697312a3f53007e2a90007d6a604b00007f00800b991280000096000000007fa9a91e7efc377d00001f00800b991280000096000000 +008128c23c808e0eae000000007fa9a91e7efc377d00001f008128c23c808e0eae000000007f47b92a7df86e63000000007fa9a91e7efc377d00001f007f47b9 +2a7df86e63000000007e2a90007d6a604b00007f007fa9a91e7efc377d00001f008128c23c7b62ce1729ffcd007f47b92a7df86e6300000000808e9e457d4f65 +6e0a3f60007f47b92a7df86e63000000007ff47a4d7f3bfcc500000000808e9e457d4f656e0a3f60007ff47a4d7f3bfcc50000000081d5835f7ca65c790000b6 +00808e9e457d4f656e0a3f600081d5835f7ca65c790000b6008128c23c7b62ce1729ffcd00808e9e457d4f656e0a3f60007f47b92a7df86e63000000008128c2 +3c808e0eae00000000808e9e457fe505b9000000008128c23c808e0eae0000000081d5835f81d19d1000000000808e9e457fe505b90000000081d5835f81d19d +10000000007ff47a4d7f3bfcc500000000808e9e457fe505b9000000007ff47a4d7f3bfcc5000000007f47b92a7df86e6300000000808e9e457fe505b9000000 +0081d5835f7ca65c790000b6007ff47a4d7f3bfcc50000000080e4fed67eb5307100002d007ff47a4d7f3bfcc5000000007ff47a4d80c404680000000080e4fe +d67eb5307100002d007ff47a4d80c404680000000081d5835f7e2e641d0000000080e4fed67eb5307100002d0081d5835f7e2e641d0000000081d5835f7ca65c +790000b60080e4fed67eb5307100002d007ff47a4d7f3bfcc50000000081d5835f81d19d100000000080e4fed6814ad0bc00002d0081d5835f81d19d10000000 +0081d5835f8359a4b30000b60080e4fed6814ad0bc00002d0081d5835f8359a4b30000b6007ff47a4d80c404680000000080e4fed6814ad0bc00002d007ff47a +4d80c40468000000007ff47a4d7f3bfcc50000000080e4fed6814ad0bc00002d0081d5835f7e2e641d000000007ff47a4d80c4046800000000808e9e45801afb +73000000007ff47a4d80c40468000000007f47b92a820792ca00000000808e9e45801afb73000000007f47b92a820792ca000000008128c23c7f71f27f000000 +00808e9e45801afb73000000008128c23c7f71f27f0000000081d5835f7e2e641d00000000808e9e45801afb73000000007ff47a4d80c404680000000081d583 +5f8359a4b30000b600808e9e4582b09bbf0a3f600081d5835f8359a4b30000b6008128c23c849d331529ffcd00808e9e4582b09bbf0a3f60008128c23c849d33 +1529ffcd007f47b92a820792ca00000000808e9e4582b09bbf0a3f60007f47b92a820792ca000000007ff47a4d80c4046800000000808e9e4582b09bbf0a3f60 +008128c23c7f71f27f000000007f47b92a820792ca000000007fa9a91e8103c9b000001f007f47b92a820792ca000000007e2a90008295a0e200007f007fa9a9 +1e8103c9b000001f007e2a90008295a0e200007f00800b991280000096000000007fa9a91e8103c9b000001f00800b991280000096000000008128c23c7f71f2 +7f000000007fa9a91e8103c9b000001f007f47b92a820792ca000000008128c23c849d331529ffcd007fa9a91e839969fc2a3f53008128c23c849d331529ffcd +00800b9912852b412d7f0000007fa9a91e839969fc2a3f5300800b9912852b412d7f0000007e2a90008295a0e200007f007fa9a91e839969fc2a3f53007e2a90 +008295a0e200007f007f47b92a820792ca000000007fa9a91e839969fc2a3f53 +> +>> +shfill +grestore + +end +showpage diff --git a/lib/matplotlib/tests/baseline_images/test_axes/pie_ccw_true.png b/lib/matplotlib/tests/baseline_images/test_axes/pie_ccw_true.png index 8af5f62b5526..c5236a34b9e1 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/pie_ccw_true.png and b/lib/matplotlib/tests/baseline_images/test_axes/pie_ccw_true.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/pie_center_radius.png b/lib/matplotlib/tests/baseline_images/test_axes/pie_center_radius.png index f7b7108ce79b..64b2244711f9 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/pie_center_radius.png and b/lib/matplotlib/tests/baseline_images/test_axes/pie_center_radius.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/pie_default.png b/lib/matplotlib/tests/baseline_images/test_axes/pie_default.png index 1c0c6c2c0577..f3935a9e159a 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/pie_default.png and b/lib/matplotlib/tests/baseline_images/test_axes/pie_default.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/pie_frame_grid.png b/lib/matplotlib/tests/baseline_images/test_axes/pie_frame_grid.png index 4eea9fb7c157..4e4edbeed0ed 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/pie_frame_grid.png and b/lib/matplotlib/tests/baseline_images/test_axes/pie_frame_grid.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/pie_linewidth_0.png b/lib/matplotlib/tests/baseline_images/test_axes/pie_linewidth_0.png index 18acc069100e..e814e061205a 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/pie_linewidth_0.png and b/lib/matplotlib/tests/baseline_images/test_axes/pie_linewidth_0.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/pie_linewidth_2.png b/lib/matplotlib/tests/baseline_images/test_axes/pie_linewidth_2.png index 68fc84a4d596..e12d743fbc45 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/pie_linewidth_2.png and b/lib/matplotlib/tests/baseline_images/test_axes/pie_linewidth_2.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/pie_no_label.png b/lib/matplotlib/tests/baseline_images/test_axes/pie_no_label.png index d2d651e421dc..c6fd5262acce 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/pie_no_label.png and b/lib/matplotlib/tests/baseline_images/test_axes/pie_no_label.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/pie_rotatelabels_true.png b/lib/matplotlib/tests/baseline_images/test_axes/pie_rotatelabels_true.png index 91664207dee7..d5875752c3cd 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_axes/pie_rotatelabels_true.png and b/lib/matplotlib/tests/baseline_images/test_axes/pie_rotatelabels_true.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_axes/test_centered_bar_label_nonlinear.svg b/lib/matplotlib/tests/baseline_images/test_axes/test_centered_bar_label_nonlinear.svg new file mode 100644 index 000000000000..cea1050932a6 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_axes/test_centered_bar_label_nonlinear.svg @@ -0,0 +1,166 @@ + + + + + + + + 2022-09-10T15:01:10.033044 + image/svg+xml + + + Matplotlib v3.5.0.dev5765+gcb3beb2f91.d20220910, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type3.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type3.pdf new file mode 100644 index 000000000000..a148a7d571b1 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type3.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type42.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type42.pdf new file mode 100644 index 000000000000..e33f8d803b12 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_backend_pdf/multi_font_type42.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_pdflatex.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_pdflatex.pdf index f9418746d453..8b5a2aaca9e6 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_pdflatex.pdf and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_pdflatex.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate1.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate1.pdf index 02fe1fcef54c..94a41dff6996 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate1.pdf and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate1.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate2.pdf b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate2.pdf index b5f395422463..d201723e06b9 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate2.pdf and b/lib/matplotlib/tests/baseline_images/test_backend_pgf/pgf_rcupdate2.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_backend_ps/colorbar_shift.eps b/lib/matplotlib/tests/baseline_images/test_backend_ps/colorbar_shift.eps new file mode 100644 index 000000000000..b88e23a33c42 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_ps/colorbar_shift.eps @@ -0,0 +1,897 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%Title: colorbar_shift.eps +%%Creator: Matplotlib v3.7.0.dev1597+g613b343238.d20230210, https://matplotlib.org/ +%%CreationDate: Fri Feb 10 16:16:04 2023 +%%Orientation: portrait +%%BoundingBox: 110 245 502 547 +%%HiResBoundingBox: 110.097762 245.509625 501.902238 546.490375 +%%EndComments +%%BeginProlog +/mpldict 11 dict def +mpldict begin +/_d { bind def } bind def +/m { moveto } _d +/l { lineto } _d +/r { rlineto } _d +/c { curveto } _d +/cl { closepath } _d +/ce { closepath eofill } _d +/box { + m + 1 index 0 r + 0 exch r + neg 0 r + cl + } _d +/clipbox { + box + clip + newpath + } _d +/sc { setcachedevice } _d +%!PS-Adobe-3.0 Resource-Font +%%Creator: Converted from TrueType to Type 3 by Matplotlib. +10 dict begin +/FontName /DejaVuSans def +/PaintType 0 def +/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def +/FontBBox [-2090 -948 3673 2524] def +/FontType 3 def +/Encoding [/period /zero /one /two /minus /four /five /six /eight /nine] def +/CharStrings 11 dict dup begin +/.notdef 0 def +/period{651 0 219 0 430 254 sc +219 254 m +430 254 l +430 0 l +219 0 l +219 254 l + +ce} _d +/zero{1303 0 135 -29 1167 1520 sc +651 1360 m +547 1360 469 1309 416 1206 c +364 1104 338 950 338 745 c +338 540 364 387 416 284 c +469 182 547 131 651 131 c +756 131 834 182 886 284 c +939 387 965 540 965 745 c +965 950 939 1104 886 1206 c +834 1309 756 1360 651 1360 c + +651 1520 m +818 1520 946 1454 1034 1321 c +1123 1189 1167 997 1167 745 c +1167 494 1123 302 1034 169 c +946 37 818 -29 651 -29 c +484 -29 356 37 267 169 c +179 302 135 494 135 745 c +135 997 179 1189 267 1321 c +356 1454 484 1520 651 1520 c + +ce} _d +/one{1303 0 225 0 1114 1493 sc +254 170 m +584 170 l +584 1309 l +225 1237 l +225 1421 l +582 1493 l +784 1493 l +784 170 l +1114 170 l +1114 0 l +254 0 l +254 170 l + +ce} _d +/two{1303 0 150 0 1098 1520 sc +393 170 m +1098 170 l +1098 0 l +150 0 l +150 170 l +227 249 331 356 463 489 c +596 623 679 709 713 748 c +778 821 823 882 848 932 c +874 983 887 1032 887 1081 c +887 1160 859 1225 803 1275 c +748 1325 675 1350 586 1350 c +523 1350 456 1339 385 1317 c +315 1295 240 1262 160 1217 c +160 1421 l +241 1454 317 1478 388 1495 c +459 1512 523 1520 582 1520 c +737 1520 860 1481 952 1404 c +1044 1327 1090 1223 1090 1094 c +1090 1033 1078 974 1055 919 c +1032 864 991 800 930 725 c +913 706 860 650 771 557 c +682 465 556 336 393 170 c + +ce} _d +/minus{1716 0 217 557 1499 727 sc +217 727 m +1499 727 l +1499 557 l +217 557 l +217 727 l + +ce} _d +/four{1303 0 100 0 1188 1493 sc +774 1317 m +264 520 l +774 520 l +774 1317 l + +721 1493 m +975 1493 l +975 520 l +1188 520 l +1188 352 l +975 352 l +975 0 l +774 0 l +774 352 l +100 352 l +100 547 l +721 1493 l + +ce} _d +/five{1303 0 158 -29 1124 1493 sc +221 1493 m +1014 1493 l +1014 1323 l +406 1323 l +406 957 l +435 967 465 974 494 979 c +523 984 553 987 582 987 c +749 987 881 941 978 850 c +1075 759 1124 635 1124 479 c +1124 318 1074 193 974 104 c +874 15 733 -29 551 -29 c +488 -29 424 -24 359 -13 c +294 -2 227 14 158 35 c +158 238 l +218 205 280 181 344 165 c +408 149 476 141 547 141 c +662 141 754 171 821 232 c +888 293 922 375 922 479 c +922 583 888 665 821 726 c +754 787 662 817 547 817 c +493 817 439 811 385 799 c +332 787 277 768 221 743 c +221 1493 l + +ce} _d +/six{1303 0 143 -29 1174 1520 sc +676 827 m +585 827 513 796 460 734 c +407 672 381 587 381 479 c +381 372 407 287 460 224 c +513 162 585 131 676 131 c +767 131 838 162 891 224 c +944 287 971 372 971 479 c +971 587 944 672 891 734 c +838 796 767 827 676 827 c + +1077 1460 m +1077 1276 l +1026 1300 975 1318 923 1331 c +872 1344 821 1350 770 1350 c +637 1350 535 1305 464 1215 c +394 1125 354 989 344 807 c +383 865 433 909 492 940 c +551 971 617 987 688 987 c +838 987 956 941 1043 850 c +1130 759 1174 636 1174 479 c +1174 326 1129 203 1038 110 c +947 17 827 -29 676 -29 c +503 -29 371 37 280 169 c +189 302 143 494 143 745 c +143 981 199 1169 311 1309 c +423 1450 573 1520 762 1520 c +813 1520 864 1515 915 1505 c +967 1495 1021 1480 1077 1460 c + +ce} _d +/eight{1303 0 139 -29 1163 1520 sc +651 709 m +555 709 479 683 424 632 c +369 581 342 510 342 420 c +342 330 369 259 424 208 c +479 157 555 131 651 131 c +747 131 823 157 878 208 c +933 260 961 331 961 420 c +961 510 933 581 878 632 c +823 683 748 709 651 709 c + +449 795 m +362 816 295 857 246 916 c +198 975 174 1048 174 1133 c +174 1252 216 1347 301 1416 c +386 1485 503 1520 651 1520 c +800 1520 916 1485 1001 1416 c +1086 1347 1128 1252 1128 1133 c +1128 1048 1104 975 1055 916 c +1007 857 940 816 854 795 c +951 772 1027 728 1081 662 c +1136 596 1163 515 1163 420 c +1163 275 1119 164 1030 87 c +942 10 816 -29 651 -29 c +486 -29 360 10 271 87 c +183 164 139 275 139 420 c +139 515 166 596 221 662 c +276 728 352 772 449 795 c + +375 1114 m +375 1037 399 976 447 933 c +496 890 564 868 651 868 c +738 868 805 890 854 933 c +903 976 928 1037 928 1114 c +928 1191 903 1252 854 1295 c +805 1338 738 1360 651 1360 c +564 1360 496 1338 447 1295 c +399 1252 375 1191 375 1114 c + +ce} _d +/nine{1303 0 129 -29 1159 1520 sc +225 31 m +225 215 l +276 191 327 173 379 160 c +431 147 482 141 532 141 c +665 141 767 186 837 275 c +908 365 948 501 958 684 c +919 627 870 583 811 552 c +752 521 686 506 614 506 c +465 506 346 551 259 641 c +172 732 129 855 129 1012 c +129 1165 174 1288 265 1381 c +356 1474 476 1520 627 1520 c +800 1520 931 1454 1022 1321 c +1113 1189 1159 997 1159 745 c +1159 510 1103 322 991 181 c +880 41 730 -29 541 -29 c +490 -29 439 -24 387 -14 c +335 -4 281 11 225 31 c + +627 664 m +718 664 789 695 842 757 c +895 819 922 904 922 1012 c +922 1119 895 1204 842 1266 c +789 1329 718 1360 627 1360 c +536 1360 464 1329 411 1266 c +358 1204 332 1119 332 1012 c +332 904 358 819 411 757 c +464 695 536 664 627 664 c + +ce} _d +end readonly def + +/BuildGlyph { + exch begin + CharStrings exch + 2 copy known not {pop /.notdef} if + true 3 1 roll get exec + end +} _d + +/BuildChar { + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec +} _d + +FontName currentdict end definefont pop +end +%%EndProlog +mpldict begin +110.098 245.51 translate +391.804 300.981 0 0 clipbox +gsave +0 0 m +391.804475 0 l +391.804475 300.98075 l +0 300.98075 l +cl +1.000 setgray +fill +grestore +gsave +36.465625 23.871875 m +322.161625 23.871875 l +322.161625 289.983875 l +36.465625 289.983875 l +cl +1.000 setgray +fill +grestore +/p0_0 { +newpath +translate +0 -3 m +0.795609 -3 1.55874 -2.683901 2.12132 -2.12132 c +2.683901 -1.55874 3 -0.795609 3 0 c +3 0.795609 2.683901 1.55874 2.12132 2.12132 c +1.55874 2.683901 0.795609 3 0 3 c +-0.795609 3 -1.55874 2.683901 -2.12132 2.12132 c +-2.683901 1.55874 -3 0.795609 -3 0 c +-3 -0.795609 -2.683901 -1.55874 -2.12132 -2.12132 c +-1.55874 -2.683901 -0.795609 -3 0 -3 c +cl + +} bind def +1.000 setlinewidth +1 setlinejoin +0 setlinecap +[] 0 setdash +0.000 0.500 0.000 setrgbcolor +gsave +285.696 266.112 36.466 23.872 clipbox +49.4518 156.928 p0_0 +gsave +fill +grestore +stroke +grestore +0.000 0.000 1.000 setrgbcolor +gsave +285.696 266.112 36.466 23.872 clipbox +309.175 156.928 p0_0 +gsave +fill +grestore +stroke +grestore +0.800 setlinewidth +0.000 setgray +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -3.5 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +49.4518 23.8719 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +41.4987 9.27812 translate +0 rotate +0 0 m /zero glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /zero glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -3.5 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +101.397 23.8719 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +93.4434 9.27812 translate +0 rotate +0 0 m /zero glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /two glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -3.5 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +153.341 23.8719 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +145.388 9.27812 translate +0 rotate +0 0 m /zero glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /four glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -3.5 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +205.286 23.8719 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +197.333 9.27812 translate +0 rotate +0 0 m /zero glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /six glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -3.5 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +257.231 23.8719 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +249.278 9.27812 translate +0 rotate +0 0 m /zero glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /eight glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +0 -3.5 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +309.175 23.8719 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +301.222 9.27812 translate +0 rotate +0 0 m /one glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /zero glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-3.5 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +36.4656 60.1599 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +7.2 56.363 translate +0 rotate +0 0 m /zero glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /nine glyphshow +15.9033 0 m /six glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-3.5 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +36.4656 108.544 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +7.2 104.747 translate +0 rotate +0 0 m /zero glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /nine glyphshow +15.9033 0 m /eight glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-3.5 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +36.4656 156.928 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +7.2 153.131 translate +0 rotate +0 0 m /one glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /zero glyphshow +15.9033 0 m /zero glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-3.5 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +36.4656 205.312 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +7.2 201.515 translate +0 rotate +0 0 m /one glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /zero glyphshow +15.9033 0 m /two glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +-0 0 m +-3.5 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +36.4656 253.696 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +7.2 249.899 translate +0 rotate +0 0 m /one glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /zero glyphshow +15.9033 0 m /four glyphshow +grestore +0 setlinejoin +2 setlinecap +gsave +36.465625 23.871875 m +36.465625 289.983875 l +stroke +grestore +gsave +322.161625 23.871875 m +322.161625 289.983875 l +stroke +grestore +gsave +36.465625 23.871875 m +322.161625 23.871875 l +stroke +grestore +gsave +36.465625 289.983875 m +322.161625 289.983875 l +stroke +grestore +gsave +340.017625 23.871875 m +353.323225 23.871875 l +353.323225 289.983875 l +340.017625 289.983875 l +cl +1.000 setgray +fill +grestore +gsave +13.306 266.112 340.018 23.872 clipbox +340.017625 23.871875 m +353.323225 23.871875 l +353.323225 112.575875 l +340.017625 112.575875 l +340.017625 23.871875 l +1.000 0.000 0.000 setrgbcolor +fill +grestore +gsave +13.306 266.112 340.018 23.872 clipbox +340.017625 112.575875 m +353.323225 112.575875 l +353.323225 201.279875 l +340.017625 201.279875 l +340.017625 112.575875 l +0.000 0.500 0.000 setrgbcolor +fill +grestore +gsave +13.306 266.112 340.018 23.872 clipbox +340.017625 201.279875 m +353.323225 201.279875 l +353.323225 289.983875 l +340.017625 289.983875 l +340.017625 201.279875 l +0.000 0.000 1.000 setrgbcolor +fill +grestore +1 setlinejoin +0 setlinecap +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +3.5 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +353.323 23.8719 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +360.323 20.075 translate +0 rotate +0 0 m /minus glyphshow +8.37891 0 m /one glyphshow +14.7412 0 m /period glyphshow +17.9199 0 m /zero glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +3.5 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +353.323 112.576 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +360.323 108.779 translate +0 rotate +0 0 m /minus glyphshow +8.37891 0 m /zero glyphshow +14.7412 0 m /period glyphshow +17.9199 0 m /five glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +3.5 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +353.323 201.28 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +360.323 197.483 translate +0 rotate +0 0 m /zero glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /five glyphshow +grestore +gsave +/o { +gsave +newpath +translate +0.8 setlinewidth +1 setlinejoin + +0 setlinecap + +0 0 m +3.5 0 l + +gsave +0.000 setgray +fill +grestore +stroke +grestore +} bind def +353.323 289.984 o +grestore +/DejaVuSans 10.000 selectfont +gsave + +360.323 286.187 translate +0 rotate +0 0 m /one glyphshow +6.3623 0 m /period glyphshow +9.54102 0 m /zero glyphshow +grestore +0 setlinejoin +2 setlinecap +gsave +340.017625 23.871875 m +346.670425 23.871875 l +353.323225 23.871875 l +353.323225 289.983875 l +346.670425 289.983875 l +340.017625 289.983875 l +340.017625 23.871875 l +cl +stroke +grestore + +end +showpage diff --git a/lib/matplotlib/tests/baseline_images/test_backend_ps/coloredhatcheszerolw.eps b/lib/matplotlib/tests/baseline_images/test_backend_ps/coloredhatcheszerolw.eps new file mode 100644 index 000000000000..c0994b3116a5 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_ps/coloredhatcheszerolw.eps @@ -0,0 +1,216 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%Title: coloredhatcheszerolw.eps +%%Creator: Matplotlib v3.6.0.dev1993+g86a08ee.d20220407, https://matplotlib.org/ +%%CreationDate: Thu Apr 7 11:52:41 2022 +%%Orientation: portrait +%%BoundingBox: 18 180 594 612 +%%HiResBoundingBox: 18.000000 180.000000 594.000000 612.000000 +%%EndComments +%%BeginProlog +/mpldict 10 dict def +mpldict begin +/_d { bind def } bind def +/m { moveto } _d +/l { lineto } _d +/r { rlineto } _d +/c { curveto } _d +/cl { closepath } _d +/ce { closepath eofill } _d +/box { + m + 1 index 0 r + 0 exch r + neg 0 r + cl + } _d +/clipbox { + box + clip + newpath + } _d +/sc { setcachedevice } _d +end +%%EndProlog +mpldict begin +18 180 translate +576 432 0 0 clipbox +gsave +0 0 m +576 0 l +576 432 l +0 432 l +cl +1.000 setgray +fill +grestore +1.000 0.000 0.000 setrgbcolor +gsave +446.4 345.6 72 43.2 clipbox +72 -129.6 m +131.193332 -129.6 187.970227 -111.392702 229.826234 -78.988052 c +271.68224 -46.583402 295.2 -2.627096 295.2 43.2 c +295.2 89.027096 271.68224 132.983402 229.826234 165.388052 c +187.970227 197.792702 131.193332 216 72 216 c +12.806668 216 -43.970227 197.792702 -85.826234 165.388052 c +-127.68224 132.983402 -151.2 89.027096 -151.2 43.2 c +-151.2 -2.627096 -127.68224 -46.583402 -85.826234 -78.988052 c +-43.970227 -111.392702 12.806668 -129.6 72 -129.6 c +cl + << /PatternType 1 + /PaintType 2 + /TilingType 2 + /BBox[0 0 72 72] + /XStep 72 + /YStep 72 + + /PaintProc { + pop + 1 setlinewidth +-36 36 m +36 108 l +-24 24 m +48 96 l +-12 12 m +60 84 l +0 0 m +72 72 l +12 -12 m +84 60 l +24 -24 m +96 48 l +36 -36 m +108 36 l + + gsave + fill + grestore + stroke + } bind + >> + matrix + 0 432 translate + makepattern + /H0 exch def +gsave +1.000000 0.000000 0.000000 H0 setpattern fill grestore +grestore +0.200 setlinewidth +0 setlinejoin +0 setlinecap +[] 0 setdash +0.000 0.500 0.000 setrgbcolor +gsave +446.4 345.6 72 43.2 clipbox +295.2 129.6 m +324.796666 129.6 353.185114 138.703649 374.113117 154.905974 c +395.04112 171.108299 406.8 193.086452 406.8 216 c +406.8 238.913548 395.04112 260.891701 374.113117 277.094026 c +353.185114 293.296351 324.796666 302.4 295.2 302.4 c +265.603334 302.4 237.214886 293.296351 216.286883 277.094026 c +195.35888 260.891701 183.6 238.913548 183.6 216 c +183.6 193.086452 195.35888 171.108299 216.286883 154.905974 c +237.214886 138.703649 265.603334 129.6 295.2 129.6 c +cl + << /PatternType 1 + /PaintType 2 + /TilingType 2 + /BBox[0 0 72 72] + /XStep 72 + /YStep 72 + + /PaintProc { + pop + 1 setlinewidth +0 6 m +72 6 l +0 18 m +72 18 l +0 30 m +72 30 l +0 42 m +72 42 l +0 54 m +72 54 l +0 66 m +72 66 l +6 0 m +6 72 l +18 0 m +18 72 l +30 0 m +30 72 l +42 0 m +42 72 l +54 0 m +54 72 l +66 0 m +66 72 l + + gsave + fill + grestore + stroke + } bind + >> + matrix + 0 432 translate + makepattern + /H1 exch def +gsave +0.000000 0.500000 0.000000 H1 setpattern fill grestore +stroke +grestore +0.000 0.000 1.000 setrgbcolor +gsave +446.4 345.6 72 43.2 clipbox +518.4 250.56 m +536.158 250.56 553.191068 265.125838 565.74787 291.049559 c +578.304672 316.973279 585.36 352.138323 585.36 388.8 c +585.36 425.461677 578.304672 460.626721 565.74787 486.550441 c +553.191068 512.474162 536.158 527.04 518.4 527.04 c +500.642 527.04 483.608932 512.474162 471.05213 486.550441 c +458.495328 460.626721 451.44 425.461677 451.44 388.8 c +451.44 352.138323 458.495328 316.973279 471.05213 291.049559 c +483.608932 265.125838 500.642 250.56 518.4 250.56 c +cl + << /PatternType 1 + /PaintType 2 + /TilingType 2 + /BBox[0 0 72 72] + /XStep 72 + /YStep 72 + + /PaintProc { + pop + 1 setlinewidth +-36 36 m +36 -36 l +-24 48 m +48 -24 l +-12 60 m +60 -12 l +0 72 m +72 0 l +12 84 m +84 12 l +24 96 m +96 24 l +36 108 m +108 36 l + + gsave + fill + grestore + stroke + } bind + >> + matrix + 0 432 translate + makepattern + /H2 exch def +gsave +0.000000 0.000000 1.000000 H2 setpattern fill grestore +grestore + +end +showpage diff --git a/lib/matplotlib/tests/baseline_images/test_backend_ps/multi_font_type3.eps b/lib/matplotlib/tests/baseline_images/test_backend_ps/multi_font_type3.eps new file mode 100644 index 000000000000..efc8fc9416a7 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_ps/multi_font_type3.eps @@ -0,0 +1,521 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%Title: multi_font_type3.eps +%%Creator: Matplotlib v3.6.0.dev2856+g4848cedd6d.d20220807, https://matplotlib.org/ +%%CreationDate: Sun Aug 7 16:45:01 2022 +%%Orientation: portrait +%%BoundingBox: 18 180 594 612 +%%HiResBoundingBox: 18.000000 180.000000 594.000000 612.000000 +%%EndComments +%%BeginProlog +/mpldict 12 dict def +mpldict begin +/_d { bind def } bind def +/m { moveto } _d +/l { lineto } _d +/r { rlineto } _d +/c { curveto } _d +/cl { closepath } _d +/ce { closepath eofill } _d +/box { + m + 1 index 0 r + 0 exch r + neg 0 r + cl + } _d +/clipbox { + box + clip + newpath + } _d +/sc { setcachedevice } _d +%!PS-Adobe-3.0 Resource-Font +%%Creator: Converted from TrueType to Type 3 by Matplotlib. +10 dict begin +/FontName /DejaVuSans def +/PaintType 0 def +/FontMatrix [0.00048828125 0 0 0.00048828125 0 0] def +/FontBBox [-2090 -948 3673 2524] def +/FontType 3 def +/Encoding [/space /exclam /b /a /e /h /i /n /r /T /t /w] def +/CharStrings 13 dict dup begin +/.notdef 0 def +/space{651 0 0 0 0 0 sc +ce} _d +/exclam{821 0 309 0 512 1493 sc +309 254 m +512 254 l +512 0 l +309 0 l +309 254 l + +309 1493 m +512 1493 l +512 838 l +492 481 l +330 481 l +309 838 l +309 1493 l + +ce} _d +/b{1300 0 186 -29 1188 1556 sc +997 559 m +997 694 969 800 913 877 c +858 954 781 993 684 993 c +587 993 510 954 454 877 c +399 800 371 694 371 559 c +371 424 399 317 454 240 c +510 163 587 125 684 125 c +781 125 858 163 913 240 c +969 317 997 424 997 559 c + +371 950 m +410 1017 458 1066 517 1098 c +576 1131 647 1147 729 1147 c +865 1147 975 1093 1060 985 c +1145 877 1188 735 1188 559 c +1188 383 1145 241 1060 133 c +975 25 865 -29 729 -29 c +647 -29 576 -13 517 19 c +458 52 410 101 371 168 c +371 0 l +186 0 l +186 1556 l +371 1556 l +371 950 l + +ce} _d +/a{1255 0 123 -29 1069 1147 sc +702 563 m +553 563 450 546 393 512 c +336 478 307 420 307 338 c +307 273 328 221 371 182 c +414 144 473 125 547 125 c +649 125 731 161 792 233 c +854 306 885 402 885 522 c +885 563 l +702 563 l + +1069 639 m +1069 0 l +885 0 l +885 170 l +843 102 791 52 728 19 c +665 -13 589 -29 498 -29 c +383 -29 292 3 224 67 c +157 132 123 218 123 326 c +123 452 165 547 249 611 c +334 675 460 707 627 707 c +885 707 l +885 725 l +885 810 857 875 801 921 c +746 968 668 991 567 991 c +503 991 441 983 380 968 c +319 953 261 930 205 899 c +205 1069 l +272 1095 338 1114 401 1127 c +464 1140 526 1147 586 1147 c +748 1147 869 1105 949 1021 c +1029 937 1069 810 1069 639 c + +ce} _d +/e{1260 0 113 -29 1151 1147 sc +1151 606 m +1151 516 l +305 516 l +313 389 351 293 419 226 c +488 160 583 127 705 127 c +776 127 844 136 910 153 c +977 170 1043 196 1108 231 c +1108 57 l +1042 29 974 8 905 -7 c +836 -22 765 -29 694 -29 c +515 -29 374 23 269 127 c +165 231 113 372 113 549 c +113 732 162 878 261 985 c +360 1093 494 1147 662 1147 c +813 1147 932 1098 1019 1001 c +1107 904 1151 773 1151 606 c + +967 660 m +966 761 937 841 882 901 c +827 961 755 991 664 991 c +561 991 479 962 417 904 c +356 846 320 764 311 659 c +967 660 l + +ce} _d +/h{1298 0 186 0 1124 1556 sc +1124 676 m +1124 0 l +940 0 l +940 670 l +940 776 919 855 878 908 c +837 961 775 987 692 987 c +593 987 514 955 457 892 c +400 829 371 742 371 633 c +371 0 l +186 0 l +186 1556 l +371 1556 l +371 946 l +415 1013 467 1064 526 1097 c +586 1130 655 1147 733 1147 c +862 1147 959 1107 1025 1027 c +1091 948 1124 831 1124 676 c + +ce} _d +/i{569 0 193 0 377 1556 sc +193 1120 m +377 1120 l +377 0 l +193 0 l +193 1120 l + +193 1556 m +377 1556 l +377 1323 l +193 1323 l +193 1556 l + +ce} _d +/n{1298 0 186 0 1124 1147 sc +1124 676 m +1124 0 l +940 0 l +940 670 l +940 776 919 855 878 908 c +837 961 775 987 692 987 c +593 987 514 955 457 892 c +400 829 371 742 371 633 c +371 0 l +186 0 l +186 1120 l +371 1120 l +371 946 l +415 1013 467 1064 526 1097 c +586 1130 655 1147 733 1147 c +862 1147 959 1107 1025 1027 c +1091 948 1124 831 1124 676 c + +ce} _d +/r{842 0 186 0 842 1147 sc +842 948 m +821 960 799 969 774 974 c +750 980 723 983 694 983 c +590 983 510 949 454 881 c +399 814 371 717 371 590 c +371 0 l +186 0 l +186 1120 l +371 1120 l +371 946 l +410 1014 460 1064 522 1097 c +584 1130 659 1147 748 1147 c +761 1147 775 1146 790 1144 c +805 1143 822 1140 841 1137 c +842 948 l + +ce} _d +/T{1251 0 -6 0 1257 1493 sc +-6 1493 m +1257 1493 l +1257 1323 l +727 1323 l +727 0 l +524 0 l +524 1323 l +-6 1323 l +-6 1493 l + +ce} _d +/t{803 0 55 0 754 1438 sc +375 1438 m +375 1120 l +754 1120 l +754 977 l +375 977 l +375 369 l +375 278 387 219 412 193 c +437 167 488 154 565 154 c +754 154 l +754 0 l +565 0 l +423 0 325 26 271 79 c +217 132 190 229 190 369 c +190 977 l +55 977 l +55 1120 l +190 1120 l +190 1438 l +375 1438 l + +ce} _d +/w{1675 0 86 0 1589 1120 sc +86 1120 m +270 1120 l +500 246 l +729 1120 l +946 1120 l +1176 246 l +1405 1120 l +1589 1120 l +1296 0 l +1079 0 l +838 918 l +596 0 l +379 0 l +86 1120 l + +ce} _d +end readonly def + +/BuildGlyph { + exch begin + CharStrings exch + 2 copy known not {pop /.notdef} if + true 3 1 roll get exec + end +} _d + +/BuildChar { + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec +} _d + +FontName currentdict end definefont pop +%!PS-Adobe-3.0 Resource-Font +%%Creator: Converted from TrueType to Type 3 by Matplotlib. +10 dict begin +/FontName /WenQuanYiZenHei def +/PaintType 0 def +/FontMatrix [0.0009765625 0 0 0.0009765625 0 0] def +/FontBBox [-129 -304 1076 986] def +/FontType 3 def +/Encoding [/uni51E0 /uni6C49 /uni4E2A /uni5B57] def +/CharStrings 5 dict dup begin +/.notdef 0 def +/uni51E0{1024 0 34 -126 1004 818 sc +935 257 m +947 232 970 219 1004 217 c +974 -26 l +973 -40 968 -51 960 -60 c +955 -63 950 -65 943 -65 c +729 -65 l +697 -65 670 -55 649 -34 c +628 -14 617 12 615 43 c +614 75 613 193 613 397 c +613 602 614 719 616 748 c +393 748 l +394 395 l +391 256 361 144 302 58 c +250 -21 184 -82 104 -126 c +89 -96 66 -77 34 -68 c +119 -34 188 23 240 103 c +292 184 318 281 318 395 c +317 818 l +692 818 l +692 55 l +692 42 695 30 702 20 c +709 11 719 6 730 6 c +886 6 l +898 7 905 12 908 23 c +911 42 912 62 913 81 c +935 257 l + +ce} _d +/uni6C49{1024 0 17 -119 990 820 sc +612 211 m +536 349 488 512 468 699 c +447 699 426 698 405 697 c +407 719 407 741 405 763 c +445 761 485 760 526 760 c +871 760 l +851 534 793 348 696 202 c +769 91 867 2 990 -65 c +963 -76 942 -94 928 -119 c +819 -56 727 31 653 143 c +561 27 446 -58 307 -112 c +289 -89 268 -69 243 -53 c +392 -2 515 86 612 211 c + +535 700 m +552 534 592 391 655 271 c +735 396 782 539 796 700 c +535 700 l + +151 -118 m +123 -102 88 -93 47 -92 c +76 -38 107 24 138 93 c +169 162 215 283 274 454 c +315 433 l +199 54 l +151 -118 l + +230 457 m +166 408 l +17 544 l +80 594 l +230 457 l + +248 626 m +202 677 152 724 97 768 c +157 820 l +214 773 268 723 317 670 c +248 626 l + +ce} _d +/uni4E2A{1024 0 14 -123 980 833 sc +547 -123 m +520 -120 492 -120 464 -123 c +467 -72 468 -21 468 30 c +468 362 l +468 413 467 465 464 516 c +492 513 520 513 547 516 c +545 465 544 413 544 362 c +544 30 l +544 -21 545 -72 547 -123 c + +980 427 m +955 410 939 387 931 358 c +846 384 767 429 695 494 c +624 559 563 631 514 711 c +383 520 236 378 71 285 c +59 314 40 337 14 354 c +113 405 204 471 285 550 c +367 630 433 724 484 833 c +499 822 515 813 531 805 c +537 808 l +542 800 l +549 796 557 792 564 789 c +555 775 l +614 672 682 590 759 531 c +824 484 898 450 980 427 c + +ce} _d +/uni5B57{1024 0 32 -132 982 872 sc +982 285 m +980 264 980 243 982 222 c +943 224 904 225 865 225 c +555 225 l +555 -29 l +555 -54 545 -76 525 -95 c +496 -120 444 -132 368 -131 c +376 -98 368 -68 344 -43 c +366 -46 392 -47 422 -47 c +452 -48 470 -46 475 -42 c +480 -38 483 -27 483 -9 c +483 225 l +148 225 l +109 225 71 224 32 222 c +34 243 34 264 32 285 c +71 283 109 282 148 282 c +483 282 l +483 355 l +648 506 l +317 506 l +278 506 239 505 200 503 c +203 524 203 545 200 566 c +239 564 278 563 317 563 c +761 563 l +769 498 l +748 493 730 483 714 469 c +555 323 l +555 282 l +865 282 l +904 282 943 283 982 285 c + +131 562 m +59 562 l +59 753 l +468 752 l +390 807 l +435 872 l +542 798 l +510 752 l +925 752 l +925 562 l +852 562 l +852 695 l +131 695 l +131 562 l + +ce} _d +end readonly def + +/BuildGlyph { + exch begin + CharStrings exch + 2 copy known not {pop /.notdef} if + true 3 1 roll get exec + end +} _d + +/BuildChar { + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec +} _d + +FontName currentdict end definefont pop +end +%%EndProlog +mpldict begin +18 180 translate +576 432 0 0 clipbox +gsave +0 0 m +576 0 l +576 432 l +0 432 l +cl +1.000 setgray +fill +grestore +0.000 setgray +/DejaVuSans 27.000 selectfont +gsave + +86.4 205.2 translate +0 rotate +0.000000 0 m /T glyphshow +16.492676 0 m /h glyphshow +33.604980 0 m /e glyphshow +50.216309 0 m /r glyphshow +61.316895 0 m /e glyphshow +77.928223 0 m /space glyphshow +86.510742 0 m /a glyphshow +103.056152 0 m /r glyphshow +114.156738 0 m /e glyphshow +130.768066 0 m /space glyphshow +grestore +/WenQuanYiZenHei 27.000 selectfont +gsave + +86.4 205.2 translate +0 rotate +139.350586 0 m /uni51E0 glyphshow +166.350586 0 m /uni4E2A glyphshow +193.350586 0 m /uni6C49 glyphshow +220.350586 0 m /uni5B57 glyphshow +grestore +/DejaVuSans 27.000 selectfont +gsave + +86.4 205.2 translate +0 rotate +247.350586 0 m /space glyphshow +255.933105 0 m /i glyphshow +263.434570 0 m /n glyphshow +280.546875 0 m /space glyphshow +289.129395 0 m /b glyphshow +306.268066 0 m /e glyphshow +322.879395 0 m /t glyphshow +333.465820 0 m /w glyphshow +355.548340 0 m /e glyphshow +372.159668 0 m /e glyphshow +388.770996 0 m /n glyphshow +405.883301 0 m /exclam glyphshow +grestore + +end +showpage diff --git a/lib/matplotlib/tests/baseline_images/test_backend_ps/multi_font_type42.eps b/lib/matplotlib/tests/baseline_images/test_backend_ps/multi_font_type42.eps new file mode 100644 index 000000000000..472824330da4 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_ps/multi_font_type42.eps @@ -0,0 +1,361 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%Title: multi_font_type42.eps +%%Creator: Matplotlib v3.6.0.dev2856+g4848cedd6d.d20220807, https://matplotlib.org/ +%%CreationDate: Sun Aug 7 16:45:01 2022 +%%Orientation: portrait +%%BoundingBox: 18 180 594 612 +%%HiResBoundingBox: 18.000000 180.000000 594.000000 612.000000 +%%EndComments +%%BeginProlog +/mpldict 12 dict def +mpldict begin +/_d { bind def } bind def +/m { moveto } _d +/l { lineto } _d +/r { rlineto } _d +/c { curveto } _d +/cl { closepath } _d +/ce { closepath eofill } _d +/box { + m + 1 index 0 r + 0 exch r + neg 0 r + cl + } _d +/clipbox { + box + clip + newpath + } _d +/sc { setcachedevice } _d +%!PS-TrueTypeFont-1.0-2.22937 +%%Title: unknown +%%Creator: Converted from TrueType to type 42 by PPR +15 dict begin +/FontName /DejaVuSans def +/PaintType 0 def +/FontMatrix[1 0 0 1 0 0]def +/FontBBox[-1021 -463 1793 1232]def +/FontType 42 def +/Encoding StandardEncoding def +/FontInfo 10 dict dup begin +/FamilyName (unknown) def +/FullName (unknown) def +/Weight (unknown) def +/Version (unknown) def +/ItalicAngle 0.0 def +/isFixedPitch false def +/UnderlinePosition -130 def +/UnderlineThickness 90 def +end readonly def +/sfnts[<0001000000090080000300106376742000691D390000009C000001FE6670676D +7134766A0000029C000000AB676C7966118399D500000348000007B668656164085DC286 +00000B0000000036686865610D9F077C00000B3800000024686D74783A5706B700000B5C +0000003C6C6F636108C30B6500000B98000000206D617870047C067100000BB800000020 +707265703B07F10000000BD800000568013500B800CB00CB00C100AA009C01A600B80066 +0000007100CB00A002B20085007500B800C301CB0189022D00CB00A600F000D300AA0087 +00CB03AA0400014A003300CB000000D9050200F4015400B4009C01390114013907060400 +044E04B4045204B804E704CD0037047304CD04600473013303A2055605A60556053903C5 +021200C9001F00B801DF007300BA03E9033303BC0444040E00DF03CD03AA00E503AA0404 +000000CB008F00A4007B00B80014016F007F027B0252008F00C705CD009A009A006F00CB +00CD019E01D300F000BA018300D5009803040248009E01D500C100CB00F600830354027F +00000333026600D300C700A400CD008F009A0073040005D5010A00FE022B00A400B4009C +00000062009C0000001D032D05D505D505D505F0007F007B005400A406B80614072301D3 +00B800CB00A601C301EC069300A000D3035C037103DB0185042304A80448008F01390114 +01390360008F05D5019A0614072306660179046004600460047B009C00000277046001AA +00E904600762007B00C5007F027B000000B4025205CD006600BC00660077061000CD013B +01850389008F007B0000001D00CD074A042F009C009C0000077D006F0000006F0335006A +006F007B00AE00B2002D0396008F027B00F600830354063705F6008F009C04E10266008F +018D02F600CD03440029006604EE00730000140000960000B707060504030201002C2010 +B002254964B040515820C859212D2CB002254964B040515820C859212D2C20100720B000 +50B00D7920B8FFFF5058041B0559B0051CB0032508B0042523E120B00050B00D7920B8FF +FF5058041B0559B0051CB0032508E12D2C4B505820B0FD454459212D2CB002254560442D +2C4B5358B00225B0022545445921212D2C45442D2CB00225B0022549B00525B005254960 +B0206368208A108A233A8A10653A2D00000201350000020005D5000300090035400F0700 +8304810208070501030400000A10FC4BB00B5458B90000FFC038593CEC32393931002FE4 +FCCC3001B6000B200B500B035D253315231133110323030135CBCBCB14A215FEFE05D5FD +71FE9B0165000001FFFA000004E905D50007004A400E0602950081040140031C00400508 +10D4E4FCE431002FF4EC3230014BB00A5458BD00080040000100080008FFC03811373859 +401300091F00100110021F071009400970099F09095D03211521112311210604EFFDEECB +FDEE05D5AAFAD5052B000002007BFFE3042D047B000A002500BC4027191F0B17090E00A9 +1706B90E1120861FBA1CB923B8118C170C001703180D09080B1F030814452610FCECCCD4 +EC323211393931002FC4E4F4FCF4EC10C6EE10EE11391139123930406E301D301E301F30 +20302130223F27401D401E401F402040214022501D501E501F5020502150225027702785 +1D871E871F8720872185229027A027F0271E301E301F30203021401E401F40204021501E +501F50205021601E601F60206021701E701F70207021801E801F80208021185D015D0122 +061514163332363D01371123350E01232226353436332135342623220607353E01333216 +02BEDFAC816F99B9B8B83FBC88ACCBFDFB0102A79760B65465BE5AF3F00233667B6273D9 +B4294CFD81AA6661C1A2BDC0127F8B2E2EAA2727FC00000200BAFFE304A40614000B001C +0038401903B90C0F09B918158C0FB81B971900121247180C06081A461D10FCEC3232F4EC +31002FECE4F4C4EC10C6EE30B6601E801EA01E03015D013426232206151416333236013E +01333200111002232226271523113303E5A79292A7A79292A7FD8E3AB17BCC00FFFFCC7B +B13AB9B9022FCBE7E7CBCBE7E702526461FEBCFEF8FEF8FEBC6164A8061400020071FFE3 +047F047B0014001B00704024001501098608880515A90105B90C01BB18B912B80C8C1C1B +1502081508004B02120F451C10FCECF4ECC4111239310010E4F4ECE410EE10EE10F4EE11 +12393040293F1D701DA01DD01DF01D053F003F013F023F153F1B052C072F082F092C0A6F +006F016F026F156F1B095D71015D0115211E0133323637150E0123200011100033320007 +2E0123220607047FFCB20CCDB76AC76263D06BFEF4FEC70129FCE20107B802A5889AB90E +025E5ABEC73434AE2A2C0138010A01130143FEDDC497B4AE9E00000100BA000004640614 +001300344019030900030E0106870E11B80C970A010208004E0D09080B461410FCEC32F4 +EC31002F3CECF4C4EC1112173930B2601501015D0111231134262322061511231133113E +013332160464B87C7C95ACB9B942B375C1C602A4FD5C029E9F9EBEA4FD870614FD9E6564 +EF00000200C100000179061400030007002B400E06BE04B100BC020501080400460810FC +3CEC3231002FE4FCEC30400B1009400950096009700905015D1333112311331523C1B8B8 +B8B80460FBA00614E900000100BA00000464047B001300364019030900030E0106870E11 +B80CBC0A010208004E0D09080B461410FCEC32F4EC31002F3CE4F4C4EC1112173930B460 +15CF1502015D0111231134262322061511231133153E013332160464B87C7C95ACB9B942 +B375C1C602A4FD5C029E9F9EBEA4FD870460AE6564EF000100BA0000034A047B00110030 +4014060B0700110B03870EB809BC070A06080008461210FCC4EC3231002FE4F4ECC4D4CC +11123930B450139F1302015D012E012322061511231133153E0133321617034A1F492C9C +A7B9B93ABA85132E1C03B41211CBBEFDB20460AE6663050500010037000002F2059E0013 +003840190E05080F03A9001101BC08870A0B08090204000810120E461410FC3CC4FC3CC4 +32393931002FECF43CC4EC3211393930B2AF1501015D01112115211114163B0115232226 +3511233533110177017BFE854B73BDBDD5A28787059EFEC28FFDA0894E9A9FD202608F01 +3E0000010056000006350460000C01EB404905550605090A0904550A0903550A0B0A0255 +01020B0B0A061107080705110405080807021103020C000C011100000C420A0502030603 +00BF0B080C0B0A09080605040302010B07000D10D44BB00A544BB011545B4BB012545B4B +B013545B4BB00B545B58B9000000403859014BB00C544BB00D545B4BB010545B58B90000 +FFC03859CC173931002F3CEC32321739304B5358071005ED071008ED071008ED071005ED +071008ED071005ED0705ED071008ED59220140FF050216021605220A350A49024905460A +400A5B025B05550A500A6E026E05660A79027F0279057F05870299029805940ABC02BC05 +CE02C703CF051D0502090306040B050A080B09040B050C1502190316041A051B081B0914 +0B150C2500250123022703210425052206220725082709240A210B230C39033604360839 +0C300E460248034604400442054006400740084409440A440B400E400E56005601560250 +0451055206520750085309540A550B6300640165026A0365046A056A066A076E09610B67 +0C6F0E7500750179027D0378047D057A067F067A077F07780879097F097B0A760B7D0C87 +0288058F0E97009701940293039C049B05980698079908402F960C9F0EA600A601A402A4 +03AB04AB05A906A907AB08A40CAF0EB502B103BD04BB05B809BF0EC402C303CC04CA0579 +5D005D13331B01331B013301230B012356B8E6E5D9E6E5B8FEDBD9F1F2D90460FC96036A +FC96036AFBA00396FC6A00000001000000025999D203C60C5F0F3CF5001F080000000000 +D17E0EE400000000D17E0EE4F7D6FC4C0E5909DC00000008000000000000000000010000 +076DFE1D00000EFEF7D6FA510E5900010000000000000000000000000000000F04CD0066 +0000000002AA0000028B00000335013504E3FFFA04E7007B051400BA04EC0071051200BA +023900C1051200BA034A00BA03230037068B0056000000000000000000000031006900FF +014B01B501F102190255028C02C903DB00010000000F0354002B0068000C000200100099 +000800000415021600080004B8028040FFFBFE03FA1403F92503F83203F79603F60E03F5 +FE03F4FE03F32503F20E03F19603F02503EF8A4105EFFE03EE9603ED9603ECFA03EBFA03 +EAFE03E93A03E84203E7FE03E63203E5E45305E59603E48A4105E45303E3E22F05E3FA03 +E22F03E1FE03E0FE03DF3203DE1403DD9603DCFE03DB1203DA7D03D9BB03D8FE03D68A41 +05D67D03D5D44705D57D03D44703D3D21B05D3FE03D21B03D1FE03D0FE03CFFE03CEFE03 +CD9603CCCB1E05CCFE03CB1E03CA3203C9FE03C6851105C61C03C51603C4FE03C3FE03C2 +FE03C1FE03C0FE03BFFE03BEFE03BDFE03BCFE03BBFE03BA1103B9862505B9FE03B8B7BB +05B8FE03B7B65D05B7BB03B78004B6B52505B65D40FF03B64004B52503B4FE03B39603B2 +FE03B1FE03B0FE03AFFE03AE6403AD0E03ACAB2505AC6403ABAA1205AB2503AA1203A98A +4105A9FA03A8FE03A7FE03A6FE03A51203A4FE03A3A20E05A33203A20E03A16403A08A41 +05A096039FFE039E9D0C059EFE039D0C039C9B19059C64039B9A10059B19039A1003990A +0398FE0397960D0597FE03960D03958A410595960394930E05942803930E0392FA039190 +BB0591FE03908F5D0590BB039080048F8E25058F5D038F40048E25038DFE038C8B2E058C +FE038B2E038A8625058A410389880B05891403880B038786250587640386851105862503 +85110384FE038382110583FE0382110381FE0380FE037FFE0340FF7E7D7D057EFE037D7D +037C64037B5415057B25037AFE0379FE03780E03770C03760A0375FE0374FA0373FA0372 +FA0371FA0370FE036FFE036EFE036C21036BFE036A1142056A530369FE03687D03671142 +0566FE0365FE0364FE0363FE0362FE03613A0360FA035E0C035DFE035BFE035AFE035958 +0A0559FA03580A035716190557320356FE03555415055542035415035301100553180352 +1403514A130551FE03500B034FFE034E4D10054EFE034D10034CFE034B4A13054BFE034A +4910054A1303491D0D05491003480D0347FE0346960345960344FE0343022D0543FA0342 +BB03414B0340FE033FFE033E3D12053E14033D3C0F053D12033C3B0D053C40FF0F033B0D +033AFE0339FE033837140538FA033736100537140336350B05361003350B03341E03330D +0332310B0532FE03310B03302F0B05300D032F0B032E2D09052E10032D09032C32032B2A +25052B64032A2912052A25032912032827250528410327250326250B05260F03250B0324 +FE0323FE03220F03210110052112032064031FFA031E1D0D051E64031D0D031C1142051C +FE031BFA031A42031911420519FE031864031716190517FE031601100516190315FE0314 +FE0313FE031211420512FE0311022D05114203107D030F64030EFE030D0C16050DFE030C +0110050C16030BFE030A100309FE0308022D0508FE030714030664030401100504FE0340 +1503022D0503FE0302011005022D0301100300FE0301B80164858D012B2B2B2B2B2B2B2B +2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B +2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B +2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B +2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B +2B2B2B2B2B2B2B2B2B2B2B2B2B002B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B +2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B +2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B +2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B +2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B2B1D00>]def +/CharStrings 13 dict dup begin +/.notdef 0 def +/space 3 def +/exclam 4 def +/b 7 def +/a 6 def +/e 8 def +/h 9 def +/i 10 def +/n 11 def +/r 12 def +/T 5 def +/t 13 def +/w 14 def +end readonly def + +systemdict/resourcestatus known + {42 /FontType resourcestatus + {pop pop false}{true}ifelse} + {true}ifelse +{/TrueDict where{pop}{(%%[ Error: no TrueType rasterizer ]%%)= flush}ifelse +/FontType 3 def + /TrueState 271 string def + TrueDict begin sfnts save + 72 0 matrix defaultmatrix dtransform dup + mul exch dup mul add sqrt cvi 0 72 matrix + defaultmatrix dtransform dup mul exch dup + mul add sqrt cvi 3 -1 roll restore + TrueState initer end + /BuildGlyph{exch begin + CharStrings dup 2 index known + {exch}{exch pop /.notdef}ifelse + get dup xcheck + {currentdict systemdict begin begin exec end end} + {TrueDict begin /bander load cvlit exch TrueState render end} + ifelse + end}bind def + /BuildChar{ + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec + }bind def +}if + +FontName currentdict end definefont pop +%!PS-TrueTypeFont-1.0-0.58982 +%%Title: unknown +%%Creator: Converted from TrueType to type 42 by PPR +15 dict begin +/FontName /WenQuanYiZenHei def +/PaintType 0 def +/FontMatrix[1 0 0 1 0 0]def +/FontBBox[-126 -297 1051 963]def +/FontType 42 def +/Encoding StandardEncoding def +/FontInfo 10 dict dup begin +/FamilyName (unknown) def +/FullName (unknown) def +/Weight (unknown) def +/Version (unknown) def +/ItalicAngle 0.0 def +/isFixedPitch false def +/UnderlinePosition -230 def +/UnderlineThickness 51 def +end readonly def +/sfnts[<00010000000700400002003063767420002202880000007C00000004676C7966 +AC20EA39000000800000024A68656164F2831BDF000002CC000000366868656107EC01A3 +0000030400000024686D747805A7004E000003280000001A6C6F636101A0011100000344 +000000126D617870008A02690000035800000020002202880002000DFF8503D50341000D +0024000005260736351134271637061511140106072E0127020726273E01371617371716 +17071617160223292A04042A290301B4250C80D74AC4F7122795F54C171806050B0B0958 +74627B04044D4C014C4D4D04044D4DFEB44C01D91A2B27C278FEE18B2C194DEFA3100C03 +0806050E9B59460000010021FF8203EC033300230000011617070607062B01222E011037 +23130607060726273E013503211114163B013637363703A712331E020C070AD6303F0503 +DF0104584E781630809C01017715119C1204040101012503F3150D053D5F02652CFE9FD0 +8176422D0D33F1AB01A7FD05141D01101D1D0002001FFF7C03D60369002A003700000106 +17262321151407062736271E01363D012122073627163321353721220736271633211706 +0F011521320123350527371707211523352103D603033A3BFECA1E2B720C24215A10FEB1 +3A3A03033A3A014FA5FEB53A3B04043B3A01BC081F189F01363BFCE74801994E2D6B2001 +9F49FD2F011D1F2003FE261C2501322604010C1BEA03201F03499703201F034108159229 +0118BF0137414A2EBE8500050010FF8803DF03350016001B00230027002D000025260322 +0736271633210207161706072627060726273613161736370126273E011317031307273F +0126273716170264721E201F03033C3D01591E916EB82915A46F8AD01B25E0441A5E7815 +FD7B2A3E2C5E5929741F40953FA845523C564AD3CF011902212103FEADDBA66510265EA8 +AE5123184D02A4F9B4BBF2FCCE180251D0010115FE850193318832204C42344650000000 +000100000000E666EDAC36235F0F3CF5003F040000000000C7BE78E900000000C7BE78E9 +FF7FFED0043403DA0000000800020000000000000001000003DAFED0005C0455FF7FFE78 +043400010000000000000000000000000000000501760022000000000000000000000000 +0400000D0021001F00100000000000000000000000000040007B00D10125000000010000 +00080165002800D10012000200000001000100000040002E0006000200>]def +/CharStrings 5 dict dup begin +/.notdef 0 def +/uni51E0 5 def +/uni6C49 7 def +/uni4E2A 4 def +/uni5B57 6 def +end readonly def + +systemdict/resourcestatus known + {42 /FontType resourcestatus + {pop pop false}{true}ifelse} + {true}ifelse +{/TrueDict where{pop}{(%%[ Error: no TrueType rasterizer ]%%)= flush}ifelse +/FontType 3 def + /TrueState 271 string def + TrueDict begin sfnts save + 72 0 matrix defaultmatrix dtransform dup + mul exch dup mul add sqrt cvi 0 72 matrix + defaultmatrix dtransform dup mul exch dup + mul add sqrt cvi 3 -1 roll restore + TrueState initer end + /BuildGlyph{exch begin + CharStrings dup 2 index known + {exch}{exch pop /.notdef}ifelse + get dup xcheck + {currentdict systemdict begin begin exec end end} + {TrueDict begin /bander load cvlit exch TrueState render end} + ifelse + end}bind def + /BuildChar{ + 1 index /Encoding get exch get + 1 index /BuildGlyph get exec + }bind def +}if + +FontName currentdict end definefont pop +end +%%EndProlog +mpldict begin +18 180 translate +576 432 0 0 clipbox +gsave +0 0 m +576 0 l +576 432 l +0 432 l +cl +1.000 setgray +fill +grestore +0.000 setgray +/DejaVuSans 27.000 selectfont +gsave + +86.4 205.2 translate +0 rotate +0.000000 0 m /T glyphshow +16.492676 0 m /h glyphshow +33.604980 0 m /e glyphshow +50.216309 0 m /r glyphshow +61.316895 0 m /e glyphshow +77.928223 0 m /space glyphshow +86.510742 0 m /a glyphshow +103.056152 0 m /r glyphshow +114.156738 0 m /e glyphshow +130.768066 0 m /space glyphshow +grestore +/WenQuanYiZenHei 27.000 selectfont +gsave + +86.4 205.2 translate +0 rotate +139.350586 0 m /uni51E0 glyphshow +166.350586 0 m /uni4E2A glyphshow +193.350586 0 m /uni6C49 glyphshow +220.350586 0 m /uni5B57 glyphshow +grestore +/DejaVuSans 27.000 selectfont +gsave + +86.4 205.2 translate +0 rotate +247.350586 0 m /space glyphshow +255.933105 0 m /i glyphshow +263.434570 0 m /n glyphshow +280.546875 0 m /space glyphshow +289.129395 0 m /b glyphshow +306.268066 0 m /e glyphshow +322.879395 0 m /t glyphshow +333.465820 0 m /w glyphshow +355.548340 0 m /e glyphshow +372.159668 0 m /e glyphshow +388.770996 0 m /n glyphshow +405.883301 0 m /exclam glyphshow +grestore + +end +showpage diff --git a/lib/matplotlib/tests/baseline_images/test_backend_ps/scatter.eps b/lib/matplotlib/tests/baseline_images/test_backend_ps/scatter.eps new file mode 100644 index 000000000000..b21ff4234af4 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_ps/scatter.eps @@ -0,0 +1,306 @@ +%!PS-Adobe-3.0 EPSF-3.0 +%%Title: scatter.eps +%%Creator: Matplotlib v3.6.0.dev2701+g27bf604984.d20220719, https://matplotlib.org/ +%%CreationDate: Tue Jul 19 12:36:23 2022 +%%Orientation: portrait +%%BoundingBox: 18 180 594 612 +%%HiResBoundingBox: 18.000000 180.000000 594.000000 612.000000 +%%EndComments +%%BeginProlog +/mpldict 10 dict def +mpldict begin +/_d { bind def } bind def +/m { moveto } _d +/l { lineto } _d +/r { rlineto } _d +/c { curveto } _d +/cl { closepath } _d +/ce { closepath eofill } _d +/box { + m + 1 index 0 r + 0 exch r + neg 0 r + cl + } _d +/clipbox { + box + clip + newpath + } _d +/sc { setcachedevice } _d +end +%%EndProlog +mpldict begin +18 180 translate +576 432 0 0 clipbox +gsave +0 0 m +576 0 l +576 432 l +0 432 l +cl +1.000 setgray +fill +grestore +/p0_0 { +newpath +translate +72 141.529351 m +17.327389 80.435325 l +126.672611 80.435325 l +cl + +} bind def +/p0_1 { +newpath +translate +72 158.4 m +-17.28 100.8 l +72 43.2 l +161.28 100.8 l +cl + +} bind def +/p0_2 { +newpath +translate +72 141.529351 m +11.959333 113.386062 l +34.892827 67.849263 l +109.107173 67.849263 l +132.040667 113.386062 l +cl + +} bind def +/p0_3 { +newpath +translate +72 158.4 m +-5.318748 129.6 l +-5.318748 72 l +72 43.2 l +149.318748 72 l +149.318748 129.6 l +cl + +} bind def +1.000 setlinewidth +1 setlinejoin +0 setlinecap +[] 0 setdash +0.000 setgray +gsave +446.4 345.6 72 43.2 clipbox +96.7145 132.649 p0_0 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +166.544 15.5782 p0_1 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +149.874 179.799 p0_2 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +34.7409 104.813 p0_3 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +145.839 37.968 p0_0 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +147.462 82.9425 p0_1 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +147.29 120.393 p0_2 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +151.565 52.8617 p0_3 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +165.375 85.5808 p0_0 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +12.8578 119.079 p0_1 +gsave +1.000 1.000 0.000 setrgbcolor +fill +grestore +stroke +grestore +0.900 0.200 0.100 setrgbcolor +gsave +446.4 345.6 72 43.2 clipbox +326.215567 311.071597 m +334.595085 306.881838 l +334.595085 315.261356 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +184.274432 293.965646 m +190.679432 290.763146 l +190.679432 297.168146 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +276.081223 354.823805 m +283.311607 351.208613 l +283.311607 358.438997 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +411.593191 219.187935 m +420.363106 214.802977 l +420.363106 223.572893 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +141.383198 139.751386 m +149.294063 135.795953 l +149.294063 143.706818 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +154.058079 131.129187 m +160.028366 128.144043 l +160.028366 134.114331 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +247.767539 370.319257 m +255.714503 366.345775 l +255.714503 374.292739 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +410.16817 374.136435 m +419.852735 369.294152 l +419.852735 378.978717 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +450.836918 106.524611 m +457.983473 102.951334 l +457.983473 110.097888 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore +gsave +446.4 345.6 72 43.2 clipbox +397.084416 298.708741 m +402.739273 295.881312 l +402.739273 301.53617 l +cl +gsave +0.000 0.000 1.000 setrgbcolor +fill +grestore +stroke +grestore + +end +showpage diff --git a/lib/matplotlib/tests/baseline_images/test_backend_svg/bold_font_output_with_none_fonttype.svg b/lib/matplotlib/tests/baseline_images/test_backend_svg/bold_font_output_with_none_fonttype.svg index 20526ec9476e..af3d2e7a8f3c 100644 --- a/lib/matplotlib/tests/baseline_images/test_backend_svg/bold_font_output_with_none_fonttype.svg +++ b/lib/matplotlib/tests/baseline_images/test_backend_svg/bold_font_output_with_none_fonttype.svg @@ -10,61 +10,61 @@ - - - - - - - - @@ -73,8 +73,8 @@ L 0 -4 - @@ -82,7 +82,7 @@ L 0 4 - 0 + 0 @@ -97,7 +97,7 @@ L 0 4 - 1 + 1 @@ -112,7 +112,7 @@ L 0 4 - 2 + 2 @@ -127,7 +127,7 @@ L 0 4 - 3 + 3 @@ -142,7 +142,7 @@ L 0 4 - 4 + 4 @@ -157,7 +157,7 @@ L 0 4 - 5 + 5 @@ -172,7 +172,7 @@ L 0 4 - 6 + 6 @@ -187,7 +187,7 @@ L 0 4 - 7 + 7 @@ -202,7 +202,7 @@ L 0 4 - 8 + 8 @@ -217,19 +217,19 @@ L 0 4 - 9 + 9 - nonbold-xlabel + nonbold-xlabel - @@ -238,8 +238,8 @@ L 4 0 - @@ -247,7 +247,7 @@ L -4 0 - 0 + 0 @@ -262,7 +262,7 @@ L -4 0 - 1 + 1 @@ -277,7 +277,7 @@ L -4 0 - 2 + 2 @@ -292,7 +292,7 @@ L -4 0 - 3 + 3 @@ -307,7 +307,7 @@ L -4 0 - 4 + 4 @@ -322,7 +322,7 @@ L -4 0 - 5 + 5 @@ -337,7 +337,7 @@ L -4 0 - 6 + 6 @@ -352,7 +352,7 @@ L -4 0 - 7 + 7 @@ -367,7 +367,7 @@ L -4 0 - 8 + 8 @@ -382,15 +382,15 @@ L -4 0 - 9 + 9 - bold-ylabel + bold-ylabel - bold-title + bold-title diff --git a/lib/matplotlib/tests/baseline_images/test_backend_svg/multi_font_aspath.svg b/lib/matplotlib/tests/baseline_images/test_backend_svg/multi_font_aspath.svg new file mode 100644 index 000000000000..7184700caf17 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_svg/multi_font_aspath.svg @@ -0,0 +1,423 @@ + + + + + + + + 2022-08-09T18:12:28.920123 + image/svg+xml + + + Matplotlib v3.6.0.dev2839+gb0bf8fb1de.d20220809, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_backend_svg/multi_font_astext.svg b/lib/matplotlib/tests/baseline_images/test_backend_svg/multi_font_astext.svg new file mode 100644 index 000000000000..373103f61b9f --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_backend_svg/multi_font_astext.svg @@ -0,0 +1,35 @@ + + + + + + + + 2022-08-09T18:42:37.025191 + image/svg+xml + + + Matplotlib v3.6.0.dev2840+g372782e258.d20220809, https://matplotlib.org/ + + + + + + + + + + + + + + There are 几个汉字 in between! + + + diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_fixed_aspect.png b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_fixed_aspect.png new file mode 100644 index 000000000000..0fd7a35e3303 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_fixed_aspect.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.svg b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.svg index c99489ca7dfb..5cf932d60cb7 100644 --- a/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.svg +++ b/lib/matplotlib/tests/baseline_images/test_bbox_tight/bbox_inches_tight_suptile_legend.svg @@ -1,21 +1,32 @@ - - + + + + + + 2022-07-07T13:08:55.409721 + image/svg+xml + + + Matplotlib v3.5.0.dev5238+g5d127c48a6.d20220707, https://matplotlib.org/ + + + + + - + +" style="fill: #ffffff"/> @@ -24,10 +35,10 @@ L 542.014375 387.36 L 542.014375 41.76 L 95.614375 41.76 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" clip-path="url(#p602791bb46)" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-linejoin: miter; stroke-linecap: square"/> - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - - - + + + + @@ -112,32 +125,33 @@ Q 19.53125 74.21875 31.78125 74.21875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -145,42 +159,43 @@ z - + - + - - - - + + + + @@ -188,50 +203,51 @@ Q 31.109375 20.453125 19.1875 8.296875 - + - + - - - - + + + + @@ -239,36 +255,38 @@ Q 46.96875 40.921875 40.578125 39.3125 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -276,43 +294,44 @@ z - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -320,47 +339,49 @@ z - + - + - - - - + + + + @@ -368,28 +389,29 @@ Q 48.484375 72.75 52.59375 71.296875 - + - + - - + + - - +" transform="scale(0.015625)"/> + @@ -397,55 +419,58 @@ z - + - + - - - - + + + + @@ -453,163 +478,172 @@ Q 18.3125 60.0625 18.3125 54.390625 - + - + - - - - + + + + - - + + - - + + - - + + - - - +M 603 4863 +L 1178 4863 +L 1178 4134 +L 603 4134 +L 603 4863 +z +" transform="scale(0.015625)"/> + + - - - - - + + + + + @@ -617,446 +651,462 @@ Q 40.578125 54.546875 44.28125 53.078125 - +" style="stroke: #000000; stroke-width: 0.5"/> - + - +" style="stroke: #000000; stroke-width: 0.5"/> - + - - + + - - +" transform="scale(0.015625)"/> + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - - + + - - + - - + - - - + - - +" transform="scale(0.015625)"/> + + + + + - - - - - - - - - - - + + + + + + + + + + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - - + + - + - + - - +" transform="scale(0.015625)"/> + - - - - - - - - - + + + + + + + + + @@ -1066,105 +1116,107 @@ L 656.183875 74.4165 L 656.183875 48.96 L 504.574375 48.96 z -" style="fill:#ffffff;stroke:#000000;stroke-linejoin:miter;"/> +" style="fill: #ffffff; stroke: #000000; stroke-linejoin: miter"/> +" style="fill: none; stroke: #0000ff; stroke-linecap: square"/> - - - + + - - +M 3481 434 +Q 3481 -459 3084 -895 +Q 2688 -1331 1869 -1331 +Q 1566 -1331 1297 -1286 +Q 1028 -1241 775 -1147 +L 775 -588 +Q 1028 -725 1275 -790 +Q 1522 -856 1778 -856 +Q 2344 -856 2625 -561 +Q 2906 -266 2906 331 +L 2906 616 +Q 2728 306 2450 153 +Q 2172 0 1784 0 +Q 1141 0 747 490 +Q 353 981 353 1791 +Q 353 2603 747 3093 +Q 1141 3584 1784 3584 +Q 2172 3584 2450 3431 +Q 2728 3278 2906 2969 +L 2906 3500 +L 3481 3500 +L 3481 434 +z +" transform="scale(0.015625)"/> + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - + + - - +" transform="scale(0.015625)"/> + - - - - - - - - - - - + + + + + + + + + + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_collections/test_check_masked_offsets.png b/lib/matplotlib/tests/baseline_images/test_collections/test_check_masked_offsets.png new file mode 100644 index 000000000000..ebac9df75ddb Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_collections/test_check_masked_offsets.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_change_lim_scale.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_change_lim_scale.png new file mode 100644 index 000000000000..2dfb3b0366da Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_change_lim_scale.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.pdf b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.pdf deleted file mode 100644 index 81271ec29c81..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.png index bb41d922189f..b4abeda53746 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.png and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.svg b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.svg deleted file mode 100644 index 5e205feadc5e..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_closed_patch.svg +++ /dev/null @@ -1,650 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extend_alpha.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extend_alpha.png new file mode 100644 index 000000000000..68cbf3d341f3 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extend_alpha.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_proportional.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_proportional.png index 9f2067a45a40..230f8d7332ba 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_proportional.png and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_proportional.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_shape_proportional.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_shape_proportional.png index d68d71bb2d0e..7dbdbbd9b767 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_shape_proportional.png and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_shape_proportional.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_shape_uniform.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_shape_uniform.png index c4e72454cd4f..eb1e8b82bf02 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_shape_uniform.png and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_shape_uniform.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_uniform.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_uniform.png index 4a10633a6253..86e4094208d6 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_uniform.png and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_extensions_uniform.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_keeping_xlabel.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_keeping_xlabel.png new file mode 100644 index 000000000000..410b9f5b0878 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_keeping_xlabel.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_single_scatter.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_single_scatter.png index 63a58245f7e0..18d9cf02add0 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_single_scatter.png and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_single_scatter.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_twoslope.png b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_twoslope.png new file mode 100644 index 000000000000..b92103ae1347 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/colorbar_twoslope.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/contour_colorbar.png b/lib/matplotlib/tests/baseline_images/test_colorbar/contour_colorbar.png new file mode 100644 index 000000000000..bf084c30ddad Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/contour_colorbar.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/contourf_extend_patches.png b/lib/matplotlib/tests/baseline_images/test_colorbar/contourf_extend_patches.png new file mode 100644 index 000000000000..0e5ef52cf549 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/contourf_extend_patches.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/double_cbar.png b/lib/matplotlib/tests/baseline_images/test_colorbar/double_cbar.png index 2ddb219eda3a..b139a4664c17 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colorbar/double_cbar.png and b/lib/matplotlib/tests/baseline_images/test_colorbar/double_cbar.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/extend_drawedges.png b/lib/matplotlib/tests/baseline_images/test_colorbar/extend_drawedges.png new file mode 100644 index 000000000000..864eeefbae10 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/extend_drawedges.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/nonorm_colorbars.svg b/lib/matplotlib/tests/baseline_images/test_colorbar/nonorm_colorbars.svg new file mode 100644 index 000000000000..85f92c3c8d64 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_colorbar/nonorm_colorbars.svg @@ -0,0 +1,151 @@ + + + + + + + + 2021-12-06T14:23:33.155376 + image/svg+xml + + + Matplotlib v3.6.0.dev954+ged9e9c2ef2.d20211206, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + 1 + + + + + + + + + + 2 + + + + + + + + + + 3 + + + + + + + + + + 4 + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/proportional_colorbars.png b/lib/matplotlib/tests/baseline_images/test_colorbar/proportional_colorbars.png new file mode 100644 index 000000000000..a1f9745230ba Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/proportional_colorbars.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colorbar/test_boundaries.png b/lib/matplotlib/tests/baseline_images/test_colorbar/test_boundaries.png new file mode 100644 index 000000000000..e2e05a375742 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_colorbar/test_boundaries.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_colors/levels_and_colors.png b/lib/matplotlib/tests/baseline_images/test_colors/levels_and_colors.png index bb759674e557..bb0e9a2538da 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_colors/levels_and_colors.png and b/lib/matplotlib/tests/baseline_images/test_colors/levels_and_colors.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11.png index 514bf02ce13c..f337d370dc33 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11rat.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11rat.png index b674803ba2e8..534903300f7a 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11rat.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout11rat.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout13.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout13.png index 5889b0583432..4233f58a8ce4 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout13.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout13.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout14.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout14.png index e030c3c9f6c1..cfe9dca14c88 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout14.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout14.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout3.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout3.png index c679609be54e..ae6420dd04e9 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout3.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout3.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.png index 2a6e55c08f64..ef6d9e417f91 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout4.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout5.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout5.png index af691c44867d..89e71b765154 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout5.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout5.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png index 757230e25363..a239947ca46c 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout8.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout9.png b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout9.png index 59fd2c76c5bc..2ac44b8a18ac 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout9.png and b/lib/matplotlib/tests/baseline_images/test_constrainedlayout/constrained_layout9.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_addlines.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_addlines.png index 2115054d13b2..07a442841e95 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_contour/contour_addlines.png and b/lib/matplotlib/tests/baseline_images/test_contour/contour_addlines.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_all_algorithms.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_all_algorithms.png new file mode 100644 index 000000000000..1ef0c15a678f Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_contour/contour_all_algorithms.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_closed_line_loop.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_closed_line_loop.png new file mode 100644 index 000000000000..2e8e73a8f1a1 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_contour/contour_closed_line_loop.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_line_start_on_corner_edge.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_line_start_on_corner_edge.png new file mode 100644 index 000000000000..d8af58f80eaf Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_contour/contour_line_start_on_corner_edge.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_log_extension.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_log_extension.png index c1feda1e9cba..316b6370edca 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_contour/contour_log_extension.png and b/lib/matplotlib/tests/baseline_images/test_contour/contour_log_extension.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_log_locator.svg b/lib/matplotlib/tests/baseline_images/test_contour/contour_log_locator.svg new file mode 100644 index 000000000000..a4a397104ef7 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_contour/contour_log_locator.svg @@ -0,0 +1,3375 @@ + + + + + + + + 2022-12-11T14:01:58.700972 + image/svg+xml + + + Matplotlib v3.6.0.dev2642+g907c10911d.d20221211, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_manual.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_manual.png new file mode 100644 index 000000000000..c98879360c45 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_contour/contour_manual.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_contour/contour_manual_colors_and_levels.png b/lib/matplotlib/tests/baseline_images/test_contour/contour_manual_colors_and_levels.png index 632d1f7e7594..b01bcb239535 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_contour/contour_manual_colors_and_levels.png and b/lib/matplotlib/tests/baseline_images/test_contour/contour_manual_colors_and_levels.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_dates/date_empty.png b/lib/matplotlib/tests/baseline_images/test_dates/date_empty.png deleted file mode 100644 index 4bb9efcba428..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_dates/date_empty.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure_double.png b/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure_double.png index 1a1ac9fd74f7..594ce7d4e72f 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure_double.png and b/lib/matplotlib/tests/baseline_images/test_figure/test_subfigure_double.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png b/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png index 904e0c3d44a0..f416faa96d5f 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png and b/lib/matplotlib/tests/baseline_images/test_image/mask_image_over_under.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png b/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png new file mode 100644 index 000000000000..cb0aa4815c4e Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/nonuniform_and_pcolor.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_image/rgba_antialias.png b/lib/matplotlib/tests/baseline_images/test_image/rgba_antialias.png new file mode 100644 index 000000000000..65476dc9a595 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_image/rgba_antialias.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf b/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf index 7930f6d68249..b0fb8a4af7f2 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf and b/lib/matplotlib/tests/baseline_images/test_legend/fancy.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_legend/fancy.png b/lib/matplotlib/tests/baseline_images/test_legend/fancy.png index aba46e19e727..fbb827bbefa5 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_legend/fancy.png and b/lib/matplotlib/tests/baseline_images/test_legend/fancy.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg b/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg index 427ab827f4a1..9e56f5ed36bb 100644 --- a/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg +++ b/lib/matplotlib/tests/baseline_images/test_legend/fancy.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-07T20:03:21.839839 + image/svg+xml + + + Matplotlib v3.3.0rc1.post625.dev0+gf3ab37aad, https://matplotlib.org/ + + + + + - + @@ -38,191 +49,191 @@ C -2.000462 -1.161816 -2.236068 -0.593012 -2.236068 0 C -2.236068 0.593012 -2.000462 1.161816 -1.581139 1.581139 C -1.161816 2.000462 -0.593012 2.236068 0 2.236068 z -" id="m27675ac663" style="stroke:#000000;"/> +" id="ma203e0a9bc" style="stroke:#000000;"/> - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +" id="m8863516224" style="stroke:#008000;stroke-width:0.5;"/> - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + +" id="m6db69444d4" style="stroke:#008000;stroke-width:0.5;"/> - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - +" id="m58dade9745" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="mc946a5ae82" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -356,104 +367,104 @@ L 0 4 +" id="m8b2567c4af" style="stroke:#000000;stroke-width:0.5;"/> - + +" id="m580a4f3bfe" style="stroke:#000000;stroke-width:0.5;"/> - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + @@ -477,144 +488,149 @@ z - - + + - + - - + + - + - - - - - +" id="DejaVuSans-65" transform="scale(0.015625)"/> + + + + @@ -633,28 +649,28 @@ L 316.669091 205.7097 - + - - - + + + @@ -718,22 +734,22 @@ L 391.117091 198.5097 - + - + - + - + @@ -753,7 +769,7 @@ L 405.517091 205.7097 - + diff --git a/lib/matplotlib/tests/baseline_images/test_lines/line_collection_dashes.pdf b/lib/matplotlib/tests/baseline_images/test_lines/line_collection_dashes.pdf index 18b2e46e0f11..44b45f925bd3 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_lines/line_collection_dashes.pdf and b/lib/matplotlib/tests/baseline_images/test_lines/line_collection_dashes.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_lines/line_collection_dashes.svg b/lib/matplotlib/tests/baseline_images/test_lines/line_collection_dashes.svg index c12572fa9384..ddc7b3c04a69 100644 --- a/lib/matplotlib/tests/baseline_images/test_lines/line_collection_dashes.svg +++ b/lib/matplotlib/tests/baseline_images/test_lines/line_collection_dashes.svg @@ -1,12 +1,23 @@ - + + + + + 2021-03-02T20:44:16.197962 + image/svg+xml + + + Matplotlib v3.3.4.post2496+g7299993ff, https://matplotlib.org/ + + + + + - + @@ -32,45 +43,45 @@ z +" id="m1794ba8a41" style="stroke:#000000;stroke-width:0.8;"/> - + - + - + - + - + - + @@ -81,250 +92,250 @@ L 0 3.5 +" id="mccea692670" style="stroke:#000000;stroke-width:0.8;"/> - + - + - + - + - + - + - + - + - - - + + - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_lines/striped_line.png b/lib/matplotlib/tests/baseline_images/test_lines/striped_line.png new file mode 100644 index 000000000000..957129cb981f Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_lines/striped_line.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext0_cm_00.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext0_cm_00.svg new file mode 100644 index 000000000000..619549bbbe68 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext0_cm_00.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + ¡ + - + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext0_dejavusans_00.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext0_dejavusans_00.svg new file mode 100644 index 000000000000..72ba7d0a32cc --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext0_dejavusans_00.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + −- + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_dejavusans_02.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_dejavusans_02.png new file mode 100644 index 000000000000..b37505cc62b0 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_dejavusans_02.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_dejavusans_03.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_dejavusans_03.png new file mode 100644 index 000000000000..f28fb31c542f Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext1_dejavusans_03.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_30.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_30.pdf deleted file mode 100644 index 503cd69d37ec..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_30.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_30.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_30.png deleted file mode 100644 index c160c75b2d2c..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_30.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_30.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_30.svg deleted file mode 100644 index c0d0e3b91162..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_30.svg +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.pdf index 94cdddaf6dcf..c463850d8d0f 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.pdf and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.png index ae23ecedc0ce..2cf6829952b2 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.svg index 1d225fbb273a..ea21bf1f35e2 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_68.svg @@ -1,160 +1,216 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + 2022-10-24T12:21:21.236679 + image/svg+xml + + + Matplotlib v3.6.0.dev4028+gdd27a7f3c3.d20221024, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_77.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_77.pdf deleted file mode 100644 index 8b8bc68dfd5b..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_77.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_77.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_77.png deleted file mode 100644 index 9ed5a18bf57b..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_77.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_77.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_77.svg deleted file mode 100644 index f423ee824f38..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_cm_77.svg +++ /dev/null @@ -1,222 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_30.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_30.pdf deleted file mode 100644 index 1783edfa14e6..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_30.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_30.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_30.png deleted file mode 100644 index 05b1e65e1419..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_30.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_30.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_30.svg deleted file mode 100644 index 13ba043bf787..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_30.svg +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.pdf index 3a95452967ac..a33f6b7d8385 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.pdf and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.png index d655592d81c5..5cc8c6ef7b31 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.svg index bdb560153175..94bd4b633188 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_68.svg @@ -1,128 +1,184 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + 2022-10-24T13:47:25.928529 + image/svg+xml + + + Matplotlib v3.6.0.dev4028+g98b55b4eed.d20221024, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.pdf index 9f4e5515c4c1..4e348e9ba03e 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.pdf and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.png index 3b0aa1e7d159..53c8295911ae 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.svg index 4134153405c0..33dc31b427a0 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_71.svg @@ -1,12 +1,23 @@ - - + + + + + + 2021-08-10T16:39:40.187285 + image/svg+xml + + + Matplotlib v3.4.2.post1645+ge771650c3a.d20210810, https://matplotlib.org/ + + + + + - + @@ -15,117 +26,125 @@ L 378 54 L 378 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - - + + - + + - + - + - + - - - - - - - - - - - - +" transform="scale(0.015625)"/> + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_77.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_77.pdf deleted file mode 100644 index 94ca3d3c6d94..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_77.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_77.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_77.png deleted file mode 100644 index 82e0821cd3ea..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_77.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_77.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_77.svg deleted file mode 100644 index 98902d769804..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavusans_77.svg +++ /dev/null @@ -1,173 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_30.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_30.pdf deleted file mode 100644 index 35697c58a00e..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_30.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_30.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_30.png deleted file mode 100644 index 718302dfb8ca..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_30.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_30.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_30.svg deleted file mode 100644 index eb8e7376d155..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_30.svg +++ /dev/null @@ -1,142 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.pdf index dc4c1802c743..64e9b2fe1a97 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.pdf and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.png index 35caf2d2b6ad..9336942936a6 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.svg index 0284835785a3..f8773bd214fc 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_68.svg @@ -1,117 +1,173 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + 2022-10-24T13:47:45.388857 + image/svg+xml + + + Matplotlib v3.6.0.dev4028+g98b55b4eed.d20221024, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_77.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_77.pdf deleted file mode 100644 index d6fc89ad9f25..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_77.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_77.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_77.png deleted file mode 100644 index b3892b25c4ee..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_77.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_77.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_77.svg deleted file mode 100644 index 3c9961669b69..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_dejavuserif_77.svg +++ /dev/null @@ -1,217 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_30.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_30.pdf deleted file mode 100644 index 49ad0e80b04a..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_30.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_30.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_30.png deleted file mode 100644 index 7001c4f0e3dd..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_30.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_30.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_30.svg deleted file mode 100644 index 70746902f681..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_30.svg +++ /dev/null @@ -1,155 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.pdf index 2aca1bfd4a52..c6cf56ddba12 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.pdf and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.png index f51fc6032d83..0aa0ac62b063 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.svg index be7cdc9f2da8..078cb0fdb8c4 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_68.svg @@ -1,127 +1,183 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + 2022-10-24T13:46:49.965486 + image/svg+xml + + + Matplotlib v3.6.0.dev4028+g98b55b4eed.d20221024, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_77.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_77.pdf deleted file mode 100644 index 3966ee6ddd59..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_77.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_77.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_77.png deleted file mode 100644 index 9ed5a18bf57b..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_77.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_77.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_77.svg deleted file mode 100644 index f423ee824f38..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stix_77.svg +++ /dev/null @@ -1,222 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_30.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_30.pdf deleted file mode 100644 index ab1a2ee082ae..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_30.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_30.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_30.png deleted file mode 100644 index 58bb828044e7..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_30.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_30.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_30.svg deleted file mode 100644 index 7bc674a36430..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_30.svg +++ /dev/null @@ -1,157 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.pdf index 323afa6acf0d..e277346b7f1e 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.pdf and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.png index a8ea567f675e..1ea8e26f8d17 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.png and b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.svg index 7fdb080a70a9..0a7baa3550a4 100644 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.svg +++ b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_68.svg @@ -1,111 +1,167 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + 2022-10-24T13:47:05.108205 + image/svg+xml + + + Matplotlib v3.6.0.dev4028+g98b55b4eed.d20221024, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_77.pdf b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_77.pdf deleted file mode 100644 index 98512f54330d..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_77.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_77.png b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_77.png deleted file mode 100644 index 9ed5a18bf57b..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_77.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_77.svg b/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_77.svg deleted file mode 100644 index f423ee824f38..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_mathtext/mathtext_stixsans_77.svg +++ /dev/null @@ -1,222 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_patches/annulus.png b/lib/matplotlib/tests/baseline_images/test_patches/annulus.png new file mode 100644 index 000000000000..549443f523cf Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_patches/annulus.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_patches/autoscale_arc.png b/lib/matplotlib/tests/baseline_images/test_patches/autoscale_arc.png new file mode 100644 index 000000000000..3a6d3324e9d8 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_patches/autoscale_arc.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_patches/autoscale_arc.svg b/lib/matplotlib/tests/baseline_images/test_patches/autoscale_arc.svg new file mode 100644 index 000000000000..dca6c1f226b3 --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_patches/autoscale_arc.svg @@ -0,0 +1,289 @@ + + + + + + + + 2022-07-01T10:48:08.014263 + image/svg+xml + + + Matplotlib v3.6.0.dev2827+g83cb036.d20220701, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_path/arrow_contains_point.png b/lib/matplotlib/tests/baseline_images/test_path/arrow_contains_point.png index 4e871e6cd02c..989b84092300 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_path/arrow_contains_point.png and b/lib/matplotlib/tests/baseline_images/test_path/arrow_contains_point.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.pdf b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.pdf index 8d0cc584e567..ba027d57a34b 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.pdf and b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.png b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.png index ebd8d95dc0ab..af91778e7d80 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.png and b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.svg b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.svg index e067b3ed266d..a075b6c60e54 100644 --- a/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.svg +++ b/lib/matplotlib/tests/baseline_images/test_patheffects/patheffect2.svg @@ -1,23 +1,23 @@ - + - + - 2020-11-06T19:00:51.436188 + 2022-02-19T11:16:23.155823 image/svg+xml - Matplotlib v3.3.2.post1573+gcdb08ceb8, https://matplotlib.org/ + Matplotlib v3.6.0.dev1697+g00762ef54b, https://matplotlib.org/ - + @@ -26,7 +26,7 @@ L 460.8 345.6 L 460.8 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -35,50 +35,50 @@ L 369.216 307.584 L 369.216 41.472 L 103.104 41.472 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - - + + - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + @@ -87,187 +87,197 @@ L 0 3.5 - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - - - - - + + + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - - + + - + - + - + - +" clip-path="url(#p9eb69e71cc)" style="fill: none; stroke: #000000; stroke-width: 1.5"/> + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - - + +" clip-path="url(#p9eb69e71cc)"/> - - + +" clip-path="url(#p9eb69e71cc)"/> - - + +" clip-path="url(#p9eb69e71cc)"/> - - + +" clip-path="url(#p9eb69e71cc)"/> - - + +" clip-path="url(#p9eb69e71cc)"/> - - + + - - + +" clip-path="url(#p9eb69e71cc)"/> - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/spaces_and_newlines.png b/lib/matplotlib/tests/baseline_images/test_patheffects/spaces_and_newlines.png new file mode 100644 index 000000000000..9dd4c1e8d7ff Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_patheffects/spaces_and_newlines.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_patheffects/tickedstroke.png b/lib/matplotlib/tests/baseline_images/test_patheffects/tickedstroke.png index 60af86eaadcb..5884d46dc1ce 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_patheffects/tickedstroke.png and b/lib/matplotlib/tests/baseline_images/test_patheffects/tickedstroke.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_pickle/multi_pickle.png b/lib/matplotlib/tests/baseline_images/test_pickle/multi_pickle.png deleted file mode 100644 index 3a6f41fa2948..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_pickle/multi_pickle.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_polar/polar_log.png b/lib/matplotlib/tests/baseline_images/test_polar/polar_log.png new file mode 100644 index 000000000000..ab8e20b482a5 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_polar/polar_log.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_spines/black_axes.pdf b/lib/matplotlib/tests/baseline_images/test_spines/black_axes.pdf new file mode 100644 index 000000000000..96eacb9308d9 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_spines/black_axes.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_spines/black_axes.png b/lib/matplotlib/tests/baseline_images/test_spines/black_axes.png new file mode 100644 index 000000000000..4fbcbe9b6756 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_spines/black_axes.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_spines/black_axes.svg b/lib/matplotlib/tests/baseline_images/test_spines/black_axes.svg new file mode 100644 index 000000000000..8f7ff932859d --- /dev/null +++ b/lib/matplotlib/tests/baseline_images/test_spines/black_axes.svg @@ -0,0 +1,64 @@ + + + + + + + + 2022-10-24T17:29:00.034663 + image/svg+xml + + + Matplotlib v3.6.0.dev4027+g68c78c9fb1.d20221024, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf index 7f29b4a9eabf..f13892ba2bc5 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.png b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.png index 99d7e1d44cd8..21f202e5edec 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.png and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.svg b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.svg index 869361f76dd0..a95118c95d97 100644 --- a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.svg +++ b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_colormap.svg @@ -1,12 +1,23 @@ - - + + + + + + 2021-08-18T03:11:20.912289 + image/svg+xml + + + Matplotlib v3.4.2.post1692+gb0554f4824.d20210818, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 460.8 345.6 L 460.8 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,60 +35,60 @@ L 343.296 307.584 L 343.296 41.472 L 57.6 41.472 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - + - + @@ -86,2474 +97,2551 @@ L 0 3.5 - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + - - - - - - - - - + + + + + + + + + - + - - - + + + - + - - - - - - - - - + + + + + + + + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + - - - - + + + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + - - - - - - - - + + + + + + + + - + - + - - - + + + - + - + - - - - - - - + + + + + + + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - + + + + - - - - - - + + + + + + + + + - - - - - - - - + + + + + + + + - - - + + + - - - - - - - - - - - - + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - + + + + + + + + + + - + - - - - - - - - - - - + + + + + + + + + + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - + - - - + + + - + - - - + + + - + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - - - - - +" clip-path="url(#p3b712018f5)" style="fill: none; stroke: #ffb000; stroke-width: 2"/> + + + + + + + + + + + + + + + + - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffb900; stroke: #ffb900; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff3a00; stroke: #ff3a00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff8a00; stroke: #ff8a00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff7b00; stroke: #ff7b00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffa800; stroke: #ffa800; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffaa00; stroke: #ffaa00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffa200; stroke: #ffa200; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff8c00; stroke: #ff8c00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff7b00; stroke: #ff7b00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff7500; stroke: #ff7500; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff4400; stroke: #ff4400; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffbf00; stroke: #ffbf00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffc100; stroke: #ffc100; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffdf00; stroke: #ffdf00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffeb00; stroke: #ffeb00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #fffb00; stroke: #fffb00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #fff200; stroke: #fff200; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #fff500; stroke: #fff500; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffed00; stroke: #ffed00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffce00; stroke: #ffce00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffc200; stroke: #ffc200; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffb300; stroke: #ffb300; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff9f00; stroke: #ff9f00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff8d00; stroke: #ff8d00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffc700; stroke: #ffc700; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff9700; stroke: #ff9700; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffa300; stroke: #ffa300; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffa700; stroke: #ffa700; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff4e00; stroke: #ff4e00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff4600; stroke: #ff4600; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff1f00; stroke: #ff1f00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffa300; stroke: #ffa300; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff4c00; stroke: #ff4c00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff8600; stroke: #ff8600; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff7400; stroke: #ff7400; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff5e00; stroke: #ff5e00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff8000; stroke: #ff8000; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff8700; stroke: #ff8700; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff4f00; stroke: #ff4f00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff7600; stroke: #ff7600; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ff5e00; stroke: #ff5e00; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffb100; stroke: #ffb100; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffe400; stroke: #ffe400; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffa400; stroke: #ffa400; stroke-width: 2; stroke-linecap: round"/> - - + +" clip-path="url(#p3b712018f5)" style="fill: #ffc300; stroke: #ffc300; stroke-width: 2; stroke-linecap: round"/> + + + + + + + + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - - + +" style="fill: #ffffff"/> - - - + + + + + - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - + - + - + + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - - + + - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.pdf b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.pdf index f1644d5b613c..1b14d45c3059 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.pdf and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.png b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.png index 51e4e5d859c6..b928223c3206 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.png and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.svg b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.svg index ba0baa1da49e..2a2bec622027 100644 --- a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.svg +++ b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_linewidth.svg @@ -1,12 +1,23 @@ - - + + + + + + 2021-08-18T03:24:40.872884 + image/svg+xml + + + Matplotlib v3.4.2.post1692+gb0554f4824.d20210818, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 460.8 345.6 L 460.8 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,60 +35,60 @@ L 414.72 307.584 L 414.72 41.472 L 57.6 41.472 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - + - + @@ -86,2462 +97,3002 @@ L 0 3.5 - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +" clip-path="url(#p122428e17d)" style="fill: none; stroke: #000000; stroke-width: 5"/> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 0.646693; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.763217; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.085853; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.024196; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.335626; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.703123; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.140137; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.446783; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 3.39267; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 0.997896; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.689053; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.412889; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.596691; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.731148; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.826019; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.324419; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 0.981644; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.611973; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.459108; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.704699; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.433977; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.835914; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.347416; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.312264; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.138664; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.604062; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.274401; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 0.315827; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 0.95716; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.721473; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.468045; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 3.216579; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.887734; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 0.377428; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.158777; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 0.644319; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.44633; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.931274; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.117826; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 3.027536; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.576524; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.682513; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.441104; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.437048; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.108591; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 2.933469; stroke-linecap: round"/> - - + +" clip-path="url(#p122428e17d)" style="stroke: #000000; stroke-width: 1.361439; stroke-linecap: round"/> + + + + + + + + + + + + + + + + + + + + + + + + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.pdf b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.pdf index f8f94f0b2a3f..7d6beca0b3ac 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.pdf and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.png b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.png index 02800cbde2c4..c3ce699983c4 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.png and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.svg b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.svg index 8b40bc4c66f9..17e4c78c2490 100644 --- a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.svg +++ b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_masks_and_nans.svg @@ -1,12 +1,23 @@ - - + + + + + + 2021-08-18T03:08:55.311923 + image/svg+xml + + + Matplotlib v3.4.2.post1692+gb0554f4824, https://matplotlib.org/ + + + + + - + @@ -15,7 +26,7 @@ L 460.8 345.6 L 460.8 0 L 0 0 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> @@ -24,60 +35,60 @@ L 414.72 307.584 L 414.72 41.472 L 57.6 41.472 z -" style="fill:#ffffff;"/> +" style="fill: #ffffff"/> - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - + - + @@ -86,3624 +97,3749 @@ L 0 3.5 - +" style="stroke: #000000; stroke-width: 0.8"/> - + - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + - - - - - - - - - + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - + - - - - - - - - + + + + + + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - + - - - - - - - - - - - - + + + + + + + + + + + + - + - - - - - - - + + + + + + + - + - - - - - - - - - + + + + + + + + + - + - - - - - - + + + + + + - + - - - - + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - + + + + + + + - - - - - + + + + + - - - - - - - + + + + + + + + + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - + + + + + + + - - - - - - + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + + + + + + - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - + - - - - - - - - - - - + + + + + + + + + + + + - + - - - - - - - + + + + + + + - + - - - - - - + + + + + + - + - - - - - - - - - - - - + + + + + + + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - - - + + + + + + + - + - - - + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - + + + + + + + - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - + + + + + + + + + + + + - + - + - + - + - - - - - +" clip-path="url(#p0ebb051c42)" style="fill: none; stroke: #0a549e; stroke-width: 1.5"/> + + + + + + + + + - - + +" clip-path="url(#p0ebb051c42)" style="fill: #2c7cba; stroke: #2c7cba; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #3e8ec4; stroke: #3e8ec4; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #3282be; stroke: #3282be; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #3a8ac2; stroke: #3a8ac2; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #56a0ce; stroke: #56a0ce; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #4292c6; stroke: #4292c6; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #7ab6d9; stroke: #7ab6d9; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #8dc1dd; stroke: #8dc1dd; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #87bddc; stroke: #87bddc; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #c7dbef; stroke: #c7dbef; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #cfe1f2; stroke: #cfe1f2; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #e1edf8; stroke: #e1edf8; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #2474b7; stroke: #2474b7; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #2070b4; stroke: #2070b4; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #115ca5; stroke: #115ca5; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #09529d; stroke: #09529d; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #084c95; stroke: #084c95; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #084285; stroke: #084285; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #083979; stroke: #083979; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #083776; stroke: #083776; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #083979; stroke: #083979; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #084184; stroke: #084184; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #084387; stroke: #084387; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #125ea6; stroke: #125ea6; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #0f5aa3; stroke: #0f5aa3; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #1c6ab0; stroke: #1c6ab0; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #2171b5; stroke: #2171b5; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #2d7dbb; stroke: #2d7dbb; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #3888c1; stroke: #3888c1; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #4493c7; stroke: #4493c7; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #519ccc; stroke: #519ccc; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #68acd5; stroke: #68acd5; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #4e9acb; stroke: #4e9acb; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #539ecd; stroke: #539ecd; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #81badb; stroke: #81badb; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #9fcae1; stroke: #9fcae1; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #60a7d2; stroke: #60a7d2; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #2979b9; stroke: #2979b9; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #9dcae1; stroke: #9dcae1; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #5ba3d0; stroke: #5ba3d0; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #c2d9ee; stroke: #c2d9ee; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #65aad4; stroke: #65aad4; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #61a7d2; stroke: #61a7d2; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #87bddc; stroke: #87bddc; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #58a1cf; stroke: #58a1cf; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #65aad4; stroke: #65aad4; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #bfd8ed; stroke: #bfd8ed; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #4b98ca; stroke: #4b98ca; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #b7d4ea; stroke: #b7d4ea; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #539ecd; stroke: #539ecd; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #add0e6; stroke: #add0e6; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #5ca4d0; stroke: #5ca4d0; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #89bedc; stroke: #89bedc; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #63a8d3; stroke: #63a8d3; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #95c5df; stroke: #95c5df; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #72b2d8; stroke: #72b2d8; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #8abfdd; stroke: #8abfdd; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #7fb9da; stroke: #7fb9da; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #4896c8; stroke: #4896c8; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #0a539e; stroke: #0a539e; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #4896c8; stroke: #4896c8; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #549fcd; stroke: #549fcd; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #75b4d8; stroke: #75b4d8; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #65aad4; stroke: #65aad4; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #2474b7; stroke: #2474b7; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #56a0ce; stroke: #56a0ce; stroke-width: 1.5; stroke-linecap: round"/> - - + +" clip-path="url(#p0ebb051c42)" style="fill: #2b7bba; stroke: #2b7bba; stroke-width: 1.5; stroke-linecap: round"/> + + + + + + + + + + + + + + + + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - + +" style="fill: none; stroke: #000000; stroke-width: 0.8; stroke-linejoin: miter; stroke-linecap: square"/> - - + + diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_maxlength_no_broken.png b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_maxlength_no_broken.png new file mode 100644 index 000000000000..69b7ca96a9c1 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_maxlength_no_broken.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.pdf b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.pdf deleted file mode 100644 index f9bf53975faf..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.pdf and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.png b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.png index c2c3e28d3eab..b572274772c0 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.png and b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.svg b/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.svg deleted file mode 100644 index fca2e620fa7c..000000000000 --- a/lib/matplotlib/tests/baseline_images/test_streamplot/streamplot_startpoints.svg +++ /dev/null @@ -1,1494 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/lib/matplotlib/tests/baseline_images/test_text/text_as_text_opacity.svg b/lib/matplotlib/tests/baseline_images/test_text/text_as_text_opacity.svg index 69d287e3536c..63cdeb8fbd47 100644 --- a/lib/matplotlib/tests/baseline_images/test_text/text_as_text_opacity.svg +++ b/lib/matplotlib/tests/baseline_images/test_text/text_as_text_opacity.svg @@ -10,22 +10,22 @@ - - 50% using `color` + 50% using `color` - 50% using `alpha` + 50% using `alpha` - 50% using `alpha` and 100% `color` + 50% using `alpha` and 100% `color` diff --git a/lib/matplotlib/tests/baseline_images/test_text/text_pdf_chars_beyond_bmp.pdf b/lib/matplotlib/tests/baseline_images/test_text/text_pdf_chars_beyond_bmp.pdf new file mode 100644 index 000000000000..8890790d2ea2 Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_text/text_pdf_chars_beyond_bmp.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_text/text_pdf_font42_kerning.pdf b/lib/matplotlib/tests/baseline_images/test_text/text_pdf_font42_kerning.pdf new file mode 100644 index 000000000000..a8ce9fca346c Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_text/text_pdf_font42_kerning.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf index 640df51ac227..43daec43d648 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf and b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.pdf differ diff --git a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png index 22b08c23bd49..e889450de817 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png and b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg index 8d52765e3ae0..091a693b0e53 100644 --- a/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg +++ b/lib/matplotlib/tests/baseline_images/test_transforms/pre_transform_data.svg @@ -1,12 +1,23 @@ - + + + + + 2020-08-08T03:00:34.113585 + image/svg+xml + + + Matplotlib v3.3.0rc1.post628+gb07dd36f3, https://matplotlib.org/ + + + + + - + @@ -27,37 +38,37 @@ z " style="fill:#ffffff;"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -487,1956 +498,1956 @@ C -2.683901 -1.55874 -3 -0.795609 -3 0 C -3 0.795609 -2.683901 1.55874 -2.12132 2.12132 C -1.55874 2.683901 -0.795609 3 0 3 z -" id="mefe0d42b30" style="stroke:#1f77b4;"/> +" id="mfb1b36b408" style="stroke:#1f77b4;"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -2446,45 +2457,45 @@ z +" id="m03b3b1dbf4" style="stroke:#000000;stroke-width:0.8;"/> - + - + - + - + - + - + @@ -2495,2971 +2506,3790 @@ L 0 3.5 +" id="m01a773088f" style="stroke:#000000;stroke-width:0.8;"/> - + - + - + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - + @@ -5467,7 +6297,7 @@ L 414.72 41.472 - + diff --git a/lib/matplotlib/tests/baseline_images/test_triangulation/tri_smooth_contouring.png b/lib/matplotlib/tests/baseline_images/test_triangulation/tri_smooth_contouring.png index 80ea47079014..fee5b15e5182 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_triangulation/tri_smooth_contouring.png and b/lib/matplotlib/tests/baseline_images/test_triangulation/tri_smooth_contouring.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_usetex/eqnarray.png b/lib/matplotlib/tests/baseline_images/test_usetex/eqnarray.png new file mode 100644 index 000000000000..249f15d238dd Binary files /dev/null and b/lib/matplotlib/tests/baseline_images/test_usetex/eqnarray.png differ diff --git a/lib/matplotlib/tests/baseline_images/test_widgets/check_bunch_of_radio_buttons.png b/lib/matplotlib/tests/baseline_images/test_widgets/check_bunch_of_radio_buttons.png deleted file mode 100644 index e071860dfde6..000000000000 Binary files a/lib/matplotlib/tests/baseline_images/test_widgets/check_bunch_of_radio_buttons.png and /dev/null differ diff --git a/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_buttons.png b/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_buttons.png index e96085d9bffd..d6e6004a1732 100644 Binary files a/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_buttons.png and b/lib/matplotlib/tests/baseline_images/test_widgets/check_radio_buttons.png differ diff --git a/lib/matplotlib/tests/conftest.py b/lib/matplotlib/tests/conftest.py index 722a7ff91484..54a1bc6cae94 100644 --- a/lib/matplotlib/tests/conftest.py +++ b/lib/matplotlib/tests/conftest.py @@ -1,4 +1,2 @@ -from matplotlib.testing.conftest import (mpl_test_settings, - mpl_image_comparison_parameters, - pytest_configure, pytest_unconfigure, - pd) +from matplotlib.testing.conftest import ( # noqa + mpl_test_settings, pytest_configure, pytest_unconfigure, pd, xr) diff --git a/lib/matplotlib/tests/test_afm.py b/lib/matplotlib/tests/test_afm.py index 2d54c16cb402..e5c6a83937cd 100644 --- a/lib/matplotlib/tests/test_afm.py +++ b/lib/matplotlib/tests/test_afm.py @@ -2,7 +2,7 @@ import pytest import logging -from matplotlib import afm +from matplotlib import _afm from matplotlib import font_manager as fm @@ -39,13 +39,13 @@ def test_nonascii_str(): inp_str = "привет" byte_str = inp_str.encode("utf8") - ret = afm._to_str(byte_str) + ret = _afm._to_str(byte_str) assert ret == inp_str def test_parse_header(): fh = BytesIO(AFM_TEST_DATA) - header = afm._parse_header(fh) + header = _afm._parse_header(fh) assert header == { b'StartFontMetrics': 2.0, b'FontName': 'MyFont-Bold', @@ -66,8 +66,8 @@ def test_parse_header(): def test_parse_char_metrics(): fh = BytesIO(AFM_TEST_DATA) - afm._parse_header(fh) # position - metrics = afm._parse_char_metrics(fh) + _afm._parse_header(fh) # position + metrics = _afm._parse_char_metrics(fh) assert metrics == ( {0: (250.0, 'space', [0, 0, 0, 0]), 42: (1141.0, 'foo', [40, 60, 800, 360]), @@ -81,13 +81,13 @@ def test_parse_char_metrics(): def test_get_familyname_guessed(): fh = BytesIO(AFM_TEST_DATA) - font = afm.AFM(fh) + font = _afm.AFM(fh) del font._header[b'FamilyName'] # remove FamilyName, so we have to guess assert font.get_familyname() == 'My Font' def test_font_manager_weight_normalization(): - font = afm.AFM(BytesIO( + font = _afm.AFM(BytesIO( AFM_TEST_DATA.replace(b"Weight Bold\n", b"Weight Custom\n"))) assert fm.afmFontProperty("", font).weight == "normal" @@ -107,7 +107,7 @@ def test_font_manager_weight_normalization(): def test_bad_afm(afm_data): fh = BytesIO(afm_data) with pytest.raises(RuntimeError): - afm._parse_header(fh) + _afm._parse_header(fh) @pytest.mark.parametrize( @@ -132,6 +132,6 @@ def test_bad_afm(afm_data): def test_malformed_header(afm_data, caplog): fh = BytesIO(afm_data) with caplog.at_level(logging.ERROR): - afm._parse_header(fh) + _afm._parse_header(fh) assert len(caplog.records) == 1 diff --git a/lib/matplotlib/tests/test_agg.py b/lib/matplotlib/tests/test_agg.py index 4d34ef3ccd4a..5285a24f01f6 100644 --- a/lib/matplotlib/tests/test_agg.py +++ b/lib/matplotlib/tests/test_agg.py @@ -7,10 +7,14 @@ from matplotlib import ( - collections, path, pyplot as plt, transforms as mtransforms, rcParams) -from matplotlib.image import imread + collections, patheffects, pyplot as plt, transforms as mtransforms, + rcParams, rc_context) +from matplotlib.backends.backend_agg import RendererAgg from matplotlib.figure import Figure +from matplotlib.image import imread +from matplotlib.path import Path from matplotlib.testing.decorators import image_comparison +from matplotlib.transforms import IdentityTransform def test_repeated_save_with_alpha(): @@ -52,7 +56,7 @@ def test_large_single_path_collection(): # applied. f, ax = plt.subplots() collection = collections.PathCollection( - [path.Path([[-10, 5], [10, 5], [10, -5], [-10, -5], [-10, 5]])]) + [Path([[-10, 5], [10, 5], [10, -5], [-10, -5], [-10, 5]])]) ax.add_artist(collection) ax.set_xlim(10**-3, 1) plt.savefig(buff) @@ -72,10 +76,10 @@ def test_marker_with_nan(): def test_long_path(): buff = io.BytesIO() - - fig, ax = plt.subplots() - np.random.seed(0) - points = np.random.rand(70000) + fig = Figure() + ax = fig.subplots() + points = np.ones(100_000) + points[::2] *= -1 ax.plot(points) fig.savefig(buff, format='png') @@ -83,7 +87,7 @@ def test_long_path(): @image_comparison(['agg_filter.png'], remove_text=True) def test_agg_filter(): def smooth1d(x, window_len): - # copied from http://www.scipy.org/Cookbook/SignalSmooth + # copied from https://scipy-cookbook.readthedocs.io/ s = np.r_[ 2*x[0] - x[window_len:1:-1], x, 2*x[-1] - x[-1:-window_len:-1]] w = np.hanning(window_len) @@ -177,10 +181,9 @@ def process_image(self, padded_src, dpi): shadow.update_from(line) # offset transform - ot = mtransforms.offset_copy(line.get_transform(), ax.figure, - x=4.0, y=-6.0, units='points') - - shadow.set_transform(ot) + transform = mtransforms.offset_copy(line.get_transform(), ax.figure, + x=4.0, y=-6.0, units='points') + shadow.set_transform(transform) # adjust zorder of the shadow lines so that it is drawn below the # original lines @@ -244,3 +247,92 @@ def test_pil_kwargs_tiff(): im = Image.open(buf) tags = {TiffTags.TAGS_V2[k].name: v for k, v in im.tag_v2.items()} assert tags["ImageDescription"] == "test image" + + +def test_pil_kwargs_webp(): + plt.plot([0, 1, 2], [0, 1, 0]) + buf_small = io.BytesIO() + pil_kwargs_low = {"quality": 1} + plt.savefig(buf_small, format="webp", pil_kwargs=pil_kwargs_low) + assert len(pil_kwargs_low) == 1 + buf_large = io.BytesIO() + pil_kwargs_high = {"quality": 100} + plt.savefig(buf_large, format="webp", pil_kwargs=pil_kwargs_high) + assert len(pil_kwargs_high) == 1 + assert buf_large.getbuffer().nbytes > buf_small.getbuffer().nbytes + + +def test_webp_alpha(): + plt.plot([0, 1, 2], [0, 1, 0]) + buf = io.BytesIO() + plt.savefig(buf, format="webp", transparent=True) + im = Image.open(buf) + assert im.mode == "RGBA" + + +def test_draw_path_collection_error_handling(): + fig, ax = plt.subplots() + ax.scatter([1], [1]).set_paths(Path([(0, 1), (2, 3)])) + with pytest.raises(TypeError): + fig.canvas.draw() + + +def test_chunksize_fails(): + # NOTE: This test covers multiple independent test scenarios in a single + # function, because each scenario uses ~2GB of memory and we don't + # want parallel test executors to accidentally run multiple of these + # at the same time. + + N = 100_000 + dpi = 500 + w = 5*dpi + h = 6*dpi + + # make a Path that spans the whole w-h rectangle + x = np.linspace(0, w, N) + y = np.ones(N) * h + y[::2] = 0 + path = Path(np.vstack((x, y)).T) + # effectively disable path simplification (but leaving it "on") + path.simplify_threshold = 0 + + # setup the minimal GraphicsContext to draw a Path + ra = RendererAgg(w, h, dpi) + gc = ra.new_gc() + gc.set_linewidth(1) + gc.set_foreground('r') + + gc.set_hatch('/') + with pytest.raises(OverflowError, match='can not split hatched path'): + ra.draw_path(gc, path, IdentityTransform()) + gc.set_hatch(None) + + with pytest.raises(OverflowError, match='can not split filled path'): + ra.draw_path(gc, path, IdentityTransform(), (1, 0, 0)) + + # Set to zero to disable, currently defaults to 0, but let's be sure. + with rc_context({'agg.path.chunksize': 0}): + with pytest.raises(OverflowError, match='Please set'): + ra.draw_path(gc, path, IdentityTransform()) + + # Set big enough that we do not try to chunk. + with rc_context({'agg.path.chunksize': 1_000_000}): + with pytest.raises(OverflowError, match='Please reduce'): + ra.draw_path(gc, path, IdentityTransform()) + + # Small enough we will try to chunk, but big enough we will fail to render. + with rc_context({'agg.path.chunksize': 90_000}): + with pytest.raises(OverflowError, match='Please reduce'): + ra.draw_path(gc, path, IdentityTransform()) + + path.should_simplify = False + with pytest.raises(OverflowError, match="should_simplify is False"): + ra.draw_path(gc, path, IdentityTransform()) + + +def test_non_tuple_rgbaface(): + # This passes rgbaFace as a ndarray to draw_path. + fig = plt.figure() + fig.add_subplot(projection="3d").scatter( + [0, 1, 2], [0, 1, 2], path_effects=[patheffects.Stroke(linewidth=4)]) + fig.canvas.draw() diff --git a/lib/matplotlib/tests/test_agg_filter.py b/lib/matplotlib/tests/test_agg_filter.py index fd54a6b4aa9e..dc8cff6858ae 100644 --- a/lib/matplotlib/tests/test_agg_filter.py +++ b/lib/matplotlib/tests/test_agg_filter.py @@ -23,7 +23,7 @@ def manual_alpha(im, dpi): # Note: Doing alpha like this is not the same as setting alpha on # the mesh itself. Currently meshes are drawn as independent patches, # and we see fine borders around the blocks of color. See the SO - # question for an example: https://stackoverflow.com/questions/20678817 + # question for an example: https://stackoverflow.com/q/20678817/ mesh.set_agg_filter(manual_alpha) # Currently we must enable rasterization for this to have an effect in diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index 7ae77cb6cf3b..49e6a374ea57 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -1,6 +1,7 @@ -import gc import os from pathlib import Path +import platform +import re import subprocess import sys import weakref @@ -11,6 +12,7 @@ import matplotlib as mpl from matplotlib import pyplot as plt from matplotlib import animation +from matplotlib.testing.decorators import check_figures_equal @pytest.fixture() @@ -69,6 +71,7 @@ def finish(self): def test_null_movie_writer(anim): # Test running an animation with NullMovieWriter. + plt.rcParams["savefig.facecolor"] = "auto" filename = "unused.null" dpi = 50 savefig_kwargs = dict(foo=0) @@ -81,16 +84,24 @@ def test_null_movie_writer(anim): assert writer.outfile == filename assert writer.dpi == dpi assert writer.args == () - assert writer.savefig_kwargs == savefig_kwargs - assert writer._count == anim.save_count + # we enrich the savefig kwargs to ensure we composite transparent + # output to an opaque background + for k, v in savefig_kwargs.items(): + assert writer.savefig_kwargs[k] == v + assert writer._count == anim._save_count @pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim']) def test_animation_delete(anim): + if platform.python_implementation() == 'PyPy': + # Something in the test setup fixture lingers around into the test and + # breaks pytest.warns on PyPy. This garbage collection fixes it. + # https://foss.heptapod.net/pypy/pypy/-/issues/3536 + np.testing.break_cycles() anim = animation.FuncAnimation(**anim) with pytest.warns(Warning, match='Animation was deleted'): del anim - gc.collect() + np.testing.break_cycles() def test_movie_writer_dpi_default(): @@ -133,8 +144,6 @@ def isAvailable(cls): WRITER_OUTPUT = [ ('ffmpeg', 'movie.mp4'), ('ffmpeg_file', 'movie.mp4'), - ('avconv', 'movie.mp4'), - ('avconv_file', 'movie.mp4'), ('imagemagick', 'movie.gif'), ('imagemagick_file', 'movie.gif'), ('pillow', 'movie.gif'), @@ -180,8 +189,8 @@ def test_save_animation_smoketest(tmpdir, writer, frame_format, output, anim): with tmpdir.as_cwd(): anim.save(output, fps=30, writer=writer, bitrate=500, dpi=dpi, codec=codec) - with pytest.warns(None): - del anim + + del anim @pytest.mark.parametrize('writer', [ @@ -201,6 +210,11 @@ def test_save_animation_smoketest(tmpdir, writer, frame_format, output, anim): ]) @pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim']) def test_animation_repr_html(writer, html, want, anim): + if platform.python_implementation() == 'PyPy': + # Something in the test setup fixture lingers around into the test and + # breaks pytest.warns on PyPy. This garbage collection fixes it. + # https://foss.heptapod.net/pypy/pypy/-/issues/3536 + np.testing.break_cycles() if (writer == 'imagemagick' and html == 'html5' # ImageMagick delegates to ffmpeg for this format. and not animation.FFMpegWriter.isAvailable()): @@ -214,13 +228,17 @@ def test_animation_repr_html(writer, html, want, anim): if want is None: assert html is None with pytest.warns(UserWarning): - del anim # Animtion was never run, so will warn on cleanup. + del anim # Animation was never run, so will warn on cleanup. + np.testing.break_cycles() else: assert want in html -@pytest.mark.parametrize('anim', [dict(frames=iter(range(5)))], - indirect=['anim']) +@pytest.mark.parametrize( + 'anim', + [{'save_count': 10, 'frames': iter(range(5))}], + indirect=['anim'] +) def test_no_length_frames(anim): anim.save('unused.null', writer=NullMovieWriter()) @@ -278,9 +296,8 @@ def test_failing_ffmpeg(tmpdir, monkeypatch, anim): with tmpdir.as_cwd(): monkeypatch.setenv("PATH", ".:" + os.environ["PATH"]) exe_path = Path(str(tmpdir), "ffmpeg") - exe_path.write_text("#!/bin/sh\n" - "[[ $@ -eq 0 ]]\n") - os.chmod(str(exe_path), 0o755) + exe_path.write_bytes(b"#!/bin/sh\n[[ $@ -eq 0 ]]\n") + os.chmod(exe_path, 0o755) with pytest.raises(subprocess.CalledProcessError): anim.save("test.mpeg") @@ -317,13 +334,16 @@ def frames_generator(): yield frame + MAX_FRAMES = 100 anim = animation.FuncAnimation(fig, animate, init_func=init, frames=frames_generator, - cache_frame_data=cache_frame_data) + cache_frame_data=cache_frame_data, + save_count=MAX_FRAMES) writer = NullMovieWriter() anim.save('unused.null', writer=writer) assert len(frames_generated) == 5 + np.testing.break_cycles() for f in frames_generated: # If cache_frame_data is True, then the weakref should be alive; # if cache_frame_data is False, then the weakref should be dead (None). @@ -358,4 +378,141 @@ def animate(i): return return_value with pytest.raises(RuntimeError): - animation.FuncAnimation(fig, animate, blit=True) + animation.FuncAnimation( + fig, animate, blit=True, cache_frame_data=False + ) + + +def test_exhausted_animation(tmpdir): + fig, ax = plt.subplots() + + def update(frame): + return [] + + anim = animation.FuncAnimation( + fig, update, frames=iter(range(10)), repeat=False, + cache_frame_data=False + ) + + with tmpdir.as_cwd(): + anim.save("test.gif", writer='pillow') + + with pytest.warns(UserWarning, match="exhausted"): + anim._start() + + +def test_no_frame_warning(tmpdir): + fig, ax = plt.subplots() + + def update(frame): + return [] + + anim = animation.FuncAnimation( + fig, update, frames=[], repeat=False, + cache_frame_data=False + ) + + with pytest.warns(UserWarning, match="exhausted"): + anim._start() + + +@check_figures_equal(extensions=["png"]) +def test_animation_frame(tmpdir, fig_test, fig_ref): + # Test the expected image after iterating through a few frames + # we save the animation to get the iteration because we are not + # in an interactive framework. + ax = fig_test.add_subplot() + ax.set_xlim(0, 2 * np.pi) + ax.set_ylim(-1, 1) + x = np.linspace(0, 2 * np.pi, 100) + line, = ax.plot([], []) + + def init(): + line.set_data([], []) + return line, + + def animate(i): + line.set_data(x, np.sin(x + i / 100)) + return line, + + anim = animation.FuncAnimation( + fig_test, animate, init_func=init, frames=5, + blit=True, repeat=False) + with tmpdir.as_cwd(): + anim.save("test.gif") + + # Reference figure without animation + ax = fig_ref.add_subplot() + ax.set_xlim(0, 2 * np.pi) + ax.set_ylim(-1, 1) + + # 5th frame's data + ax.plot(x, np.sin(x + 4 / 100)) + + +@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim']) +def test_save_count_override_warnings_has_length(anim): + + save_count = 5 + frames = list(range(2)) + match_target = ( + f'You passed in an explicit {save_count=} ' + "which is being ignored in favor of " + f"{len(frames)=}." + ) + + with pytest.warns(UserWarning, match=re.escape(match_target)): + anim = animation.FuncAnimation( + **{**anim, 'frames': frames, 'save_count': save_count} + ) + assert anim._save_count == len(frames) + anim._init_draw() + + +@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim']) +def test_save_count_override_warnings_scaler(anim): + save_count = 5 + frames = 7 + match_target = ( + f'You passed in an explicit {save_count=} ' + + "which is being ignored in favor of " + + f"{frames=}." + ) + + with pytest.warns(UserWarning, match=re.escape(match_target)): + anim = animation.FuncAnimation( + **{**anim, 'frames': frames, 'save_count': save_count} + ) + + assert anim._save_count == frames + anim._init_draw() + + +@pytest.mark.parametrize('anim', [dict(klass=dict)], indirect=['anim']) +def test_disable_cache_warning(anim): + cache_frame_data = True + frames = iter(range(5)) + match_target = ( + f"{frames=!r} which we can infer the length of, " + "did not pass an explicit *save_count* " + f"and passed {cache_frame_data=}. To avoid a possibly " + "unbounded cache, frame data caching has been disabled. " + "To suppress this warning either pass " + "`cache_frame_data=False` or `save_count=MAX_FRAMES`." + ) + with pytest.warns(UserWarning, match=re.escape(match_target)): + anim = animation.FuncAnimation( + **{**anim, 'cache_frame_data': cache_frame_data, 'frames': frames} + ) + assert anim._cache_frame_data is False + anim._init_draw() + + +def test_movie_writer_invalid_path(anim): + if sys.platform == "win32": + match_str = re.escape("[WinError 3] The system cannot find the path specified:") + else: + match_str = re.escape("[Errno 2] No such file or directory: '/foo") + with pytest.raises(FileNotFoundError, match=match_str): + anim.save("/foo/bar/aardvark/thiscannotreallyexist.mp4", + writer=animation.FFMpegFileWriter()) diff --git a/lib/matplotlib/tests/test_api.py b/lib/matplotlib/tests/test_api.py index 29ca4caf5ca0..28933ff63fa1 100644 --- a/lib/matplotlib/tests/test_api.py +++ b/lib/matplotlib/tests/test_api.py @@ -34,6 +34,24 @@ def f(cls): a.f +def test_deprecate_privatize_attribute(): + class C: + def __init__(self): self._attr = 1 + def _meth(self, arg): return arg + attr = _api.deprecate_privatize_attribute("0.0") + meth = _api.deprecate_privatize_attribute("0.0") + + c = C() + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert c.attr == 1 + with pytest.warns(_api.MatplotlibDeprecationWarning): + c.attr = 2 + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert c.attr == 2 + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert c.meth(42) == 42 + + def test_delete_parameter(): @_api.delete_parameter("3.0", "foo") def func1(foo=None): @@ -67,3 +85,16 @@ def func(pre, arg, post=None): func(1, 2) with pytest.warns(_api.MatplotlibDeprecationWarning): func(1, 2, 3) + + +def test_deprecation_alternative(): + alternative = "`.f1`, `f2`, `f3(x) <.f3>` or `f4(x)`" + @_api.deprecated("1", alternative=alternative) + def f(): + pass + assert alternative in f.__doc__ + + +def test_empty_check_in_list(): + with pytest.raises(TypeError, match="No argument to check!"): + _api.check_in_list(["a"]) diff --git a/lib/matplotlib/tests/test_arrow_patches.py b/lib/matplotlib/tests/test_arrow_patches.py index 3c95535e0c21..8d573b4adb1b 100644 --- a/lib/matplotlib/tests/test_arrow_patches.py +++ b/lib/matplotlib/tests/test_arrow_patches.py @@ -126,7 +126,8 @@ def test_arrow_styles(): fig.subplots_adjust(left=0, right=1, bottom=0, top=1) for i, stylename in enumerate(sorted(styles)): - patch = mpatches.FancyArrowPatch((0.1, i), (0.45, i), + patch = mpatches.FancyArrowPatch((0.1 + (i % 2)*0.05, i), + (0.45 + (i % 2)*0.05, i), arrowstyle=stylename, mutation_scale=25) ax.add_patch(patch) diff --git a/lib/matplotlib/tests/test_artist.py b/lib/matplotlib/tests/test_artist.py index c7936be4b5fa..9bfb4ebce1bd 100644 --- a/lib/matplotlib/tests/test_artist.py +++ b/lib/matplotlib/tests/test_artist.py @@ -5,6 +5,7 @@ import pytest +import matplotlib.colors as mcolors import matplotlib.pyplot as plt import matplotlib.patches as mpatches import matplotlib.lines as mlines @@ -12,6 +13,8 @@ import matplotlib.transforms as mtransforms import matplotlib.collections as mcollections import matplotlib.artist as martist +import matplotlib.backend_bases as mbackend_bases +import matplotlib as mpl from matplotlib.testing.decorators import check_figures_equal, image_comparison @@ -90,7 +93,7 @@ def test_collection_transform_of_none(): transform=mtransforms.IdentityTransform(), alpha=0.5) ax.add_collection(c) - assert isinstance(c._transOffset, mtransforms.IdentityTransform) + assert isinstance(c.get_offset_transform(), mtransforms.IdentityTransform) @image_comparison(["clip_path_clipping"], remove_text=True) @@ -165,20 +168,18 @@ def test_hatching(): rect1 = mpatches.Rectangle((0, 0), 3, 4, hatch='/') ax.add_patch(rect1) - rect2 = mcollections.RegularPolyCollection(4, sizes=[16000], - offsets=[(1.5, 6.5)], - transOffset=ax.transData, - hatch='/') + rect2 = mcollections.RegularPolyCollection( + 4, sizes=[16000], offsets=[(1.5, 6.5)], offset_transform=ax.transData, + hatch='/') ax.add_collection(rect2) # Ensure edge color is not applied to hatching. rect3 = mpatches.Rectangle((4, 0), 3, 4, hatch='/', edgecolor='C1') ax.add_patch(rect3) - rect4 = mcollections.RegularPolyCollection(4, sizes=[16000], - offsets=[(5.5, 6.5)], - transOffset=ax.transData, - hatch='/', edgecolor='C1') + rect4 = mcollections.RegularPolyCollection( + 4, sizes=[16000], offsets=[(5.5, 6.5)], offset_transform=ax.transData, + hatch='/', edgecolor='C1') ax.add_collection(rect4) ax.set_xlim(0, 7) @@ -340,3 +341,224 @@ def func(artist): art.remove_callback(oid) art.pchanged() # must not call the callback anymore assert func.counter == 2 + + +def test_set_signature(): + """Test autogenerated ``set()`` for Artist subclasses.""" + class MyArtist1(martist.Artist): + def set_myparam1(self, val): + pass + + assert hasattr(MyArtist1.set, '_autogenerated_signature') + assert 'myparam1' in MyArtist1.set.__doc__ + + class MyArtist2(MyArtist1): + def set_myparam2(self, val): + pass + + assert hasattr(MyArtist2.set, '_autogenerated_signature') + assert 'myparam1' in MyArtist2.set.__doc__ + assert 'myparam2' in MyArtist2.set.__doc__ + + +def test_set_is_overwritten(): + """set() defined in Artist subclasses should not be overwritten.""" + class MyArtist3(martist.Artist): + + def set(self, **kwargs): + """Not overwritten.""" + + assert not hasattr(MyArtist3.set, '_autogenerated_signature') + assert MyArtist3.set.__doc__ == "Not overwritten." + + class MyArtist4(MyArtist3): + pass + + assert MyArtist4.set is MyArtist3.set + + +def test_format_cursor_data_BoundaryNorm(): + """Test if cursor data is correct when using BoundaryNorm.""" + X = np.empty((3, 3)) + X[0, 0] = 0.9 + X[0, 1] = 0.99 + X[0, 2] = 0.999 + X[1, 0] = -1 + X[1, 1] = 0 + X[1, 2] = 1 + X[2, 0] = 0.09 + X[2, 1] = 0.009 + X[2, 2] = 0.0009 + + # map range -1..1 to 0..256 in 0.1 steps + fig, ax = plt.subplots() + fig.suptitle("-1..1 to 0..256 in 0.1") + norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 20), 256) + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + + labels_list = [ + "[0.9]", + "[1.]", + "[1.]", + "[-1.0]", + "[0.0]", + "[1.0]", + "[0.09]", + "[0.009]", + "[0.0009]", + ] + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.1)) + assert img.format_cursor_data(v) == label + + plt.close() + + # map range -1..1 to 0..256 in 0.01 steps + fig, ax = plt.subplots() + fig.suptitle("-1..1 to 0..256 in 0.01") + cmap = mpl.colormaps['RdBu_r'].resampled(200) + norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 200), 200) + img = ax.imshow(X, cmap=cmap, norm=norm) + + labels_list = [ + "[0.90]", + "[0.99]", + "[1.0]", + "[-1.00]", + "[0.00]", + "[1.00]", + "[0.09]", + "[0.009]", + "[0.0009]", + ] + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.01)) + assert img.format_cursor_data(v) == label + + plt.close() + + # map range -1..1 to 0..256 in 0.01 steps + fig, ax = plt.subplots() + fig.suptitle("-1..1 to 0..256 in 0.001") + cmap = mpl.colormaps['RdBu_r'].resampled(2000) + norm = mcolors.BoundaryNorm(np.linspace(-1, 1, 2000), 2000) + img = ax.imshow(X, cmap=cmap, norm=norm) + + labels_list = [ + "[0.900]", + "[0.990]", + "[0.999]", + "[-1.000]", + "[0.000]", + "[1.000]", + "[0.090]", + "[0.009]", + "[0.0009]", + ] + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.001)) + assert img.format_cursor_data(v) == label + + plt.close() + + # different testing data set with + # out of bounds values for 0..1 range + X = np.empty((7, 1)) + X[0] = -1.0 + X[1] = 0.0 + X[2] = 0.1 + X[3] = 0.5 + X[4] = 0.9 + X[5] = 1.0 + X[6] = 2.0 + + labels_list = [ + "[-1.0]", + "[0.0]", + "[0.1]", + "[0.5]", + "[0.9]", + "[1.0]", + "[2.0]", + ] + + fig, ax = plt.subplots() + fig.suptitle("noclip, neither") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='neither') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() + + fig, ax = plt.subplots() + fig.suptitle("noclip, min") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='min') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() + + fig, ax = plt.subplots() + fig.suptitle("noclip, max") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='max') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() + + fig, ax = plt.subplots() + fig.suptitle("noclip, both") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=False, extend='both') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() + + fig, ax = plt.subplots() + fig.suptitle("clip, neither") + norm = mcolors.BoundaryNorm( + np.linspace(0, 1, 4, endpoint=True), 256, clip=True, extend='neither') + img = ax.imshow(X, cmap='RdBu_r', norm=norm) + for v, label in zip(X.flat, labels_list): + # label = "[{:-#.{}g}]".format(v, cbook._g_sig_digits(v, 0.33)) + assert img.format_cursor_data(v) == label + + plt.close() + + +def test_auto_no_rasterize(): + class Gen1(martist.Artist): + ... + + assert 'draw' in Gen1.__dict__ + assert Gen1.__dict__['draw'] is Gen1.draw + + class Gen2(Gen1): + ... + + assert 'draw' not in Gen2.__dict__ + assert Gen2.draw is Gen1.draw + + +def test_draw_wraper_forward_input(): + class TestKlass(martist.Artist): + def draw(self, renderer, extra): + return extra + + art = TestKlass() + renderer = mbackend_bases.RendererBase() + + assert 'aardvark' == art.draw(renderer, 'aardvark') + assert 'aardvark' == art.draw(renderer, extra='aardvark') diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 7950e5b8306e..004f6320de1f 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1,14 +1,13 @@ +import contextlib from collections import namedtuple import datetime from decimal import Decimal +from functools import partial +import inspect import io from itertools import product import platform from types import SimpleNamespace -try: - from contextlib import nullcontext -except ImportError: - from contextlib import ExitStack as nullcontext # Py3.6. import dateutil.tz @@ -19,21 +18,27 @@ import matplotlib import matplotlib as mpl -from matplotlib.testing.decorators import ( - image_comparison, check_figures_equal, remove_ticks_and_titles) +from matplotlib import rc_context +from matplotlib._api import MatplotlibDeprecationWarning import matplotlib.colors as mcolors import matplotlib.dates as mdates from matplotlib.figure import Figure +from matplotlib.axes import Axes import matplotlib.font_manager as mfont_manager import matplotlib.markers as mmarkers import matplotlib.patches as mpatches +import matplotlib.path as mpath +from matplotlib.projections.geo import HammerAxes +from matplotlib.projections.polar import PolarAxes import matplotlib.pyplot as plt +import matplotlib.text as mtext import matplotlib.ticker as mticker import matplotlib.transforms as mtransforms +import mpl_toolkits.axisartist as AA from numpy.testing import ( assert_allclose, assert_array_equal, assert_array_almost_equal) -from matplotlib import rc_context -from matplotlib.cbook import MatplotlibDeprecationWarning +from matplotlib.testing.decorators import ( + image_comparison, check_figures_equal, remove_ticks_and_titles) # Note: Some test cases are run twice: once normally and once with labeled data # These two must be defined in the same test function or need to have @@ -41,6 +46,12 @@ # the tests with multiple threads. +@check_figures_equal(extensions=["png"]) +def test_invisible_axes(fig_test, fig_ref): + ax = fig_test.subplots() + ax.set_visible(False) + + def test_get_labels(): fig, ax = plt.subplots() ax.set_xlabel('x label') @@ -49,6 +60,17 @@ def test_get_labels(): assert ax.get_ylabel() == 'y label' +def test_repr(): + fig, ax = plt.subplots() + ax.set_label('label') + ax.set_title('title') + ax.set_xlabel('x') + ax.set_ylabel('y') + assert repr(ax) == ( + "") + + @check_figures_equal() def test_label_loc_vertical(fig_test, fig_ref): ax = fig_test.subplots() @@ -108,6 +130,28 @@ def test_label_loc_rc(fig_test, fig_ref): cbar.set_label("Z Label", x=1, ha='right') +def test_label_shift(): + fig, ax = plt.subplots() + + # Test label re-centering on x-axis + ax.set_xlabel("Test label", loc="left") + ax.set_xlabel("Test label", loc="center") + assert ax.xaxis.get_label().get_horizontalalignment() == "center" + ax.set_xlabel("Test label", loc="right") + assert ax.xaxis.get_label().get_horizontalalignment() == "right" + ax.set_xlabel("Test label", loc="center") + assert ax.xaxis.get_label().get_horizontalalignment() == "center" + + # Test label re-centering on y-axis + ax.set_ylabel("Test label", loc="top") + ax.set_ylabel("Test label", loc="center") + assert ax.yaxis.get_label().get_horizontalalignment() == "center" + ax.set_ylabel("Test label", loc="bottom") + assert ax.yaxis.get_label().get_horizontalalignment() == "left" + ax.set_ylabel("Test label", loc="center") + assert ax.yaxis.get_label().get_horizontalalignment() == "center" + + @check_figures_equal(extensions=["png"]) def test_acorr(fig_test, fig_ref): np.random.seed(19680801) @@ -420,8 +464,10 @@ def test_inverted_cla(): assert not ax.xaxis_inverted() assert ax.yaxis_inverted() - # 5. two shared axes. Inverting the master axis should invert the shared - # axes; clearing the master axis should bring axes in shared + for ax in fig.axes: + ax.remove() + # 5. two shared axes. Inverting the leader axis should invert the shared + # axes; clearing the leader axis should bring axes in shared # axes back to normal. ax0 = plt.subplot(211) ax1 = plt.subplot(212, sharey=ax0) @@ -431,7 +477,7 @@ def test_inverted_cla(): ax0.cla() assert not ax1.yaxis_inverted() ax1.cla() - # 6. clearing the nonmaster should not touch limits + # 6. clearing the follower should not touch limits ax0.imshow(img) ax1.plot(x, np.cos(x)) ax1.cla() @@ -441,6 +487,65 @@ def test_inverted_cla(): plt.close(fig) +def test_subclass_clear_cla(): + # Ensure that subclasses of Axes call cla/clear correctly. + # Note, we cannot use mocking here as we want to be sure that the + # superclass fallback does not recurse. + + with pytest.warns(PendingDeprecationWarning, + match='Overriding `Axes.cla`'): + class ClaAxes(Axes): + def cla(self): + nonlocal called + called = True + + with pytest.warns(PendingDeprecationWarning, + match='Overriding `Axes.cla`'): + class ClaSuperAxes(Axes): + def cla(self): + nonlocal called + called = True + super().cla() + + class SubClaAxes(ClaAxes): + pass + + class ClearAxes(Axes): + def clear(self): + nonlocal called + called = True + + class ClearSuperAxes(Axes): + def clear(self): + nonlocal called + called = True + super().clear() + + class SubClearAxes(ClearAxes): + pass + + fig = Figure() + for axes_class in [ClaAxes, ClaSuperAxes, SubClaAxes, + ClearAxes, ClearSuperAxes, SubClearAxes]: + called = False + ax = axes_class(fig, [0, 0, 1, 1]) + # Axes.__init__ has already called clear (which aliases to cla or is in + # the subclass). + assert called + + called = False + ax.cla() + assert called + + +def test_cla_not_redefined_internally(): + for klass in Axes.__subclasses__(): + # Check that cla does not get redefined in our Axes subclasses, except + # for in the above test function. + if 'test_subclass_clear_cla' not in klass.__qualname__: + assert 'cla' not in klass.__dict__ + + @check_figures_equal(extensions=["png"]) def test_minorticks_on_rcParams_both(fig_test, fig_ref): with matplotlib.rc_context({"xtick.minor.visible": True, @@ -461,7 +566,7 @@ def test_autoscale_tiny_range(): ax.plot([0, 1], [1, 1 + y1]) -@pytest.mark.style('default') +@mpl.style.context('default') def test_autoscale_tight(): fig, ax = plt.subplots(1, 1) ax.plot([1, 2, 3, 4]) @@ -470,8 +575,22 @@ def test_autoscale_tight(): assert_allclose(ax.get_xlim(), (-0.15, 3.15)) assert_allclose(ax.get_ylim(), (1.0, 4.0)) + # Check that autoscale is on + assert ax.get_autoscalex_on() + assert ax.get_autoscaley_on() + assert ax.get_autoscale_on() + # Set enable to None + ax.autoscale(enable=None) + # Same limits + assert_allclose(ax.get_xlim(), (-0.15, 3.15)) + assert_allclose(ax.get_ylim(), (1.0, 4.0)) + # autoscale still on + assert ax.get_autoscalex_on() + assert ax.get_autoscaley_on() + assert ax.get_autoscale_on() + -@pytest.mark.style('default') +@mpl.style.context('default') def test_autoscale_log_shared(): # related to github #7587 # array starts at zero to trigger _minpos handling @@ -489,7 +608,7 @@ def test_autoscale_log_shared(): assert_allclose(ax2.get_ylim(), (x[0], x[-1])) -@pytest.mark.style('default') +@mpl.style.context('default') def test_use_sticky_edges(): fig, ax = plt.subplots() ax.imshow([[0, 1], [2, 3]], origin='lower') @@ -510,8 +629,8 @@ def test_use_sticky_edges(): @check_figures_equal(extensions=["png"]) def test_sticky_shared_axes(fig_test, fig_ref): - # Check that sticky edges work whether they are set in an axes that is a - # "master" in a share, or an axes that is a "follower". + # Check that sticky edges work whether they are set in an Axes that is a + # "leader" in a share, or an Axes that is a "follower". Z = np.arange(15).reshape(3, 5) ax0 = fig_test.add_subplot(211) @@ -539,14 +658,6 @@ def test_basic_annotate(): xytext=(3, 3), textcoords='offset points') -def test_annotate_parameter_warn(): - fig, ax = plt.subplots() - with pytest.warns(MatplotlibDeprecationWarning, - match=r"The \'s\' parameter of annotate\(\) " - "has been renamed \'text\'"): - ax.annotate(s='now named text', xy=(0, 1)) - - @image_comparison(['arrow_simple.png'], remove_text=True) def test_arrow_simple(): # Simple image test for ax.arrow @@ -596,6 +707,16 @@ def test_annotate_default_arrow(): assert ann.arrow_patch is not None +def test_annotate_signature(): + """Check that the signature of Axes.annotate() matches Annotation.""" + fig, ax = plt.subplots() + annotate_params = inspect.signature(ax.annotate).parameters + annotation_params = inspect.signature(mtext.Annotation).parameters + assert list(annotate_params.keys()) == list(annotation_params.keys()) + for p1, p2 in zip(annotate_params.values(), annotation_params.values()): + assert p1 == p2 + + @image_comparison(['fill_units.png'], savefig_kwarg={'dpi': 60}) def test_fill_units(): import matplotlib.testing.jpl_units as units @@ -641,6 +762,20 @@ def test_plot_format_kwarg_redundant(): plt.errorbar([0], [0], fmt='none', color='blue') +@check_figures_equal(extensions=["png"]) +def test_errorbar_dashes(fig_test, fig_ref): + x = [1, 2, 3, 4] + y = np.sin(x) + + ax_ref = fig_ref.gca() + ax_test = fig_test.gca() + + line, *_ = ax_ref.errorbar(x, y, xerr=np.abs(y), yerr=np.abs(y)) + line.set_dashes([2, 2]) + + ax_test.errorbar(x, y, xerr=np.abs(y), yerr=np.abs(y), dashes=[2, 2]) + + @image_comparison(['single_point', 'single_point']) def test_single_point(): # Issue #1796: don't let lines.marker affect the grid @@ -762,11 +897,15 @@ def test_hexbin_extent(): ax.hexbin("x", "y", extent=[.1, .3, .6, .7], data=data) -@image_comparison(['hexbin_empty.png'], remove_text=True) +@image_comparison(['hexbin_empty.png', 'hexbin_empty.png'], remove_text=True) def test_hexbin_empty(): # From #3886: creating hexbin from empty dataset raises ValueError - ax = plt.gca() + fig, ax = plt.subplots() ax.hexbin([], []) + fig, ax = plt.subplots() + # From #23922: creating hexbin with log scaling from empty + # dataset raises ValueError + ax.hexbin([], [], bins='log') def test_hexbin_pickable(): @@ -793,10 +932,31 @@ def test_hexbin_log(): y = np.power(2, y * 0.5) fig, ax = plt.subplots() - h = ax.hexbin(x, y, yscale='log', bins='log') + h = ax.hexbin(x, y, yscale='log', bins='log', + marginals=True, reduce_C_function=np.sum) plt.colorbar(h) +@image_comparison(["hexbin_linear.png"], style="mpl20", remove_text=True) +def test_hexbin_linear(): + # Issue #21165 + np.random.seed(19680801) + n = 100000 + x = np.random.standard_normal(n) + y = 2.0 + 3.0 * x + 4.0 * np.random.standard_normal(n) + + fig, ax = plt.subplots() + ax.hexbin(x, y, gridsize=(10, 5), marginals=True, + reduce_C_function=np.sum) + + +def test_hexbin_log_clim(): + x, y = np.arange(200).reshape((2, 100)) + fig, ax = plt.subplots() + h = ax.hexbin(x, y, bins='log', vmin=2, vmax=100) + assert h.get_clim() == (2, 100) + + def test_inverted_limits(): # Test gh:1553 # Calling invert_xaxis prior to plotting should not disable autoscaling @@ -834,7 +994,7 @@ def test_nonfinite_limits(): ax.plot(x, y) -@pytest.mark.style('default') +@mpl.style.context('default') @pytest.mark.parametrize('plot_fun', ['scatter', 'plot', 'fill_between']) @check_figures_equal(extensions=["png"]) @@ -872,7 +1032,9 @@ def test_imshow(): ax.imshow("r", data=data) -@image_comparison(['imshow_clip'], style='mpl20') +@image_comparison( + ['imshow_clip'], style='mpl20', + tol=1.24 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) def test_imshow_clip(): # As originally reported by Gellule Xg # use former defaults to match existing baseline image @@ -899,16 +1061,13 @@ def test_imshow_clip(): ax.imshow(r, clip_path=clip_path) -@check_figures_equal(extensions=["png"]) -def test_imshow_norm_vminvmax(fig_test, fig_ref): - """Parameters vmin, vmax should be ignored if norm is given.""" +def test_imshow_norm_vminvmax(): + """Parameters vmin, vmax should error if norm is given.""" a = [[1, 2], [3, 4]] - ax = fig_ref.subplots() - ax.imshow(a, vmin=0, vmax=5) - ax = fig_test.subplots() - with pytest.warns(MatplotlibDeprecationWarning, - match="Passing parameters norm and vmin/vmax " - "simultaneously is deprecated."): + ax = plt.axes() + with pytest.raises(ValueError, + match="Passing a Normalize instance simultaneously " + "with vmin/vmax is not supported."): ax.imshow(a, norm=mcolors.Normalize(-10, 10), vmin=0, vmax=5) @@ -1002,6 +1161,23 @@ def test_fill_between_interpolate_decreasing(): ax.set_ylim(800, 600) +@image_comparison(['fill_between_interpolate_nan'], remove_text=True) +def test_fill_between_interpolate_nan(): + # Tests fix for issue #18986. + x = np.arange(10) + y1 = np.asarray([8, 18, np.nan, 18, 8, 18, 24, 18, 8, 18]) + y2 = np.asarray([18, 11, 8, 11, 18, 26, 32, 30, np.nan, np.nan]) + + fig, ax = plt.subplots() + + ax.plot(x, y1, c='k') + ax.plot(x, y2, c='b') + ax.fill_between(x, y1, y2, where=y2 >= y1, facecolor="green", + interpolate=True, alpha=0.5) + ax.fill_between(x, y1, y2, where=y1 >= y2, facecolor="red", + interpolate=True, alpha=0.5) + + # test_symlog and test_symlog2 used to have baseline images in all three # formats, but the png and svg baselines got invalidated by the removal of # minor tick overstriking. @@ -1068,6 +1244,27 @@ def test_pcolormesh(): ax3.pcolormesh(Qx, Qz, Z, shading="gouraud") +@image_comparison(['pcolormesh_small'], extensions=["eps"]) +def test_pcolormesh_small(): + n = 3 + x = np.linspace(-1.5, 1.5, n) + y = np.linspace(-1.5, 1.5, n*2) + X, Y = np.meshgrid(x, y) + Qx = np.cos(Y) - np.cos(X) + Qz = np.sin(Y) + np.sin(X) + Qx = (Qx + 1.1) + Z = np.hypot(X, Y) / 5 + Z = (Z - Z.min()) / Z.ptp() + Zm = ma.masked_where(np.abs(Qz) < 0.5 * np.max(Qz), Z) + + fig, (ax1, ax2, ax3) = plt.subplots(1, 3) + ax1.pcolormesh(Qx, Qz, Zm[:-1, :-1], lw=0.5, edgecolors='k') + ax2.pcolormesh(Qx, Qz, Zm[:-1, :-1], lw=2, edgecolors=['b', 'w']) + ax3.pcolormesh(Qx, Qz, Zm, shading="gouraud") + for ax in fig.axes: + ax.set_axis_off() + + @image_comparison(['pcolormesh_alpha'], extensions=["png", "pdf"], remove_text=True) def test_pcolormesh_alpha(): @@ -1083,7 +1280,7 @@ def test_pcolormesh_alpha(): Qy = Y + np.sin(X) Z = np.hypot(X, Y) / 5 Z = (Z - Z.min()) / Z.ptp() - vir = plt.get_cmap("viridis", 16) + vir = mpl.colormaps["viridis"].resampled(16) # make another colormap with varying alpha colors = vir(np.arange(16)) colors[:, 3] = 0.5 + 0.5*np.sin(np.arange(16)) @@ -1103,8 +1300,18 @@ def test_pcolormesh_alpha(): ax4.pcolormesh(Qx, Qy, Z, cmap=cmap, shading='gouraud', zorder=1) -@image_comparison(['pcolormesh_datetime_axis.png'], - remove_text=False, style='mpl20') +@pytest.mark.parametrize("dims,alpha", [(3, 1), (4, 0.5)]) +@check_figures_equal(extensions=["png"]) +def test_pcolormesh_rgba(fig_test, fig_ref, dims, alpha): + ax = fig_test.subplots() + c = np.ones((5, 6, dims), dtype=float) / 2 + ax.pcolormesh(c) + + ax = fig_ref.subplots() + ax.pcolormesh(c[..., 0], cmap="gray", vmin=0, vmax=1, alpha=alpha) + + +@image_comparison(['pcolormesh_datetime_axis.png'], style='mpl20') def test_pcolormesh_datetime_axis(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -1132,8 +1339,7 @@ def test_pcolormesh_datetime_axis(): label.set_rotation(30) -@image_comparison(['pcolor_datetime_axis.png'], - remove_text=False, style='mpl20') +@image_comparison(['pcolor_datetime_axis.png'], style='mpl20') def test_pcolor_datetime_axis(): fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) @@ -1223,38 +1429,41 @@ def test_pcolornearestunits(fig_test, fig_ref): ax.pcolormesh(x2, y2, Z, shading='nearest') -@check_figures_equal(extensions=["png"]) -def test_pcolordropdata(fig_test, fig_ref): - ax = fig_test.subplots() - x = np.arange(0, 10) - y = np.arange(0, 4) +def test_pcolorflaterror(): + fig, ax = plt.subplots() + x = np.arange(0, 9) + y = np.arange(0, 3) np.random.seed(19680801) Z = np.random.randn(3, 9) - # fake dropping the data - ax.pcolormesh(x[:-1], y[:-1], Z[:-1, :-1], shading='flat') + with pytest.raises(TypeError, match='Dimensions of C'): + ax.pcolormesh(x, y, Z, shading='flat') - ax = fig_ref.subplots() - # test dropping the data... - x2 = x[:-1] - y2 = y[:-1] - with pytest.warns(MatplotlibDeprecationWarning): - ax.pcolormesh(x2, y2, Z, shading='flat') +def test_samesizepcolorflaterror(): + fig, ax = plt.subplots() + x, y = np.meshgrid(np.arange(5), np.arange(3)) + Z = x + y + with pytest.raises(TypeError, match=r".*one smaller than X"): + ax.pcolormesh(x, y, Z, shading='flat') + +@pytest.mark.parametrize('snap', [False, True]) @check_figures_equal(extensions=["png"]) -def test_pcolorauto(fig_test, fig_ref): +def test_pcolorauto(fig_test, fig_ref, snap): ax = fig_test.subplots() x = np.arange(0, 10) y = np.arange(0, 4) np.random.seed(19680801) Z = np.random.randn(3, 9) - ax.pcolormesh(x, y, Z, shading='auto') + # this is the same as flat; note that auto is default + ax.pcolormesh(x, y, Z, snap=snap) ax = fig_ref.subplots() # specify the centers x2 = x[:-1] + np.diff(x) / 2 y2 = y[:-1] + np.diff(y) / 2 - ax.pcolormesh(x2, y2, Z, shading='auto') + # this is same as nearest: + ax.pcolormesh(x2, y2, Z, snap=snap) @image_comparison(['canonical']) @@ -1312,7 +1521,7 @@ def test_arc_ellipse(): [np.cos(rtheta), -np.sin(rtheta)], [np.sin(rtheta), np.cos(rtheta)]]) - x, y = np.dot(R, np.array([x, y])) + x, y = np.dot(R, [x, y]) x += xcenter y += ycenter @@ -1356,8 +1565,12 @@ def test_markevery(): ax.legend() -@image_comparison(['markevery_line'], remove_text=True) +@image_comparison(['markevery_line'], remove_text=True, tol=0.005) def test_markevery_line(): + # TODO: a slight change in rendering between Inkscape versions may explain + # why one had to introduce a small non-zero tolerance for the SVG test + # to pass. One may try to remove this hack once Travis' Inkscape version + # is modern enough. FWIW, no failure with 0.92.3 on my computer (#11358). x = np.linspace(0, 10, 100) y = np.sin(x) * np.sqrt(x/10 + 0.5) @@ -1473,6 +1686,32 @@ def test_markevery_polar(): plt.plot(theta, r, 'o', ls='-', ms=4, markevery=case) +@image_comparison(['markevery_linear_scales_nans'], remove_text=True) +def test_markevery_linear_scales_nans(): + cases = [None, + 8, + (30, 8), + [16, 24, 30], [0, -1], + slice(100, 200, 3), + 0.1, 0.3, 1.5, + (0.0, 0.1), (0.45, 0.1)] + + cols = 3 + gs = matplotlib.gridspec.GridSpec(len(cases) // cols + 1, cols) + + delta = 0.11 + x = np.linspace(0, 10 - 2 * delta, 200) + delta + y = np.sin(x) + 1.0 + delta + y[:10] = y[-20:] = y[50:70] = np.nan + + for i, case in enumerate(cases): + row = (i // cols) + col = i % cols + plt.subplot(gs[row, col]) + plt.title('markevery=%s' % str(case)) + plt.plot(x, y, 'o', ls='-', ms=4, markevery=case) + + @image_comparison(['marker_edges'], remove_text=True) def test_marker_edges(): x = np.linspace(0, 1, 10) @@ -1637,6 +1876,23 @@ def test_boxplot_dates_pandas(pd): plt.boxplot(data, positions=years) +def test_boxplot_capwidths(): + data = np.random.rand(5, 3) + fig, axs = plt.subplots(9) + + axs[0].boxplot(data, capwidths=[0.3, 0.2, 0.1], widths=[0.1, 0.2, 0.3]) + axs[1].boxplot(data, capwidths=[0.3, 0.2, 0.1], widths=0.2) + axs[2].boxplot(data, capwidths=[0.3, 0.2, 0.1]) + + axs[3].boxplot(data, capwidths=0.5, widths=[0.1, 0.2, 0.3]) + axs[4].boxplot(data, capwidths=0.5, widths=0.2) + axs[5].boxplot(data, capwidths=0.5) + + axs[6].boxplot(data, widths=[0.1, 0.2, 0.3]) + axs[7].boxplot(data, widths=0.2) + axs[8].boxplot(data) + + def test_pcolor_regression(pd): from pandas.plotting import ( register_matplotlib_converters, @@ -1694,8 +1950,8 @@ def test_bar_pandas_indexed(pd): ax.bar(df.x, 1., width=df.width) +@mpl.style.context('default') @check_figures_equal() -@pytest.mark.style('default') def test_bar_hatches(fig_test, fig_ref): ax_test = fig_test.subplots() ax_ref = fig_ref.subplots() @@ -1709,13 +1965,46 @@ def test_bar_hatches(fig_test, fig_ref): ax_test.bar(x, y, hatch=hatches) +@pytest.mark.parametrize( + ("x", "width", "label", "expected_labels", "container_label"), + [ + ("x", 1, "x", ["_nolegend_"], "x"), + (["a", "b", "c"], [10, 20, 15], ["A", "B", "C"], + ["A", "B", "C"], "_nolegend_"), + (["a", "b", "c"], [10, 20, 15], ["R", "Y", "_nolegend_"], + ["R", "Y", "_nolegend_"], "_nolegend_"), + (["a", "b", "c"], [10, 20, 15], "bars", + ["_nolegend_", "_nolegend_", "_nolegend_"], "bars"), + ] +) +def test_bar_labels(x, width, label, expected_labels, container_label): + _, ax = plt.subplots() + bar_container = ax.bar(x, width, label=label) + bar_labels = [bar.get_label() for bar in bar_container] + assert expected_labels == bar_labels + assert bar_container.get_label() == container_label + + +def test_bar_labels_length(): + _, ax = plt.subplots() + with pytest.raises(ValueError): + ax.bar(["x", "y"], [1, 2], label=["X", "Y", "Z"]) + _, ax = plt.subplots() + with pytest.raises(ValueError): + ax.bar(["x", "y"], [1, 2], label=["X"]) + + def test_pandas_minimal_plot(pd): - # smoke test that series and index objcets do not warn - x = pd.Series([1, 2], dtype="float64") - plt.plot(x, x) - plt.plot(x.index, x) - plt.plot(x) - plt.plot(x.index) + # smoke test that series and index objects do not warn + for x in [pd.Series([1, 2], dtype="float64"), + pd.Series([1, 2], dtype="Float64")]: + plt.plot(x, x) + plt.plot(x.index, x) + plt.plot(x) + plt.plot(x.index) + df = pd.DataFrame({'col': [1, 2, 3]}) + plt.plot(df) + plt.plot(df, df) @image_comparison(['hist_log'], remove_text=True) @@ -1759,6 +2048,21 @@ def test_hist_bar_empty(): ax.hist([], histtype='bar') +def test_hist_float16(): + np.random.seed(19680801) + values = np.clip( + np.random.normal(0.5, 0.3, size=1000), 0, 1).astype(np.float16) + h = plt.hist(values, bins=3, alpha=0.5) + bc = h[2] + # Check that there are no overlapping rectangles + for r in range(1, len(bc)): + rleft = bc[r-1].get_corners() + rright = bc[r].get_corners() + # right hand position of left rectangle <= + # left hand position of right rectangle + assert rleft[1][0] <= rright[0][0] + + @image_comparison(['hist_step_empty.png'], remove_text=True) def test_hist_step_empty(): # From #3886: creating hist from empty dataset raises ValueError @@ -1815,7 +2119,7 @@ def test_hist_datetime_datasets(): @pytest.mark.parametrize("bins_preprocess", [mpl.dates.date2num, lambda bins: bins, - lambda bins: np.asarray(bins).astype('datetime64')], + lambda bins: np.asarray(bins, 'datetime64')], ids=['date2num', 'datetime.datetime', 'np.datetime64']) def test_hist_datetime_datasets_bins(bins_preprocess): @@ -2015,7 +2319,7 @@ def test_stairs_options(): ax.stairs(y[::-1]*3+14, x, baseline=26, color='purple', ls='--', lw=2, label="F") ax.stairs(yn[::-1]*3+15, x+1, baseline=np.linspace(27, 25, len(y)), - color='blue', ls='--', lw=2, label="G", fill=True) + color='blue', ls='--', label="G", fill=True) ax.stairs(y[:-1][::-1]*2+11, x[:-1]+0.5, color='black', ls='--', lw=2, baseline=12, hatch='//', label="H") ax.legend(loc=0) @@ -2030,6 +2334,18 @@ def test_stairs_datetime(): plt.xticks(rotation=30) +@check_figures_equal(extensions=['png']) +def test_stairs_edge_handling(fig_test, fig_ref): + # Test + test_ax = fig_test.add_subplot() + test_ax.stairs([1, 2, 3], color='red', fill=True) + + # Ref + ref_ax = fig_ref.add_subplot() + st = ref_ax.stairs([1, 2, 3], fill=True) + st.set_color('red') + + def contour_dat(): x = np.linspace(-3, 5, 150) y = np.linspace(-3, 5, 120) @@ -2042,20 +2358,19 @@ def test_contour_hatching(): x, y, z = contour_dat() fig, ax = plt.subplots() ax.contourf(x, y, z, 7, hatches=['/', '\\', '//', '-'], - cmap=plt.get_cmap('gray'), + cmap=mpl.colormaps['gray'], extend='both', alpha=0.5) -@image_comparison(['contour_colorbar'], style='mpl20') +@image_comparison( + ['contour_colorbar'], style='mpl20', + tol=0.02 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) def test_contour_colorbar(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - x, y, z = contour_dat() fig, ax = plt.subplots() cs = ax.contourf(x, y, z, levels=np.arange(-1.8, 1.801, 0.2), - cmap=plt.get_cmap('RdBu'), + cmap=mpl.colormaps['RdBu'], vmin=-0.6, vmax=0.6, extend='both') @@ -2178,6 +2493,25 @@ def test_scatter_color(self): with pytest.raises(ValueError): plt.scatter([1, 2, 3], [1, 2, 3], color=[1, 2, 3]) + @pytest.mark.parametrize('kwargs', + [ + {'cmap': 'gray'}, + {'norm': mcolors.Normalize()}, + {'vmin': 0}, + {'vmax': 0} + ]) + def test_scatter_color_warning(self, kwargs): + warn_match = "No data for colormapping provided " + # Warn for cases where 'cmap', 'norm', 'vmin', 'vmax' + # kwargs are being overridden + with pytest.warns(Warning, match=warn_match): + plt.scatter([], [], **kwargs) + with pytest.warns(Warning, match=warn_match): + plt.scatter([1, 2], [3, 4], c=[], **kwargs) + # Do not warn for cases where 'c' matches 'x' and 'y' + plt.scatter([], [], c=[], **kwargs) + plt.scatter([1, 2], [3, 4], c=[4, 5], **kwargs) + def test_scatter_unfilled(self): coll = plt.scatter([0, 1, 2], [1, 3, 2], c=['0.1', '0.3', '0.5'], marker=mmarkers.MarkerStyle('o', fillstyle='none'), @@ -2188,7 +2522,7 @@ def test_scatter_unfilled(self): [0.5, 0.5, 0.5, 1]]) assert_array_equal(coll.get_linewidths(), [1.1, 1.2, 1.3]) - @pytest.mark.style('default') + @mpl.style.context('default') def test_scatter_unfillable(self): coll = plt.scatter([0, 1, 2], [1, 3, 2], c=['0.1', '0.3', '0.5'], marker='x', @@ -2209,7 +2543,7 @@ def test_scatter_size_arg_size(self): plt.scatter(x, x, 'foo') def test_scatter_edgecolor_RGB(self): - # Github issue 19066 + # GitHub issue 19066 coll = plt.scatter([1, 2, 3], [1, np.nan, np.nan], edgecolor=(1, 0, 0)) assert mcolors.same_color(coll.get_edgecolor(), (1, 0, 0)) @@ -2220,7 +2554,7 @@ def test_scatter_edgecolor_RGB(self): @check_figures_equal(extensions=["png"]) def test_scatter_invalid_color(self, fig_test, fig_ref): ax = fig_test.subplots() - cmap = plt.get_cmap("viridis", 16) + cmap = mpl.colormaps["viridis"].resampled(16) cmap.set_bad("k", 1) # Set a nonuniform size to prevent the last call to `scatter` (plotting # the invalid points separately in fig_ref) from using the marker @@ -2229,15 +2563,15 @@ def test_scatter_invalid_color(self, fig_test, fig_ref): c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4], cmap=cmap, plotnonfinite=True) ax = fig_ref.subplots() - cmap = plt.get_cmap("viridis", 16) + cmap = mpl.colormaps["viridis"].resampled(16) ax.scatter([0, 2], [0, 2], c=[1, 2], s=[1, 3], cmap=cmap) ax.scatter([1, 3], [1, 3], s=[2, 4], color="k") @check_figures_equal(extensions=["png"]) def test_scatter_no_invalid_color(self, fig_test, fig_ref): - # With plotninfinite=False we plot only 2 points. + # With plotnonfinite=False we plot only 2 points. ax = fig_test.subplots() - cmap = plt.get_cmap("viridis", 16) + cmap = mpl.colormaps["viridis"].resampled(16) cmap.set_bad("k", 1) ax.scatter(range(4), range(4), c=[1, np.nan, 2, np.nan], s=[1, 2, 3, 4], @@ -2245,16 +2579,13 @@ def test_scatter_no_invalid_color(self, fig_test, fig_ref): ax = fig_ref.subplots() ax.scatter([0, 2], [0, 2], c=[1, 2], s=[1, 3], cmap=cmap) - @check_figures_equal(extensions=["png"]) - def test_scatter_norm_vminvmax(self, fig_test, fig_ref): - """Parameters vmin, vmax should be ignored if norm is given.""" + def test_scatter_norm_vminvmax(self): + """Parameters vmin, vmax should error if norm is given.""" x = [1, 2, 3] - ax = fig_ref.subplots() - ax.scatter(x, x, c=x, vmin=0, vmax=5) - ax = fig_test.subplots() - with pytest.warns(MatplotlibDeprecationWarning, - match="Passing parameters norm and vmin/vmax " - "simultaneously is deprecated."): + ax = plt.axes() + with pytest.raises(ValueError, + match="Passing a Normalize instance simultaneously " + "with vmin/vmax is not supported."): ax.scatter(x, x, c=x, norm=mcolors.Normalize(-10, 10), vmin=0, vmax=5) @@ -2331,17 +2662,19 @@ def get_next_color(): "conversion": "^'c' argument must be a color", # bad vals } - if re_key is None: + assert_context = ( + pytest.raises(ValueError, match=REGEXP[re_key]) + if re_key is not None + else pytest.warns(match="argument looks like a single numeric RGB") + if isinstance(c_case, list) and len(c_case) == 3 + else contextlib.nullcontext() + ) + with assert_context: mpl.axes.Axes._parse_scatter_color_args( c=c_case, edgecolors="black", kwargs={}, xsize=xsize, get_next_color_func=get_next_color) - else: - with pytest.raises(ValueError, match=REGEXP[re_key]): - mpl.axes.Axes._parse_scatter_color_args( - c=c_case, edgecolors="black", kwargs={}, xsize=xsize, - get_next_color_func=get_next_color) - @pytest.mark.style('default') + @mpl.style.context('default') @check_figures_equal(extensions=["png"]) def test_scatter_single_color_c(self, fig_test, fig_ref): rgb = [[1, 0.5, 0.05]] @@ -2443,8 +2776,6 @@ def get_next_color(): def test_as_mpl_axes_api(): # tests the _as_mpl_axes api - from matplotlib.projections.polar import PolarAxes - class Polar: def __init__(self): self.theta_offset = 0 @@ -2456,47 +2787,15 @@ def _as_mpl_axes(self): prj = Polar() prj2 = Polar() prj2.theta_offset = np.pi - prj3 = Polar() # testing axes creation with plt.axes ax = plt.axes([0, 0, 1, 1], projection=prj) assert type(ax) == PolarAxes - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - ax_via_gca = plt.gca(projection=prj) - assert ax_via_gca is ax - plt.close() - - # testing axes creation with gca - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - ax = plt.gca(projection=prj) - assert type(ax) == mpl.axes._subplots.subplot_class_factory(PolarAxes) - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - ax_via_gca = plt.gca(projection=prj) - assert ax_via_gca is ax - # try getting the axes given a different polar projection - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - ax_via_gca = plt.gca(projection=prj2) - assert ax_via_gca is ax - assert ax.get_theta_offset() == 0 - # try getting the axes given an == (not is) polar projection - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - ax_via_gca = plt.gca(projection=prj3) - assert ax_via_gca is ax plt.close() # testing axes creation with subplot ax = plt.subplot(121, projection=prj) - assert type(ax) == mpl.axes._subplots.subplot_class_factory(PolarAxes) + assert type(ax) == PolarAxes plt.close() @@ -2511,13 +2810,48 @@ def test_pyplot_axes(): plt.close(fig2) -@image_comparison(['log_scales']) def test_log_scales(): fig, ax = plt.subplots() ax.plot(np.log(np.linspace(0.1, 100))) ax.set_yscale('log', base=5.5) ax.invert_yaxis() ax.set_xscale('log', base=9.0) + xticks, yticks = [ + [(t.get_loc(), t.label1.get_text()) for t in axis._update_ticks()] + for axis in [ax.xaxis, ax.yaxis] + ] + assert xticks == [ + (1.0, '$\\mathdefault{9^{0}}$'), + (9.0, '$\\mathdefault{9^{1}}$'), + (81.0, '$\\mathdefault{9^{2}}$'), + (2.0, ''), + (3.0, ''), + (4.0, ''), + (5.0, ''), + (6.0, ''), + (7.0, ''), + (8.0, ''), + (18.0, ''), + (27.0, ''), + (36.0, ''), + (45.0, ''), + (54.0, ''), + (63.0, ''), + (72.0, ''), + ] + assert yticks == [ + (0.18181818181818182, '$\\mathdefault{5.5^{-1}}$'), + (1.0, '$\\mathdefault{5.5^{0}}$'), + (5.5, '$\\mathdefault{5.5^{1}}$'), + (0.36363636363636365, ''), + (0.5454545454545454, ''), + (0.7272727272727273, ''), + (0.9090909090909092, ''), + (2.0, ''), + (3.0, ''), + (4.0, ''), + (5.0, ''), + ] def test_log_scales_no_data(): @@ -2530,10 +2864,10 @@ def test_log_scales_no_data(): def test_log_scales_invalid(): fig, ax = plt.subplots() ax.set_xscale('log') - with pytest.warns(UserWarning, match='Attempted to set non-positive'): + with pytest.warns(UserWarning, match='Attempt to set non-positive'): ax.set_xlim(-1, 10) ax.set_yscale('log') - with pytest.warns(UserWarning, match='Attempted to set non-positive'): + with pytest.warns(UserWarning, match='Attempt to set non-positive'): ax.set_ylim(-1, 10) @@ -2549,10 +2883,11 @@ def test_stackplot(): ax.set_xlim((0, 10)) ax.set_ylim((0, 70)) - # Reuse testcase from above for a labeled data test + # Reuse testcase from above for a test with labeled data and with colours + # from the Axes property cycle. data = {"x": x, "y1": y1, "y2": y2, "y3": y3} fig, ax = plt.subplots() - ax.stackplot("x", "y1", "y2", "y3", data=data) + ax.stackplot("x", "y1", "y2", "y3", data=data, colors=["C0", "C1", "C2"]) ax.set_xlim((0, 10)) ax.set_ylim((0, 70)) @@ -2817,6 +3152,25 @@ def test_bxp_bad_positions(): _bxp_test_helper(bxp_kwargs=dict(positions=[2, 3])) +@image_comparison(['bxp_custom_capwidths.png'], + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_custom_capwidths(): + _bxp_test_helper(bxp_kwargs=dict(capwidths=[0.0, 0.1, 0.5, 1.0])) + + +@image_comparison(['bxp_custom_capwidth.png'], + savefig_kwarg={'dpi': 40}, + style='default') +def test_bxp_custom_capwidth(): + _bxp_test_helper(bxp_kwargs=dict(capwidths=0.6)) + + +def test_bxp_bad_capwidths(): + with pytest.raises(ValueError): + _bxp_test_helper(bxp_kwargs=dict(capwidths=[1])) + + @image_comparison(['boxplot', 'boxplot'], tol=1.28, style='default') def test_boxplot(): # Randomness used for bootstrapping. @@ -2836,6 +3190,17 @@ def test_boxplot(): ax.set_ylim((-30, 30)) +@image_comparison(['boxplot_custom_capwidths.png'], + savefig_kwarg={'dpi': 40}, style='default') +def test_boxplot_custom_capwidths(): + + x = np.linspace(-7, 7, 140) + x = np.hstack([-25, x, 25]) + fig, ax = plt.subplots() + + ax.boxplot([x, x], notch=1, capwidths=[0.01, 0.2]) + + @image_comparison(['boxplot_sym2.png'], remove_text=True, style='default') def test_boxplot_sym2(): # Randomness used for bootstrapping. @@ -3295,7 +3660,9 @@ def test_tick_space_size_0(): @image_comparison(['errorbar_basic', 'errorbar_mixed', 'errorbar_basic']) def test_errorbar(): - x = np.arange(0.1, 4, 0.5) + # longdouble due to floating point rounding issues with certain + # computer chipsets + x = np.arange(0.1, 4, 0.5, dtype=np.longdouble) y = np.exp(-x) yerr = 0.1 + 0.2*np.sqrt(x) @@ -3333,6 +3700,8 @@ def test_errorbar(): ax.errorbar(x, y, yerr=[yerr_lower, 2*yerr], xerr=xerr, fmt='o', ecolor='g', capthick=2) ax.set_title('Mixed sym., log y') + # Force limits due to floating point slop potentially expanding the range + ax.set_ylim(1e-2, 1e1) fig.suptitle('Variable errorbars') @@ -3344,6 +3713,41 @@ def test_errorbar(): ax.set_title("Simplest errorbars, 0.2 in x, 0.4 in y") +@image_comparison(['mixed_errorbar_polar_caps'], extensions=['png'], + remove_text=True) +def test_mixed_errorbar_polar_caps(): + """ + Mix several polar errorbar use cases in a single test figure. + + It is advisable to position individual points off the grid. If there are + problems with reproducibility of this test, consider removing grid. + """ + fig = plt.figure() + ax = plt.subplot(111, projection='polar') + + # symmetric errorbars + th_sym = [1, 2, 3] + r_sym = [0.9]*3 + ax.errorbar(th_sym, r_sym, xerr=0.35, yerr=0.2, fmt="o") + + # long errorbars + th_long = [np.pi/2 + .1, np.pi + .1] + r_long = [1.8, 2.2] + ax.errorbar(th_long, r_long, xerr=0.8 * np.pi, yerr=0.15, fmt="o") + + # asymmetric errorbars + th_asym = [4*np.pi/3 + .1, 5*np.pi/3 + .1, 2*np.pi-0.1] + r_asym = [1.1]*3 + xerr = [[.3, .3, .2], [.2, .3, .3]] + yerr = [[.35, .5, .5], [.5, .35, .5]] + ax.errorbar(th_asym, r_asym, xerr=xerr, yerr=yerr, fmt="o") + + # overlapping errorbar + th_over = [2.1] + r_over = [3.1] + ax.errorbar(th_over, r_over, xerr=10, yerr=.2, fmt="o") + + def test_errorbar_colorcycle(): f, ax = plt.subplots() @@ -3439,7 +3843,7 @@ def test_errorbar_limits(): ax.set_title('Errorbar upper and lower limits') -def test_errobar_nonefmt(): +def test_errorbar_nonefmt(): # Check that passing 'none' as a format still plots errorbars x = np.arange(5) y = np.arange(5) @@ -3505,6 +3909,27 @@ def test_errorbar_every_invalid(): ax.errorbar(x, y, yerr, errorevery='foobar') +def test_xerr_yerr_not_negative(): + ax = plt.figure().subplots() + + with pytest.raises(ValueError, + match="'xerr' must not contain negative values"): + ax.errorbar(x=[0], y=[0], xerr=[[-0.5], [1]], yerr=[[-0.5], [1]]) + with pytest.raises(ValueError, + match="'xerr' must not contain negative values"): + ax.errorbar(x=[0], y=[0], xerr=[[-0.5], [1]]) + with pytest.raises(ValueError, + match="'yerr' must not contain negative values"): + ax.errorbar(x=[0], y=[0], yerr=[[-0.5], [1]]) + with pytest.raises(ValueError, + match="'yerr' must not contain negative values"): + x = np.arange(5) + y = [datetime.datetime(2021, 9, i * 2 + 1) for i in x] + ax.errorbar(x=x, + y=y, + yerr=datetime.timedelta(days=-10)) + + @check_figures_equal() def test_errorbar_every(fig_test, fig_ref): x = np.linspace(0, 1, 15) @@ -3556,6 +3981,18 @@ def test_errorbar_linewidth_type(elinewidth): plt.errorbar([1, 2, 3], [1, 2, 3], yerr=[1, 2, 3], elinewidth=elinewidth) +@check_figures_equal(extensions=["png"]) +def test_errorbar_nan(fig_test, fig_ref): + ax = fig_test.add_subplot() + xs = range(5) + ys = np.array([1, 2, np.nan, np.nan, 3]) + es = np.array([4, 5, np.nan, np.nan, 6]) + ax.errorbar(xs, ys, es) + ax = fig_ref.add_subplot() + ax.errorbar([0, 1], [1, 2], [4, 5]) + ax.errorbar([4], [3], [6], fmt="C0") + + @image_comparison(['hist_stacked_stepfilled', 'hist_stacked_stepfilled']) def test_hist_stacked_stepfilled(): # make some data @@ -3619,23 +4056,102 @@ def test_stem(use_line_collection): fig, ax = plt.subplots() # Label is a single space to force a legend to be drawn, but to avoid any # text being drawn - ax.stem(x, np.cos(x), - linefmt='C2-.', markerfmt='k+', basefmt='C1-.', label=' ', - use_line_collection=use_line_collection) + if use_line_collection: + ax.stem(x, np.cos(x), + linefmt='C2-.', markerfmt='k+', basefmt='C1-.', label=' ') + else: + with pytest.warns(MatplotlibDeprecationWarning, match='deprecated'): + ax.stem(x, np.cos(x), + linefmt='C2-.', markerfmt='k+', basefmt='C1-.', label=' ', + use_line_collection=False) ax.legend() def test_stem_args(): + """Test that stem() correctly identifies x and y values.""" + def _assert_equal(stem_container, expected): + x, y = map(list, stem_container.markerline.get_data()) + assert x == expected[0] + assert y == expected[1] + fig, ax = plt.subplots() - x = list(range(10)) - y = list(range(10)) + x = [1, 3, 5] + y = [9, 8, 7] # Test the call signatures - ax.stem(y) - ax.stem(x, y) - ax.stem(x, y, 'r--') - ax.stem(x, y, 'r--', basefmt='b--') + _assert_equal(ax.stem(y), expected=([0, 1, 2], y)) + _assert_equal(ax.stem(x, y), expected=(x, y)) + _assert_equal(ax.stem(x, y, linefmt='r--'), expected=(x, y)) + _assert_equal(ax.stem(x, y, 'r--'), expected=(x, y)) + _assert_equal(ax.stem(x, y, linefmt='r--', basefmt='b--'), expected=(x, y)) + _assert_equal(ax.stem(y, linefmt='r--'), expected=([0, 1, 2], y)) + _assert_equal(ax.stem(y, 'r--'), expected=([0, 1, 2], y)) + + +def test_stem_markerfmt(): + """Test that stem(..., markerfmt=...) produces the intended markers.""" + def _assert_equal(stem_container, linecolor=None, markercolor=None, + marker=None): + """ + Check that the given StemContainer has the properties listed as + keyword-arguments. + """ + if linecolor is not None: + assert mcolors.same_color( + stem_container.stemlines.get_color(), + linecolor) + if markercolor is not None: + assert mcolors.same_color( + stem_container.markerline.get_color(), + markercolor) + if marker is not None: + assert stem_container.markerline.get_marker() == marker + assert stem_container.markerline.get_linestyle() == 'None' + + fig, ax = plt.subplots() + + x = [1, 3, 5] + y = [9, 8, 7] + + # no linefmt + _assert_equal(ax.stem(x, y), markercolor='C0', marker='o') + _assert_equal(ax.stem(x, y, markerfmt='x'), markercolor='C0', marker='x') + _assert_equal(ax.stem(x, y, markerfmt='rx'), markercolor='r', marker='x') + + # positional linefmt + _assert_equal( + ax.stem(x, y, 'r'), # marker color follows linefmt if not given + linecolor='r', markercolor='r', marker='o') + _assert_equal( + ax.stem(x, y, 'rx'), # the marker is currently not taken from linefmt + linecolor='r', markercolor='r', marker='o') + _assert_equal( + ax.stem(x, y, 'r', markerfmt='x'), # only marker type specified + linecolor='r', markercolor='r', marker='x') + _assert_equal( + ax.stem(x, y, 'r', markerfmt='g'), # only marker color specified + linecolor='r', markercolor='g', marker='o') + _assert_equal( + ax.stem(x, y, 'r', markerfmt='gx'), # marker type and color specified + linecolor='r', markercolor='g', marker='x') + _assert_equal( + ax.stem(x, y, 'r', markerfmt=' '), # markerfmt=' ' for no marker + linecolor='r', markercolor='r', marker='None') + _assert_equal( + ax.stem(x, y, 'r', markerfmt=''), # markerfmt='' for no marker + linecolor='r', markercolor='r', marker='None') + + # with linefmt kwarg + _assert_equal( + ax.stem(x, y, linefmt='r'), + linecolor='r', markercolor='r', marker='o') + _assert_equal( + ax.stem(x, y, linefmt='r', markerfmt='x'), + linecolor='r', markercolor='r', marker='x') + _assert_equal( + ax.stem(x, y, linefmt='r', markerfmt='gx'), + linecolor='r', markercolor='g', marker='x') def test_stem_dates(): @@ -3643,7 +4159,7 @@ def test_stem_dates(): xs = [dateutil.parser.parse("2013-9-28 11:00:00"), dateutil.parser.parse("2013-9-28 12:00:00")] ys = [100, 200] - ax.stem(xs, ys, "*-") + ax.stem(xs, ys) @pytest.mark.parametrize("use_line_collection", [True, False], @@ -3653,9 +4169,16 @@ def test_stem_orientation(use_line_collection): x = np.linspace(0.1, 2*np.pi, 50) fig, ax = plt.subplots() - ax.stem(x, np.cos(x), - linefmt='C2-.', markerfmt='kx', basefmt='C1-.', - use_line_collection=use_line_collection, orientation='horizontal') + if use_line_collection: + ax.stem(x, np.cos(x), + linefmt='C2-.', markerfmt='kx', basefmt='C1-.', + orientation='horizontal') + else: + with pytest.warns(MatplotlibDeprecationWarning, match='deprecated'): + ax.stem(x, np.cos(x), + linefmt='C2-.', markerfmt='kx', basefmt='C1-.', + use_line_collection=False, + orientation='horizontal') @image_comparison(['hist_stacked_stepfilled_alpha']) @@ -3838,7 +4361,7 @@ def test_hist_stacked_bar(): fig, ax = plt.subplots() ax.hist(d, bins=10, histtype='barstacked', align='mid', color=colors, label=labels) - ax.legend(loc='upper right', bbox_to_anchor=(1.0, 1.0), ncol=1) + ax.legend(loc='upper right', bbox_to_anchor=(1.0, 1.0), ncols=1) def test_hist_barstacked_bottom_unchanged(): @@ -4062,12 +4585,32 @@ def test_eventplot_colors(colors): assert_allclose(coll.get_color(), color) +def test_eventplot_alpha(): + fig, ax = plt.subplots() + + # one alpha for all + collections = ax.eventplot([[0, 2, 4], [1, 3, 5, 7]], alpha=0.7) + assert collections[0].get_alpha() == 0.7 + assert collections[1].get_alpha() == 0.7 + + # one alpha per collection + collections = ax.eventplot([[0, 2, 4], [1, 3, 5, 7]], alpha=[0.5, 0.7]) + assert collections[0].get_alpha() == 0.5 + assert collections[1].get_alpha() == 0.7 + + with pytest.raises(ValueError, match="alpha and positions are unequal"): + ax.eventplot([[0, 2, 4], [1, 3, 5, 7]], alpha=[0.5, 0.7, 0.9]) + + with pytest.raises(ValueError, match="alpha and positions are unequal"): + ax.eventplot([0, 2, 4], alpha=[0.5, 0.7]) + + @image_comparison(['test_eventplot_problem_kwargs.png'], remove_text=True) def test_eventplot_problem_kwargs(recwarn): """ test that 'singular' versions of LineCollection props raise an - IgnoredKeywordWarning rather than overriding the 'plural' versions (e.g. - to prevent 'color' from overriding 'colors', see issue #4297) + MatplotlibDeprecationWarning rather than overriding the 'plural' versions + (e.g., to prevent 'color' from overriding 'colors', see issue #4297) """ np.random.seed(0) @@ -4086,7 +4629,6 @@ def test_eventplot_problem_kwargs(recwarn): linestyles=['solid', 'dashed'], linestyle=['dashdot', 'dotted']) - # check that three IgnoredKeywordWarnings were raised assert len(recwarn) == 3 assert all(issubclass(wi.category, MatplotlibDeprecationWarning) for wi in recwarn) @@ -4099,23 +4641,42 @@ def test_empty_eventplot(): @pytest.mark.parametrize('data', [[[]], [[], [0, 1]], [[0, 1], []]]) -@pytest.mark.parametrize( - 'orientation', ['_empty', 'vertical', 'horizontal', None, 'none']) +@pytest.mark.parametrize('orientation', [None, 'vertical', 'horizontal']) def test_eventplot_orientation(data, orientation): """Introduced when fixing issue #6412.""" - opts = {} if orientation == "_empty" else {'orientation': orientation} + opts = {} if orientation is None else {'orientation': orientation} fig, ax = plt.subplots(1, 1) - with (pytest.warns(MatplotlibDeprecationWarning) - if orientation in [None, 'none'] else nullcontext()): - ax.eventplot(data, **opts) + ax.eventplot(data, **opts) plt.draw() +@check_figures_equal(extensions=['png']) +def test_eventplot_units_list(fig_test, fig_ref): + # test that list of lists converted properly: + ts_1 = [datetime.datetime(2021, 1, 1), datetime.datetime(2021, 1, 2), + datetime.datetime(2021, 1, 3)] + ts_2 = [datetime.datetime(2021, 1, 15), datetime.datetime(2021, 1, 16)] + + ax = fig_ref.subplots() + ax.eventplot(ts_1, lineoffsets=0) + ax.eventplot(ts_2, lineoffsets=1) + + ax = fig_test.subplots() + ax.eventplot([ts_1, ts_2]) + + @image_comparison(['marker_styles.png'], remove_text=True) def test_marker_styles(): fig, ax = plt.subplots() - for y, marker in enumerate(sorted(matplotlib.markers.MarkerStyle.markers, - key=lambda x: str(type(x))+str(x))): + # Since generation of the test image, None was removed but 'none' was + # added. By moving 'none' to the front (=former sorted place of None) + # we can avoid regenerating the test image. This can be removed if the + # test image has to be regenerated for other reasons. + markers = sorted(matplotlib.markers.MarkerStyle.markers, + key=lambda x: str(type(x))+str(x)) + markers.remove('none') + markers = ['none', *markers] + for y, marker in enumerate(markers): ax.plot((y % 2)*5 + np.arange(10)*10, np.ones(10)*10*y, linestyle='', marker=marker, markersize=10+y/5, label=marker) @@ -4660,6 +5221,31 @@ def test_spectrum(): ax.set(xlabel="", ylabel="") +def test_psd_csd_edge_cases(): + # Inverted yaxis or fully zero inputs used to throw exceptions. + axs = plt.figure().subplots(2) + for ax in axs: + ax.yaxis.set(inverted=True) + with np.errstate(divide="ignore"): + axs[0].psd(np.zeros(5)) + axs[1].csd(np.zeros(5), np.zeros(5)) + + +@check_figures_equal(extensions=['png']) +def test_twin_remove(fig_test, fig_ref): + ax_test = fig_test.add_subplot() + ax_twinx = ax_test.twinx() + ax_twiny = ax_test.twiny() + ax_twinx.remove() + ax_twiny.remove() + + ax_ref = fig_ref.add_subplot() + # Ideally we also undo tick changes when calling ``remove()``, but for now + # manually set the ticks of the reference image to match the test image + ax_ref.xaxis.tick_bottom() + ax_ref.yaxis.tick_left() + + @image_comparison(['twin_spines.png'], remove_text=True) def test_twin_spines(): @@ -4780,6 +5366,26 @@ def test_reset_grid(): assert ax.xaxis.majorTicks[0].gridline.get_visible() +@check_figures_equal(extensions=['png']) +def test_reset_ticks(fig_test, fig_ref): + for fig in [fig_ref, fig_test]: + ax = fig.add_subplot() + ax.grid(True) + ax.tick_params( + direction='in', length=10, width=5, color='C0', pad=12, + labelsize=14, labelcolor='C1', labelrotation=45, + grid_color='C2', grid_alpha=0.8, grid_linewidth=3, + grid_linestyle='--') + fig.draw_without_rendering() + + # After we've changed any setting on ticks, reset_ticks will mean + # re-creating them from scratch. This *should* appear the same as not + # resetting them. + for ax in fig_test.axes: + ax.xaxis.reset_ticks() + ax.yaxis.reset_ticks() + + def test_vline_limit(): fig = plt.figure() ax = fig.gca() @@ -4860,6 +5466,57 @@ def test_shared_with_aspect_3(): assert round(expected, 4) == round(ax.get_aspect(), 4) +def test_shared_aspect_error(): + fig, axes = plt.subplots(1, 2, sharex=True, sharey=True) + axes[0].axis("equal") + with pytest.raises(RuntimeError, match=r"set_aspect\(..., adjustable="): + fig.draw_without_rendering() + + +@pytest.mark.parametrize('err, args, kwargs, match', + ((TypeError, (1, 2), {}, + r"axis\(\) takes from 0 to 1 positional arguments " + "but 2 were given"), + (ValueError, ('foo', ), {}, + "Unrecognized string 'foo' to axis; try 'on' or " + "'off'"), + (TypeError, ([1, 2], ), {}, + "the first argument to axis*"), + (TypeError, tuple(), {'foo': None}, + r"axis\(\) got an unexpected keyword argument " + "'foo'"), + )) +def test_axis_errors(err, args, kwargs, match): + with pytest.raises(err, match=match): + plt.axis(*args, **kwargs) + + +def test_axis_method_errors(): + ax = plt.gca() + with pytest.raises(ValueError, match="unknown value for which: 'foo'"): + ax.get_xaxis_transform('foo') + with pytest.raises(ValueError, match="unknown value for which: 'foo'"): + ax.get_yaxis_transform('foo') + with pytest.raises(TypeError, match="Cannot supply both positional and"): + ax.set_prop_cycle('foo', label='bar') + with pytest.raises(ValueError, match="argument must be among"): + ax.set_anchor('foo') + with pytest.raises(ValueError, match="scilimits must be a sequence"): + ax.ticklabel_format(scilimits=1) + with pytest.raises(TypeError, match="Specifying 'loc' is disallowed"): + ax.set_xlabel('foo', loc='left', x=1) + with pytest.raises(TypeError, match="Specifying 'loc' is disallowed"): + ax.set_ylabel('foo', loc='top', y=1) + with pytest.raises(TypeError, match="Cannot pass both 'left'"): + ax.set_xlim(left=0, xmin=1) + with pytest.raises(TypeError, match="Cannot pass both 'right'"): + ax.set_xlim(right=0, xmax=1) + with pytest.raises(TypeError, match="Cannot pass both 'bottom'"): + ax.set_ylim(bottom=0, ymin=1) + with pytest.raises(TypeError, match="Cannot pass both 'top'"): + ax.set_ylim(top=0, ymax=1) + + @pytest.mark.parametrize('twin', ('x', 'y')) def test_twin_with_aspect(twin): fig, ax = plt.subplots() @@ -4919,7 +5576,7 @@ def test_pie_default(): @image_comparison(['pie_linewidth_0', 'pie_linewidth_0', 'pie_linewidth_0'], - extensions=['png']) + extensions=['png'], style='mpl20') def test_pie_linewidth_0(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -4951,7 +5608,7 @@ def test_pie_linewidth_0(): plt.axis('equal') -@image_comparison(['pie_center_radius.png']) +@image_comparison(['pie_center_radius.png'], style='mpl20') def test_pie_center_radius(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -4971,7 +5628,7 @@ def test_pie_center_radius(): plt.axis('equal') -@image_comparison(['pie_linewidth_2.png']) +@image_comparison(['pie_linewidth_2.png'], style='mpl20') def test_pie_linewidth_2(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -4986,7 +5643,7 @@ def test_pie_linewidth_2(): plt.axis('equal') -@image_comparison(['pie_ccw_true.png']) +@image_comparison(['pie_ccw_true.png'], style='mpl20') def test_pie_ccw_true(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -5001,7 +5658,7 @@ def test_pie_ccw_true(): plt.axis('equal') -@image_comparison(['pie_frame_grid.png']) +@image_comparison(['pie_frame_grid.png'], style='mpl20') def test_pie_frame_grid(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -5028,7 +5685,7 @@ def test_pie_frame_grid(): plt.axis('equal') -@image_comparison(['pie_rotatelabels_true.png']) +@image_comparison(['pie_rotatelabels_true.png'], style='mpl20') def test_pie_rotatelabels_true(): # The slices will be ordered and plotted counter-clockwise. labels = 'Hogwarts', 'Frogs', 'Dogs', 'Logs' @@ -5086,12 +5743,6 @@ def test_pie_get_negative_values(): ax.pie([5, 5, -3], explode=[0, .1, .2]) -def test_normalize_kwarg_warn_pie(): - fig, ax = plt.subplots() - with pytest.warns(MatplotlibDeprecationWarning): - ax.pie(x=[0], normalize=None) - - def test_normalize_kwarg_pie(): fig, ax = plt.subplots() x = [0.3, 0.3, 0.1] @@ -5101,6 +5752,24 @@ def test_normalize_kwarg_pie(): assert abs(t2[0][-1].theta2 - 360.) > 1e-3 +@check_figures_equal() +def test_pie_hatch_single(fig_test, fig_ref): + x = [0.3, 0.3, 0.1] + hatch = '+' + fig_test.subplots().pie(x, hatch=hatch) + wedges, _ = fig_ref.subplots().pie(x) + [w.set_hatch(hatch) for w in wedges] + + +@check_figures_equal() +def test_pie_hatch_multi(fig_test, fig_ref): + x = [0.3, 0.3, 0.1] + hatch = ['/', '+', '.'] + fig_test.subplots().pie(x, hatch=hatch) + wedges, _ = fig_ref.subplots().pie(x) + [w.set_hatch(hp) for w, hp in zip(wedges, hatch)] + + @image_comparison(['set_get_ticklabels.png']) def test_set_get_ticklabels(): # test issue 2246 @@ -5127,6 +5796,53 @@ def test_set_get_ticklabels(): ax[1].set_yticklabels(ax[0].get_yticklabels()) +def test_set_ticks_kwargs_raise_error_without_labels(): + """ + When labels=None and any kwarg is passed, axis.set_ticks() raises a + ValueError. + """ + fig, ax = plt.subplots() + ticks = [1, 2, 3] + with pytest.raises(ValueError): + ax.xaxis.set_ticks(ticks, alpha=0.5) + + +@check_figures_equal(extensions=["png"]) +def test_set_ticks_with_labels(fig_test, fig_ref): + """ + Test that these two are identical:: + + set_xticks(ticks); set_xticklabels(labels, **kwargs) + set_xticks(ticks, labels, **kwargs) + + """ + ax = fig_ref.subplots() + ax.set_xticks([1, 2, 4, 6]) + ax.set_xticklabels(['a', 'b', 'c', 'd'], fontweight='bold') + ax.set_yticks([1, 3, 5]) + ax.set_yticks([2, 4], minor=True) + ax.set_yticklabels(['A', 'B'], minor=True) + + ax = fig_test.subplots() + ax.set_xticks([1, 2, 4, 6], ['a', 'b', 'c', 'd'], fontweight='bold') + ax.set_yticks([1, 3, 5]) + ax.set_yticks([2, 4], ['A', 'B'], minor=True) + + +def test_xticks_bad_args(): + ax = plt.figure().add_subplot() + with pytest.raises(TypeError, match='must be a sequence'): + ax.set_xticks([2, 9], 3.1) + with pytest.raises(ValueError, match='must be 1D'): + plt.xticks(np.arange(4).reshape((-1, 1))) + with pytest.raises(ValueError, match='must be 1D'): + plt.xticks(np.arange(4).reshape((1, -1))) + with pytest.raises(ValueError, match='must be 1D'): + plt.xticks(np.arange(4).reshape((-1, 1)), labels=range(4)) + with pytest.raises(ValueError, match='must be 1D'): + plt.xticks(np.arange(4).reshape((1, -1)), labels=range(4)) + + def test_subsampled_ticklabels(): # test issue 11937 fig, ax = plt.subplots() @@ -5237,12 +5953,54 @@ def test_set_margin_updates_limits(): assert ax.get_xlim() == (1, 2) +@pytest.mark.parametrize('err, args, kwargs, match', ( + (ValueError, (-1,), {}, r'margin must be greater than -0\.5'), + (ValueError, (1, -1), {}, r'margin must be greater than -0\.5'), + (ValueError, tuple(), {'x': -1}, r'margin must be greater than -0\.5'), + (ValueError, tuple(), {'y': -1}, r'margin must be greater than -0\.5'), + (TypeError, (1, ), {'x': 1, 'y': 1}, + 'Cannot pass both positional and keyword arguments for x and/or y'), + (TypeError, (1, ), {'x': 1}, + 'Cannot pass both positional and keyword arguments for x and/or y'), + (TypeError, (1, 1, 1), {}, 'Must pass a single positional argument'), +)) +def test_margins_errors(err, args, kwargs, match): + with pytest.raises(err, match=match): + fig = plt.figure() + ax = fig.add_subplot() + ax.margins(*args, **kwargs) + + def test_length_one_hist(): fig, ax = plt.subplots() ax.hist(1) ax.hist([1]) +def test_set_xy_bound(): + fig = plt.figure() + ax = fig.add_subplot() + ax.set_xbound(2.0, 3.0) + assert ax.get_xbound() == (2.0, 3.0) + assert ax.get_xlim() == (2.0, 3.0) + ax.set_xbound(upper=4.0) + assert ax.get_xbound() == (2.0, 4.0) + assert ax.get_xlim() == (2.0, 4.0) + ax.set_xbound(lower=3.0) + assert ax.get_xbound() == (3.0, 4.0) + assert ax.get_xlim() == (3.0, 4.0) + + ax.set_ybound(2.0, 3.0) + assert ax.get_ybound() == (2.0, 3.0) + assert ax.get_ylim() == (2.0, 3.0) + ax.set_ybound(upper=4.0) + assert ax.get_ybound() == (2.0, 4.0) + assert ax.get_ylim() == (2.0, 4.0) + ax.set_ybound(lower=3.0) + assert ax.get_ybound() == (3.0, 4.0) + assert ax.get_ylim() == (3.0, 4.0) + + def test_pathological_hexbin(): # issue #2863 mylist = [10] * 100 @@ -5533,6 +6291,17 @@ def test_title_location_roundtrip(): ax.set_title('fail', loc='foo') +@pytest.mark.parametrize('sharex', [True, False]) +def test_title_location_shared(sharex): + fig, axs = plt.subplots(2, 1, sharex=sharex) + axs[0].set_title('A', pad=-40) + axs[1].set_title('B', pad=-40) + fig.draw_without_rendering() + x, y1 = axs[0].title.get_position() + x, y2 = axs[1].title.get_position() + assert y1 == y2 == 1.0 + + @image_comparison(["loglog.png"], remove_text=True, tol=0.02) def test_loglog(): fig, ax = plt.subplots() @@ -5542,41 +6311,29 @@ def test_loglog(): ax.tick_params(length=15, width=2, which='minor') -@pytest.mark.parametrize("new_api", [False, True]) @image_comparison(["test_loglog_nonpos.png"], remove_text=True, style='mpl20') -def test_loglog_nonpos(new_api): +def test_loglog_nonpos(): fig, axs = plt.subplots(3, 3) x = np.arange(1, 11) y = x**3 y[7] = -3. x[4] = -10 - for (i, j), ax in np.ndenumerate(axs): - mcx = ['mask', 'clip', ''][j] - mcy = ['mask', 'clip', ''][i] - if new_api: - if mcx == mcy: - if mcx: - ax.loglog(x, y**3, lw=2, nonpositive=mcx) - else: - ax.loglog(x, y**3, lw=2) + for (mcy, mcx), ax in zip(product(['mask', 'clip', ''], repeat=2), + axs.flat): + if mcx == mcy: + if mcx: + ax.loglog(x, y**3, lw=2, nonpositive=mcx) else: ax.loglog(x, y**3, lw=2) - if mcx: - ax.set_xscale("log", nonpositive=mcx) - if mcy: - ax.set_yscale("log", nonpositive=mcy) else: - kws = {} + ax.loglog(x, y**3, lw=2) if mcx: - kws['nonposx'] = mcx + ax.set_xscale("log", nonpositive=mcx) if mcy: - kws['nonposy'] = mcy - with (pytest.warns(MatplotlibDeprecationWarning) if kws - else nullcontext()): - ax.loglog(x, y**3, lw=2, **kws) + ax.set_yscale("log", nonpositive=mcy) -@pytest.mark.style('default') +@mpl.style.context('default') def test_axes_margins(): fig, ax = plt.subplots() ax.plot([0, 1, 2, 3]) @@ -5690,18 +6447,21 @@ def test_adjust_numtick_aspect(): assert len(ax.yaxis.get_major_locator()()) > 2 -@image_comparison(["auto_numticks.png"], style='default') +@mpl.style.context("default") def test_auto_numticks(): - # Make tiny, empty subplots, verify that there are only 3 ticks. - plt.subplots(4, 4) + axs = plt.figure().subplots(4, 4) + for ax in axs.flat: # Tiny, empty subplots have only 3 ticks. + assert [*ax.get_xticks()] == [*ax.get_yticks()] == [0, 0.5, 1] -@image_comparison(["auto_numticks_log.png"], style='default') +@mpl.style.context("default") def test_auto_numticks_log(): # Verify that there are not too many ticks with a large log range. fig, ax = plt.subplots() - matplotlib.rcParams['axes.autolimit_mode'] = 'round_numbers' + mpl.rcParams['axes.autolimit_mode'] = 'round_numbers' ax.loglog([1e-20, 1e5], [1e-16, 10]) + assert (np.log10(ax.get_xticks()) == np.arange(-26, 18, 4)).all() + assert (np.log10(ax.get_yticks()) == np.arange(-20, 10, 3)).all() def test_broken_barh_empty(): @@ -5729,7 +6489,7 @@ def test_pandas_pcolormesh(pd): def test_pandas_indexing_dates(pd): dates = np.arange('2005-02', '2005-03', dtype='datetime64[D]') - values = np.sin(np.array(range(len(dates)))) + values = np.sin(range(len(dates))) df = pd.DataFrame({'dates': dates, 'values': values}) ax = plt.gca() @@ -5772,6 +6532,33 @@ def test_pandas_bar_align_center(pd): fig.canvas.draw() +def test_axis_get_tick_params(): + axis = plt.subplot().yaxis + initial_major_style_translated = {**axis.get_tick_params(which='major')} + initial_minor_style_translated = {**axis.get_tick_params(which='minor')} + + translated_major_kw = axis._translate_tick_params( + axis._major_tick_kw, reverse=True + ) + translated_minor_kw = axis._translate_tick_params( + axis._minor_tick_kw, reverse=True + ) + + assert translated_major_kw == initial_major_style_translated + assert translated_minor_kw == initial_minor_style_translated + axis.set_tick_params(labelsize=30, labelcolor='red', + direction='out', which='both') + + new_major_style_translated = {**axis.get_tick_params(which='major')} + new_minor_style_translated = {**axis.get_tick_params(which='minor')} + new_major_style = axis._translate_tick_params(new_major_style_translated) + new_minor_style = axis._translate_tick_params(new_minor_style_translated) + assert initial_major_style_translated != new_major_style_translated + assert axis._major_tick_kw == new_major_style + assert initial_minor_style_translated != new_minor_style_translated + assert axis._minor_tick_kw == new_minor_style + + def test_axis_set_tick_params_labelsize_labelcolor(): # Tests fix for issue 4346 axis_1 = plt.subplot() @@ -5909,6 +6696,7 @@ def test_axisbelow(): left=False, right=False) ax.spines[:].set_visible(False) ax.set_axisbelow(setting) + assert ax.get_axisbelow() == setting def test_titletwiny(): @@ -5962,8 +6750,38 @@ def test_title_xticks_top_both(): assert ax.title.get_position()[1] > 1.04 +@pytest.mark.parametrize( + 'left, center', [ + ('left', ''), + ('', 'center'), + ('left', 'center') + ], ids=[ + 'left title moved', + 'center title kept', + 'both titles aligned' + ] +) +def test_title_above_offset(left, center): + # Test that title moves if overlaps with yaxis offset text. + mpl.rcParams['axes.titley'] = None + fig, ax = plt.subplots() + ax.set_ylim(1e11) + ax.set_title(left, loc='left') + ax.set_title(center) + fig.draw_without_rendering() + if left and not center: + assert ax._left_title.get_position()[1] > 1.0 + elif not left and center: + assert ax.title.get_position()[1] == 1.0 + else: + yleft = ax._left_title.get_position()[1] + ycenter = ax.title.get_position()[1] + assert yleft > 1.0 + assert ycenter == yleft + + def test_title_no_move_off_page(): - # If an axes is off the figure (ie. if it is cropped during a save) + # If an Axes is off the figure (ie. if it is cropped during a save) # make sure that the automatic title repositioning does not get done. mpl.rcParams['axes.titley'] = None fig = plt.figure() @@ -6043,7 +6861,7 @@ def test_tick_param_label_rotation(): assert text.get_rotation() == 35 -@pytest.mark.style('default') +@mpl.style.context('default') def test_fillbetween_cycle(): fig, ax = plt.subplots() @@ -6092,9 +6910,9 @@ def test_color_length_mismatch(): fig, ax = plt.subplots() with pytest.raises(ValueError): ax.scatter(x, y, c=colors) - c_rgb = (0.5, 0.5, 0.5) - ax.scatter(x, y, c=c_rgb) - ax.scatter(x, y, c=[c_rgb] * N) + with pytest.warns(match="argument looks like a single numeric RGB"): + ax.scatter(x, y, c=(0.5, 0.5, 0.5)) + ax.scatter(x, y, c=[(0.5, 0.5, 0.5)] * N) def test_eventplot_legend(): @@ -6249,13 +7067,38 @@ def test_zoom_inset(): fig.canvas.draw() xx = np.array([[1.5, 2.], [2.15, 2.5]]) - assert(np.all(rec.get_bbox().get_points() == xx)) + assert np.all(rec.get_bbox().get_points() == xx) xx = np.array([[0.6325, 0.692308], [0.8425, 0.907692]]) np.testing.assert_allclose( axin1.get_position().get_points(), xx, rtol=1e-4) +@image_comparison(['inset_polar.png'], remove_text=True, style='mpl20') +def test_inset_polar(): + _, ax = plt.subplots() + axins = ax.inset_axes([0.5, 0.1, 0.45, 0.45], polar=True) + assert isinstance(axins, PolarAxes) + + r = np.arange(0, 2, 0.01) + theta = 2 * np.pi * r + + ax.plot(theta, r) + axins.plot(theta, r) + + +def test_inset_projection(): + _, ax = plt.subplots() + axins = ax.inset_axes([0.2, 0.2, 0.3, 0.3], projection="hammer") + assert isinstance(axins, HammerAxes) + + +def test_inset_subclass(): + _, ax = plt.subplots() + axins = ax.inset_axes([0.2, 0.2, 0.3, 0.3], axes_class=AA.Axes) + assert isinstance(axins, AA.Axes) + + @pytest.mark.parametrize('x_inverted', [False, True]) @pytest.mark.parametrize('y_inverted', [False, True]) def test_indicate_inset_inverted(x_inverted, y_inverted): @@ -6305,19 +7148,40 @@ def test_spines_properbbox_after_zoom(): np.testing.assert_allclose(bb.get_points(), bb2.get_points(), rtol=1e-6) -def test_cartopy_backcompat(): - - class Dummy(matplotlib.axes.Axes): - ... - - class DummySubplot(matplotlib.axes.SubplotBase, Dummy): - _axes_class = Dummy - - matplotlib.axes._subplots._subplot_classes[Dummy] = DummySubplot - - FactoryDummySubplot = matplotlib.axes.subplot_class_factory(Dummy) - - assert DummySubplot is FactoryDummySubplot +def test_limits_after_scroll_zoom(): + fig, ax = plt.subplots() + # + xlim = (-0.5, 0.5) + ylim = (-1, 2) + ax.set_xlim(xlim) + ax.set_ylim(ymin=ylim[0], ymax=ylim[1]) + # This is what scroll zoom calls: + # Zoom with factor 1, small numerical change + ax._set_view_from_bbox((200, 200, 1.)) + np.testing.assert_allclose(xlim, ax.get_xlim(), atol=1e-16) + np.testing.assert_allclose(ylim, ax.get_ylim(), atol=1e-16) + + # Zoom in + ax._set_view_from_bbox((200, 200, 2.)) + # Hard-coded values + new_xlim = (-0.3790322580645161, 0.12096774193548387) + new_ylim = (-0.40625, 1.09375) + + res_xlim = ax.get_xlim() + res_ylim = ax.get_ylim() + np.testing.assert_allclose(res_xlim[1] - res_xlim[0], 0.5) + np.testing.assert_allclose(res_ylim[1] - res_ylim[0], 1.5) + np.testing.assert_allclose(new_xlim, res_xlim, atol=1e-16) + np.testing.assert_allclose(new_ylim, res_ylim) + + # Zoom out, should be same as before, except for numerical issues + ax._set_view_from_bbox((200, 200, 0.5)) + res_xlim = ax.get_xlim() + res_ylim = ax.get_ylim() + np.testing.assert_allclose(res_xlim[1] - res_xlim[0], 1) + np.testing.assert_allclose(res_ylim[1] - res_ylim[0], 3) + np.testing.assert_allclose(xlim, res_xlim, atol=1e-16) + np.testing.assert_allclose(ylim, res_ylim, atol=1e-16) def test_gettightbbox_ignore_nan(): @@ -6440,7 +7304,31 @@ def test_secondary_formatter(): secax.xaxis.get_major_formatter(), mticker.ScalarFormatter) -def color_boxes(fig, axs): +def test_secondary_repr(): + fig, ax = plt.subplots() + secax = ax.secondary_xaxis("top") + assert repr(secax) == '' + + +@image_comparison(['axis_options.png'], remove_text=True, style='mpl20') +def test_axis_options(): + fig, axes = plt.subplots(2, 3) + for i, option in enumerate(('scaled', 'tight', 'image')): + # Draw a line and a circle fitting within the boundaries of the line + # The circle should look like a circle for 'scaled' and 'image' + # High/narrow aspect ratio + axes[0, i].plot((1, 2), (1, 3.2)) + axes[0, i].axis(option) + axes[0, i].add_artist(mpatches.Circle((1.5, 1.5), radius=0.5, + facecolor='none', edgecolor='k')) + # Low/wide aspect ratio + axes[1, i].plot((1, 2.25), (1, 1.75)) + axes[1, i].axis(option) + axes[1, i].add_artist(mpatches.Circle((1.5, 1.25), radius=0.25, + facecolor='none', edgecolor='k')) + + +def color_boxes(fig, ax): """ Helper for the tests below that test the extents of various axes elements """ @@ -6448,10 +7336,10 @@ def color_boxes(fig, axs): renderer = fig.canvas.get_renderer() bbaxis = [] - for nn, axx in enumerate([axs.xaxis, axs.yaxis]): + for nn, axx in enumerate([ax.xaxis, ax.yaxis]): bb = axx.get_tightbbox(renderer) if bb: - axisr = plt.Rectangle( + axisr = mpatches.Rectangle( (bb.x0, bb.y0), width=bb.width, height=bb.height, linewidth=0.7, edgecolor='y', facecolor="none", transform=None, zorder=3) @@ -6460,24 +7348,24 @@ def color_boxes(fig, axs): bbspines = [] for nn, a in enumerate(['bottom', 'top', 'left', 'right']): - bb = axs.spines[a].get_window_extent(renderer) - spiner = plt.Rectangle( + bb = ax.spines[a].get_window_extent(renderer) + spiner = mpatches.Rectangle( (bb.x0, bb.y0), width=bb.width, height=bb.height, linewidth=0.7, edgecolor="green", facecolor="none", transform=None, zorder=3) fig.add_artist(spiner) bbspines += [bb] - bb = axs.get_window_extent() - rect2 = plt.Rectangle( + bb = ax.get_window_extent() + rect2 = mpatches.Rectangle( (bb.x0, bb.y0), width=bb.width, height=bb.height, linewidth=1.5, edgecolor="magenta", facecolor="none", transform=None, zorder=2) fig.add_artist(rect2) bbax = bb - bb2 = axs.get_tightbbox(renderer) - rect2 = plt.Rectangle( + bb2 = ax.get_tightbbox(renderer) + rect2 = mpatches.Rectangle( (bb2.x0, bb2.y0), width=bb2.width, height=bb2.height, linewidth=3, edgecolor="red", facecolor="none", transform=None, zorder=1) @@ -6663,17 +7551,21 @@ def test_axis_extent_arg(): assert (ymin, ymax) == ax.get_ylim() -def test_datetime_masked(): - # make sure that all-masked data falls back to the viewlim - # set in convert.axisinfo.... - x = np.array([datetime.datetime(2017, 1, n) for n in range(1, 6)]) - y = np.array([1, 2, 3, 4, 5]) - m = np.ma.masked_greater(y, 0) - +def test_axis_extent_arg2(): + # Same as test_axis_extent_arg, but with keyword arguments fig, ax = plt.subplots() - ax.plot(x, m) - dt = mdates.date2num(np.datetime64('0000-12-31')) - assert ax.get_xlim() == (730120.0 + dt, 733773.0 + dt) + xmin = 5 + xmax = 10 + ymin = 15 + ymax = 20 + extent = ax.axis(xmin=xmin, xmax=xmax, ymin=ymin, ymax=ymax) + + # test that the docstring is correct + assert tuple(extent) == (xmin, xmax, ymin, ymax) + + # test that limits were set per the docstring + assert (xmin, xmax) == ax.get_xlim() + assert (ymin, ymax) == ax.get_ylim() def test_hist_auto_bins(): @@ -6723,6 +7615,8 @@ def test_set_ticks_inverted(): ax.invert_xaxis() ax.set_xticks([.3, .7]) assert ax.get_xlim() == (1, 0) + ax.set_xticks([-1]) + assert ax.get_xlim() == (1, -1) def test_aspect_nonlinear_adjustable_box(): @@ -6762,6 +7656,7 @@ def test_box_aspect(): axtwin.plot([12, 344]) ax1.set_box_aspect(1) + assert ax1.get_box_aspect() == 1.0 fig2, ax2 = plt.subplots() ax2.margins(0) @@ -6817,6 +7712,18 @@ def test_bbox_aspect_axes_init(): assert_allclose(sizes, sizes[0]) +def test_set_aspect_negative(): + fig, ax = plt.subplots() + with pytest.raises(ValueError, match="must be finite and positive"): + ax.set_aspect(-1) + with pytest.raises(ValueError, match="must be finite and positive"): + ax.set_aspect(0) + with pytest.raises(ValueError, match="must be finite and positive"): + ax.set_aspect(np.inf) + with pytest.raises(ValueError, match="must be finite and positive"): + ax.set_aspect(-np.inf) + + def test_redraw_in_frame(): fig, ax = plt.subplots(1, 1) ax.plot([1, 2, 3]) @@ -6824,7 +7731,7 @@ def test_redraw_in_frame(): ax.redraw_in_frame() -def test_invisible_axes(): +def test_invisible_axes_events(): # invisible axes should not respond to events... fig, ax = plt.subplots() assert fig.canvas.inaxes((200, 200)) is not None @@ -6876,7 +7783,7 @@ def test_polar_interpolation_steps_variable_r(fig_test, fig_ref): np.linspace(0, np.pi/2, 101), np.linspace(1, 2, 101)) -@pytest.mark.style('default') +@mpl.style.context('default') def test_autoscale_tiny_sticky(): fig, ax = plt.subplots() ax.bar(0, 1e-9) @@ -6906,7 +7813,7 @@ def test_ytickcolor_is_not_yticklabelcolor(): @pytest.mark.parametrize('size', [size for size in mfont_manager.font_scalings if size is not None] + [8, 10, 12]) -@pytest.mark.style('default') +@mpl.style.context('default') def test_relative_ticklabel_sizes(size): mpl.rcParams['xtick.labelsize'] = size mpl.rcParams['ytick.labelsize'] = size @@ -6942,7 +7849,8 @@ def test_2dcolor_plot(fig_test, fig_ref): # plot with 1D-color: axs = fig_test.subplots(5) axs[0].plot([1, 2], [1, 2], c=color.reshape(-1)) - axs[1].scatter([1, 2], [1, 2], c=color.reshape(-1)) + with pytest.warns(match="argument looks like a single numeric RGB"): + axs[1].scatter([1, 2], [1, 2], c=color.reshape(-1)) axs[2].step([1, 2], [1, 2], c=color.reshape(-1)) axs[3].hist(np.arange(10), color=color.reshape(-1)) axs[4].bar(np.arange(10), np.arange(10), color=color.reshape(-1)) @@ -7007,6 +7915,20 @@ def test_bar_label_location_vertical(): assert labels[1].get_va() == 'top' +def test_bar_label_location_vertical_yinverted(): + ax = plt.gca() + ax.invert_yaxis() + xs, heights = [1, 2], [3, -4] + rects = ax.bar(xs, heights) + labels = ax.bar_label(rects) + assert labels[0].xy == (xs[0], heights[0]) + assert labels[0].get_ha() == 'center' + assert labels[0].get_va() == 'top' + assert labels[1].xy == (xs[1], heights[1]) + assert labels[1].get_ha() == 'center' + assert labels[1].get_va() == 'bottom' + + def test_bar_label_location_horizontal(): ax = plt.gca() ys, widths = [1, 2], [3, -4] @@ -7020,19 +7942,72 @@ def test_bar_label_location_horizontal(): assert labels[1].get_va() == 'center' +def test_bar_label_location_horizontal_yinverted(): + ax = plt.gca() + ax.invert_yaxis() + ys, widths = [1, 2], [3, -4] + rects = ax.barh(ys, widths) + labels = ax.bar_label(rects) + assert labels[0].xy == (widths[0], ys[0]) + assert labels[0].get_ha() == 'left' + assert labels[0].get_va() == 'center' + assert labels[1].xy == (widths[1], ys[1]) + assert labels[1].get_ha() == 'right' + assert labels[1].get_va() == 'center' + + +def test_bar_label_location_horizontal_xinverted(): + ax = plt.gca() + ax.invert_xaxis() + ys, widths = [1, 2], [3, -4] + rects = ax.barh(ys, widths) + labels = ax.bar_label(rects) + assert labels[0].xy == (widths[0], ys[0]) + assert labels[0].get_ha() == 'right' + assert labels[0].get_va() == 'center' + assert labels[1].xy == (widths[1], ys[1]) + assert labels[1].get_ha() == 'left' + assert labels[1].get_va() == 'center' + + +def test_bar_label_location_horizontal_xyinverted(): + ax = plt.gca() + ax.invert_xaxis() + ax.invert_yaxis() + ys, widths = [1, 2], [3, -4] + rects = ax.barh(ys, widths) + labels = ax.bar_label(rects) + assert labels[0].xy == (widths[0], ys[0]) + assert labels[0].get_ha() == 'right' + assert labels[0].get_va() == 'center' + assert labels[1].xy == (widths[1], ys[1]) + assert labels[1].get_ha() == 'left' + assert labels[1].get_va() == 'center' + + def test_bar_label_location_center(): ax = plt.gca() ys, widths = [1, 2], [3, -4] rects = ax.barh(ys, widths) labels = ax.bar_label(rects, label_type='center') - assert labels[0].xy == (widths[0] / 2, ys[0]) + assert labels[0].xy == (0.5, 0.5) assert labels[0].get_ha() == 'center' assert labels[0].get_va() == 'center' - assert labels[1].xy == (widths[1] / 2, ys[1]) + assert labels[1].xy == (0.5, 0.5) assert labels[1].get_ha() == 'center' assert labels[1].get_va() == 'center' +@image_comparison(['test_centered_bar_label_nonlinear.svg']) +def test_centered_bar_label_nonlinear(): + _, ax = plt.subplots() + bar_container = ax.barh(['c', 'b', 'a'], [1_000, 5_000, 7_000]) + ax.set_xscale('log') + ax.set_xlim(1, None) + ax.bar_label(bar_container, label_type='center') + ax.set_axis_off() + + def test_bar_label_location_errorbars(): ax = plt.gca() xs, heights = [1, 2], [3, -4] @@ -7046,14 +8021,24 @@ def test_bar_label_location_errorbars(): assert labels[1].get_va() == 'top' -def test_bar_label_fmt(): +@pytest.mark.parametrize('fmt', [ + '%.2f', '{:.2f}', '{:.2f}'.format +]) +def test_bar_label_fmt(fmt): ax = plt.gca() rects = ax.bar([1, 2], [3, -4]) - labels = ax.bar_label(rects, fmt='%.2f') + labels = ax.bar_label(rects, fmt=fmt) assert labels[0].get_text() == '3.00' assert labels[1].get_text() == '-4.00' +def test_bar_label_fmt_error(): + ax = plt.gca() + rects = ax.bar([1, 2], [3, -4]) + with pytest.raises(TypeError, match='str or callable'): + _ = ax.bar_label(rects, fmt=10) + + def test_bar_label_labels(): ax = plt.gca() rects = ax.bar([1, 2], [3, -4]) @@ -7071,19 +8056,390 @@ def test_bar_label_nan_ydata(): assert labels[0].get_va() == 'bottom' +def test_bar_label_nan_ydata_inverted(): + ax = plt.gca() + ax.yaxis_inverted() + bars = ax.bar([2, 3], [np.nan, 1]) + labels = ax.bar_label(bars) + assert [l.get_text() for l in labels] == ['', '1'] + assert labels[0].xy == (2, 0) + assert labels[0].get_va() == 'bottom' + + +def test_nan_barlabels(): + fig, ax = plt.subplots() + bars = ax.bar([1, 2, 3], [np.nan, 1, 2], yerr=[0.2, 0.4, 0.6]) + labels = ax.bar_label(bars) + assert [l.get_text() for l in labels] == ['', '1', '2'] + assert np.allclose(ax.get_ylim(), (0.0, 3.0)) + + fig, ax = plt.subplots() + bars = ax.bar([1, 2, 3], [0, 1, 2], yerr=[0.2, np.nan, 0.6]) + labels = ax.bar_label(bars) + assert [l.get_text() for l in labels] == ['0', '1', '2'] + assert np.allclose(ax.get_ylim(), (-0.5, 3.0)) + + fig, ax = plt.subplots() + bars = ax.bar([1, 2, 3], [np.nan, 1, 2], yerr=[np.nan, np.nan, 0.6]) + labels = ax.bar_label(bars) + assert [l.get_text() for l in labels] == ['', '1', '2'] + assert np.allclose(ax.get_ylim(), (0.0, 3.0)) + + def test_patch_bounds(): # PR 19078 fig, ax = plt.subplots() - ax.add_patch(mpatches.Wedge((0, -1), 1.05, 60, 120, 0.1)) + ax.add_patch(mpatches.Wedge((0, -1), 1.05, 60, 120, width=0.1)) bot = 1.9*np.sin(15*np.pi/180)**2 np.testing.assert_array_almost_equal_nulp( np.array((-0.525, -(bot+0.05), 1.05, bot+0.1)), ax.dataLim.bounds, 16) -@pytest.mark.style('default') +@mpl.style.context('default') def test_warn_ignored_scatter_kwargs(): with pytest.warns(UserWarning, match=r"You passed a edgecolor/edgecolors"): + plt.scatter([0], [0], marker="+", s=500, facecolor="r", edgecolor="b") + + +def test_artist_sublists(): + fig, ax = plt.subplots() + lines = [ax.plot(np.arange(i, i + 5))[0] for i in range(6)] + col = ax.scatter(np.arange(5), np.arange(5)) + im = ax.imshow(np.zeros((5, 5))) + patch = ax.add_patch(mpatches.Rectangle((0, 0), 5, 5)) + text = ax.text(0, 0, 'foo') + + # Get items, which should not be mixed. + assert list(ax.collections) == [col] + assert list(ax.images) == [im] + assert list(ax.lines) == lines + assert list(ax.patches) == [patch] + assert not ax.tables + assert list(ax.texts) == [text] + + # Get items should work like lists/tuple. + assert ax.lines[0] is lines[0] + assert ax.lines[-1] is lines[-1] + with pytest.raises(IndexError, match='out of range'): + ax.lines[len(lines) + 1] + + # Adding to other lists should produce a regular list. + assert ax.lines + [1, 2, 3] == [*lines, 1, 2, 3] + assert [1, 2, 3] + ax.lines == [1, 2, 3, *lines] + + # Adding to other tuples should produce a regular tuples. + assert ax.lines + (1, 2, 3) == (*lines, 1, 2, 3) + assert (1, 2, 3) + ax.lines == (1, 2, 3, *lines) + + # Lists should be empty after removing items. + col.remove() + assert not ax.collections + im.remove() + assert not ax.images + patch.remove() + assert not ax.patches + assert not ax.tables + text.remove() + assert not ax.texts + + for ln in ax.lines: + ln.remove() + assert len(ax.lines) == 0 + + +def test_empty_line_plots(): + # Incompatible nr columns, plot "nothing" + x = np.ones(10) + y = np.ones((10, 0)) + _, ax = plt.subplots() + line = ax.plot(x, y) + assert len(line) == 0 + + # Ensure plot([],[]) creates line + _, ax = plt.subplots() + line = ax.plot([], []) + assert len(line) == 1 + + +@pytest.mark.parametrize('fmt, match', ( + ("f", r"'f' is not a valid format string \(unrecognized character 'f'\)"), + ("o+", r"'o\+' is not a valid format string \(two marker symbols\)"), + (":-", r"':-' is not a valid format string \(two linestyle symbols\)"), + ("rk", r"'rk' is not a valid format string \(two color symbols\)"), + (":o-r", r"':o-r' is not a valid format string \(two linestyle symbols\)"), +)) +@pytest.mark.parametrize("data", [None, {"string": range(3)}]) +def test_plot_format_errors(fmt, match, data): + fig, ax = plt.subplots() + if data is not None: + match = match.replace("not", "neither a data key nor") + with pytest.raises(ValueError, match=r"\A" + match + r"\Z"): + ax.plot("string", fmt, data=data) + + +def test_plot_format(): + fig, ax = plt.subplots() + line = ax.plot([1, 2, 3], '1.0') + assert line[0].get_color() == (1.0, 1.0, 1.0, 1.0) + assert line[0].get_marker() == 'None' + fig, ax = plt.subplots() + line = ax.plot([1, 2, 3], '1') + assert line[0].get_marker() == '1' + fig, ax = plt.subplots() + line = ax.plot([1, 2], [1, 2], '1.0', "1") + fig.canvas.draw() + assert line[0].get_color() == (1.0, 1.0, 1.0, 1.0) + assert ax.get_yticklabels()[0].get_text() == '1' + fig, ax = plt.subplots() + line = ax.plot([1, 2], [1, 2], '1', "1.0") + fig.canvas.draw() + assert line[0].get_marker() == '1' + assert ax.get_yticklabels()[0].get_text() == '1.0' + fig, ax = plt.subplots() + line = ax.plot([1, 2, 3], 'k3') + assert line[0].get_marker() == '3' + assert line[0].get_color() == 'k' + + +def test_automatic_legend(): + fig, ax = plt.subplots() + ax.plot("a", "b", data={"d": 2}) + leg = ax.legend() + fig.canvas.draw() + assert leg.get_texts()[0].get_text() == 'a' + assert ax.get_yticklabels()[0].get_text() == 'a' + + fig, ax = plt.subplots() + ax.plot("a", "b", "c", data={"d": 2}) + leg = ax.legend() + fig.canvas.draw() + assert leg.get_texts()[0].get_text() == 'b' + assert ax.get_xticklabels()[0].get_text() == 'a' + assert ax.get_yticklabels()[0].get_text() == 'b' + + +def test_plot_errors(): + with pytest.raises(TypeError, match=r"plot\(\) got an unexpected keyword"): + plt.plot([1, 2, 3], x=1) + with pytest.raises(ValueError, match=r"plot\(\) with multiple groups"): + plt.plot([1, 2, 3], [1, 2, 3], [2, 3, 4], [2, 3, 4], label=['1', '2']) + with pytest.raises(ValueError, match="x and y must have same first"): + plt.plot([1, 2, 3], [1]) + with pytest.raises(ValueError, match="x and y can be no greater than"): + plt.plot(np.ones((2, 2, 2))) + with pytest.raises(ValueError, match="Using arbitrary long args with"): + plt.plot("a", "b", "c", "d", data={"a": 2}) + + +def test_clim(): + ax = plt.figure().add_subplot() + for plot_method in [ + partial(ax.scatter, range(3), range(3), c=range(3)), + partial(ax.imshow, [[0, 1], [2, 3]]), + partial(ax.pcolor, [[0, 1], [2, 3]]), + partial(ax.pcolormesh, [[0, 1], [2, 3]]), + partial(ax.pcolorfast, [[0, 1], [2, 3]]), + ]: + clim = (7, 8) + norm = plot_method(clim=clim).norm + assert (norm.vmin, norm.vmax) == clim + + +def test_bezier_autoscale(): + # Check that bezier curves autoscale to their curves, and not their + # control points + verts = [[-1, 0], + [0, -1], + [1, 0], + [1, 0]] + codes = [mpath.Path.MOVETO, + mpath.Path.CURVE3, + mpath.Path.CURVE3, + mpath.Path.CLOSEPOLY] + p = mpath.Path(verts, codes) + + fig, ax = plt.subplots() + ax.add_patch(mpatches.PathPatch(p)) + ax.autoscale() + # Bottom ylim should be at the edge of the curve (-0.5), and not include + # the control point (at -1) + assert ax.get_ylim()[0] == -0.5 + + +def test_small_autoscale(): + # Check that paths with small values autoscale correctly #24097. + verts = np.array([ + [-5.45, 0.00], [-5.45, 0.00], [-5.29, 0.00], [-5.29, 0.00], + [-5.13, 0.00], [-5.13, 0.00], [-4.97, 0.00], [-4.97, 0.00], + [-4.81, 0.00], [-4.81, 0.00], [-4.65, 0.00], [-4.65, 0.00], + [-4.49, 0.00], [-4.49, 0.00], [-4.33, 0.00], [-4.33, 0.00], + [-4.17, 0.00], [-4.17, 0.00], [-4.01, 0.00], [-4.01, 0.00], + [-3.85, 0.00], [-3.85, 0.00], [-3.69, 0.00], [-3.69, 0.00], + [-3.53, 0.00], [-3.53, 0.00], [-3.37, 0.00], [-3.37, 0.00], + [-3.21, 0.00], [-3.21, 0.01], [-3.05, 0.01], [-3.05, 0.01], + [-2.89, 0.01], [-2.89, 0.01], [-2.73, 0.01], [-2.73, 0.02], + [-2.57, 0.02], [-2.57, 0.04], [-2.41, 0.04], [-2.41, 0.04], + [-2.25, 0.04], [-2.25, 0.06], [-2.09, 0.06], [-2.09, 0.08], + [-1.93, 0.08], [-1.93, 0.10], [-1.77, 0.10], [-1.77, 0.12], + [-1.61, 0.12], [-1.61, 0.14], [-1.45, 0.14], [-1.45, 0.17], + [-1.30, 0.17], [-1.30, 0.19], [-1.14, 0.19], [-1.14, 0.22], + [-0.98, 0.22], [-0.98, 0.25], [-0.82, 0.25], [-0.82, 0.27], + [-0.66, 0.27], [-0.66, 0.29], [-0.50, 0.29], [-0.50, 0.30], + [-0.34, 0.30], [-0.34, 0.32], [-0.18, 0.32], [-0.18, 0.33], + [-0.02, 0.33], [-0.02, 0.32], [0.13, 0.32], [0.13, 0.33], [0.29, 0.33], + [0.29, 0.31], [0.45, 0.31], [0.45, 0.30], [0.61, 0.30], [0.61, 0.28], + [0.77, 0.28], [0.77, 0.25], [0.93, 0.25], [0.93, 0.22], [1.09, 0.22], + [1.09, 0.19], [1.25, 0.19], [1.25, 0.17], [1.41, 0.17], [1.41, 0.15], + [1.57, 0.15], [1.57, 0.12], [1.73, 0.12], [1.73, 0.10], [1.89, 0.10], + [1.89, 0.08], [2.05, 0.08], [2.05, 0.07], [2.21, 0.07], [2.21, 0.05], + [2.37, 0.05], [2.37, 0.04], [2.53, 0.04], [2.53, 0.02], [2.69, 0.02], + [2.69, 0.02], [2.85, 0.02], [2.85, 0.01], [3.01, 0.01], [3.01, 0.01], + [3.17, 0.01], [3.17, 0.00], [3.33, 0.00], [3.33, 0.00], [3.49, 0.00], + [3.49, 0.00], [3.65, 0.00], [3.65, 0.00], [3.81, 0.00], [3.81, 0.00], + [3.97, 0.00], [3.97, 0.00], [4.13, 0.00], [4.13, 0.00], [4.29, 0.00], + [4.29, 0.00], [4.45, 0.00], [4.45, 0.00], [4.61, 0.00], [4.61, 0.00], + [4.77, 0.00], [4.77, 0.00], [4.93, 0.00], [4.93, 0.00], + ]) + + minx = np.min(verts[:, 0]) + miny = np.min(verts[:, 1]) + maxx = np.max(verts[:, 0]) + maxy = np.max(verts[:, 1]) + + p = mpath.Path(verts) + + fig, ax = plt.subplots() + ax.add_patch(mpatches.PathPatch(p)) + ax.autoscale() + + assert ax.get_xlim()[0] <= minx + assert ax.get_xlim()[1] >= maxx + assert ax.get_ylim()[0] <= miny + assert ax.get_ylim()[1] >= maxy + + +def test_get_xticklabel(): + fig, ax = plt.subplots() + ax.plot(np.arange(10)) + for ind in range(10): + assert ax.get_xticklabels()[ind].get_text() == f'{ind}' + assert ax.get_yticklabels()[ind].get_text() == f'{ind}' + + +def test_bar_leading_nan(): + + barx = np.arange(3, dtype=float) + barheights = np.array([0.5, 1.5, 2.0]) + barstarts = np.array([0.77]*3) + + barx[0] = np.NaN + + fig, ax = plt.subplots() + + bars = ax.bar(barx, barheights, bottom=barstarts) + + hbars = ax.barh(barx, barheights, left=barstarts) + + for bar_set in (bars, hbars): + # the first bar should have a nan in the location + nanful, *rest = bar_set + assert (~np.isfinite(nanful.xy)).any() + assert np.isfinite(nanful.get_width()) + for b in rest: + assert np.isfinite(b.xy).all() + assert np.isfinite(b.get_width()) - c = plt.scatter( - [0], [0], marker="+", s=500, facecolor="r", edgecolor="b" + +@check_figures_equal(extensions=["png"]) +def test_bar_all_nan(fig_test, fig_ref): + mpl.style.use("mpl20") + ax_test = fig_test.subplots() + ax_ref = fig_ref.subplots() + + ax_test.bar([np.nan], [np.nan]) + ax_test.bar([1], [1]) + + ax_ref.bar([1], [1]).remove() + ax_ref.bar([1], [1]) + + +@image_comparison(["extent_units.png"], style="mpl20") +def test_extent_units(): + _, axs = plt.subplots(2, 2) + date_first = np.datetime64('2020-01-01', 'D') + date_last = np.datetime64('2020-01-11', 'D') + arr = [[i+j for i in range(10)] for j in range(10)] + + axs[0, 0].set_title('Date extents on y axis') + im = axs[0, 0].imshow(arr, origin='lower', + extent=[1, 11, date_first, date_last], + cmap=mpl.colormaps["plasma"]) + + axs[0, 1].set_title('Date extents on x axis (Day of Jan 2020)') + im = axs[0, 1].imshow(arr, origin='lower', + extent=[date_first, date_last, 1, 11], + cmap=mpl.colormaps["plasma"]) + axs[0, 1].xaxis.set_major_formatter(mdates.DateFormatter('%d')) + + im = axs[1, 0].imshow(arr, origin='lower', + extent=[date_first, date_last, + date_first, date_last], + cmap=mpl.colormaps["plasma"]) + axs[1, 0].xaxis.set_major_formatter(mdates.DateFormatter('%d')) + axs[1, 0].set(xlabel='Day of Jan 2020') + + im = axs[1, 1].imshow(arr, origin='lower', + cmap=mpl.colormaps["plasma"]) + im.set_extent([date_last, date_first, date_last, date_first]) + axs[1, 1].xaxis.set_major_formatter(mdates.DateFormatter('%d')) + axs[1, 1].set(xlabel='Day of Jan 2020') + + with pytest.raises(TypeError, match=r"set_extent\(\) got an unexpected"): + im.set_extent([2, 12, date_first, date_last], clip=False) + + +def test_cla_clears_children_axes_and_fig(): + fig, ax = plt.subplots() + lines = ax.plot([], [], [], []) + img = ax.imshow([[1]]) + for art in lines + [img]: + assert art.axes is ax + assert art.figure is fig + ax.clear() + for art in lines + [img]: + assert art.axes is None + assert art.figure is None + + +def test_scatter_color_repr_error(): + + def get_next_color(): + return 'blue' # pragma: no cover + msg = ( + r"'c' argument must be a color, a sequence of colors" + r", or a sequence of numbers, not 'red\\n'" ) + with pytest.raises(ValueError, match=msg): + c = 'red\n' + mpl.axes.Axes._parse_scatter_color_args( + c, None, kwargs={}, xsize=2, get_next_color_func=get_next_color) + + +def test_zorder_and_explicit_rasterization(): + fig, ax = plt.subplots() + ax.set_rasterization_zorder(5) + ln, = ax.plot(range(5), rasterized=True, zorder=1) + with io.BytesIO() as b: + fig.savefig(b, format='pdf') + + +@mpl.style.context('default') +def test_rc_axes_label_formatting(): + mpl.rcParams['axes.labelcolor'] = 'red' + mpl.rcParams['axes.labelsize'] = 20 + mpl.rcParams['axes.labelweight'] = 'bold' + + ax = plt.axes() + assert ax.xaxis.label.get_color() == 'red' + assert ax.xaxis.label.get_fontsize() == 20 + assert ax.xaxis.label.get_fontweight() == 'bold' diff --git a/lib/matplotlib/tests/test_backend_bases.py b/lib/matplotlib/tests/test_backend_bases.py index 1550d3256c04..4cbd1bc98b67 100644 --- a/lib/matplotlib/tests/test_backend_bases.py +++ b/lib/matplotlib/tests/test_backend_bases.py @@ -1,21 +1,22 @@ import re -from matplotlib.testing import _check_for_pgf +from matplotlib import path, transforms from matplotlib.backend_bases import ( - FigureCanvasBase, LocationEvent, MouseButton, MouseEvent, + FigureCanvasBase, KeyEvent, LocationEvent, MouseButton, MouseEvent, NavigationToolbar2, RendererBase) -from matplotlib.backend_tools import (ToolZoom, ToolPan, RubberbandBase, - ToolViewsPositions, _views_positions) +from matplotlib.backend_tools import RubberbandBase from matplotlib.figure import Figure +from matplotlib.testing._markers import needs_pgf_xelatex import matplotlib.pyplot as plt -import matplotlib.transforms as transforms -import matplotlib.path as path import numpy as np import pytest -needs_xelatex = pytest.mark.skipif(not _check_for_pgf('xelatex'), - reason='xelatex + pgf is required') + +_EXPECTED_WARNING_TOOLMANAGER = ( + r"Treat the new Tool classes introduced in " + r"v[0-9]*.[0-9]* as experimental for now; " + "the API and rcParam may change in future versions.") def test_uses_per_path(): @@ -34,8 +35,7 @@ def check(master_transform, paths, all_transforms, gc = rb.new_gc() ids = [path_id for xo, yo, path_id, gc0, rgbFace in rb._iter_collection( - gc, master_transform, all_transforms, - range(len(raw_paths)), offsets, + gc, range(len(raw_paths)), offsets, transforms.AffineDeltaTransform(master_transform), facecolors, edgecolors, [], [], [False], [], 'screen')] @@ -124,12 +124,18 @@ def test_pick(): fig = plt.figure() fig.text(.5, .5, "hello", ha="center", va="center", picker=True) fig.canvas.draw() + picks = [] - fig.canvas.mpl_connect("pick_event", lambda event: picks.append(event)) - start_event = MouseEvent( - "button_press_event", fig.canvas, *fig.transFigure.transform((.5, .5)), - MouseButton.LEFT) - fig.canvas.callbacks.process(start_event.name, start_event) + def handle_pick(event): + assert event.mouseevent.key == "a" + picks.append(event) + fig.canvas.mpl_connect("pick_event", handle_pick) + + KeyEvent("key_press_event", fig.canvas, "a")._process() + MouseEvent("button_press_event", fig.canvas, + *fig.transFigure.transform((.5, .5)), + MouseButton.LEFT)._process() + KeyEvent("key_release_event", fig.canvas, "a")._process() assert len(picks) == 1 @@ -181,23 +187,83 @@ def test_interactive_zoom(): assert not ax.get_autoscalex_on() and not ax.get_autoscaley_on() +def test_widgetlock_zoompan(): + fig, ax = plt.subplots() + ax.plot([0, 1], [0, 1]) + fig.canvas.widgetlock(ax) + tb = NavigationToolbar2(fig.canvas) + tb.zoom() + assert ax.get_navigate_mode() is None + tb.pan() + assert ax.get_navigate_mode() is None + + +@pytest.mark.parametrize("plot_func", ["imshow", "contourf"]) +@pytest.mark.parametrize("orientation", ["vertical", "horizontal"]) +@pytest.mark.parametrize("tool,button,expected", + [("zoom", MouseButton.LEFT, (4, 6)), # zoom in + ("zoom", MouseButton.RIGHT, (-20, 30)), # zoom out + ("pan", MouseButton.LEFT, (-2, 8)), + ("pan", MouseButton.RIGHT, (1.47, 7.78))]) # zoom +def test_interactive_colorbar(plot_func, orientation, tool, button, expected): + fig, ax = plt.subplots() + data = np.arange(12).reshape((4, 3)) + vmin0, vmax0 = 0, 10 + coll = getattr(ax, plot_func)(data, vmin=vmin0, vmax=vmax0) + + cb = fig.colorbar(coll, ax=ax, orientation=orientation) + if plot_func == "contourf": + # Just determine we can't navigate and exit out of the test + assert not cb.ax.get_navigate() + return + + assert cb.ax.get_navigate() + + # Mouse from 4 to 6 (data coordinates, "d"). + vmin, vmax = 4, 6 + # The y coordinate doesn't matter, it just needs to be between 0 and 1 + # However, we will set d0/d1 to the same y coordinate to test that small + # pixel changes in that coordinate doesn't cancel the zoom like a normal + # axes would. + d0 = (vmin, 0.5) + d1 = (vmax, 0.5) + # Swap them if the orientation is vertical + if orientation == "vertical": + d0 = d0[::-1] + d1 = d1[::-1] + # 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 = cb.ax.transData.transform(d0).astype(int) + s1 = cb.ax.transData.transform(d1).astype(int) + + # Set up the mouse movements + start_event = MouseEvent( + "button_press_event", fig.canvas, *s0, button) + stop_event = MouseEvent( + "button_release_event", fig.canvas, *s1, button) + + tb = NavigationToolbar2(fig.canvas) + if tool == "zoom": + tb.zoom() + tb.press_zoom(start_event) + tb.drag_zoom(stop_event) + tb.release_zoom(stop_event) + else: + tb.pan() + tb.press_pan(start_event) + tb.drag_pan(stop_event) + tb.release_pan(stop_event) + + # Should be close, but won't be exact due to screen integer resolution + assert (cb.vmin, cb.vmax) == pytest.approx(expected, abs=0.15) + + def test_toolbar_zoompan(): - expected_warning_regex = ( - r"Treat the new Tool classes introduced in " - r"v[0-9]*.[0-9]* as experimental for now; " - "the API and rcParam may change in future versions.") - with pytest.warns(UserWarning, match=expected_warning_regex): + with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER): plt.rcParams['toolbar'] = 'toolmanager' ax = plt.gca() assert ax.get_navigate_mode() is None - ax.figure.canvas.manager.toolmanager.add_tool(name="zoom", - tool=ToolZoom) - ax.figure.canvas.manager.toolmanager.add_tool(name="pan", - tool=ToolPan) - ax.figure.canvas.manager.toolmanager.add_tool(name=_views_positions, - tool=ToolViewsPositions) - ax.figure.canvas.manager.toolmanager.add_tool(name='rubberband', - tool=RubberbandBase) ax.figure.canvas.manager.toolmanager.trigger_tool('zoom') assert ax.get_navigate_mode() == "ZOOM" ax.figure.canvas.manager.toolmanager.trigger_tool('pan') @@ -205,7 +271,8 @@ def test_toolbar_zoompan(): @pytest.mark.parametrize( - "backend", ['svg', 'ps', 'pdf', pytest.param('pgf', marks=needs_xelatex)] + "backend", ['svg', 'ps', 'pdf', + pytest.param('pgf', marks=needs_pgf_xelatex)] ) def test_draw(backend): from matplotlib.figure import Figure @@ -237,3 +304,98 @@ def test_draw(backend): for ref, test in zip(layed_out_pos_agg, layed_out_pos_test): np.testing.assert_allclose(ref, test, atol=0.005) + + +@pytest.mark.parametrize( + "key,mouseend,expectedxlim,expectedylim", + [(None, (0.2, 0.2), (3.49, 12.49), (2.7, 11.7)), + (None, (0.2, 0.5), (3.49, 12.49), (0, 9)), + (None, (0.5, 0.2), (0, 9), (2.7, 11.7)), + (None, (0.5, 0.5), (0, 9), (0, 9)), # No move + (None, (0.8, 0.25), (-3.47, 5.53), (2.25, 11.25)), + (None, (0.2, 0.25), (3.49, 12.49), (2.25, 11.25)), + (None, (0.8, 0.85), (-3.47, 5.53), (-3.14, 5.86)), + (None, (0.2, 0.85), (3.49, 12.49), (-3.14, 5.86)), + ("shift", (0.2, 0.4), (3.49, 12.49), (0, 9)), # snap to x + ("shift", (0.4, 0.2), (0, 9), (2.7, 11.7)), # snap to y + ("shift", (0.2, 0.25), (3.49, 12.49), (3.49, 12.49)), # snap to diagonal + ("shift", (0.8, 0.25), (-3.47, 5.53), (3.47, 12.47)), # snap to diagonal + ("shift", (0.8, 0.9), (-3.58, 5.41), (-3.58, 5.41)), # snap to diagonal + ("shift", (0.2, 0.85), (3.49, 12.49), (-3.49, 5.51)), # snap to diagonal + ("x", (0.2, 0.1), (3.49, 12.49), (0, 9)), # only x + ("y", (0.1, 0.2), (0, 9), (2.7, 11.7)), # only y + ("control", (0.2, 0.2), (3.49, 12.49), (3.49, 12.49)), # diagonal + ("control", (0.4, 0.2), (2.72, 11.72), (2.72, 11.72)), # diagonal + ]) +def test_interactive_pan(key, mouseend, expectedxlim, expectedylim): + fig, ax = plt.subplots() + ax.plot(np.arange(10)) + assert ax.get_navigate() + # Set equal aspect ratio to easier see diagonal snap + ax.set_aspect('equal') + + # Mouse move starts from 0.5, 0.5 + mousestart = (0.5, 0.5) + # 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. + sstart = ax.transData.transform(mousestart).astype(int) + send = ax.transData.transform(mouseend).astype(int) + + # Set up the mouse movements + start_event = MouseEvent( + "button_press_event", fig.canvas, *sstart, button=MouseButton.LEFT, + key=key) + stop_event = MouseEvent( + "button_release_event", fig.canvas, *send, button=MouseButton.LEFT, + key=key) + + tb = NavigationToolbar2(fig.canvas) + tb.pan() + tb.press_pan(start_event) + tb.drag_pan(stop_event) + tb.release_pan(stop_event) + # Should be close, but won't be exact due to screen integer resolution + assert tuple(ax.get_xlim()) == pytest.approx(expectedxlim, abs=0.02) + assert tuple(ax.get_ylim()) == pytest.approx(expectedylim, abs=0.02) + + +def test_toolmanager_remove(): + with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER): + plt.rcParams['toolbar'] = 'toolmanager' + fig = plt.gcf() + initial_len = len(fig.canvas.manager.toolmanager.tools) + assert 'forward' in fig.canvas.manager.toolmanager.tools + fig.canvas.manager.toolmanager.remove_tool('forward') + assert len(fig.canvas.manager.toolmanager.tools) == initial_len - 1 + assert 'forward' not in fig.canvas.manager.toolmanager.tools + + +def test_toolmanager_get_tool(): + with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER): + plt.rcParams['toolbar'] = 'toolmanager' + fig = plt.gcf() + rubberband = fig.canvas.manager.toolmanager.get_tool('rubberband') + assert isinstance(rubberband, RubberbandBase) + assert fig.canvas.manager.toolmanager.get_tool(rubberband) is rubberband + with pytest.warns(UserWarning, + match="ToolManager does not control tool 'foo'"): + assert fig.canvas.manager.toolmanager.get_tool('foo') is None + assert fig.canvas.manager.toolmanager.get_tool('foo', warn=False) is None + + with pytest.warns(UserWarning, + match="ToolManager does not control tool 'foo'"): + assert fig.canvas.manager.toolmanager.trigger_tool('foo') is None + + +def test_toolmanager_update_keymap(): + with pytest.warns(UserWarning, match=_EXPECTED_WARNING_TOOLMANAGER): + plt.rcParams['toolbar'] = 'toolmanager' + fig = plt.gcf() + assert 'v' in fig.canvas.manager.toolmanager.get_tool_keymap('forward') + with pytest.warns(UserWarning, + match="Key c changed from back to forward"): + fig.canvas.manager.toolmanager.update_keymap('forward', 'c') + assert fig.canvas.manager.toolmanager.get_tool_keymap('forward') == ['c'] + with pytest.raises(KeyError, match="'foo' not in Tools"): + fig.canvas.manager.toolmanager.update_keymap('foo', 'c') diff --git a/lib/matplotlib/tests/test_backend_gtk3.py b/lib/matplotlib/tests/test_backend_gtk3.py index 5442930d117f..937ddef5a13f 100644 --- a/lib/matplotlib/tests/test_backend_gtk3.py +++ b/lib/matplotlib/tests/test_backend_gtk3.py @@ -6,7 +6,7 @@ pytest.importorskip("matplotlib.backends.backend_gtk3agg") -@pytest.mark.backend("gtk3agg") +@pytest.mark.backend("gtk3agg", skip_on_importerror=True) def test_correct_key(): pytest.xfail("test_widget_send_event is not triggering key_press_event") diff --git a/lib/matplotlib/tests/test_backend_macosx.py b/lib/matplotlib/tests/test_backend_macosx.py new file mode 100644 index 000000000000..c460da374c8c --- /dev/null +++ b/lib/matplotlib/tests/test_backend_macosx.py @@ -0,0 +1,46 @@ +import os + +import pytest + +import matplotlib as mpl +import matplotlib.pyplot as plt +try: + from matplotlib.backends import _macosx +except ImportError: + pytest.skip("These are mac only tests", allow_module_level=True) + + +@pytest.mark.backend('macosx') +def test_cached_renderer(): + # Make sure that figures have an associated renderer after + # a fig.canvas.draw() call + fig = plt.figure(1) + fig.canvas.draw() + assert fig.canvas.get_renderer()._renderer is not None + + fig = plt.figure(2) + fig.draw_without_rendering() + assert fig.canvas.get_renderer()._renderer is not None + + +@pytest.mark.backend('macosx') +def test_savefig_rcparam(monkeypatch, tmp_path): + + def new_choose_save_file(title, directory, filename): + # Replacement function instead of opening a GUI window + # Make a new directory for testing the update of the rcParams + assert directory == str(tmp_path) + os.makedirs(f"{directory}/test") + return f"{directory}/test/{filename}" + + monkeypatch.setattr(_macosx, "choose_save_file", new_choose_save_file) + fig = plt.figure() + with mpl.rc_context({"savefig.directory": tmp_path}): + fig.canvas.toolbar.save_figure() + # Check the saved location got created + save_file = f"{tmp_path}/test/{fig.canvas.get_default_filename()}" + assert os.path.exists(save_file) + + # Check the savefig.directory rcParam got updated because + # we added a subdirectory "test" + assert mpl.rcParams["savefig.directory"] == f"{tmp_path}/test" diff --git a/lib/matplotlib/tests/test_backend_nbagg.py b/lib/matplotlib/tests/test_backend_nbagg.py index a7060ed2a7fd..4ebf3e1f56d1 100644 --- a/lib/matplotlib/tests/test_backend_nbagg.py +++ b/lib/matplotlib/tests/test_backend_nbagg.py @@ -6,6 +6,8 @@ import pytest nbformat = pytest.importorskip('nbformat') +pytest.importorskip('nbconvert') +pytest.importorskip('ipykernel') # From https://blog.thedataincubator.com/2016/06/testing-jupyter-notebooks/ diff --git a/lib/matplotlib/tests/test_backend_pdf.py b/lib/matplotlib/tests/test_backend_pdf.py index 06bfe1cdb1ab..8ffca8295ea5 100644 --- a/lib/matplotlib/tests/test_backend_pdf.py +++ b/lib/matplotlib/tests/test_backend_pdf.py @@ -9,14 +9,17 @@ import pytest import matplotlib as mpl -from matplotlib import dviread, pyplot as plt, checkdep_usetex, rcParams +from matplotlib import ( + pyplot as plt, rcParams, font_manager as fm +) +from matplotlib.cbook import _get_data_path +from matplotlib.ft2font import FT2Font +from matplotlib.font_manager import findfont, FontProperties +from matplotlib.backends._backend_pdf_ps import get_glyphs_subset from matplotlib.backends.backend_pdf import PdfPages +from matplotlib.patches import Rectangle from matplotlib.testing.decorators import check_figures_equal, image_comparison - - -needs_usetex = pytest.mark.skipif( - not checkdep_usetex(True), - reason="This test needs a TeX installation") +from matplotlib.testing._markers import needs_usetex @image_comparison(['pdf_use14corefonts.pdf']) @@ -39,12 +42,20 @@ def test_use14corefonts(): ax.axhline(0.5, linewidth=0.5) -def test_type42(): - rcParams['pdf.fonttype'] = 42 +@pytest.mark.parametrize('fontname, fontfile', [ + ('DejaVu Sans', 'DejaVuSans.ttf'), + ('WenQuanYi Zen Hei', 'wqy-zenhei.ttc'), +]) +@pytest.mark.parametrize('fonttype', [3, 42]) +def test_embed_fonts(fontname, fontfile, fonttype): + if Path(findfont(FontProperties(family=[fontname]))).name != fontfile: + pytest.skip(f'Font {fontname!r} may be missing') + rcParams['pdf.fonttype'] = fonttype fig, ax = plt.subplots() ax.plot([1, 2, 3]) - fig.savefig(io.BytesIO()) + ax.set_title('Axes Title', font=fontname) + fig.savefig(io.BytesIO(), format='pdf') def test_multipage_pagecount(): @@ -234,10 +245,37 @@ def test_text_urls(): (a for a in annots if a.A.URI == f'{test_url}{fragment}'), None) assert annot is not None + assert getattr(annot, 'QuadPoints', None) is None # Positions in points (72 per inch.) assert annot.Rect[1] == decimal.Decimal(y) * 72 +def test_text_rotated_urls(): + pikepdf = pytest.importorskip('pikepdf') + + test_url = 'https://test_text_urls.matplotlib.org/' + + fig = plt.figure(figsize=(1, 1)) + fig.text(0.1, 0.1, 'N', rotation=45, url=f'{test_url}') + + with io.BytesIO() as fd: + fig.savefig(fd, format='pdf') + + with pikepdf.Pdf.open(fd) as pdf: + annots = pdf.pages[0].Annots + + # Iteration over Annots must occur within the context manager, + # otherwise it may fail depending on the pdf structure. + annot = next( + (a for a in annots if a.A.URI == f'{test_url}'), + None) + assert annot is not None + assert getattr(annot, 'QuadPoints', None) is not None + # Positions in points (72 per inch) + assert annot.Rect[0] == \ + annot.QuadPoints[6] - decimal.Decimal('0.00001') + + @needs_usetex def test_text_urls_tex(): pikepdf = pytest.importorskip('pikepdf') @@ -273,8 +311,8 @@ def test_hatching_legend(): """Test for correct hatching on patches in legend""" fig = plt.figure(figsize=(1, 2)) - a = plt.Rectangle([0, 0], 0, 0, facecolor="green", hatch="XXXX") - b = plt.Rectangle([0, 0], 0, 0, facecolor="blue", hatch="XXXX") + a = Rectangle([0, 0], 0, 0, facecolor="green", hatch="XXXX") + b = Rectangle([0, 0], 0, 0, facecolor="blue", hatch="XXXX") fig.legend([a, b, a, b], ["", "", "", ""]) @@ -291,24 +329,7 @@ def test_grayscale_alpha(): ax.set_yticks([]) -# This tests tends to hit a TeX cache lock on AppVeyor. -@pytest.mark.flaky(reruns=3) -@needs_usetex -def test_missing_psfont(monkeypatch): - """An error is raised if a TeX font lacks a Type-1 equivalent""" - def psfont(*args, **kwargs): - return dviread.PsFont(texname='texfont', psname='Some Font', - effects=None, encoding=None, filename=None) - - monkeypatch.setattr(dviread.PsfontsMap, '__getitem__', psfont) - rcParams['text.usetex'] = True - fig, ax = plt.subplots() - ax.text(0.5, 0.5, 'hello') - with NamedTemporaryFile() as tmpfile, pytest.raises(ValueError): - fig.savefig(tmpfile, format='pdf') - - -@pytest.mark.style('default') +@mpl.style.context('default') @check_figures_equal(extensions=["pdf", "eps"]) def test_pdf_eps_savefig_when_color_is_none(fig_test, fig_ref): ax_test = fig_test.add_subplot() @@ -339,3 +360,54 @@ def test_kerning(): s = "AVAVAVAVAVAVAVAV€AAVV" fig.text(0, .25, s, size=5) fig.text(0, .75, s, size=20) + + +def test_glyphs_subset(): + fpath = str(_get_data_path("fonts/ttf/DejaVuSerif.ttf")) + chars = "these should be subsetted! 1234567890" + + # non-subsetted FT2Font + nosubfont = FT2Font(fpath) + nosubfont.set_text(chars) + + # subsetted FT2Font + subfont = FT2Font(get_glyphs_subset(fpath, chars)) + subfont.set_text(chars) + + nosubcmap = nosubfont.get_charmap() + subcmap = subfont.get_charmap() + + # all unique chars must be available in subsetted font + assert set(chars) == set(chr(key) for key in subcmap.keys()) + + # subsetted font's charmap should have less entries + assert len(subcmap) < len(nosubcmap) + + # since both objects are assigned same characters + assert subfont.get_num_glyphs() == nosubfont.get_num_glyphs() + + +@image_comparison(["multi_font_type3.pdf"], tol=4.6) +def test_multi_font_type3(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27) + plt.rc('pdf', fonttype=3) + + fig = plt.figure() + fig.text(0.15, 0.475, "There are 几个汉字 in between!") + + +@image_comparison(["multi_font_type42.pdf"], tol=2.2) +def test_multi_font_type42(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27) + plt.rc('pdf', fonttype=42) + + fig = plt.figure() + fig.text(0.15, 0.475, "There are 几个汉字 in between!") diff --git a/lib/matplotlib/tests/test_backend_pgf.py b/lib/matplotlib/tests/test_backend_pgf.py index 040b2e714655..482bc073a766 100644 --- a/lib/matplotlib/tests/test_backend_pgf.py +++ b/lib/matplotlib/tests/test_backend_pgf.py @@ -4,28 +4,22 @@ import shutil import numpy as np +from packaging.version import parse as parse_version import pytest import matplotlib as mpl import matplotlib.pyplot as plt from matplotlib.testing import _has_tex_package, _check_for_pgf from matplotlib.testing.compare import compare_images, ImageComparisonFailure -from matplotlib.backends.backend_pgf import PdfPages, common_texification -from matplotlib.testing.decorators import (_image_directories, - check_figures_equal, - image_comparison) +from matplotlib.backends.backend_pgf import PdfPages, _tex_escape +from matplotlib.testing.decorators import ( + _image_directories, check_figures_equal, image_comparison) +from matplotlib.testing._markers import ( + needs_ghostscript, needs_pgf_lualatex, needs_pgf_pdflatex, + needs_pgf_xelatex) -baseline_dir, result_dir = _image_directories(lambda: 'dummy func') -needs_xelatex = pytest.mark.skipif(not _check_for_pgf('xelatex'), - reason='xelatex + pgf is required') -needs_pdflatex = pytest.mark.skipif(not _check_for_pgf('pdflatex'), - reason='pdflatex + pgf is required') -needs_lualatex = pytest.mark.skipif(not _check_for_pgf('lualatex'), - reason='lualatex + pgf is required') -needs_ghostscript = pytest.mark.skipif( - "eps" not in mpl.testing.compare.converter, - reason="This test needs a ghostscript installation") +baseline_dir, result_dir = _image_directories(lambda: 'dummy func') def compare_figure(fname, savefig_kwargs={}, tol=0): @@ -39,6 +33,23 @@ def compare_figure(fname, savefig_kwargs={}, tol=0): raise ImageComparisonFailure(err) +@pytest.mark.parametrize('plain_text, escaped_text', [ + (r'quad_sum: $\sum x_i^2$', r'quad_sum: \(\displaystyle \sum x_i^2\)'), + ('% not a comment', r'\% not a comment'), + ('^not', r'\^not'), +]) +def test_tex_escape(plain_text, escaped_text): + assert _tex_escape(plain_text) == escaped_text + + +@needs_pgf_xelatex +@pytest.mark.backend('pgf') +def test_tex_special_chars(tmp_path): + fig = plt.figure() + fig.text(.5, .5, "_^ $a_b^c$") + fig.savefig(tmp_path / "test.pdf") # Should not error. + + def create_figure(): plt.figure() x = np.linspace(0, 1, 15) @@ -65,19 +76,8 @@ def create_figure(): plt.ylim(0, 1) -@pytest.mark.parametrize('plain_text, escaped_text', [ - (r'quad_sum: $\sum x_i^2$', r'quad\_sum: \(\displaystyle \sum x_i^2\)'), - (r'no \$splits \$ here', r'no \$splits \$ here'), - ('with_underscores', r'with\_underscores'), - ('% not a comment', r'\% not a comment'), - ('^not', r'\^not'), -]) -def test_common_texification(plain_text, escaped_text): - assert common_texification(plain_text) == escaped_text - - # test compiling a figure to pdf with xelatex -@needs_xelatex +@needs_pgf_xelatex @pytest.mark.backend('pgf') @image_comparison(['pgf_xelatex.pdf'], style='default') def test_xelatex(): @@ -87,11 +87,19 @@ def test_xelatex(): create_figure() +try: + _old_gs_version = \ + mpl._get_executable_info('gs').version < parse_version('9.50') +except mpl.ExecutableNotFoundError: + _old_gs_version = True + + # test compiling a figure to pdf with pdflatex -@needs_pdflatex +@needs_pgf_pdflatex @pytest.mark.skipif(not _has_tex_package('ucs'), reason='needs ucs.sty') @pytest.mark.backend('pgf') -@image_comparison(['pgf_pdflatex.pdf'], style='default') +@image_comparison(['pgf_pdflatex.pdf'], style='default', + tol=11.7 if _old_gs_version else 0) def test_pdflatex(): if os.environ.get('APPVEYOR'): pytest.xfail("pdflatex test does not work on appveyor due to missing " @@ -107,9 +115,9 @@ def test_pdflatex(): # test updating the rc parameters for each figure -@needs_xelatex -@needs_pdflatex -@pytest.mark.style('default') +@needs_pgf_xelatex +@needs_pgf_pdflatex +@mpl.style.context('default') @pytest.mark.backend('pgf') def test_rcupdate(): rc_sets = [{'font.family': 'sans-serif', @@ -127,7 +135,7 @@ def test_rcupdate(): 'pgf.preamble': ('\\usepackage[utf8x]{inputenc}' '\\usepackage[T1]{fontenc}' '\\usepackage{sfmath}')}] - tol = [6, 0] + tol = [0, 13.2] if _old_gs_version else [0, 0] for i, rc_set in enumerate(rc_sets): with mpl.rc_context(rc_set): for substring, pkg in [('sfmath', 'sfmath'), ('utf8x', 'ucs')]: @@ -135,23 +143,31 @@ def test_rcupdate(): and not _has_tex_package(pkg)): pytest.skip(f'needs {pkg}.sty') create_figure() - compare_figure('pgf_rcupdate%d.pdf' % (i + 1), tol=tol[i]) + compare_figure(f'pgf_rcupdate{i + 1}.pdf', tol=tol[i]) # test backend-side clipping, since large numbers are not supported by TeX -@needs_xelatex -@pytest.mark.style('default') +@needs_pgf_xelatex +@mpl.style.context('default') @pytest.mark.backend('pgf') def test_pathclip(): + np.random.seed(19680801) mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False}) - plt.plot([0., 1e100], [0., 1e100]) - plt.xlim(0, 1) - plt.ylim(0, 1) - plt.savefig(BytesIO(), format="pdf") # No image comparison. + fig, axs = plt.subplots(1, 2) + + axs[0].plot([0., 1e100], [0., 1e100]) + axs[0].set_xlim(0, 1) + axs[0].set_ylim(0, 1) + + axs[1].scatter([0, 1], [1, 1]) + axs[1].hist(np.random.normal(size=1000), bins=20, range=[-10, 10]) + axs[1].set_xscale('log') + + fig.savefig(BytesIO(), format="pdf") # No image comparison. # test mixed mode rendering -@needs_xelatex +@needs_pgf_xelatex @pytest.mark.backend('pgf') @image_comparison(['pgf_mixedmode.pdf'], style='default') def test_mixedmode(): @@ -161,8 +177,8 @@ def test_mixedmode(): # test bbox_inches clipping -@needs_xelatex -@pytest.mark.style('default') +@needs_pgf_xelatex +@mpl.style.context('default') @pytest.mark.backend('pgf') def test_bbox_inches(): mpl.rcParams.update({'font.family': 'serif', 'pgf.rcfonts': False}) @@ -175,12 +191,12 @@ def test_bbox_inches(): tol=0) -@pytest.mark.style('default') +@mpl.style.context('default') @pytest.mark.backend('pgf') @pytest.mark.parametrize('system', [ - pytest.param('lualatex', marks=[needs_lualatex]), - pytest.param('pdflatex', marks=[needs_pdflatex]), - pytest.param('xelatex', marks=[needs_xelatex]), + pytest.param('lualatex', marks=[needs_pgf_lualatex]), + pytest.param('pdflatex', marks=[needs_pgf_pdflatex]), + pytest.param('xelatex', marks=[needs_pgf_xelatex]), ]) def test_pdf_pages(system): rc_pdflatex = { @@ -217,12 +233,12 @@ def test_pdf_pages(system): assert pdf.get_pagecount() == 3 -@pytest.mark.style('default') +@mpl.style.context('default') @pytest.mark.backend('pgf') @pytest.mark.parametrize('system', [ - pytest.param('lualatex', marks=[needs_lualatex]), - pytest.param('pdflatex', marks=[needs_pdflatex]), - pytest.param('xelatex', marks=[needs_xelatex]), + pytest.param('lualatex', marks=[needs_pgf_lualatex]), + pytest.param('pdflatex', marks=[needs_pgf_pdflatex]), + pytest.param('xelatex', marks=[needs_pgf_xelatex]), ]) def test_pdf_pages_metadata_check(monkeypatch, system): # Basically the same as test_pdf_pages, but we keep it separate to leave @@ -274,7 +290,7 @@ def test_pdf_pages_metadata_check(monkeypatch, system): } -@needs_xelatex +@needs_pgf_xelatex def test_tex_restart_after_error(): fig = plt.figure() fig.suptitle(r"\oops") @@ -286,22 +302,24 @@ def test_tex_restart_after_error(): fig.savefig(BytesIO(), format="pgf") -@needs_xelatex +@needs_pgf_xelatex def test_bbox_inches_tight(): fig, ax = plt.subplots() ax.imshow([[0, 1], [2, 3]]) fig.savefig(BytesIO(), format="pdf", backend="pgf", bbox_inches="tight") -@needs_xelatex +@needs_pgf_xelatex @needs_ghostscript -def test_png(): - # Just a smoketest. - fig, ax = plt.subplots() - fig.savefig(BytesIO(), format="png", backend="pgf") +def test_png_transparency(): # Actually, also just testing that png works. + buf = BytesIO() + plt.figure().savefig(buf, format="png", backend="pgf", transparent=True) + buf.seek(0) + t = plt.imread(buf) + assert (t[..., 3] == 0).all() # fully transparent. -@needs_xelatex +@needs_pgf_xelatex def test_unknown_font(caplog): with caplog.at_level("WARNING"): mpl.rcParams["font.family"] = "this-font-does-not-exist" @@ -320,3 +338,30 @@ def test_minus_signs_with_tex(fig_test, fig_ref, texsystem): mpl.rcParams["pgf.texsystem"] = texsystem fig_test.text(.5, .5, "$-1$") fig_ref.text(.5, .5, "$\N{MINUS SIGN}1$") + + +@pytest.mark.backend("pgf") +def test_sketch_params(): + fig, ax = plt.subplots(figsize=(3, 3)) + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_frame_on(False) + handle, = ax.plot([0, 1]) + handle.set_sketch_params(scale=5, length=30, randomness=42) + + with BytesIO() as fd: + fig.savefig(fd, format='pgf') + buf = fd.getvalue().decode() + + baseline = r"""\pgfpathmoveto{\pgfqpoint{0.375000in}{0.300000in}}% +\pgfpathlineto{\pgfqpoint{2.700000in}{2.700000in}}% +\usepgfmodule{decorations}% +\usepgflibrary{decorations.pathmorphing}% +\pgfkeys{/pgf/decoration/.cd, """ \ + r"""segment length = 0.150000in, amplitude = 0.100000in}% +\pgfmathsetseed{42}% +\pgfdecoratecurrentpath{random steps}% +\pgfusepath{stroke}%""" + # \pgfdecoratecurrentpath must be after the path definition and before the + # path is used (\pgfusepath) + assert baseline in buf diff --git a/lib/matplotlib/tests/test_backend_ps.py b/lib/matplotlib/tests/test_backend_ps.py index e0b9a34be4ab..57d1172126f8 100644 --- a/lib/matplotlib/tests/test_backend_ps.py +++ b/lib/matplotlib/tests/test_backend_ps.py @@ -1,23 +1,22 @@ -import io +from collections import Counter from pathlib import Path +import io import re import tempfile +import numpy as np import pytest +from matplotlib import cbook, path, patheffects, font_manager as fm +from matplotlib._api import MatplotlibDeprecationWarning +from matplotlib.figure import Figure +from matplotlib.patches import Ellipse +from matplotlib.testing._markers import needs_ghostscript, needs_usetex +from matplotlib.testing.decorators import check_figures_equal, image_comparison import matplotlib as mpl +import matplotlib.collections as mcollections +import matplotlib.colors as mcolors import matplotlib.pyplot as plt -from matplotlib import cbook, patheffects -from matplotlib.testing.decorators import check_figures_equal, image_comparison -from matplotlib.cbook import MatplotlibDeprecationWarning - - -needs_ghostscript = pytest.mark.skipif( - "eps" not in mpl.testing.compare.converter, - reason="This test needs a ghostscript installation") -needs_usetex = pytest.mark.skipif( - not mpl.checkdep_usetex(True), - reason="This test needs a TeX installation") # This tests tends to hit a TeX cache lock on AppVeyor. @@ -68,6 +67,8 @@ def test_savefig_to_stringio(format, use_log, rcParams, orientation): except tuple(allowable_exceptions) as exc: pytest.skip(str(exc)) + assert not s_buf.closed + assert not b_buf.closed s_val = s_buf.getvalue().encode('ascii') b_val = b_buf.getvalue() @@ -116,6 +117,16 @@ def test_transparency(): ax.text(.5, .5, "foo", color="r", alpha=0) +@needs_usetex +@image_comparison(["empty.eps"]) +def test_transparency_tex(): + mpl.rcParams['text.usetex'] = True + fig, ax = plt.subplots() + ax.set_axis_off() + ax.plot([0, 1], color="r", alpha=0) + ax.text(.5, .5, "foo", color="r", alpha=0) + + def test_bbox(): fig, ax = plt.subplots() with io.BytesIO() as buf: @@ -147,10 +158,22 @@ def test_failing_latex(): @needs_usetex def test_partial_usetex(caplog): caplog.set_level("WARNING") - plt.figtext(.5, .5, "foo", usetex=True) + plt.figtext(.1, .1, "foo", usetex=True) + plt.figtext(.2, .2, "bar", usetex=True) + plt.savefig(io.BytesIO(), format="ps") + record, = caplog.records # asserts there's a single record. + assert "as if usetex=False" in record.getMessage() + + +@needs_usetex +def test_usetex_preamble(caplog): + mpl.rcParams.update({ + "text.usetex": True, + # Check that these don't conflict with the packages loaded by default. + "text.latex.preamble": r"\usepackage{color,graphicx,textcomp}", + }) + plt.figtext(.5, .5, "foo") plt.savefig(io.BytesIO(), format="ps") - assert caplog.records and all("as if usetex=False" in record.getMessage() - for record in caplog.records) @image_comparison(["useafm.eps"]) @@ -167,6 +190,18 @@ def test_type3_font(): plt.figtext(.5, .5, "I/J") +@image_comparison(["coloredhatcheszerolw.eps"]) +def test_colored_hatch_zero_linewidth(): + ax = plt.gca() + ax.add_patch(Ellipse((0, 0), 1, 1, hatch='/', facecolor='none', + edgecolor='r', linewidth=0)) + ax.add_patch(Ellipse((0.5, 0.5), 0.5, 0.5, hatch='+', facecolor='none', + edgecolor='g', linewidth=0.2)) + ax.add_patch(Ellipse((1, 1), 0.3, 0.8, hatch='\\', facecolor='none', + edgecolor='b', linewidth=0)) + ax.set_axis_off() + + @check_figures_equal(extensions=["eps"]) def test_text_clip(fig_test, fig_ref): ax = fig_test.add_subplot() @@ -193,3 +228,112 @@ def test_type42_font_without_prep(): mpl.rcParams["mathtext.fontset"] = "stix" plt.figtext(0.5, 0.5, "Mass $m$") + + +@pytest.mark.parametrize('fonttype', ["3", "42"]) +def test_fonttype(fonttype): + mpl.rcParams["ps.fonttype"] = fonttype + fig, ax = plt.subplots() + + ax.text(0.25, 0.5, "Forty-two is the answer to everything!") + + buf = io.BytesIO() + fig.savefig(buf, format="ps") + + test = b'/FontType ' + bytes(f"{fonttype}", encoding='utf-8') + b' def' + + assert re.search(test, buf.getvalue(), re.MULTILINE) + + +def test_linedash(): + """Test that dashed lines do not break PS output""" + fig, ax = plt.subplots() + + ax.plot([0, 1], linestyle="--") + + buf = io.BytesIO() + fig.savefig(buf, format="ps") + + assert buf.tell() > 0 + + +def test_empty_line(): + # Smoke-test for gh#23954 + figure = Figure() + figure.text(0.5, 0.5, "\nfoo\n\n") + buf = io.BytesIO() + figure.savefig(buf, format='eps') + figure.savefig(buf, format='ps') + + +def test_no_duplicate_definition(): + + fig = Figure() + axs = fig.subplots(4, 4, subplot_kw=dict(projection="polar")) + for ax in axs.flat: + ax.set(xticks=[], yticks=[]) + ax.plot([1, 2]) + fig.suptitle("hello, world") + + buf = io.StringIO() + fig.savefig(buf, format='eps') + buf.seek(0) + + wds = [ln.partition(' ')[0] for + ln in buf.readlines() + if ln.startswith('/')] + + assert max(Counter(wds).values()) == 1 + + +@image_comparison(["multi_font_type3.eps"], tol=0.51) +def test_multi_font_type3(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27) + plt.rc('ps', fonttype=3) + + fig = plt.figure() + fig.text(0.15, 0.475, "There are 几个汉字 in between!") + + +@image_comparison(["multi_font_type42.eps"], tol=1.6) +def test_multi_font_type42(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27) + plt.rc('ps', fonttype=42) + + fig = plt.figure() + fig.text(0.15, 0.475, "There are 几个汉字 in between!") + + +@image_comparison(["scatter.eps"]) +def test_path_collection(): + rng = np.random.default_rng(19680801) + xvals = rng.uniform(0, 1, 10) + yvals = rng.uniform(0, 1, 10) + sizes = rng.uniform(30, 100, 10) + fig, ax = plt.subplots() + ax.scatter(xvals, yvals, sizes, edgecolor=[0.9, 0.2, 0.1], marker='<') + ax.set_axis_off() + paths = [path.Path.unit_regular_polygon(i) for i in range(3, 7)] + offsets = rng.uniform(0, 200, 20).reshape(10, 2) + sizes = [0.02, 0.04] + pc = mcollections.PathCollection(paths, sizes, zorder=-1, + facecolors='yellow', offsets=offsets) + ax.add_collection(pc) + ax.set_xlim(0, 1) + + +@image_comparison(["colorbar_shift.eps"], savefig_kwarg={"bbox_inches": "tight"}, + style="mpl20") +def test_colorbar_shift(tmp_path): + cmap = mcolors.ListedColormap(["r", "g", "b"]) + norm = mcolors.BoundaryNorm([-1, -0.5, 0.5, 1], cmap.N) + plt.scatter([0, 1], [1, 1], c=[0, 1], cmap=cmap, norm=norm) + plt.colorbar() diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index d0d997e1ce2e..f79546323c47 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -1,47 +1,43 @@ import copy +import importlib +import inspect +import os import signal +import subprocess +import sys + +from datetime import date, datetime from unittest import mock +import pytest + import matplotlib from matplotlib import pyplot as plt from matplotlib._pylab_helpers import Gcf - -import pytest +from matplotlib import _c_internal_utils try: - from matplotlib.backends.qt_compat import QtGui + from matplotlib.backends.qt_compat import QtGui, QtWidgets # noqa + from matplotlib.backends.qt_editor import _formlayout except ImportError: - pytestmark = pytest.mark.skip('No usable Qt5 bindings') + pytestmark = pytest.mark.skip('No usable Qt bindings') + + +_test_timeout = 60 # A reasonably safe value for slower architectures. @pytest.fixture def qt_core(request): - backend, = request.node.get_closest_marker('backend').args qt_compat = pytest.importorskip('matplotlib.backends.qt_compat') QtCore = qt_compat.QtCore - if backend == 'Qt4Agg': - try: - py_qt_ver = int(QtCore.PYQT_VERSION_STR.split('.')[0]) - except AttributeError: - py_qt_ver = QtCore.__version_info__[0] - if py_qt_ver != 4: - pytest.skip('Qt4 is not available') - return QtCore -@pytest.mark.parametrize('backend', [ - # Note: the value is irrelevant; the important part is the marker. - pytest.param( - 'Qt4Agg', - marks=pytest.mark.backend('Qt4Agg', skip_on_importerror=True)), - pytest.param( - 'Qt5Agg', - marks=pytest.mark.backend('Qt5Agg', skip_on_importerror=True)), -]) -def test_fig_close(backend): +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_fig_close(): + # save the state of Gcf.figs init_figs = copy.copy(Gcf.figs) @@ -57,19 +53,158 @@ def test_fig_close(backend): assert init_figs == Gcf.figs +class WaitForStringPopen(subprocess.Popen): + """ + A Popen that passes flags that allow triggering KeyboardInterrupt. + """ + + def __init__(self, *args, **kwargs): + if sys.platform == 'win32': + kwargs['creationflags'] = subprocess.CREATE_NEW_CONSOLE + super().__init__( + *args, **kwargs, + # Force Agg so that each test can switch to its desired Qt backend. + env={**os.environ, "MPLBACKEND": "Agg", "SOURCE_DATE_EPOCH": "0"}, + stdout=subprocess.PIPE, universal_newlines=True) + + def wait_for(self, terminator): + """Read until the terminator is reached.""" + buf = '' + while True: + c = self.stdout.read(1) + if not c: + raise RuntimeError( + f'Subprocess died before emitting expected {terminator!r}') + buf += c + if buf.endswith(terminator): + return + + +def _test_sigint_impl(backend, target_name, kwargs): + import sys + import matplotlib.pyplot as plt + import os + import threading + + plt.switch_backend(backend) + from matplotlib.backends.qt_compat import QtCore # noqa + + def interrupter(): + if sys.platform == 'win32': + import win32api + win32api.GenerateConsoleCtrlEvent(0, 0) + else: + import signal + os.kill(os.getpid(), signal.SIGINT) + + target = getattr(plt, target_name) + timer = threading.Timer(1, interrupter) + fig = plt.figure() + fig.canvas.mpl_connect( + 'draw_event', + lambda *args: print('DRAW', flush=True) + ) + fig.canvas.mpl_connect( + 'draw_event', + lambda *args: timer.start() + ) + try: + target(**kwargs) + except KeyboardInterrupt: + print('SUCCESS', flush=True) + + +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +@pytest.mark.parametrize("target, kwargs", [ + ('show', {'block': True}), + ('pause', {'interval': 10}) +]) +def test_sigint(target, kwargs): + backend = plt.get_backend() + proc = WaitForStringPopen( + [sys.executable, "-c", + inspect.getsource(_test_sigint_impl) + + f"\n_test_sigint_impl({backend!r}, {target!r}, {kwargs!r})"]) + try: + proc.wait_for('DRAW') + stdout, _ = proc.communicate(timeout=_test_timeout) + except: + proc.kill() + stdout, _ = proc.communicate() + raise + print(stdout) + assert 'SUCCESS' in stdout + + +def _test_other_signal_before_sigint_impl(backend, target_name, kwargs): + import signal + import matplotlib.pyplot as plt + plt.switch_backend(backend) + from matplotlib.backends.qt_compat import QtCore # noqa + + target = getattr(plt, target_name) + + fig = plt.figure() + fig.canvas.mpl_connect('draw_event', + lambda *args: print('DRAW', flush=True)) + + timer = fig.canvas.new_timer(interval=1) + timer.single_shot = True + timer.add_callback(print, 'SIGUSR1', flush=True) + + def custom_signal_handler(signum, frame): + timer.start() + signal.signal(signal.SIGUSR1, custom_signal_handler) + + try: + target(**kwargs) + except KeyboardInterrupt: + print('SUCCESS', flush=True) + + +@pytest.mark.skipif(sys.platform == 'win32', + reason='No other signal available to send on Windows') +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +@pytest.mark.parametrize("target, kwargs", [ + ('show', {'block': True}), + ('pause', {'interval': 10}) +]) +def test_other_signal_before_sigint(target, kwargs): + backend = plt.get_backend() + proc = WaitForStringPopen( + [sys.executable, "-c", + inspect.getsource(_test_other_signal_before_sigint_impl) + + "\n_test_other_signal_before_sigint_impl(" + f"{backend!r}, {target!r}, {kwargs!r})"]) + try: + proc.wait_for('DRAW') + os.kill(proc.pid, signal.SIGUSR1) + proc.wait_for('SIGUSR1') + os.kill(proc.pid, signal.SIGINT) + stdout, _ = proc.communicate(timeout=_test_timeout) + except: + proc.kill() + stdout, _ = proc.communicate() + raise + print(stdout) + assert 'SUCCESS' in stdout + plt.figure() + + @pytest.mark.backend('Qt5Agg', skip_on_importerror=True) -def test_fig_signals(qt_core): +def test_fig_sigint_override(qt_core): + from matplotlib.backends.backend_qt5 import _BackendQT5 # Create a figure plt.figure() - # Access signals - event_loop_signal = None + # Variable to access the handler from the inside of the event loop + event_loop_handler = None # Callback to fire during event loop: save SIGINT handler, then exit def fire_signal_and_quit(): # Save event loop signal - nonlocal event_loop_signal - event_loop_signal = signal.getsignal(signal.SIGINT) + nonlocal event_loop_handler + event_loop_handler = signal.getsignal(signal.SIGINT) # Request event loop exit qt_core.QCoreApplication.exit() @@ -78,45 +213,69 @@ def fire_signal_and_quit(): qt_core.QTimer.singleShot(0, fire_signal_and_quit) # Save original SIGINT handler - original_signal = signal.getsignal(signal.SIGINT) + original_handler = signal.getsignal(signal.SIGINT) # Use our own SIGINT handler to be 100% sure this is working - def CustomHandler(signum, frame): + def custom_handler(signum, frame): pass - signal.signal(signal.SIGINT, CustomHandler) + signal.signal(signal.SIGINT, custom_handler) + + try: + # mainloop() sets SIGINT, starts Qt event loop (which triggers timer + # and exits) and then mainloop() resets SIGINT + matplotlib.backends.backend_qt._BackendQT.mainloop() + + # Assert: signal handler during loop execution is changed + # (can't test equality with func) + assert event_loop_handler != custom_handler - # mainloop() sets SIGINT, starts Qt event loop (which triggers timer and - # exits) and then mainloop() resets SIGINT - matplotlib.backends.backend_qt5._BackendQT5.mainloop() + # Assert: current signal handler is the same as the one we set before + assert signal.getsignal(signal.SIGINT) == custom_handler - # Assert: signal handler during loop execution is signal.SIG_DFL - assert event_loop_signal == signal.SIG_DFL + # Repeat again to test that SIG_DFL and SIG_IGN will not be overridden + for custom_handler in (signal.SIG_DFL, signal.SIG_IGN): + qt_core.QTimer.singleShot(0, fire_signal_and_quit) + signal.signal(signal.SIGINT, custom_handler) - # Assert: current signal handler is the same as the one we set before - assert CustomHandler == signal.getsignal(signal.SIGINT) + _BackendQT5.mainloop() - # Reset SIGINT handler to what it was before the test - signal.signal(signal.SIGINT, original_signal) + assert event_loop_handler == custom_handler + assert signal.getsignal(signal.SIGINT) == custom_handler + + finally: + # Reset SIGINT handler to what it was before the test + signal.signal(signal.SIGINT, original_handler) @pytest.mark.parametrize( - 'qt_key, qt_mods, answer', + "qt_key, qt_mods, answer", [ - ('Key_A', ['ShiftModifier'], 'A'), - ('Key_A', [], 'a'), - ('Key_A', ['ControlModifier'], 'ctrl+a'), - ('Key_Aacute', ['ShiftModifier'], - '\N{LATIN CAPITAL LETTER A WITH ACUTE}'), - ('Key_Aacute', [], - '\N{LATIN SMALL LETTER A WITH ACUTE}'), - ('Key_Control', ['AltModifier'], 'alt+control'), - ('Key_Alt', ['ControlModifier'], 'ctrl+alt'), - ('Key_Aacute', ['ControlModifier', 'AltModifier', 'MetaModifier'], - 'ctrl+alt+super+\N{LATIN SMALL LETTER A WITH ACUTE}'), - ('Key_Play', [], None), - ('Key_Backspace', [], 'backspace'), - ('Key_Backspace', ['ControlModifier'], 'ctrl+backspace'), + ("Key_A", ["ShiftModifier"], "A"), + ("Key_A", [], "a"), + ("Key_A", ["ControlModifier"], ("ctrl+a")), + ( + "Key_Aacute", + ["ShiftModifier"], + "\N{LATIN CAPITAL LETTER A WITH ACUTE}", + ), + ("Key_Aacute", [], "\N{LATIN SMALL LETTER A WITH ACUTE}"), + ("Key_Control", ["AltModifier"], ("alt+control")), + ("Key_Alt", ["ControlModifier"], "ctrl+alt"), + ( + "Key_Aacute", + ["ControlModifier", "AltModifier", "MetaModifier"], + ("ctrl+alt+meta+\N{LATIN SMALL LETTER A WITH ACUTE}"), + ), + # We do not currently map the media keys, this may change in the + # future. This means the callback will never fire + ("Key_Play", [], None), + ("Key_Backspace", [], "backspace"), + ( + "Key_Backspace", + ["ControlModifier"], + "ctrl+backspace", + ), ], ids=[ 'shift', @@ -134,45 +293,56 @@ def CustomHandler(signum, frame): ) @pytest.mark.parametrize('backend', [ # Note: the value is irrelevant; the important part is the marker. - pytest.param( - 'Qt4Agg', - marks=pytest.mark.backend('Qt4Agg', skip_on_importerror=True)), pytest.param( 'Qt5Agg', marks=pytest.mark.backend('Qt5Agg', skip_on_importerror=True)), + pytest.param( + 'QtAgg', + marks=pytest.mark.backend('QtAgg', skip_on_importerror=True)), ]) -def test_correct_key(backend, qt_core, qt_key, qt_mods, answer): +def test_correct_key(backend, qt_core, qt_key, qt_mods, answer, monkeypatch): """ Make a figure. Send a key_press_event event (using non-public, qtX backend specific api). Catch the event. Assert sent and caught keys are the same. """ - qt_mod = qt_core.Qt.NoModifier + from matplotlib.backends.qt_compat import _enum, _to_int + + if sys.platform == "darwin" and answer is not None: + answer = answer.replace("ctrl", "cmd") + answer = answer.replace("control", "cmd") + answer = answer.replace("meta", "ctrl") + result = None + qt_mod = _enum("QtCore.Qt.KeyboardModifier").NoModifier for mod in qt_mods: - qt_mod |= getattr(qt_core.Qt, mod) + qt_mod |= getattr(_enum("QtCore.Qt.KeyboardModifier"), mod) class _Event: def isAutoRepeat(self): return False - def key(self): return getattr(qt_core.Qt, qt_key) - def modifiers(self): return qt_mod + def key(self): return _to_int(getattr(_enum("QtCore.Qt.Key"), qt_key)) + + monkeypatch.setattr(QtWidgets.QApplication, "keyboardModifiers", + lambda self: qt_mod) def on_key_press(event): - assert event.key == answer + nonlocal result + result = event.key qt_canvas = plt.figure().canvas qt_canvas.mpl_connect('key_press_event', on_key_press) qt_canvas.keyPressEvent(_Event()) + assert result == answer -@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) -def test_pixel_ratio_change(): +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_device_pixel_ratio_change(): """ Make sure that if the pixel ratio changes, the figure dpi changes but the - widget remains the same physical size. + widget remains the same logical size. """ - prop = 'matplotlib.backends.backend_qt5.FigureCanvasQT.devicePixelRatioF' + prop = 'matplotlib.backends.backend_qt.FigureCanvasQT.devicePixelRatioF' with mock.patch(prop) as p: p.return_value = 3 @@ -180,10 +350,8 @@ def test_pixel_ratio_change(): qt_canvas = fig.canvas qt_canvas.show() - def set_pixel_ratio(ratio): + def set_device_pixel_ratio(ratio): p.return_value = ratio - # Make sure the mocking worked - assert qt_canvas._dpi_ratio == ratio # The value here doesn't matter, as we can't mock the C++ QScreen # object, but can override the functional wrapper around it. @@ -194,71 +362,82 @@ def set_pixel_ratio(ratio): qt_canvas.draw() qt_canvas.flush_events() + # Make sure the mocking worked + assert qt_canvas.device_pixel_ratio == ratio + qt_canvas.manager.show() size = qt_canvas.size() screen = qt_canvas.window().windowHandle().screen() - set_pixel_ratio(3) + set_device_pixel_ratio(3) # The DPI and the renderer width/height change assert fig.dpi == 360 assert qt_canvas.renderer.width == 1800 assert qt_canvas.renderer.height == 720 - # The actual widget size and figure physical size don't change + # The actual widget size and figure logical size don't change. assert size.width() == 600 assert size.height() == 240 assert qt_canvas.get_width_height() == (600, 240) assert (fig.get_size_inches() == (5, 2)).all() - set_pixel_ratio(2) + set_device_pixel_ratio(2) # The DPI and the renderer width/height change assert fig.dpi == 240 assert qt_canvas.renderer.width == 1200 assert qt_canvas.renderer.height == 480 - # The actual widget size and figure physical size don't change + # The actual widget size and figure logical size don't change. assert size.width() == 600 assert size.height() == 240 assert qt_canvas.get_width_height() == (600, 240) assert (fig.get_size_inches() == (5, 2)).all() - set_pixel_ratio(1.5) + set_device_pixel_ratio(1.5) # The DPI and the renderer width/height change assert fig.dpi == 180 assert qt_canvas.renderer.width == 900 assert qt_canvas.renderer.height == 360 - # The actual widget size and figure physical size don't change + # The actual widget size and figure logical size don't change. assert size.width() == 600 assert size.height() == 240 assert qt_canvas.get_width_height() == (600, 240) assert (fig.get_size_inches() == (5, 2)).all() -@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) +@pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_subplottool(): fig, ax = plt.subplots() - with mock.patch( - "matplotlib.backends.backend_qt5.SubplotToolQt.exec_", - lambda self: None): + with mock.patch("matplotlib.backends.qt_compat._exec", lambda obj: None): fig.canvas.manager.toolbar.configure_subplots() -@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) +@pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_figureoptions(): fig, ax = plt.subplots() ax.plot([1, 2]) ax.imshow([[1]]) ax.scatter(range(3), range(3), c=range(3)) - with mock.patch( - "matplotlib.backends.qt_editor._formlayout.FormDialog.exec_", - lambda self: None): + with mock.patch("matplotlib.backends.qt_compat._exec", lambda obj: None): fig.canvas.manager.toolbar.edit_parameters() -@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_figureoptions_with_datetime_axes(): + fig, ax = plt.subplots() + xydata = [ + datetime(year=2021, month=1, day=1), + datetime(year=2021, month=2, day=1) + ] + ax.plot(xydata, xydata) + with mock.patch("matplotlib.backends.qt_compat._exec", lambda obj: None): + fig.canvas.manager.toolbar.edit_parameters() + + +@pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_double_resize(): # Check that resizing a figure twice keeps the same window size fig, ax = plt.subplots() @@ -278,9 +457,9 @@ def test_double_resize(): assert window.height() == old_height -@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) +@pytest.mark.backend('QtAgg', skip_on_importerror=True) def test_canvas_reinit(): - from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg + from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg called = False @@ -295,3 +474,171 @@ def crashing_callback(fig, stale): canvas = FigureCanvasQTAgg(fig) fig.stale = True assert called + + +@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) +def test_form_widget_get_with_datetime_and_date_fields(): + from matplotlib.backends.backend_qt import _create_qApp + _create_qApp() + + form = [ + ("Datetime field", datetime(year=2021, month=3, day=11)), + ("Date field", date(year=2021, month=3, day=11)) + ] + widget = _formlayout.FormWidget(form) + widget.setup() + values = widget.get() + assert values == [ + datetime(year=2021, month=3, day=11), + date(year=2021, month=3, day=11) + ] + + +# The source of this function gets extracted and run in another process, so it +# must be fully self-contained. +def _test_enums_impl(): + import sys + + from matplotlib.backends.qt_compat import _enum, _to_int + from matplotlib.backend_bases import cursors, MouseButton + + _enum("QtGui.QDoubleValidator.State").Acceptable + + _enum("QtWidgets.QDialogButtonBox.StandardButton").Ok + _enum("QtWidgets.QDialogButtonBox.StandardButton").Cancel + _enum("QtWidgets.QDialogButtonBox.StandardButton").Apply + for btn_type in ["Ok", "Cancel"]: + getattr(_enum("QtWidgets.QDialogButtonBox.StandardButton"), btn_type) + + _enum("QtGui.QImage.Format").Format_ARGB32_Premultiplied + _enum("QtGui.QImage.Format").Format_ARGB32_Premultiplied + # SPECIAL_KEYS are Qt::Key that do *not* return their Unicode name instead + # they have manually specified names. + SPECIAL_KEYS = { + _to_int(getattr(_enum("QtCore.Qt.Key"), k)): v + for k, v in [ + ("Key_Escape", "escape"), + ("Key_Tab", "tab"), + ("Key_Backspace", "backspace"), + ("Key_Return", "enter"), + ("Key_Enter", "enter"), + ("Key_Insert", "insert"), + ("Key_Delete", "delete"), + ("Key_Pause", "pause"), + ("Key_SysReq", "sysreq"), + ("Key_Clear", "clear"), + ("Key_Home", "home"), + ("Key_End", "end"), + ("Key_Left", "left"), + ("Key_Up", "up"), + ("Key_Right", "right"), + ("Key_Down", "down"), + ("Key_PageUp", "pageup"), + ("Key_PageDown", "pagedown"), + ("Key_Shift", "shift"), + # In OSX, the control and super (aka cmd/apple) keys are switched. + ("Key_Control", "control" if sys.platform != "darwin" else "cmd"), + ("Key_Meta", "meta" if sys.platform != "darwin" else "control"), + ("Key_Alt", "alt"), + ("Key_CapsLock", "caps_lock"), + ("Key_F1", "f1"), + ("Key_F2", "f2"), + ("Key_F3", "f3"), + ("Key_F4", "f4"), + ("Key_F5", "f5"), + ("Key_F6", "f6"), + ("Key_F7", "f7"), + ("Key_F8", "f8"), + ("Key_F9", "f9"), + ("Key_F10", "f10"), + ("Key_F10", "f11"), + ("Key_F12", "f12"), + ("Key_Super_L", "super"), + ("Key_Super_R", "super"), + ] + } + # Define which modifier keys are collected on keyboard events. Elements + # are (Qt::KeyboardModifiers, Qt::Key) tuples. Order determines the + # modifier order (ctrl+alt+...) reported by Matplotlib. + _MODIFIER_KEYS = [ + ( + _to_int(getattr(_enum("QtCore.Qt.KeyboardModifier"), mod)), + _to_int(getattr(_enum("QtCore.Qt.Key"), key)), + ) + for mod, key in [ + ("ControlModifier", "Key_Control"), + ("AltModifier", "Key_Alt"), + ("ShiftModifier", "Key_Shift"), + ("MetaModifier", "Key_Meta"), + ] + ] + cursord = { + k: getattr(_enum("QtCore.Qt.CursorShape"), v) + for k, v in [ + (cursors.MOVE, "SizeAllCursor"), + (cursors.HAND, "PointingHandCursor"), + (cursors.POINTER, "ArrowCursor"), + (cursors.SELECT_REGION, "CrossCursor"), + (cursors.WAIT, "WaitCursor"), + ] + } + + buttond = { + getattr(_enum("QtCore.Qt.MouseButton"), k): v + for k, v in [ + ("LeftButton", MouseButton.LEFT), + ("RightButton", MouseButton.RIGHT), + ("MiddleButton", MouseButton.MIDDLE), + ("XButton1", MouseButton.BACK), + ("XButton2", MouseButton.FORWARD), + ] + } + + _enum("QtCore.Qt.WidgetAttribute").WA_OpaquePaintEvent + _enum("QtCore.Qt.FocusPolicy").StrongFocus + _enum("QtCore.Qt.ToolBarArea").TopToolBarArea + _enum("QtCore.Qt.ToolBarArea").TopToolBarArea + _enum("QtCore.Qt.AlignmentFlag").AlignRight + _enum("QtCore.Qt.AlignmentFlag").AlignVCenter + _enum("QtWidgets.QSizePolicy.Policy").Expanding + _enum("QtWidgets.QSizePolicy.Policy").Ignored + _enum("QtCore.Qt.MaskMode").MaskOutColor + _enum("QtCore.Qt.ToolBarArea").TopToolBarArea + _enum("QtCore.Qt.ToolBarArea").TopToolBarArea + _enum("QtCore.Qt.AlignmentFlag").AlignRight + _enum("QtCore.Qt.AlignmentFlag").AlignVCenter + _enum("QtWidgets.QSizePolicy.Policy").Expanding + _enum("QtWidgets.QSizePolicy.Policy").Ignored + + +def _get_testable_qt_backends(): + envs = [] + for deps, env in [ + ([qt_api], {"MPLBACKEND": "qtagg", "QT_API": qt_api}) + for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"] + ]: + reason = None + missing = [dep for dep in deps if not importlib.util.find_spec(dep)] + if (sys.platform == "linux" and + not _c_internal_utils.display_is_valid()): + reason = "$DISPLAY and $WAYLAND_DISPLAY are unset" + elif missing: + reason = "{} cannot be imported".format(", ".join(missing)) + elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'): + reason = "macosx backend fails on Azure" + marks = [] + if reason: + marks.append(pytest.mark.skip( + reason=f"Skipping {env} because {reason}")) + envs.append(pytest.param(env, marks=marks, id=str(env))) + return envs + + +@pytest.mark.parametrize("env", _get_testable_qt_backends()) +def test_enums_available(env): + proc = subprocess.run( + [sys.executable, "-c", + inspect.getsource(_test_enums_impl) + "\n_test_enums_impl()"], + env={**os.environ, "SOURCE_DATE_EPOCH": "0", **env}, + timeout=_test_timeout, check=True, + stdout=subprocess.PIPE, universal_newlines=True) diff --git a/lib/matplotlib/tests/test_backend_svg.py b/lib/matplotlib/tests/test_backend_svg.py index 66ec48a477bf..e99a5aadcc51 100644 --- a/lib/matplotlib/tests/test_backend_svg.py +++ b/lib/matplotlib/tests/test_backend_svg.py @@ -1,22 +1,21 @@ import datetime from io import BytesIO -import tempfile +from pathlib import Path import xml.etree.ElementTree import xml.parsers.expat -import numpy as np import pytest +import numpy as np + import matplotlib as mpl -from matplotlib import dviread from matplotlib.figure import Figure +from matplotlib.text import Text import matplotlib.pyplot as plt -from matplotlib.testing.decorators import image_comparison, check_figures_equal - - -needs_usetex = pytest.mark.skipif( - not mpl.checkdep_usetex(True), - reason="This test needs a TeX installation") +from matplotlib.testing.decorators import check_figures_equal, image_comparison +from matplotlib.testing._markers import needs_usetex +from matplotlib import font_manager as fm +from matplotlib.offsetbox import (OffsetImage, AnnotationBbox) def test_visibility(): @@ -124,6 +123,27 @@ def test_rasterized_ordering(fig_test, fig_ref): ax_test.plot(x+1, y, "-", c="b", lw=10, rasterized=False, zorder=1.2) +@check_figures_equal(tol=5, extensions=['svg', 'pdf']) +def test_prevent_rasterization(fig_test, fig_ref): + loc = [0.05, 0.05] + + ax_ref = fig_ref.subplots() + + ax_ref.plot([loc[0]], [loc[1]], marker="x", c="black", zorder=2) + + b = mpl.offsetbox.TextArea("X") + abox = mpl.offsetbox.AnnotationBbox(b, loc, zorder=2.1) + ax_ref.add_artist(abox) + + ax_test = fig_test.subplots() + ax_test.plot([loc[0]], [loc[1]], marker="x", c="black", zorder=2, + rasterized=True) + + b = mpl.offsetbox.TextArea("X") + abox = mpl.offsetbox.AnnotationBbox(b, loc, zorder=2.1) + ax_test.add_artist(abox) + + def test_count_bitmaps(): def count_tag(fig, tag): with BytesIO() as fd: @@ -181,24 +201,8 @@ def count_tag(fig, tag): assert count_tag(fig5, "path") == 1 # axis patch -@needs_usetex -def test_missing_psfont(monkeypatch): - """An error is raised if a TeX font lacks a Type-1 equivalent""" - - def psfont(*args, **kwargs): - return dviread.PsFont(texname='texfont', psname='Some Font', - effects=None, encoding=None, filename=None) - - monkeypatch.setattr(dviread.PsfontsMap, '__getitem__', psfont) - mpl.rc('text', usetex=True) - fig, ax = plt.subplots() - ax.text(0.5, 0.5, 'hello') - with tempfile.TemporaryFile() as tmpfile, pytest.raises(ValueError): - fig.savefig(tmpfile, format='svg') - - # Use Computer Modern Sans Serif, not Helvetica (which has no \textwon). -@pytest.mark.style('default') +@mpl.style.context('default') @needs_usetex def test_unicode_won(): fig = Figure() @@ -216,7 +220,7 @@ def test_unicode_won(): def test_svgnone_with_data_coordinates(): - plt.rcParams['svg.fonttype'] = 'none' + plt.rcParams.update({'svg.fonttype': 'none', 'font.stretch': 'condensed'}) expected = 'Unlikely to appear by chance' fig, ax = plt.subplots() @@ -229,9 +233,7 @@ def test_svgnone_with_data_coordinates(): fd.seek(0) buf = fd.read().decode() - assert expected in buf - for prop in ["family", "weight", "stretch", "style", "size"]: - assert f"font-{prop}:" in buf + assert expected in buf and "condensed" in buf def test_gid(): @@ -272,7 +274,7 @@ def include(gid, obj): # we need to exclude certain objects which will not appear in the svg if isinstance(obj, OffsetBox): return False - if isinstance(obj, plt.Text): + if isinstance(obj, Text): if obj.get_text() == "": return False elif obj.axes is None: @@ -310,17 +312,17 @@ def test_url(): # collections s = ax.scatter([1, 2, 3], [4, 5, 6]) - s.set_urls(['http://example.com/foo', 'http://example.com/bar', None]) + s.set_urls(['https://example.com/foo', 'https://example.com/bar', None]) # Line2D p, = plt.plot([1, 3], [6, 5]) - p.set_url('http://example.com/baz') + p.set_url('https://example.com/baz') b = BytesIO() fig.savefig(b, format='svg') b = b.getvalue() for v in [b'foo', b'bar', b'baz']: - assert b'http://example.com/' + v in b + assert b'https://example.com/' + v in b def test_url_tick(monkeypatch): @@ -329,13 +331,13 @@ def test_url_tick(monkeypatch): fig1, ax = plt.subplots() ax.scatter([1, 2, 3], [4, 5, 6]) for i, tick in enumerate(ax.yaxis.get_major_ticks()): - tick.set_url(f'http://example.com/{i}') + tick.set_url(f'https://example.com/{i}') fig2, ax = plt.subplots() ax.scatter([1, 2, 3], [4, 5, 6]) for i, tick in enumerate(ax.yaxis.get_major_ticks()): - tick.label1.set_url(f'http://example.com/{i}') - tick.label2.set_url(f'http://example.com/{i}') + tick.label1.set_url(f'https://example.com/{i}') + tick.label2.set_url(f'https://example.com/{i}') b1 = BytesIO() fig1.savefig(b1, format='svg') @@ -346,7 +348,7 @@ def test_url_tick(monkeypatch): b2 = b2.getvalue() for i in range(len(ax.yaxis.get_major_ticks())): - assert f'http://example.com/{i}'.encode('ascii') in b1 + assert f'https://example.com/{i}'.encode('ascii') in b1 assert b1 == b2 @@ -445,7 +447,7 @@ def test_svg_metadata(): **{k: [f'{k} bar', f'{k} baz'] for k in multi_value}, } - fig, ax = plt.subplots() + fig = plt.figure() with BytesIO() as fd: fig.savefig(fd, format='svg', metadata=metadata) buf = fd.getvalue().decode() @@ -488,3 +490,154 @@ def test_svg_metadata(): values = [node.text for node in rdf.findall(f'./{CCNS}Work/{DCNS}subject/{RDFNS}Bag/{RDFNS}li')] assert values == metadata['Keywords'] + + +@image_comparison(["multi_font_aspath.svg"], tol=1.8) +def test_multi_font_type3(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27) + plt.rc('svg', fonttype='path') + + fig = plt.figure() + fig.text(0.15, 0.475, "There are 几个汉字 in between!") + + +@image_comparison(["multi_font_astext.svg"]) +def test_multi_font_type42(): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + fig = plt.figure() + plt.rc('svg', fonttype='none') + + plt.rc('font', family=['DejaVu Sans', 'WenQuanYi Zen Hei'], size=27) + fig.text(0.15, 0.475, "There are 几个汉字 in between!") + + +@pytest.mark.parametrize('metadata,error,message', [ + ({'Date': 1}, TypeError, "Invalid type for Date metadata. Expected str"), + ({'Date': [1]}, TypeError, + "Invalid type for Date metadata. Expected iterable"), + ({'Keywords': 1}, TypeError, + "Invalid type for Keywords metadata. Expected str"), + ({'Keywords': [1]}, TypeError, + "Invalid type for Keywords metadata. Expected iterable"), + ({'Creator': 1}, TypeError, + "Invalid type for Creator metadata. Expected str"), + ({'Creator': [1]}, TypeError, + "Invalid type for Creator metadata. Expected iterable"), + ({'Title': 1}, TypeError, + "Invalid type for Title metadata. Expected str"), + ({'Format': 1}, TypeError, + "Invalid type for Format metadata. Expected str"), + ({'Foo': 'Bar'}, ValueError, "Unknown metadata key"), + ]) +def test_svg_incorrect_metadata(metadata, error, message): + with pytest.raises(error, match=message), BytesIO() as fd: + fig = plt.figure() + fig.savefig(fd, format='svg', metadata=metadata) + + +def test_svg_escape(): + fig = plt.figure() + fig.text(0.5, 0.5, "<\'\"&>", gid="<\'\"&>") + with BytesIO() as fd: + fig.savefig(fd, format='svg') + buf = fd.getvalue().decode() + assert '<'"&>"' in buf + + +@pytest.mark.parametrize("font_str", [ + "'DejaVu Sans', 'WenQuanYi Zen Hei', 'Arial', sans-serif", + "'DejaVu Serif', 'WenQuanYi Zen Hei', 'Times New Roman', serif", + "'Arial', 'WenQuanYi Zen Hei', cursive", + "'Impact', 'WenQuanYi Zen Hei', fantasy", + "'DejaVu Sans Mono', 'WenQuanYi Zen Hei', 'Courier New', monospace", + # These do not work because the logic to get the font metrics will not find + # WenQuanYi as the fallback logic stops with the first fallback font: + # "'DejaVu Sans Mono', 'Courier New', 'WenQuanYi Zen Hei', monospace", + # "'DejaVu Sans', 'Arial', 'WenQuanYi Zen Hei', sans-serif", + # "'DejaVu Serif', 'Times New Roman', 'WenQuanYi Zen Hei', serif", +]) +@pytest.mark.parametrize("include_generic", [True, False]) +def test_svg_font_string(font_str, include_generic): + fp = fm.FontProperties(family=["WenQuanYi Zen Hei"]) + if Path(fm.findfont(fp)).name != "wqy-zenhei.ttc": + pytest.skip("Font may be missing") + + explicit, *rest, generic = map( + lambda x: x.strip("'"), font_str.split(", ") + ) + size = len(generic) + if include_generic: + rest = rest + [generic] + plt.rcParams[f"font.{generic}"] = rest + plt.rcParams["font.size"] = size + plt.rcParams["svg.fonttype"] = "none" + + fig, ax = plt.subplots() + if generic == "sans-serif": + generic_options = ["sans", "sans-serif", "sans serif"] + else: + generic_options = [generic] + + for generic_name in generic_options: + # test that fallback works + ax.text(0.5, 0.5, "There are 几个汉字 in between!", + family=[explicit, generic_name], ha="center") + # test deduplication works + ax.text(0.5, 0.1, "There are 几个汉字 in between!", + family=[explicit, *rest, generic_name], ha="center") + ax.axis("off") + + with BytesIO() as fd: + fig.savefig(fd, format="svg") + buf = fd.getvalue() + + tree = xml.etree.ElementTree.fromstring(buf) + ns = "http://www.w3.org/2000/svg" + text_count = 0 + for text_element in tree.findall(f".//{{{ns}}}text"): + text_count += 1 + font_info = dict( + map(lambda x: x.strip(), _.strip().split(":")) + for _ in dict(text_element.items())["style"].split(";") + )["font"] + + assert font_info == f"{size}px {font_str}" + assert text_count == len(ax.texts) + + +def test_annotationbbox_gid(): + # Test that object gid appears in the AnnotationBbox + # in output svg. + fig = plt.figure() + ax = fig.add_subplot() + arr_img = np.ones((32, 32)) + xy = (0.3, 0.55) + + imagebox = OffsetImage(arr_img, zoom=0.1) + imagebox.image.axes = ax + + ab = AnnotationBbox(imagebox, xy, + xybox=(120., -80.), + xycoords='data', + boxcoords="offset points", + pad=0.5, + arrowprops=dict( + arrowstyle="->", + connectionstyle="angle,angleA=0,angleB=90,rad=3") + ) + ab.set_gid("a test for issue 20044") + ax.add_artist(ab) + + with BytesIO() as fd: + fig.savefig(fd, format='svg') + buf = fd.getvalue().decode('utf-8') + + expected = '' + assert expected in buf diff --git a/lib/matplotlib/tests/test_backend_template.py b/lib/matplotlib/tests/test_backend_template.py new file mode 100644 index 000000000000..d7e2a5cd1266 --- /dev/null +++ b/lib/matplotlib/tests/test_backend_template.py @@ -0,0 +1,51 @@ +""" +Backend-loading machinery tests, using variations on the template backend. +""" + +import sys +from types import SimpleNamespace +from unittest.mock import MagicMock + +import matplotlib as mpl +from matplotlib import pyplot as plt +from matplotlib.backends import backend_template +from matplotlib.backends.backend_template import ( + FigureCanvasTemplate, FigureManagerTemplate) + + +def test_load_template(): + mpl.use("template") + assert type(plt.figure().canvas) == FigureCanvasTemplate + + +def test_load_old_api(monkeypatch): + mpl_test_backend = SimpleNamespace(**vars(backend_template)) + mpl_test_backend.new_figure_manager = ( + lambda num, *args, FigureClass=mpl.figure.Figure, **kwargs: + FigureManagerTemplate( + FigureCanvasTemplate(FigureClass(*args, **kwargs)), num)) + monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend) + mpl.use("module://mpl_test_backend") + assert type(plt.figure().canvas) == FigureCanvasTemplate + plt.draw_if_interactive() + + +def test_show(monkeypatch): + mpl_test_backend = SimpleNamespace(**vars(backend_template)) + mock_show = MagicMock() + monkeypatch.setattr( + mpl_test_backend.FigureManagerTemplate, "pyplot_show", mock_show) + monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend) + mpl.use("module://mpl_test_backend") + plt.show() + mock_show.assert_called_with() + + +def test_show_old_global_api(monkeypatch): + mpl_test_backend = SimpleNamespace(**vars(backend_template)) + mock_show = MagicMock() + monkeypatch.setattr(mpl_test_backend, "show", mock_show, raising=False) + monkeypatch.setitem(sys.modules, "mpl_test_backend", mpl_test_backend) + mpl.use("module://mpl_test_backend") + plt.show() + mock_show.assert_called_with() diff --git a/lib/matplotlib/tests/test_backend_tk.py b/lib/matplotlib/tests/test_backend_tk.py index a129aff0bd95..55a7ae0b51aa 100644 --- a/lib/matplotlib/tests/test_backend_tk.py +++ b/lib/matplotlib/tests/test_backend_tk.py @@ -1,236 +1,258 @@ +import functools +import importlib import os +import platform import subprocess import sys import pytest -_test_timeout = 10 # Empirically, 1s is not enough on CI. +from matplotlib import _c_internal_utils +from matplotlib.testing import subprocess_run_helper -# NOTE: TkAgg tests seem to have interactions between tests, -# So isolate each test in a subprocess. See GH#18261 +_test_timeout = 60 # A reasonably safe value for slower architectures. -@pytest.mark.backend('TkAgg', skip_on_importerror=True) + +def _isolated_tk_test(success_count, func=None): + """ + A decorator to run *func* in a subprocess and assert that it prints + "success" *success_count* times and nothing on stderr. + + TkAgg tests seem to have interactions between tests, so isolate each test + in a subprocess. See GH#18261 + """ + + if func is None: + return functools.partial(_isolated_tk_test, success_count) + + if "MPL_TEST_ESCAPE_HATCH" in os.environ: + # set in subprocess_run_helper() below + return func + + @pytest.mark.skipif( + not importlib.util.find_spec('tkinter'), + reason="missing tkinter" + ) + @pytest.mark.skipif( + sys.platform == "linux" and not _c_internal_utils.display_is_valid(), + reason="$DISPLAY and $WAYLAND_DISPLAY are unset" + ) + @functools.wraps(func) + def test_func(): + # even if the package exists, may not actually be importable this can + # be the case on some CI systems. + pytest.importorskip('tkinter') + try: + proc = subprocess_run_helper( + func, timeout=_test_timeout, extra_env=dict( + MPLBACKEND="TkAgg", MPL_TEST_ESCAPE_HATCH="1")) + except subprocess.TimeoutExpired: + pytest.fail("Subprocess timed out") + except subprocess.CalledProcessError as e: + pytest.fail("Subprocess failed to test intended behavior\n" + + str(e.stderr)) + else: + # macOS may actually emit irrelevant errors about Accelerated + # OpenGL vs. software OpenGL, or some permission error on Azure, so + # suppress them. + # Asserting stderr first (and printing it on failure) should be + # more helpful for debugging that printing a failed success count. + ignored_lines = ["OpenGL", "CFMessagePort: bootstrap_register", + "/usr/include/servers/bootstrap_defs.h"] + assert not [line for line in proc.stderr.splitlines() + if all(msg not in line for msg in ignored_lines)] + assert proc.stdout.count("success") == success_count + + return test_func + + +@_isolated_tk_test(success_count=6) # len(bad_boxes) def test_blit(): - script = """ -import matplotlib.pyplot as plt -import numpy as np -from matplotlib.backends import _tkagg -def evil_blit(photoimage, aggimage, offsets, bboxptr): - data = np.asarray(aggimage) + import matplotlib.pyplot as plt + import numpy as np + import matplotlib.backends.backend_tkagg # noqa + from matplotlib.backends import _tkagg + + fig, ax = plt.subplots() + photoimage = fig.canvas._tkphoto + data = np.ones((4, 4, 4)) height, width = data.shape[:2] dataptr = (height, width, data.ctypes.data) - _tkagg.blit( - photoimage.tk.interpaddr(), str(photoimage), dataptr, offsets, - bboxptr) - -fig, ax = plt.subplots() -bad_boxes = ((-1, 2, 0, 2), - (2, 0, 0, 2), - (1, 6, 0, 2), - (0, 2, -1, 2), - (0, 2, 2, 0), - (0, 2, 1, 6)) -for bad_box in bad_boxes: - try: - evil_blit(fig.canvas._tkphoto, - np.ones((4, 4, 4)), - (0, 1, 2, 3), - bad_box) - except ValueError: - print("success") -""" - try: - proc = subprocess.run( - [sys.executable, "-c", script], - env={**os.environ, - "MPLBACKEND": "TkAgg", - "SOURCE_DATE_EPOCH": "0"}, - timeout=_test_timeout, - stdout=subprocess.PIPE, - check=True, - universal_newlines=True, - ) - except subprocess.TimeoutExpired: - pytest.fail("Subprocess timed out") - except subprocess.CalledProcessError: - pytest.fail("Likely regression on out-of-bounds data access" - " in _tkagg.cpp") - else: - print(proc.stdout) - assert proc.stdout.count("success") == 6 # len(bad_boxes) + # Test out of bounds blitting. + bad_boxes = ((-1, 2, 0, 2), + (2, 0, 0, 2), + (1, 6, 0, 2), + (0, 2, -1, 2), + (0, 2, 2, 0), + (0, 2, 1, 6)) + for bad_box in bad_boxes: + try: + _tkagg.blit( + photoimage.tk.interpaddr(), str(photoimage), dataptr, 0, + (0, 1, 2, 3), bad_box) + except ValueError: + print("success") -@pytest.mark.backend('TkAgg', skip_on_importerror=True) +@_isolated_tk_test(success_count=1) def test_figuremanager_preserves_host_mainloop(): - script = """ -import tkinter -import matplotlib.pyplot as plt -success = False - -def do_plot(): - plt.figure() - plt.plot([1, 2], [3, 5]) - plt.close() - root.after(0, legitimate_quit) - -def legitimate_quit(): - root.quit() - global success - success = True - -root = tkinter.Tk() -root.after(0, do_plot) -root.mainloop() - -if success: - print("success") -""" - try: - proc = subprocess.run( - [sys.executable, "-c", script], - env={**os.environ, - "MPLBACKEND": "TkAgg", - "SOURCE_DATE_EPOCH": "0"}, - timeout=_test_timeout, - stdout=subprocess.PIPE, - check=True, - universal_newlines=True, - ) - except subprocess.TimeoutExpired: - pytest.fail("Subprocess timed out") - except subprocess.CalledProcessError: - pytest.fail("Subprocess failed to test intended behavior") - else: - assert proc.stdout.count("success") == 1 + import tkinter + import matplotlib.pyplot as plt + success = [] + def do_plot(): + plt.figure() + plt.plot([1, 2], [3, 5]) + plt.close() + root.after(0, legitimate_quit) -@pytest.mark.backend('TkAgg', skip_on_importerror=True) + def legitimate_quit(): + root.quit() + success.append(True) + + root = tkinter.Tk() + root.after(0, do_plot) + root.mainloop() + + if success: + print("success") + + +@pytest.mark.skipif(platform.python_implementation() != 'CPython', + reason='PyPy does not support Tkinter threading: ' + 'https://foss.heptapod.net/pypy/pypy/-/issues/1929') @pytest.mark.flaky(reruns=3) +@_isolated_tk_test(success_count=1) def test_figuremanager_cleans_own_mainloop(): - script = ''' -import tkinter -import time -import matplotlib.pyplot as plt -import threading -from matplotlib.cbook import _get_running_interactive_framework - -root = tkinter.Tk() -plt.plot([1, 2, 3], [1, 2, 5]) - -def target(): - while not 'tk' == _get_running_interactive_framework(): - time.sleep(.01) - plt.close() - if show_finished_event.wait(): - print('success') - -show_finished_event = threading.Event() -thread = threading.Thread(target=target, daemon=True) -thread.start() -plt.show(block=True) # testing if this function hangs -show_finished_event.set() -thread.join() - -''' - try: - proc = subprocess.run( - [sys.executable, "-c", script], - env={**os.environ, - "MPLBACKEND": "TkAgg", - "SOURCE_DATE_EPOCH": "0"}, - timeout=_test_timeout, - stdout=subprocess.PIPE, - universal_newlines=True, - check=True - ) - except subprocess.TimeoutExpired: - pytest.fail("Most likely plot.show(block=True) hung") - except subprocess.CalledProcessError: - pytest.fail("Subprocess failed to test intended behavior") - assert proc.stdout.count("success") == 1 + import tkinter + import time + import matplotlib.pyplot as plt + import threading + from matplotlib.cbook import _get_running_interactive_framework + + root = tkinter.Tk() + plt.plot([1, 2, 3], [1, 2, 5]) + + def target(): + while not 'tk' == _get_running_interactive_framework(): + time.sleep(.01) + plt.close() + if show_finished_event.wait(): + print('success') + + show_finished_event = threading.Event() + thread = threading.Thread(target=target, daemon=True) + thread.start() + plt.show(block=True) # Testing if this function hangs. + show_finished_event.set() + thread.join() @pytest.mark.backend('TkAgg', skip_on_importerror=True) @pytest.mark.flaky(reruns=3) +@_isolated_tk_test(success_count=0) def test_never_update(): - script = """ -import tkinter -del tkinter.Misc.update -del tkinter.Misc.update_idletasks - -import matplotlib.pyplot as plt -fig = plt.figure() -plt.show(block=False) - -# regression test on FigureCanvasTkAgg -plt.draw() -# regression test on NavigationToolbar2Tk -fig.canvas.toolbar.configure_subplots() - -# check for update() or update_idletasks() in the event queue -# functionally equivalent to tkinter.Misc.update -# must pause >= 1 ms to process tcl idle events plus -# extra time to avoid flaky tests on slow systems -plt.pause(0.1) - -# regression test on FigureCanvasTk filter_destroy callback -plt.close(fig) -""" - try: - proc = subprocess.run( - [sys.executable, "-c", script], - env={**os.environ, - "MPLBACKEND": "TkAgg", - "SOURCE_DATE_EPOCH": "0"}, - timeout=_test_timeout, - capture_output=True, - universal_newlines=True, - ) - except subprocess.TimeoutExpired: - pytest.fail("Subprocess timed out") - else: - # test framework doesn't see tkinter callback exceptions normally - # see tkinter.Misc.report_callback_exception - assert "Exception in Tkinter callback" not in proc.stderr - # make sure we can see other issues - print(proc.stderr, file=sys.stderr) - # Checking return code late so the Tkinter assertion happens first - if proc.returncode: - pytest.fail("Subprocess failed to test intended behavior") + import tkinter + del tkinter.Misc.update + del tkinter.Misc.update_idletasks + import matplotlib.pyplot as plt + fig = plt.figure() + plt.show(block=False) -@pytest.mark.backend('TkAgg', skip_on_importerror=True) + plt.draw() # Test FigureCanvasTkAgg. + fig.canvas.toolbar.configure_subplots() # Test NavigationToolbar2Tk. + # Test FigureCanvasTk filter_destroy callback + fig.canvas.get_tk_widget().after(100, plt.close, fig) + + # Check for update() or update_idletasks() in the event queue, functionally + # equivalent to tkinter.Misc.update. + plt.show(block=True) + + # Note that exceptions would be printed to stderr; _isolated_tk_test + # checks them. + + +@_isolated_tk_test(success_count=2) def test_missing_back_button(): - script = """ -import matplotlib.pyplot as plt -from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk -class Toolbar(NavigationToolbar2Tk): - # only display the buttons we need - toolitems = [t for t in NavigationToolbar2Tk.toolitems if - t[0] in ('Home', 'Pan', 'Zoom')] - -fig = plt.figure() -print("setup complete") -# this should not raise -Toolbar(fig.canvas, fig.canvas.manager.window) -print("success") -""" - try: - proc = subprocess.run( - [sys.executable, "-c", script], - env={**os.environ, - "MPLBACKEND": "TkAgg", - "SOURCE_DATE_EPOCH": "0"}, - timeout=_test_timeout, - stdout=subprocess.PIPE, - universal_newlines=True, - ) - except subprocess.TimeoutExpired: - pytest.fail("Subprocess timed out") - else: - assert proc.stdout.count("setup complete") == 1 - assert proc.stdout.count("success") == 1 - # Checking return code late so the stdout assertions happen first - if proc.returncode: - pytest.fail("Subprocess failed to test intended behavior") + import matplotlib.pyplot as plt + from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk + + class Toolbar(NavigationToolbar2Tk): + # Only display the buttons we need. + toolitems = [t for t in NavigationToolbar2Tk.toolitems if + t[0] in ('Home', 'Pan', 'Zoom')] + + fig = plt.figure() + print("success") + Toolbar(fig.canvas, fig.canvas.manager.window) # This should not raise. + print("success") + + +@pytest.mark.backend('TkAgg', skip_on_importerror=True) +@_isolated_tk_test(success_count=1) +def test_canvas_focus(): + import tkinter as tk + import matplotlib.pyplot as plt + success = [] + + def check_focus(): + tkcanvas = fig.canvas.get_tk_widget() + # Give the plot window time to appear + if not tkcanvas.winfo_viewable(): + tkcanvas.wait_visibility() + # Make sure the canvas has the focus, so that it's able to receive + # keyboard events. + if tkcanvas.focus_lastfor() == tkcanvas: + success.append(True) + plt.close() + root.destroy() + + root = tk.Tk() + fig = plt.figure() + plt.plot([1, 2, 3]) + root.after(0, plt.show) + root.after(100, check_focus) + root.mainloop() + + if success: + print("success") + + +@_isolated_tk_test(success_count=2) +def test_embedding(): + import tkinter as tk + from matplotlib.backends.backend_tkagg import ( + FigureCanvasTkAgg, NavigationToolbar2Tk) + from matplotlib.backend_bases import key_press_handler + from matplotlib.figure import Figure + + root = tk.Tk() + + def test_figure(master): + fig = Figure() + ax = fig.add_subplot() + ax.plot([1, 2, 3]) + + canvas = FigureCanvasTkAgg(fig, master=master) + canvas.draw() + canvas.mpl_connect("key_press_event", key_press_handler) + canvas.get_tk_widget().pack(expand=True, fill="both") + + toolbar = NavigationToolbar2Tk(canvas, master, pack_toolbar=False) + toolbar.pack(expand=True, fill="x") + + canvas.get_tk_widget().forget() + toolbar.forget() + + test_figure(root) + print("success") + + # Test with a dark button color. Doesn't actually check whether the icon + # color becomes lighter, just that the code doesn't break. + + root.tk_setPalette(background="sky blue", selectColor="midnight blue", + foreground="white") + test_figure(root) + print("success") diff --git a/lib/matplotlib/tests/test_backend_webagg.py b/lib/matplotlib/tests/test_backend_webagg.py index 5c6ddfa258c0..992827863b01 100644 --- a/lib/matplotlib/tests/test_backend_webagg.py +++ b/lib/matplotlib/tests/test_backend_webagg.py @@ -2,6 +2,7 @@ import os import sys import pytest +import matplotlib.backends.backend_webagg_core @pytest.mark.parametrize("backend", ["webagg", "nbagg"]) @@ -25,3 +26,8 @@ def test_webagg_fallback(backend): ret = subprocess.call([sys.executable, "-c", test_code], env=env) assert ret == 0 + + +def test_webagg_core_no_toolbar(): + fm = matplotlib.backends.backend_webagg_core.FigureManagerWebAgg + assert fm._toolbar2_class is None diff --git a/lib/matplotlib/tests/test_backends_interactive.py b/lib/matplotlib/tests/test_backends_interactive.py index dc72b68468b8..24d47bb1cf75 100644 --- a/lib/matplotlib/tests/test_backends_interactive.py +++ b/lib/matplotlib/tests/test_backends_interactive.py @@ -3,16 +3,22 @@ import inspect import json import os +import platform import signal import subprocess import sys +import tempfile import time import urllib.request +from PIL import Image + import pytest import matplotlib as mpl from matplotlib import _c_internal_utils +from matplotlib.backend_tools import ToolToggleBase +from matplotlib.testing import subprocess_run_helper as _run_helper # Minimal smoke-testing of the backends for which the dependencies are @@ -20,24 +26,20 @@ # versions so we don't fail on missing backends. def _get_testable_interactive_backends(): - try: - from matplotlib.backends.qt_compat import QtGui # noqa - have_qt5 = True - except ImportError: - have_qt5 = False - - backends = [] - for deps, backend in [ - (["cairo", "gi"], "gtk3agg"), - (["cairo", "gi"], "gtk3cairo"), - (["PyQt5"], "qt5agg"), - (["PyQt5", "cairocffi"], "qt5cairo"), - (["PySide2"], "qt5agg"), - (["PySide2", "cairocffi"], "qt5cairo"), - (["tkinter"], "tkagg"), - (["wx"], "wx"), - (["wx"], "wxagg"), - (["matplotlib.backends._macosx"], "macosx"), + envs = [] + for deps, env in [ + *[([qt_api], + {"MPLBACKEND": "qtagg", "QT_API": qt_api}) + for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]], + *[([qt_api, "cairocffi"], + {"MPLBACKEND": "qtcairo", "QT_API": qt_api}) + for qt_api in ["PyQt6", "PySide6", "PyQt5", "PySide2"]], + *[(["cairo", "gi"], {"MPLBACKEND": f"gtk{version}{renderer}"}) + for version in [3, 4] for renderer in ["agg", "cairo"]], + (["tkinter"], {"MPLBACKEND": "tkagg"}), + (["wx"], {"MPLBACKEND": "wx"}), + (["wx"], {"MPLBACKEND": "wxagg"}), + (["matplotlib.backends._macosx"], {"MPLBACKEND": "macosx"}), ]: reason = None missing = [dep for dep in deps if not importlib.util.find_spec(dep)] @@ -46,32 +48,56 @@ def _get_testable_interactive_backends(): reason = "$DISPLAY and $WAYLAND_DISPLAY are unset" elif missing: reason = "{} cannot be imported".format(", ".join(missing)) - elif backend == 'macosx' and os.environ.get('TF_BUILD'): + elif env["MPLBACKEND"] == 'macosx' and os.environ.get('TF_BUILD'): reason = "macosx backend fails on Azure" - elif 'qt5' in backend and not have_qt5: - reason = "no usable Qt5 bindings" + elif env["MPLBACKEND"].startswith('gtk'): + import gi + version = env["MPLBACKEND"][3] + repo = gi.Repository.get_default() + if f'{version}.0' not in repo.enumerate_versions('Gtk'): + reason = "no usable GTK bindings" marks = [] if reason: marks.append(pytest.mark.skip( - reason=f"Skipping {backend} because {reason}")) - elif backend.startswith('wx') and sys.platform == 'darwin': + reason=f"Skipping {env} because {reason}")) + elif env["MPLBACKEND"].startswith('wx') and sys.platform == 'darwin': # ignore on OSX because that's currently broken (github #16849) marks.append(pytest.mark.xfail(reason='github #16849')) - backend = pytest.param(backend, marks=marks) - backends.append(backend) - return backends + envs.append( + pytest.param( + {**env, 'BACKEND_DEPS': ','.join(deps)}, + marks=marks, id=str(env) + ) + ) + return envs + + +_test_timeout = 120 # A reasonably safe value for slower architectures. -_test_timeout = 10 # Empirically, 1s is not enough on CI. +def _test_toolbar_button_la_mode_icon(fig): + # test a toolbar button icon using an image in LA mode (GH issue 25174) + # create an icon in LA mode + with tempfile.TemporaryDirectory() as tempdir: + img = Image.new("LA", (26, 26)) + tmp_img_path = os.path.join(tempdir, "test_la_icon.png") + img.save(tmp_img_path) + + class CustomTool(ToolToggleBase): + image = tmp_img_path + description = "" # gtk3 backend does not allow None + + toolmanager = fig.canvas.manager.toolmanager + toolbar = fig.canvas.manager.toolbar + toolmanager.add_tool("test", CustomTool) + toolbar.add_tool("test", "group") # The source of this function gets extracted and run in another process, so it # must be fully self-contained. # Using a timer not only allows testing of timers (on other backends), but is -# also necessary on gtk3 and wx, where a direct call to key_press_event("q") -# from draw_event causes breakage due to the canvas widget being deleted too -# early. Also, gtk3 redefines key_press_event with a different signature, so -# we directly invoke it from the superclass instead. +# also necessary on gtk3 and wx, where directly processing a KeyEvent() for "q" +# from draw_event causes breakage as the canvas widget gets deleted too early. def _test_interactive_impl(): import importlib.util import io @@ -80,20 +106,19 @@ def _test_interactive_impl(): from unittest import TestCase import matplotlib as mpl - from matplotlib import pyplot as plt, rcParams - from matplotlib.backend_bases import FigureCanvasBase - - rcParams.update({ + from matplotlib import pyplot as plt + from matplotlib.backend_bases import KeyEvent + mpl.rcParams.update({ "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])) + + mpl.rcParams.update(json.loads(sys.argv[1])) backend = plt.rcParams["backend"].lower() assert_equal = TestCase().assertEqual assert_raises = TestCase().assertRaises - if backend.endswith("agg") and not backend.startswith(("gtk3", "web")): + if backend.endswith("agg") and not backend.startswith(("gtk", "web")): # Force interactive framework setup. plt.figure() @@ -119,7 +144,6 @@ def check_alt_backend(alt_backend): if importlib.util.find_spec("cairocffi"): check_alt_backend(backend[:-3] + "cairo") check_alt_backend("svg") - mpl.use(backend, force=True) fig, ax = plt.subplots() @@ -127,10 +151,16 @@ def check_alt_backend(alt_backend): type(fig.canvas).__module__, "matplotlib.backends.backend_{}".format(backend)) + if mpl.rcParams["toolbar"] == "toolmanager": + # test toolbar button icon LA mode see GH issue 25174 + _test_toolbar_button_la_mode_icon(fig) + ax.plot([0, 1], [2, 3]) + if fig.canvas.toolbar: # i.e toolbar2. + fig.canvas.toolbar.draw_rubberband(None, 1., 1, 2., 2) - timer = fig.canvas.new_timer(1.) # Test floats casting to int as needed. - timer.add_callback(FigureCanvasBase.key_press_event, fig.canvas, "q") + timer = fig.canvas.new_timer(1.) # Test that floats are cast to int. + timer.add_callback(KeyEvent("key_press_event", fig.canvas, "q")._process) # Trigger quitting upon draw. fig.canvas.mpl_connect("draw_event", lambda event: timer.start()) fig.canvas.mpl_connect("close_event", print) @@ -154,43 +184,39 @@ def check_alt_backend(alt_backend): assert_equal(result.getvalue(), result_after.getvalue()) -@pytest.mark.parametrize("backend", _get_testable_interactive_backends()) +@pytest.mark.parametrize("env", _get_testable_interactive_backends()) @pytest.mark.parametrize("toolbar", ["toolbar2", "toolmanager"]) @pytest.mark.flaky(reruns=3) -def test_interactive_backend(backend, toolbar): - if backend == "macosx": +def test_interactive_backend(env, toolbar): + if env["MPLBACKEND"] == "macosx": if toolbar == "toolmanager": pytest.skip("toolmanager is not implemented for macosx.") - - proc = subprocess.run( - [sys.executable, "-c", - inspect.getsource(_test_interactive_impl) - + "\n_test_interactive_impl()", - json.dumps({"toolbar": toolbar})], - env={**os.environ, "MPLBACKEND": backend, "SOURCE_DATE_EPOCH": "0"}, - timeout=_test_timeout, - stdout=subprocess.PIPE, universal_newlines=True) - if proc.returncode: - pytest.fail("The subprocess returned with non-zero exit status " - f"{proc.returncode}.") + if env["MPLBACKEND"] == "wx": + pytest.skip("wx backend is deprecated; tests failed on appveyor") + try: + proc = _run_helper( + _test_interactive_impl, + json.dumps({"toolbar": toolbar}), + timeout=_test_timeout, + extra_env=env, + ) + except subprocess.CalledProcessError as err: + pytest.fail( + "Subprocess failed to test intended behavior\n" + + str(err.stderr)) assert proc.stdout.count("CloseEvent") == 1 -# The source of this function gets extracted and run in another process, so it -# must be fully self-contained. def _test_thread_impl(): from concurrent.futures import ThreadPoolExecutor - import json - import sys - from matplotlib import pyplot as plt, rcParams + import matplotlib as mpl + from matplotlib import pyplot as plt - rcParams.update({ + mpl.rcParams.update({ "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])) # Test artist creation and drawing does not crash from thread # No other guarantees! @@ -205,14 +231,16 @@ def _test_thread_impl(): future = ThreadPoolExecutor().submit(fig.canvas.draw) plt.pause(0.5) # flush_events fails here on at least Tkagg (bpo-41176) future.result() # Joins the thread; rethrows any exception. - plt.close() - fig.canvas.flush_events() # pause doesn't process events after close + plt.close() # backend is responsible for flushing any events here + if plt.rcParams["backend"].startswith("WX"): + # TODO: debug why WX needs this only on py3.8 + fig.canvas.flush_events() _thread_safe_backends = _get_testable_interactive_backends() # Known unsafe backends. Remove the xfails if they start to pass! for param in _thread_safe_backends: - backend = param.values[0] + backend = param.values[0]["MPLBACKEND"] if "cairo" in backend: # Cairo backends save a cairo_t on the graphics context, and sharing # these is not threadsafe. @@ -222,22 +250,157 @@ def _test_thread_impl(): param.marks.append( pytest.mark.xfail(raises=subprocess.CalledProcessError)) elif backend == "macosx": + from packaging.version import parse + mac_ver = platform.mac_ver()[0] + # Note, macOS Big Sur is both 11 and 10.16, depending on SDK that + # Python was compiled against. + if mac_ver and parse(mac_ver) < parse('10.16'): + param.marks.append( + pytest.mark.xfail(raises=subprocess.TimeoutExpired, + strict=True)) + elif param.values[0].get("QT_API") == "PySide2": + param.marks.append( + pytest.mark.xfail(raises=subprocess.CalledProcessError)) + elif backend == "tkagg" and platform.python_implementation() != 'CPython': param.marks.append( - pytest.mark.xfail(raises=subprocess.TimeoutExpired, strict=True)) + pytest.mark.xfail( + reason='PyPy does not support Tkinter threading: ' + 'https://foss.heptapod.net/pypy/pypy/-/issues/1929', + strict=True)) -@pytest.mark.parametrize("backend", _thread_safe_backends) +@pytest.mark.parametrize("env", _thread_safe_backends) @pytest.mark.flaky(reruns=3) -def test_interactive_thread_safety(backend): - proc = subprocess.run( - [sys.executable, "-c", - inspect.getsource(_test_thread_impl) + "\n_test_thread_impl()"], - env={**os.environ, "MPLBACKEND": backend, "SOURCE_DATE_EPOCH": "0"}, - timeout=_test_timeout, check=True, - stdout=subprocess.PIPE, universal_newlines=True) +def test_interactive_thread_safety(env): + proc = _run_helper(_test_thread_impl, timeout=_test_timeout, extra_env=env) assert proc.stdout.count("CloseEvent") == 1 +def _impl_test_lazy_auto_backend_selection(): + import matplotlib + import matplotlib.pyplot as plt + # just importing pyplot should not be enough to trigger resolution + bk = matplotlib.rcParams._get('backend') + assert not isinstance(bk, str) + assert plt._backend_mod is None + # but actually plotting should + plt.plot(5) + assert plt._backend_mod is not None + bk = matplotlib.rcParams._get('backend') + assert isinstance(bk, str) + + +def test_lazy_auto_backend_selection(): + _run_helper(_impl_test_lazy_auto_backend_selection, + timeout=_test_timeout) + + +def _implqt5agg(): + import matplotlib.backends.backend_qt5agg # noqa + import sys + + assert 'PyQt6' not in sys.modules + assert 'pyside6' not in sys.modules + assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules + + import matplotlib.backends.backend_qt5 + with pytest.warns(DeprecationWarning, + match="QtWidgets.QApplication.instance"): + matplotlib.backends.backend_qt5.qApp + + +def _implcairo(): + import matplotlib.backends.backend_qt5cairo # noqa + import sys + + assert 'PyQt6' not in sys.modules + assert 'pyside6' not in sys.modules + assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules + + import matplotlib.backends.backend_qt5 + with pytest.warns(DeprecationWarning, + match="QtWidgets.QApplication.instance"): + matplotlib.backends.backend_qt5.qApp + + +def _implcore(): + import matplotlib.backends.backend_qt5 + import sys + + assert 'PyQt6' not in sys.modules + assert 'pyside6' not in sys.modules + assert 'PyQt5' in sys.modules or 'pyside2' in sys.modules + + with pytest.warns(DeprecationWarning, + match="QtWidgets.QApplication.instance"): + matplotlib.backends.backend_qt5.qApp + + +def test_qt5backends_uses_qt5(): + qt5_bindings = [ + dep for dep in ['PyQt5', 'pyside2'] + if importlib.util.find_spec(dep) is not None + ] + qt6_bindings = [ + dep for dep in ['PyQt6', 'pyside6'] + if importlib.util.find_spec(dep) is not None + ] + if len(qt5_bindings) == 0 or len(qt6_bindings) == 0: + pytest.skip('need both QT6 and QT5 bindings') + _run_helper(_implqt5agg, timeout=_test_timeout) + if importlib.util.find_spec('pycairo') is not None: + _run_helper(_implcairo, timeout=_test_timeout) + _run_helper(_implcore, timeout=_test_timeout) + + +def _impl_test_cross_Qt_imports(): + import sys + import importlib + import pytest + + _, host_binding, mpl_binding = sys.argv + # import the mpl binding. This will force us to use that binding + importlib.import_module(f'{mpl_binding}.QtCore') + mpl_binding_qwidgets = importlib.import_module(f'{mpl_binding}.QtWidgets') + import matplotlib.backends.backend_qt + host_qwidgets = importlib.import_module(f'{host_binding}.QtWidgets') + + host_app = host_qwidgets.QApplication(["mpl testing"]) + with pytest.warns(UserWarning, match="Mixing Qt major"): + matplotlib.backends.backend_qt._create_qApp() + + +def test_cross_Qt_imports(): + qt5_bindings = [ + dep for dep in ['PyQt5', 'PySide2'] + if importlib.util.find_spec(dep) is not None + ] + qt6_bindings = [ + dep for dep in ['PyQt6', 'PySide6'] + if importlib.util.find_spec(dep) is not None + ] + if len(qt5_bindings) == 0 or len(qt6_bindings) == 0: + pytest.skip('need both QT6 and QT5 bindings') + + for qt5 in qt5_bindings: + for qt6 in qt6_bindings: + for pair in ([qt5, qt6], [qt6, qt5]): + try: + _run_helper(_impl_test_cross_Qt_imports, + *pair, + timeout=_test_timeout) + except subprocess.CalledProcessError as ex: + # if segfault, carry on. We do try to warn the user they + # are doing something that we do not expect to work + if ex.returncode == -signal.SIGSEGV: + continue + # We got the abort signal which is likely because the Qt5 / + # Qt6 cross import is unhappy, carry on. + elif ex.returncode == -signal.SIGABRT: + continue + raise + + @pytest.mark.skipif('TF_BUILD' in os.environ, reason="this test fails an azure for unknown reasons") @pytest.mark.skipif(os.name == "nt", reason="Cannot send SIGINT on Windows.") @@ -246,7 +409,7 @@ def test_webagg(): proc = subprocess.Popen( [sys.executable, "-c", inspect.getsource(_test_interactive_impl) - + "\n_test_interactive_impl()"], + + "\n_test_interactive_impl()", "{}"], env={**os.environ, "MPLBACKEND": "webagg", "SOURCE_DATE_EPOCH": "0"}) url = "http://{}:{}".format( mpl.rcParams["webagg.address"], mpl.rcParams["webagg.port"]) @@ -268,36 +431,189 @@ def test_webagg(): assert proc.wait(timeout=_test_timeout) == 0 +def _lazy_headless(): + import os + import sys + + backend, deps = sys.argv[1:] + deps = deps.split(',') + + # make it look headless + os.environ.pop('DISPLAY', None) + os.environ.pop('WAYLAND_DISPLAY', None) + for dep in deps: + assert dep not in sys.modules + + # we should fast-track to Agg + import matplotlib.pyplot as plt + assert plt.get_backend() == 'agg' + for dep in deps: + assert dep not in sys.modules + + # make sure we really have dependencies installed + for dep in deps: + importlib.import_module(dep) + assert dep in sys.modules + + # try to switch and make sure we fail with ImportError + try: + plt.switch_backend(backend) + except ImportError: + ... + else: + sys.exit(1) + + @pytest.mark.skipif(sys.platform != "linux", reason="this a linux-only test") -@pytest.mark.backend('Qt5Agg', skip_on_importerror=True) -def test_lazy_linux_headless(): - test_script = """ -import os -import sys +@pytest.mark.parametrize("env", _get_testable_interactive_backends()) +def test_lazy_linux_headless(env): + proc = _run_helper( + _lazy_headless, + env.pop('MPLBACKEND'), env.pop("BACKEND_DEPS"), + timeout=_test_timeout, + extra_env={**env, 'DISPLAY': '', 'WAYLAND_DISPLAY': ''} + ) + + +def _qApp_warn_impl(): + import matplotlib.backends.backend_qt + import pytest + + with pytest.warns( + DeprecationWarning, match="QtWidgets.QApplication.instance"): + matplotlib.backends.backend_qt.qApp + + +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_qApp_warn(): + _run_helper(_qApp_warn_impl, timeout=_test_timeout) + + +def _test_number_of_draws_script(): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + + # animated=True tells matplotlib to only draw the artist when we + # explicitly request it + ln, = ax.plot([0, 1], [1, 2], animated=True) + + # make sure the window is raised, but the script keeps going + plt.show(block=False) + plt.pause(0.3) + # Connect to draw_event to count the occurrences + fig.canvas.mpl_connect('draw_event', print) + + # get copy of entire figure (everything inside fig.bbox) + # sans animated artist + bg = fig.canvas.copy_from_bbox(fig.bbox) + # draw the animated artist, this uses a cached renderer + ax.draw_artist(ln) + # show the result to the screen + fig.canvas.blit(fig.bbox) + + for j in range(10): + # reset the background back in the canvas state, screen unchanged + fig.canvas.restore_region(bg) + # Create a **new** artist here, this is poor usage of blitting + # but good for testing to make sure that this doesn't create + # excessive draws + ln, = ax.plot([0, 1], [1, 2]) + # render the artist, updating the canvas state, but not the screen + ax.draw_artist(ln) + # copy the image to the GUI state, but screen might not changed yet + fig.canvas.blit(fig.bbox) + # flush any pending GUI events, re-painting the screen if needed + fig.canvas.flush_events() + + # Let the event loop process everything before leaving + plt.pause(0.1) + + +_blit_backends = _get_testable_interactive_backends() +for param in _blit_backends: + backend = param.values[0]["MPLBACKEND"] + if backend == "gtk3cairo": + # copy_from_bbox only works when rendering to an ImageSurface + param.marks.append( + pytest.mark.skip("gtk3cairo does not support blitting")) + elif backend == "gtk4cairo": + # copy_from_bbox only works when rendering to an ImageSurface + param.marks.append( + pytest.mark.skip("gtk4cairo does not support blitting")) + elif backend == "wx": + param.marks.append( + pytest.mark.skip("wx does not support blitting")) + + +@pytest.mark.parametrize("env", _blit_backends) +# subprocesses can struggle to get the display, so rerun a few times +@pytest.mark.flaky(reruns=4) +def test_blitting_events(env): + proc = _run_helper( + _test_number_of_draws_script, timeout=_test_timeout, extra_env=env) + # Count the number of draw_events we got. We could count some initial + # canvas draws (which vary in number by backend), but the critical + # check here is that it isn't 10 draws, which would be called if + # blitting is not properly implemented + ndraws = proc.stdout.count("DrawEvent") + assert 0 < ndraws < 5 + + +# The source of this function gets extracted and run in another process, so it +# must be fully self-contained. +def _test_figure_leak(): + import gc + import sys -# make it look headless -os.environ.pop('DISPLAY', None) -os.environ.pop('WAYLAND_DISPLAY', None) - -# we should fast-track to Agg -import matplotlib.pyplot as plt -plt.get_backend() == 'agg' -assert 'PyQt5' not in sys.modules - -# make sure we really have pyqt installed -import PyQt5 -assert 'PyQt5' in sys.modules - -# try to switch and make sure we fail with ImportError -try: - plt.switch_backend('qt5agg') -except ImportError: - ... -else: - sys.exit(1) - -""" - proc = subprocess.run([sys.executable, "-c", test_script]) - if proc.returncode: - pytest.fail("The subprocess returned with non-zero exit status " - f"{proc.returncode}.") + import psutil + from matplotlib import pyplot as plt + # Second argument is pause length, but if zero we should skip pausing + t = float(sys.argv[1]) + p = psutil.Process() + + # Warmup cycle, this reasonably allocates a lot + for _ in range(2): + fig = plt.figure() + if t: + plt.pause(t) + plt.close(fig) + mem = p.memory_info().rss + gc.collect() + + for _ in range(5): + fig = plt.figure() + if t: + plt.pause(t) + plt.close(fig) + gc.collect() + growth = p.memory_info().rss - mem + + print(growth) + + +# TODO: "0.1" memory threshold could be reduced 10x by fixing tkagg +@pytest.mark.skipif(sys.platform == "win32", + reason="appveyor tests fail; gh-22988 suggests reworking") +@pytest.mark.parametrize("env", _get_testable_interactive_backends()) +@pytest.mark.parametrize("time_mem", [(0.0, 2_000_000), (0.1, 30_000_000)]) +def test_figure_leak_20490(env, time_mem): + pytest.importorskip("psutil", reason="psutil needed to run this test") + + # We haven't yet directly identified the leaks so test with a memory growth + # threshold. + pause_time, acceptable_memory_leakage = time_mem + if env["MPLBACKEND"] == "wx": + pytest.skip("wx backend is deprecated; tests failed on appveyor") + + if env["MPLBACKEND"] == "macosx" or ( + env["MPLBACKEND"] == "tkagg" and sys.platform == 'darwin' + ): + acceptable_memory_leakage += 11_000_000 + + result = _run_helper( + _test_figure_leak, str(pause_time), + timeout=_test_timeout, extra_env=env) + + growth = int(result.stdout) + assert growth <= acceptable_memory_leakage diff --git a/lib/matplotlib/tests/test_bbox_tight.py b/lib/matplotlib/tests/test_bbox_tight.py index c018db5eaa01..91ff7fe20963 100644 --- a/lib/matplotlib/tests/test_bbox_tight.py +++ b/lib/matplotlib/tests/test_bbox_tight.py @@ -24,7 +24,7 @@ def test_bbox_inches_tight(): rows = len(data) ind = np.arange(len(col_labels)) + 0.3 # the x locations for the groups cell_text = [] - width = 0.4 # the width of the bars + width = 0.4 # the width of the bars yoff = np.zeros(len(col_labels)) # the bottom values for stacked bar chart fig, ax = plt.subplots(1, 1) @@ -43,7 +43,7 @@ def test_bbox_inches_tight(): @image_comparison(['bbox_inches_tight_suptile_legend'], - remove_text=False, savefig_kwarg={'bbox_inches': 'tight'}) + savefig_kwarg={'bbox_inches': 'tight'}) def test_bbox_inches_tight_suptile_legend(): plt.plot(np.arange(10), label='a straight line') plt.legend(bbox_to_anchor=(0.9, 1), loc='upper left') @@ -62,7 +62,7 @@ def y_formatter(y, pos): @image_comparison(['bbox_inches_tight_suptile_non_default.png'], - remove_text=False, savefig_kwarg={'bbox_inches': 'tight'}, + savefig_kwarg={'bbox_inches': 'tight'}, tol=0.1) # large tolerance because only testing clipping. def test_bbox_inches_tight_suptitle_non_default(): fig, ax = plt.subplots() @@ -146,3 +146,13 @@ def test_noop_tight_bbox(): assert (im[:, :, 3] == 255).all() assert not (im[:, :, :3] == 255).all() assert im.shape == (7, 10, 4) + + +@image_comparison(['bbox_inches_fixed_aspect'], extensions=['png'], + remove_text=True, savefig_kwarg={'bbox_inches': 'tight'}) +def test_bbox_inches_fixed_aspect(): + with plt.rc_context({'figure.constrained_layout.use': True}): + fig, ax = plt.subplots() + ax.plot([0, 1]) + ax.set_xlim(0, 1) + ax.set_aspect('equal') diff --git a/lib/matplotlib/tests/test_category.py b/lib/matplotlib/tests/test_category.py index 2d2f6e5c29e5..87dece6346f7 100644 --- a/lib/matplotlib/tests/test_category.py +++ b/lib/matplotlib/tests/test_category.py @@ -2,6 +2,7 @@ import pytest import numpy as np +import matplotlib as mpl from matplotlib.axes import Axes import matplotlib.pyplot as plt import matplotlib.category as cat @@ -99,15 +100,6 @@ def test_convert(self, vals): def test_convert_one_string(self, value): assert self.cc.convert(value, self.unit, self.ax) == 0 - def test_convert_one_number(self): - actual = self.cc.convert(0.0, self.unit, self.ax) - np.testing.assert_allclose(actual, np.array([0.])) - - def test_convert_float_array(self): - data = np.array([1, 2, 3], dtype=float) - actual = self.cc.convert(data, self.unit, self.ax) - np.testing.assert_allclose(actual, np.array([1., 2., 3.])) - @pytest.mark.parametrize("fvals", fvalues, ids=fids) def test_convert_fail(self, fvals): with pytest.raises(TypeError): @@ -122,11 +114,6 @@ def test_default_units(self): assert isinstance(self.cc.default_units(["a"], self.ax), cat.UnitData) -@pytest.fixture -def ax(): - return plt.figure().subplots() - - PLOT_LIST = [Axes.scatter, Axes.plot, Axes.bar] PLOT_IDS = ["scatter", "plot", "bar"] @@ -139,7 +126,8 @@ def test_StrCategoryLocator(self): np.testing.assert_array_equal(ticks.tick_values(None, None), locs) @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) - def test_StrCategoryLocatorPlot(self, ax, plotter): + def test_StrCategoryLocatorPlot(self, plotter): + ax = plt.figure().subplots() plotter(ax, [1, 2, 3], ["a", "b", "c"]) np.testing.assert_array_equal(ax.yaxis.major.locator(), range(3)) @@ -151,7 +139,7 @@ class TestStrCategoryFormatter: ids, cases = zip(*test_cases) @pytest.mark.parametrize("ydata", cases, ids=ids) - def test_StrCategoryFormatter(self, ax, ydata): + def test_StrCategoryFormatter(self, ydata): unit = cat.UnitData(ydata) labels = cat.StrCategoryFormatter(unit._mapping) for i, d in enumerate(ydata): @@ -160,7 +148,8 @@ def test_StrCategoryFormatter(self, ax, ydata): @pytest.mark.parametrize("ydata", cases, ids=ids) @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) - def test_StrCategoryFormatterPlot(self, ax, ydata, plotter): + def test_StrCategoryFormatterPlot(self, ydata, plotter): + ax = plt.figure().subplots() plotter(ax, range(len(ydata)), ydata) for i, d in enumerate(ydata): assert ax.yaxis.major.formatter(i) == d @@ -186,7 +175,8 @@ class TestPlotBytes: @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) @pytest.mark.parametrize("bdata", bytes_data, ids=bytes_ids) - def test_plot_bytes(self, ax, plotter, bdata): + def test_plot_bytes(self, plotter, bdata): + ax = plt.figure().subplots() counts = np.array([4, 6, 5]) plotter(ax, bdata, counts) axis_test(ax.xaxis, bdata) @@ -201,7 +191,8 @@ class TestPlotNumlike: @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) @pytest.mark.parametrize("ndata", numlike_data, ids=numlike_ids) - def test_plot_numlike(self, ax, plotter, ndata): + def test_plot_numlike(self, plotter, ndata): + ax = plt.figure().subplots() counts = np.array([4, 6, 5]) plotter(ax, ndata, counts) axis_test(ax.xaxis, ndata) @@ -209,7 +200,8 @@ def test_plot_numlike(self, ax, plotter, ndata): class TestPlotTypes: @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) - def test_plot_unicode(self, ax, plotter): + def test_plot_unicode(self, plotter): + ax = plt.figure().subplots() words = ['ЗдравÑтвуйте', 'привет'] plotter(ax, words, [0, 1]) axis_test(ax.xaxis, words) @@ -223,25 +215,29 @@ def test_data(self): @pytest.mark.usefixtures("test_data") @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) - def test_plot_xaxis(self, ax, test_data, plotter): + def test_plot_xaxis(self, test_data, plotter): + ax = plt.figure().subplots() plotter(ax, self.x, self.xy) axis_test(ax.xaxis, self.x) @pytest.mark.usefixtures("test_data") @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) - def test_plot_yaxis(self, ax, test_data, plotter): + def test_plot_yaxis(self, test_data, plotter): + ax = plt.figure().subplots() plotter(ax, self.yx, self.y) axis_test(ax.yaxis, self.y) @pytest.mark.usefixtures("test_data") @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) - def test_plot_xyaxis(self, ax, test_data, plotter): + def test_plot_xyaxis(self, test_data, plotter): + ax = plt.figure().subplots() plotter(ax, self.x, self.y) axis_test(ax.xaxis, self.x) axis_test(ax.yaxis, self.y) @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) - def test_update_plot(self, ax, plotter): + def test_update_plot(self, plotter): + ax = plt.figure().subplots() plotter(ax, ['a', 'b'], ['e', 'g']) plotter(ax, ['a', 'b', 'd'], ['f', 'a', 'b']) plotter(ax, ['b', 'c', 'd'], ['g', 'e', 'd']) @@ -260,19 +256,21 @@ def test_update_plot(self, ax, plotter): @pytest.mark.parametrize("plotter", plotters) @pytest.mark.parametrize("xdata", fvalues, ids=fids) - def test_mixed_type_exception(self, ax, plotter, xdata): + def test_mixed_type_exception(self, plotter, xdata): + ax = plt.figure().subplots() with pytest.raises(TypeError): plotter(ax, xdata, [1, 2]) @pytest.mark.parametrize("plotter", plotters) @pytest.mark.parametrize("xdata", fvalues, ids=fids) - def test_mixed_type_update_exception(self, ax, plotter, xdata): + def test_mixed_type_update_exception(self, plotter, xdata): + ax = plt.figure().subplots() with pytest.raises(TypeError): plotter(ax, [0, 3], [1, 3]) plotter(ax, xdata, [1, 2]) -@pytest.mark.style('default') +@mpl.style.context('default') @check_figures_equal(extensions=["png"]) def test_overriding_units_in_plot(fig_test, fig_ref): from datetime import datetime @@ -297,6 +295,15 @@ def test_overriding_units_in_plot(fig_test, fig_ref): assert y_units is ax.yaxis.units +def test_no_deprecation_on_empty_data(): + """ + Smoke test to check that no deprecation warning is emitted. See #22640. + """ + f, ax = plt.subplots() + ax.xaxis.update_units(["a", "b"]) + ax.plot([], []) + + def test_hist(): fig, ax = plt.subplots() n, bins, patches = ax.hist(['a', 'b', 'a', 'c', 'ff']) diff --git a/lib/matplotlib/tests/test_cbook.py b/lib/matplotlib/tests/test_cbook.py index f5651f3da3c6..aa5c999b7079 100644 --- a/lib/matplotlib/tests/test_cbook.py +++ b/lib/matplotlib/tests/test_cbook.py @@ -4,15 +4,14 @@ from weakref import ref from unittest.mock import patch, Mock -from datetime import datetime +from datetime import datetime, date, timedelta import numpy as np from numpy.testing import (assert_array_equal, assert_approx_equal, assert_array_almost_equal) import pytest -from matplotlib import _api -import matplotlib.cbook as cbook +from matplotlib import _api, cbook import matplotlib.colors as mcolors from matplotlib.cbook import delete_masked_points @@ -52,7 +51,7 @@ def test_rgba(self): class Test_boxplot_stats: - def setup(self): + def setup_method(self): np.random.seed(937) self.nrows = 37 self.ncols = 4 @@ -143,7 +142,7 @@ def test_results_whiskers_percentiles(self): assert_array_almost_equal(res[key], value) def test_results_withlabels(self): - labels = ['Test1', 2, 'ardvark', 4] + labels = ['Test1', 2, 'Aardvark', 4] results = cbook.boxplot_stats(self.data, labels=labels) for lab, res in zip(labels, results): assert res['label'] == lab @@ -178,15 +177,15 @@ def test_boxplot_stats_autorange_false(self): class Test_callback_registry: - def setup(self): + def setup_method(self): self.signal = 'test' self.callbacks = cbook.CallbackRegistry() def connect(self, s, func, pickle): - cid = self.callbacks.connect(s, func) if pickle: - self.callbacks._pickled_cids.add(cid) - return cid + return self.callbacks.connect(s, func) + else: + return self.callbacks._connect_picklable(s, func) def disconnect(self, cid): return self.callbacks.disconnect(cid) @@ -198,11 +197,13 @@ def count(self): return count1 def is_empty(self): + np.testing.break_cycles() assert self.callbacks._func_cid_map == {} assert self.callbacks.callbacks == {} assert self.callbacks._pickled_cids == set() def is_not_empty(self): + np.testing.break_cycles() assert self.callbacks._func_cid_map != {} assert self.callbacks.callbacks != {} @@ -361,6 +362,73 @@ def test_callbackregistry_custom_exception_handler(monkeypatch, cb, excp): cb.process('foo') +def test_callbackregistry_signals(): + cr = cbook.CallbackRegistry(signals=["foo"]) + results = [] + def cb(x): results.append(x) + cr.connect("foo", cb) + with pytest.raises(ValueError): + cr.connect("bar", cb) + cr.process("foo", 1) + with pytest.raises(ValueError): + cr.process("bar", 1) + assert results == [1] + + +def test_callbackregistry_blocking(): + # Needs an exception handler for interactive testing environments + # that would only print this out instead of raising the exception + def raise_handler(excp): + raise excp + cb = cbook.CallbackRegistry(exception_handler=raise_handler) + def test_func1(): + raise ValueError("1 should be blocked") + def test_func2(): + raise ValueError("2 should be blocked") + cb.connect("test1", test_func1) + cb.connect("test2", test_func2) + + # block all of the callbacks to make sure they aren't processed + with cb.blocked(): + cb.process("test1") + cb.process("test2") + + # block individual callbacks to make sure the other is still processed + with cb.blocked(signal="test1"): + # Blocked + cb.process("test1") + # Should raise + with pytest.raises(ValueError, match="2 should be blocked"): + cb.process("test2") + + # Make sure the original callback functions are there after blocking + with pytest.raises(ValueError, match="1 should be blocked"): + cb.process("test1") + with pytest.raises(ValueError, match="2 should be blocked"): + cb.process("test2") + + +@pytest.mark.parametrize('line, result', [ + ('a : no_comment', 'a : no_comment'), + ('a : "quoted str"', 'a : "quoted str"'), + ('a : "quoted str" # comment', 'a : "quoted str"'), + ('a : "#000000"', 'a : "#000000"'), + ('a : "#000000" # comment', 'a : "#000000"'), + ('a : ["#000000", "#FFFFFF"]', 'a : ["#000000", "#FFFFFF"]'), + ('a : ["#000000", "#FFFFFF"] # comment', 'a : ["#000000", "#FFFFFF"]'), + ('a : val # a comment "with quotes"', 'a : val'), + ('# only comment "with quotes" xx', ''), +]) +def test_strip_comment(line, result): + """Strip everything from the first unquoted #.""" + assert cbook._strip_comment(line) == result + + +def test_strip_comment_invalid(): + with pytest.raises(ValueError, match="Missing closing quote"): + cbook._strip_comment('grid.color: "aa') + + def test_sanitize_sequence(): d = {'a': 1, 'b': 2, 'c': 3} k = ['a', 'b', 'c'] @@ -374,32 +442,14 @@ def test_sanitize_sequence(): fail_mapping = ( - ({'a': 1}, {'forbidden': ('a')}), - ({'a': 1}, {'required': ('b')}), - ({'a': 1, 'b': 2}, {'required': ('a'), 'allowed': ()}), ({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['b']}}), - ({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['b']}, 'allowed': ('a',)}), ({'a': 1, 'b': 2}, {'alias_mapping': {'a': ['a', 'b']}}), - ({'a': 1, 'b': 2, 'c': 3}, - {'alias_mapping': {'a': ['b']}, 'required': ('a', )}), ) pass_mapping = ( (None, {}, {}), ({'a': 1, 'b': 2}, {'a': 1, 'b': 2}, {}), ({'b': 2}, {'a': 2}, {'alias_mapping': {'a': ['a', 'b']}}), - ({'b': 2}, {'a': 2}, - {'alias_mapping': {'a': ['b']}, 'forbidden': ('b', )}), - ({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, - {'required': ('a', ), 'allowed': ('c', )}), - ({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, - {'required': ('a', 'c'), 'allowed': ('c', )}), - ({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, - {'required': ('a', 'c'), 'allowed': ('a', 'c')}), - ({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, - {'required': ('a', 'c'), 'allowed': ()}), - ({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'required': ('a', 'c')}), - ({'a': 1, 'c': 3}, {'a': 1, 'c': 3}, {'allowed': ('a', 'c')}), ) @@ -552,7 +602,7 @@ def test_flatiter(): it = x.flat assert 0 == next(it) assert 1 == next(it) - ret = cbook.safe_first_element(it) + ret = cbook._safe_first_finite(it) assert ret == 0 assert 0 == next(it) @@ -593,6 +643,13 @@ class Dummy: assert isinstance(xnew[1], np.ndarray) and xnew[1].shape == (1,) assert isinstance(xnew[2], np.ndarray) and xnew[2].shape == (1,) + # Test a list of zero-dimensional arrays + x = [np.array(0), np.array(1), np.array(2)] + xnew = cbook._reshape_2D(x, 'x') + assert isinstance(xnew, list) + assert len(xnew) == 1 + assert isinstance(xnew[0], np.ndarray) and xnew[0].shape == (3,) + # Now test with a list of lists with different lengths, which means the # array will internally be converted to a 1D object array of lists x = [[1, 2, 3], [3, 4], [2]] @@ -644,14 +701,37 @@ def test_reshape2d_pandas(pd): for x, xnew in zip(X.T, Xnew): np.testing.assert_array_equal(x, xnew) + +def test_reshape2d_xarray(xr): + # separate to allow the rest of the tests to run if no xarray... X = np.arange(30).reshape(10, 3) - x = pd.DataFrame(X, columns=["a", "b", "c"]) + x = xr.DataArray(X, dims=["x", "y"]) Xnew = cbook._reshape_2D(x, 'x') # Need to check each row because _reshape_2D returns a list of arrays: for x, xnew in zip(X.T, Xnew): np.testing.assert_array_equal(x, xnew) +def test_index_of_pandas(pd): + # separate to allow the rest of the tests to run if no pandas... + X = np.arange(30).reshape(10, 3) + x = pd.DataFrame(X, columns=["a", "b", "c"]) + Idx, Xnew = cbook.index_of(x) + np.testing.assert_array_equal(X, Xnew) + IdxRef = np.arange(10) + np.testing.assert_array_equal(Idx, IdxRef) + + +def test_index_of_xarray(xr): + # separate to allow the rest of the tests to run if no xarray... + X = np.arange(30).reshape(10, 3) + x = xr.DataArray(X, dims=["x", "y"]) + Idx, Xnew = cbook.index_of(x) + np.testing.assert_array_equal(X, Xnew) + IdxRef = np.arange(10) + np.testing.assert_array_equal(Idx, IdxRef) + + def test_contiguous_regions(): a, b, c = 3, 4, 5 # Starts and ends with True @@ -678,7 +758,7 @@ def test_contiguous_regions(): def test_safe_first_element_pandas_series(pd): # deliberately create a pandas series with index not starting from 0 s = pd.Series(range(5), index=range(10, 15)) - actual = cbook.safe_first_element(s) + actual = cbook._safe_first_finite(s) assert actual == 0 @@ -808,3 +888,26 @@ def test_format_approx(): assert f(0.0012345600001, 5) == '0.00123' assert f(-0.0012345600001, 5) == '-0.00123' assert f(0.0012345600001, 8) == f(0.0012345600001, 10) == '0.00123456' + + +def test_safe_first_element_with_none(): + datetime_lst = [date.today() + timedelta(days=i) for i in range(10)] + datetime_lst[0] = None + actual = cbook._safe_first_finite(datetime_lst) + assert actual is not None and actual == datetime_lst[1] + + +@pytest.mark.parametrize('fmt, value, result', [ + ('%.2f m', 0.2, '0.20 m'), + ('{:.2f} m', 0.2, '0.20 m'), + ('{} m', 0.2, '0.2 m'), + ('const', 0.2, 'const'), + ('%d or {}', 0.2, '0 or {}'), + ('{{{:,.0f}}}', 2e5, '{200,000}'), + ('{:.2%}', 2/3, '66.67%'), + ('$%g', 2.54, '$2.54'), +]) +def test_auto_format_str(fmt, value, result): + """Apply *value* to the format string *fmt*.""" + assert cbook._auto_format_str(fmt, value) == result + assert cbook._auto_format_str(fmt, np.float64(value)) == result diff --git a/lib/matplotlib/tests/test_collections.py b/lib/matplotlib/tests/test_collections.py index 536703233978..ac1faa3c1cdc 100644 --- a/lib/matplotlib/tests/test_collections.py +++ b/lib/matplotlib/tests/test_collections.py @@ -1,4 +1,6 @@ +from datetime import datetime import io +import re from types import SimpleNamespace import numpy as np @@ -9,11 +11,11 @@ import matplotlib.pyplot as plt import matplotlib.collections as mcollections import matplotlib.colors as mcolors +import matplotlib.path as mpath import matplotlib.transforms as mtransforms from matplotlib.collections import (Collection, LineCollection, EventCollection, PolyCollection) from matplotlib.testing.decorators import check_figures_equal, image_comparison -from matplotlib._api.deprecation import MatplotlibDeprecationWarning def generate_EventCollection_plot(): @@ -291,19 +293,29 @@ def test_null_collection_datalim(): mtransforms.Bbox.null().get_points()) +def test_no_offsets_datalim(): + # A collection with no offsets and a non transData + # transform should return a null bbox + ax = plt.axes() + coll = mcollections.PathCollection([mpath.Path([(0, 0), (1, 0)])]) + ax.add_collection(coll) + coll_data_lim = coll.get_datalim(mtransforms.IdentityTransform()) + assert_array_equal(coll_data_lim.get_points(), + mtransforms.Bbox.null().get_points()) + + def test_add_collection(): # Test if data limits are unchanged by adding an empty collection. # GitHub issue #1490, pull #1497. plt.figure() ax = plt.axes() - coll = ax.scatter([0, 1], [0, 1]) - ax.add_collection(coll) + ax.scatter([0, 1], [0, 1]) bounds = ax.dataLim.bounds - coll = ax.scatter([], []) + ax.scatter([], []) assert ax.dataLim.bounds == bounds -@pytest.mark.style('mpl20') +@mpl.style.context('mpl20') @check_figures_equal(extensions=['png']) def test_collection_log_datalim(fig_test, fig_ref): # Data limits should respect the minimum x/y when using log scale. @@ -373,11 +385,9 @@ def test_EllipseCollection(): hh = Y / y[-1] aa = np.ones_like(ww) * 20 # first axis is 20 degrees CCW from x axis - ec = mcollections.EllipseCollection(ww, hh, aa, - units='x', - offsets=XY, - transOffset=ax.transData, - facecolors='none') + ec = mcollections.EllipseCollection( + ww, hh, aa, units='x', offsets=XY, offset_transform=ax.transData, + facecolors='none') ax.add_collection(ec) ax.autoscale_view() @@ -393,7 +403,7 @@ def test_polycollection_close(): [[3., 0.], [3., 1.], [4., 1.], [4., 0.]]] fig = plt.figure() - ax = fig.add_axes(Axes3D(fig, auto_add_to_figure=False)) + ax = fig.add_axes(Axes3D(fig)) colors = ['r', 'g', 'b', 'y', 'k'] zpos = list(range(5)) @@ -429,7 +439,7 @@ def test_regularpolycollection_rotate(): for xy, alpha in zip(xy_points, rotations): col = mcollections.RegularPolyCollection( 4, sizes=(100,), rotation=alpha, - offsets=[xy], transOffset=ax.transData) + offsets=[xy], offset_transform=ax.transData) ax.add_collection(col, autolim=True) ax.autoscale_view() @@ -457,8 +467,8 @@ def get_transform(self): xy = [(0, 0)] # Unit square has a half-diagonal of `1/sqrt(2)`, so `pi * r**2` equals... circle_areas = [np.pi / 2] - squares = SquareCollection(sizes=circle_areas, offsets=xy, - transOffset=ax.transData) + squares = SquareCollection( + sizes=circle_areas, offsets=xy, offset_transform=ax.transData) ax.add_collection(squares, autolim=True) ax.axis([-1, 1, -1, 1]) @@ -473,6 +483,81 @@ def test_picking(): assert_array_equal(indices['ind'], [0]) +def test_quadmesh_contains(): + x = np.arange(4) + X = x[:, None] * x[None, :] + + fig, ax = plt.subplots() + mesh = ax.pcolormesh(X) + fig.draw_without_rendering() + xdata, ydata = 0.5, 0.5 + x, y = mesh.get_transform().transform((xdata, ydata)) + mouse_event = SimpleNamespace(xdata=xdata, ydata=ydata, x=x, y=y) + found, indices = mesh.contains(mouse_event) + assert found + assert_array_equal(indices['ind'], [0]) + + xdata, ydata = 1.5, 1.5 + x, y = mesh.get_transform().transform((xdata, ydata)) + mouse_event = SimpleNamespace(xdata=xdata, ydata=ydata, x=x, y=y) + found, indices = mesh.contains(mouse_event) + assert found + assert_array_equal(indices['ind'], [5]) + + +def test_quadmesh_contains_concave(): + # Test a concave polygon, V-like shape + x = [[0, -1], [1, 0]] + y = [[0, 1], [1, -1]] + fig, ax = plt.subplots() + mesh = ax.pcolormesh(x, y, [[0]]) + fig.draw_without_rendering() + # xdata, ydata, expected + points = [(-0.5, 0.25, True), # left wing + (0, 0.25, False), # between the two wings + (0.5, 0.25, True), # right wing + (0, -0.25, True), # main body + ] + for point in points: + xdata, ydata, expected = point + x, y = mesh.get_transform().transform((xdata, ydata)) + mouse_event = SimpleNamespace(xdata=xdata, ydata=ydata, x=x, y=y) + found, indices = mesh.contains(mouse_event) + assert found is expected + + +def test_quadmesh_cursor_data(): + x = np.arange(4) + X = x[:, None] * x[None, :] + + fig, ax = plt.subplots() + mesh = ax.pcolormesh(X) + # Empty array data + mesh._A = None + fig.draw_without_rendering() + xdata, ydata = 0.5, 0.5 + x, y = mesh.get_transform().transform((xdata, ydata)) + mouse_event = SimpleNamespace(xdata=xdata, ydata=ydata, x=x, y=y) + # Empty collection should return None + assert mesh.get_cursor_data(mouse_event) is None + + # Now test adding the array data, to make sure we do get a value + mesh.set_array(np.ones((X.shape))) + assert_array_equal(mesh.get_cursor_data(mouse_event), [1]) + + +def test_quadmesh_cursor_data_multiple_points(): + x = [1, 2, 1, 2] + fig, ax = plt.subplots() + mesh = ax.pcolormesh(x, x, np.ones((3, 3))) + fig.draw_without_rendering() + xdata, ydata = 1.5, 1.5 + x, y = mesh.get_transform().transform((xdata, ydata)) + mouse_event = SimpleNamespace(xdata=xdata, ydata=ydata, x=x, y=y) + # All quads are covering the same square + assert_array_equal(mesh.get_cursor_data(mouse_event), np.ones(9)) + + def test_linestyle_single_dashes(): plt.scatter([0, 1, 2], [0, 1, 2], linestyle=(0., [2., 2.])) plt.draw() @@ -486,10 +571,8 @@ def test_size_in_xy(): widths = 10, 10 coords = [(10, 10), (15, 15)] e = mcollections.EllipseCollection( - widths, heights, angles, - units='xy', - offsets=coords, - transOffset=ax.transData) + widths, heights, angles, units='xy', + offsets=coords, offset_transform=ax.transData) ax.add_collection(e) @@ -514,7 +597,7 @@ def test_pandas_indexing(pd): Collection(antialiaseds=aa) -@pytest.mark.style('default') +@mpl.style.context('default') def test_lslw_bcast(): col = mcollections.PathCollection([]) col.set_linestyles(['-', '-']) @@ -528,7 +611,13 @@ def test_lslw_bcast(): assert (col.get_linewidths() == [1, 2, 3]).all() -@pytest.mark.style('default') +def test_set_wrong_linestyle(): + c = Collection() + with pytest.raises(ValueError, match="Do not know how to convert 'fuzzy'"): + c.set_linestyle('fuzzy') + + +@mpl.style.context('default') def test_capstyle(): col = mcollections.PathCollection([], capstyle='round') assert col.get_capstyle() == 'round' @@ -536,7 +625,7 @@ def test_capstyle(): assert col.get_capstyle() == 'butt' -@pytest.mark.style('default') +@mpl.style.context('default') def test_joinstyle(): col = mcollections.PathCollection([], joinstyle='round') assert col.get_joinstyle() == 'round' @@ -613,7 +702,7 @@ def test_pathcollection_legend_elements(): h, l = sc.legend_elements(fmt="{x:g}") assert len(h) == 5 - assert_array_equal(np.array(l).astype(float), np.arange(5)) + assert l == ["0", "1", "2", "3", "4"] colors = np.array([line.get_color() for line in h]) colors2 = sc.cmap(np.arange(5)/4) assert_array_equal(colors, colors2) @@ -624,16 +713,14 @@ def test_pathcollection_legend_elements(): l2 = ax.legend(h2, lab2, loc=2) h, l = sc.legend_elements(prop="sizes", alpha=0.5, color="red") - alpha = np.array([line.get_alpha() for line in h]) - assert_array_equal(alpha, 0.5) - color = np.array([line.get_markerfacecolor() for line in h]) - assert_array_equal(color, "red") + assert all(line.get_alpha() == 0.5 for line in h) + assert all(line.get_markerfacecolor() == "red" for line in h) l3 = ax.legend(h, l, loc=4) h, l = sc.legend_elements(prop="sizes", num=4, fmt="{x:.2f}", func=lambda x: 2*x) actsizes = [line.get_markersize() for line in h] - labeledsizes = np.sqrt(np.array(l).astype(float)/2) + labeledsizes = np.sqrt(np.array(l, float) / 2) assert_array_almost_equal(actsizes, labeledsizes) l4 = ax.legend(h, l, loc=3) @@ -644,7 +731,7 @@ def test_pathcollection_legend_elements(): levels = [-1, 0, 55.4, 260] h6, lab6 = sc.legend_elements(num=levels, prop="sizes", fmt="{x:g}") - assert_array_equal(np.array(lab6).astype(float), levels[2:]) + assert [float(l) for l in lab6] == levels[2:] for l in [l1, l2, l3, l4]: ax.add_artist(l) @@ -677,6 +764,22 @@ def test_collection_set_verts_array(): assert np.array_equal(ap._codes, atp._codes) +def test_collection_set_array(): + vals = [*range(10)] + + # Test set_array with list + c = Collection() + c.set_array(vals) + + # Test set_array with wrong dtype + with pytest.raises(TypeError, match="^Image data of dtype"): + c.set_array("wrong_input") + + # Test if array kwarg is copied + vals[5] = 45 + assert np.not_equal(vals, c.get_array()).any() + + def test_blended_collection_autolim(): a = [1, 2, 4] height = .2 @@ -698,6 +801,82 @@ def test_singleton_autolim(): np.testing.assert_allclose(ax.get_xlim(), [-0.06, 0.06]) +@pytest.mark.parametrize("transform, expected", [ + ("transData", (-0.5, 3.5)), + ("transAxes", (2.8, 3.2)), +]) +def test_autolim_with_zeros(transform, expected): + # 1) Test that a scatter at (0, 0) data coordinates contributes to + # autoscaling even though any(offsets) would be False in that situation. + # 2) Test that specifying transAxes for the transform does not contribute + # to the autoscaling. + fig, ax = plt.subplots() + ax.scatter(0, 0, transform=getattr(ax, transform)) + ax.scatter(3, 3) + np.testing.assert_allclose(ax.get_ylim(), expected) + np.testing.assert_allclose(ax.get_xlim(), expected) + + +def test_quadmesh_set_array_validation(): + x = np.arange(11) + y = np.arange(8) + z = np.random.random((7, 10)) + fig, ax = plt.subplots() + coll = ax.pcolormesh(x, y, z) + + with pytest.raises(ValueError, match=re.escape( + "For X (11) and Y (8) with flat shading, A should have shape " + "(7, 10, 3) or (7, 10, 4) or (7, 10) or (70,), not (10, 7)")): + coll.set_array(z.reshape(10, 7)) + + z = np.arange(54).reshape((6, 9)) + with pytest.raises(ValueError, match=re.escape( + "For X (11) and Y (8) with flat shading, A should have shape " + "(7, 10, 3) or (7, 10, 4) or (7, 10) or (70,), not (6, 9)")): + coll.set_array(z) + with pytest.raises(ValueError, match=re.escape( + "For X (11) and Y (8) with flat shading, A should have shape " + "(7, 10, 3) or (7, 10, 4) or (7, 10) or (70,), not (54,)")): + coll.set_array(z.ravel()) + + # RGB(A) tests + z = np.ones((9, 6, 3)) # RGB with wrong X/Y dims + with pytest.raises(ValueError, match=re.escape( + "For X (11) and Y (8) with flat shading, A should have shape " + "(7, 10, 3) or (7, 10, 4) or (7, 10) or (70,), not (9, 6, 3)")): + coll.set_array(z) + + z = np.ones((9, 6, 4)) # RGBA with wrong X/Y dims + with pytest.raises(ValueError, match=re.escape( + "For X (11) and Y (8) with flat shading, A should have shape " + "(7, 10, 3) or (7, 10, 4) or (7, 10) or (70,), not (9, 6, 4)")): + coll.set_array(z) + + z = np.ones((7, 10, 2)) # Right X/Y dims, bad 3rd dim + with pytest.raises(ValueError, match=re.escape( + "For X (11) and Y (8) with flat shading, A should have shape " + "(7, 10, 3) or (7, 10, 4) or (7, 10) or (70,), not (7, 10, 2)")): + coll.set_array(z) + + x = np.arange(10) + y = np.arange(7) + z = np.random.random((7, 10)) + fig, ax = plt.subplots() + coll = ax.pcolormesh(x, y, z, shading='gouraud') + + +def test_quadmesh_get_coordinates(): + x = [0, 1, 2] + y = [2, 4, 6] + z = np.ones(shape=(2, 2)) + xx, yy = np.meshgrid(x, y) + coll = plt.pcolormesh(xx, yy, z) + + # shape (3, 3, 2) + coords = np.stack([xx.T, yy.T]).T + assert_array_equal(coll.get_coordinates(), coords) + + def test_quadmesh_set_array(): x = np.arange(4) y = np.arange(4) @@ -714,6 +893,35 @@ def test_quadmesh_set_array(): fig.canvas.draw() assert np.array_equal(coll.get_array(), np.ones(9)) + z = np.arange(16).reshape((4, 4)) + fig, ax = plt.subplots() + coll = ax.pcolormesh(x, y, np.ones(z.shape), shading='gouraud') + # Test that the collection is able to update with a 2d array + coll.set_array(z) + fig.canvas.draw() + assert np.array_equal(coll.get_array(), z) + + # Check that pre-flattened arrays work too + coll.set_array(np.ones(16)) + fig.canvas.draw() + assert np.array_equal(coll.get_array(), np.ones(16)) + + +def test_quadmesh_vmin_vmax(): + # test when vmin/vmax on the norm changes, the quadmesh gets updated + fig, ax = plt.subplots() + cmap = mpl.colormaps['plasma'] + norm = mpl.colors.Normalize(vmin=0, vmax=1) + coll = ax.pcolormesh([[1]], cmap=cmap, norm=norm) + fig.canvas.draw() + assert np.array_equal(coll.get_facecolors()[0, :], cmap(norm(1))) + + # Change the vmin/vmax of the norm so that the color is from + # the bottom of the colormap now + norm.vmin, norm.vmax = 1, 2 + fig.canvas.draw() + assert np.array_equal(coll.get_facecolors()[0, :], cmap(norm(1))) + def test_quadmesh_alpha_array(): x = np.arange(4) @@ -775,7 +983,7 @@ def test_legend_inverse_size_label_relationship(): assert_array_almost_equal(handle_sizes, legend_sizes, decimal=1) -@pytest.mark.style('default') +@mpl.style.context('default') @pytest.mark.parametrize('pcfunc', [plt.pcolor, plt.pcolormesh]) def test_color_logic(pcfunc): z = np.arange(12).reshape(3, 4) @@ -785,7 +993,7 @@ def test_color_logic(pcfunc): # Define 2 reference "colors" here for multiple use. face_default = mcolors.to_rgba_array(pc._get_default_facecolor()) mapped = pc.get_cmap()(pc.norm((z.ravel()))) - # Github issue #1302: + # GitHub issue #1302: assert mcolors.same_color(pc.get_edgecolor(), 'red') # Check setting attributes after initialization: pc = pcfunc(z) @@ -846,12 +1054,12 @@ def test_color_logic(pcfunc): def test_LineCollection_args(): - with pytest.warns(MatplotlibDeprecationWarning): - lc = LineCollection(None, 2.2, 'r', zorder=3, facecolors=[0, 1, 0, 1]) - assert lc.get_linewidth()[0] == 2.2 - assert mcolors.same_color(lc.get_edgecolor(), 'r') - assert lc.get_zorder() == 3 - assert mcolors.same_color(lc.get_facecolor(), [[0, 1, 0, 1]]) + lc = LineCollection(None, linewidth=2.2, edgecolor='r', + zorder=3, facecolors=[0, 1, 0, 1]) + assert lc.get_linewidth()[0] == 2.2 + assert mcolors.same_color(lc.get_edgecolor(), 'r') + assert lc.get_zorder() == 3 + assert mcolors.same_color(lc.get_facecolor(), [[0, 1, 0, 1]]) # To avoid breaking mplot3d, LineCollection internally sets the facecolor # kwarg if it has not been specified. Hence we need the following test # for LineCollection._set_default(). @@ -868,6 +1076,9 @@ def test_array_wrong_dimensions(): pc = plt.pcolormesh(z) pc.set_array(z) # 2D is OK for Quadmesh pc.update_scalarmappable() + # 3D RGB is OK as well + z = np.arange(36).reshape(3, 4, 3) + pc.set_array(z) def test_get_segments(): @@ -877,3 +1088,106 @@ def test_get_segments(): readback, = lc.get_segments() # these should comeback un-changed! assert np.all(segments == readback) + + +def test_set_offsets_late(): + identity = mtransforms.IdentityTransform() + sizes = [2] + + null = mcollections.CircleCollection(sizes=sizes) + + init = mcollections.CircleCollection(sizes=sizes, offsets=(10, 10)) + + late = mcollections.CircleCollection(sizes=sizes) + late.set_offsets((10, 10)) + + # Bbox.__eq__ doesn't compare bounds + null_bounds = null.get_datalim(identity).bounds + init_bounds = init.get_datalim(identity).bounds + late_bounds = late.get_datalim(identity).bounds + + # offsets and transform are applied when set after initialization + assert null_bounds != init_bounds + assert init_bounds == late_bounds + + +def test_set_offset_transform(): + skew = mtransforms.Affine2D().skew(2, 2) + init = mcollections.Collection(offset_transform=skew) + + late = mcollections.Collection() + late.set_offset_transform(skew) + + assert skew == init.get_offset_transform() == late.get_offset_transform() + + +def test_set_offset_units(): + # passing the offsets in initially (i.e. via scatter) + # should yield the same results as `set_offsets` + x = np.linspace(0, 10, 5) + y = np.sin(x) + d = x * np.timedelta64(24, 'h') + np.datetime64('2021-11-29') + + sc = plt.scatter(d, y) + off0 = sc.get_offsets() + sc.set_offsets(list(zip(d, y))) + np.testing.assert_allclose(off0, sc.get_offsets()) + + # try the other way around + fig, ax = plt.subplots() + sc = ax.scatter(y, d) + off0 = sc.get_offsets() + sc.set_offsets(list(zip(y, d))) + np.testing.assert_allclose(off0, sc.get_offsets()) + + +@image_comparison(baseline_images=["test_check_masked_offsets"], + extensions=["png"], remove_text=True, style="mpl20") +def test_check_masked_offsets(): + # Check if masked data is respected by scatter + # Ref: Issue #24545 + unmasked_x = [ + datetime(2022, 12, 15, 4, 49, 52), + datetime(2022, 12, 15, 4, 49, 53), + datetime(2022, 12, 15, 4, 49, 54), + datetime(2022, 12, 15, 4, 49, 55), + datetime(2022, 12, 15, 4, 49, 56), + ] + + masked_y = np.ma.array([1, 2, 3, 4, 5], mask=[0, 1, 1, 0, 0]) + + fig, ax = plt.subplots() + ax.scatter(unmasked_x, masked_y) + + +@check_figures_equal(extensions=["png"]) +def test_masked_set_offsets(fig_ref, fig_test): + x = np.ma.array([1, 2, 3, 4, 5], mask=[0, 0, 1, 1, 0]) + y = np.arange(1, 6) + + ax_test = fig_test.add_subplot() + scat = ax_test.scatter(x, y) + scat.set_offsets(np.ma.column_stack([x, y])) + ax_test.set_xticks([]) + ax_test.set_yticks([]) + + ax_ref = fig_ref.add_subplot() + ax_ref.scatter([1, 2, 5], [1, 2, 5]) + ax_ref.set_xticks([]) + ax_ref.set_yticks([]) + + +def test_check_offsets_dtype(): + # Check that setting offsets doesn't change dtype + x = np.ma.array([1, 2, 3, 4, 5], mask=[0, 0, 1, 1, 0]) + y = np.arange(1, 6) + + fig, ax = plt.subplots() + scat = ax.scatter(x, y) + masked_offsets = np.ma.column_stack([x, y]) + scat.set_offsets(masked_offsets) + assert isinstance(scat.get_offsets(), type(masked_offsets)) + + unmasked_offsets = np.column_stack([x, y]) + scat.set_offsets(unmasked_offsets) + assert isinstance(scat.get_offsets(), type(unmasked_offsets)) diff --git a/lib/matplotlib/tests/test_colorbar.py b/lib/matplotlib/tests/test_colorbar.py index bbd1e9c5f590..e39d0073786b 100644 --- a/lib/matplotlib/tests/test_colorbar.py +++ b/lib/matplotlib/tests/test_colorbar.py @@ -1,16 +1,23 @@ +import platform + import numpy as np import pytest +from matplotlib import _api from matplotlib import cm import matplotlib.colors as mcolors +import matplotlib as mpl + from matplotlib import rc_context from matplotlib.testing.decorators import image_comparison import matplotlib.pyplot as plt -from matplotlib.colors import (BoundaryNorm, LogNorm, PowerNorm, Normalize, - TwoSlopeNorm) -from matplotlib.colorbar import ColorbarBase, _ColorbarLogLocator -from matplotlib.ticker import FixedLocator +from matplotlib.colors import ( + BoundaryNorm, LogNorm, PowerNorm, Normalize, NoNorm +) +from matplotlib.colorbar import Colorbar +from matplotlib.ticker import FixedLocator, LogFormatter +from matplotlib.testing.decorators import check_figures_equal def _get_cmap_norms(): @@ -22,7 +29,7 @@ def _get_cmap_norms(): colorbar_extension_length. """ # Create a colormap and specify the levels it represents. - cmap = cm.get_cmap("RdBu", lut=5) + cmap = mpl.colormaps["RdBu"].resampled(5) clevs = [-5., -2.5, -.5, .5, 1.5, 3.5] # Define norms for the colormaps. norms = dict() @@ -54,10 +61,10 @@ def _colorbar_extension_shape(spacing): # Create a subplot. cax = fig.add_subplot(4, 1, i + 1) # Generate the colorbar. - ColorbarBase(cax, cmap=cmap, norm=norm, - boundaries=boundaries, values=values, - extend=extension_type, extendrect=True, - orientation='horizontal', spacing=spacing) + Colorbar(cax, cmap=cmap, norm=norm, + boundaries=boundaries, values=values, + extend=extension_type, extendrect=True, + orientation='horizontal', spacing=spacing) # Turn off text and ticks. cax.tick_params(left=False, labelleft=False, bottom=False, labelbottom=False) @@ -86,13 +93,13 @@ def _colorbar_extension_length(spacing): # Create a subplot. cax = fig.add_subplot(12, 1, i*3 + j + 1) # Generate the colorbar. - ColorbarBase(cax, cmap=cmap, norm=norm, - boundaries=boundaries, values=values, - extend=extension_type, extendfrac=extendfrac, - orientation='horizontal', spacing=spacing) + Colorbar(cax, cmap=cmap, norm=norm, + boundaries=boundaries, values=values, + extend=extension_type, extendfrac=extendfrac, + orientation='horizontal', spacing=spacing) # Turn off text and ticks. cax.tick_params(left=False, labelleft=False, - bottom=False, labelbottom=False) + bottom=False, labelbottom=False) # Return the figure to the caller. return fig @@ -122,6 +129,30 @@ def test_colorbar_extension_length(): _colorbar_extension_length('proportional') +@pytest.mark.parametrize("orientation", ["horizontal", "vertical"]) +@pytest.mark.parametrize("extend,expected", [("min", (0, 0, 0, 1)), + ("max", (1, 1, 1, 1)), + ("both", (1, 1, 1, 1))]) +def test_colorbar_extension_inverted_axis(orientation, extend, expected): + """Test extension color with an inverted axis""" + data = np.arange(12).reshape(3, 4) + fig, ax = plt.subplots() + cmap = mpl.colormaps["viridis"].with_extremes(under=(0, 0, 0, 1), + over=(1, 1, 1, 1)) + im = ax.imshow(data, cmap=cmap) + cbar = fig.colorbar(im, orientation=orientation, extend=extend) + if orientation == "horizontal": + cbar.ax.invert_xaxis() + else: + cbar.ax.invert_yaxis() + assert cbar._extend_patches[0].get_facecolor() == expected + if extend == "both": + assert len(cbar._extend_patches) == 2 + assert cbar._extend_patches[1].get_facecolor() == (0, 0, 0, 1) + else: + assert len(cbar._extend_patches) == 1 + + @pytest.mark.parametrize('use_gridspec', [True, False]) @image_comparison(['cbar_with_orientation', 'cbar_locationing', @@ -184,6 +215,39 @@ def test_colorbar_positioning(use_gridspec): anchor=(0.8, 0.5), shrink=0.6, use_gridspec=use_gridspec) +def test_colorbar_single_ax_panchor_false(): + # Note that this differs from the tests above with panchor=False because + # there use_gridspec is actually ineffective: passing *ax* as lists always + # disables use_gridspec. + ax = plt.subplot(111, anchor='N') + plt.imshow([[0, 1]]) + plt.colorbar(panchor=False) + assert ax.get_anchor() == 'N' + + +@pytest.mark.parametrize('constrained', [False, True], + ids=['standard', 'constrained']) +def test_colorbar_single_ax_panchor_east(constrained): + fig = plt.figure(constrained_layout=constrained) + ax = fig.add_subplot(111, anchor='N') + plt.imshow([[0, 1]]) + plt.colorbar(panchor='E') + assert ax.get_anchor() == 'E' + + +@image_comparison( + ['contour_colorbar.png'], remove_text=True, + tol=0.01 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) +def test_contour_colorbar(): + fig, ax = plt.subplots(figsize=(4, 2)) + data = np.arange(1200).reshape(30, 40) - 500 + levels = np.array([0, 200, 400, 600, 800, 1000, 1200]) - 500 + + CS = ax.contour(data, levels=levels, extend='both') + fig.colorbar(CS, orientation='horizontal', extend='both') + fig.colorbar(CS, orientation='vertical') + + @image_comparison(['cbar_with_subplots_adjust.png'], remove_text=True, savefig_kwarg={'dpi': 40}) def test_gridspec_make_colorbar(): @@ -211,7 +275,7 @@ def test_colorbar_single_scatter(): plt.figure() x = y = [0] z = [50] - cmap = plt.get_cmap('jet', 16) + cmap = mpl.colormaps['jet'].resampled(16) cs = plt.scatter(x, y, z, c=z, cmap=cmap) plt.colorbar(cs) @@ -220,10 +284,10 @@ def test_colorbar_single_scatter(): ids=['no gridspec', 'with gridspec']) def test_remove_from_figure(use_gridspec): """ - Test `remove_from_figure` with the specified ``use_gridspec`` setting + Test `remove` with the specified ``use_gridspec`` setting """ fig, ax = plt.subplots() - sc = ax.scatter([1, 2], [3, 4], cmap="spring") + sc = ax.scatter([1, 2], [3, 4]) sc.set_array(np.array([5, 6])) pre_position = ax.get_position() cb = fig.colorbar(sc, use_gridspec=use_gridspec) @@ -234,13 +298,39 @@ def test_remove_from_figure(use_gridspec): assert (pre_position.get_points() == post_position.get_points()).all() +def test_remove_from_figure_cl(): + """ + Test `remove` with constrained_layout + """ + fig, ax = plt.subplots(constrained_layout=True) + sc = ax.scatter([1, 2], [3, 4]) + sc.set_array(np.array([5, 6])) + fig.draw_without_rendering() + pre_position = ax.get_position() + cb = fig.colorbar(sc) + cb.remove() + fig.draw_without_rendering() + post_position = ax.get_position() + np.testing.assert_allclose(pre_position.get_points(), + post_position.get_points()) + + def test_colorbarbase(): # smoke test from #3805 ax = plt.gca() - ColorbarBase(ax, cmap=plt.cm.bone) + Colorbar(ax, cmap=plt.cm.bone) + + +def test_parentless_mappable(): + pc = mpl.collections.PatchCollection([], cmap=plt.get_cmap('viridis')) + pc.set_array([]) + with pytest.warns(_api.MatplotlibDeprecationWarning, + match='Unable to determine Axes to steal'): + plt.colorbar(pc) -@image_comparison(['colorbar_closed_patch'], remove_text=True) + +@image_comparison(['colorbar_closed_patch.png'], remove_text=True) def test_colorbar_closed_patch(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -252,7 +342,7 @@ def test_colorbar_closed_patch(): ax4 = fig.add_axes([0.05, 0.25, 0.9, 0.1]) ax5 = fig.add_axes([0.05, 0.05, 0.9, 0.1]) - cmap = cm.get_cmap("RdBu", lut=5) + cmap = mpl.colormaps["RdBu"].resampled(5) im = ax1.pcolormesh(np.linspace(0, 10, 16).reshape((4, 4)), cmap=cmap) @@ -310,8 +400,8 @@ def test_colorbar_minorticks_on_off(): cbar.minorticks_on() np.testing.assert_almost_equal( cbar.ax.yaxis.get_minorticklocs(), - [-1.2, -1.1, -0.9, -0.8, -0.7, -0.6, -0.4, -0.3, -0.2, -0.1, - 0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9, 1.1, 1.2]) + [-1.1, -0.9, -0.8, -0.7, -0.6, -0.4, -0.3, -0.2, -0.1, + 0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9, 1.1, 1.2, 1.3]) # tests for github issue #13257 and PR #13265 data = np.random.uniform(low=1, high=10, size=(20, 20)) @@ -319,8 +409,8 @@ def test_colorbar_minorticks_on_off(): fig, ax = plt.subplots() im = ax.pcolormesh(data, norm=LogNorm()) cbar = fig.colorbar(im) + fig.canvas.draw() default_minorticklocks = cbar.ax.yaxis.get_minorticklocs() - # test that minorticks turn off for LogNorm cbar.minorticks_off() np.testing.assert_equal(cbar.ax.yaxis.get_minorticklocs(), []) @@ -381,10 +471,12 @@ def test_colorbar_autoticks(): pcm = ax[1].pcolormesh(X, Y, Z) cbar2 = fig.colorbar(pcm, ax=ax[1], extend='both', orientation='vertical', shrink=0.4) + # note only -10 to 10 are visible, np.testing.assert_almost_equal(cbar.ax.yaxis.get_ticklocs(), - np.arange(-10, 11, 5)) + np.arange(-15, 16, 5)) + # note only -10 to 10 are visible np.testing.assert_almost_equal(cbar2.ax.yaxis.get_ticklocs(), - np.arange(-10, 11, 10)) + np.arange(-20, 21, 10)) def test_colorbar_autotickslog(): @@ -403,10 +495,12 @@ def test_colorbar_autotickslog(): pcm = ax[1].pcolormesh(X, Y, 10**Z, norm=LogNorm()) cbar2 = fig.colorbar(pcm, ax=ax[1], extend='both', orientation='vertical', shrink=0.4) + # note only -12 to +12 are visible np.testing.assert_almost_equal(cbar.ax.yaxis.get_ticklocs(), - 10**np.arange(-12., 12.2, 4.)) + 10**np.arange(-16., 16.2, 4.)) + # note only -24 to +24 are visible np.testing.assert_almost_equal(cbar2.ax.yaxis.get_ticklocs(), - 10**np.arange(-12., 13., 12.)) + 10**np.arange(-24., 25., 12.)) def test_colorbar_get_ticks(): @@ -426,27 +520,44 @@ def test_colorbar_get_ticks(): assert userTicks.get_ticks().tolist() == [600, 700, 800] # testing for getter after calling set_ticks with some ticks out of bounds - userTicks.set_ticks([600, 1300, 1400, 1500]) - assert userTicks.get_ticks().tolist() == [600] + # removed #20054: other axes don't trim fixed lists, so colorbars + # should not either: + # userTicks.set_ticks([600, 1300, 1400, 1500]) + # assert userTicks.get_ticks().tolist() == [600] # testing getter when no ticks are assigned defTicks = plt.colorbar(orientation='horizontal') - assert defTicks.get_ticks().tolist() == levels + np.testing.assert_allclose(defTicks.get_ticks().tolist(), levels) + + # test normal ticks and minor ticks + fig, ax = plt.subplots() + x = np.arange(-3.0, 4.001) + y = np.arange(-4.0, 3.001) + X, Y = np.meshgrid(x, y) + Z = X * Y + Z = Z[:-1, :-1] + pcm = ax.pcolormesh(X, Y, Z) + cbar = fig.colorbar(pcm, ax=ax, extend='both', + orientation='vertical') + ticks = cbar.get_ticks() + np.testing.assert_allclose(ticks, np.arange(-15, 16, 5)) + assert len(cbar.get_ticks(minor=True)) == 0 -def test_colorbar_lognorm_extension(): +@pytest.mark.parametrize("extend", ['both', 'min', 'max']) +def test_colorbar_lognorm_extension(extend): # Test that colorbar with lognorm is extended correctly f, ax = plt.subplots() - cb = ColorbarBase(ax, norm=LogNorm(vmin=0.1, vmax=1000.0), - orientation='vertical', extend='both') + cb = Colorbar(ax, norm=LogNorm(vmin=0.1, vmax=1000.0), + orientation='vertical', extend=extend) assert cb._values[0] >= 0.0 def test_colorbar_powernorm_extension(): # Test that colorbar with powernorm is extended correctly f, ax = plt.subplots() - cb = ColorbarBase(ax, norm=PowerNorm(gamma=0.5, vmin=0.0, vmax=1.0), - orientation='vertical', extend='both') + cb = Colorbar(ax, norm=PowerNorm(gamma=0.5, vmin=0.0, vmax=1.0), + orientation='vertical', extend='both') assert cb._values[0] >= 0.0 @@ -465,13 +576,13 @@ def test_colorbar_log_minortick_labels(): pcm = ax.imshow([[10000, 50000]], norm=LogNorm()) cb = fig.colorbar(pcm) fig.canvas.draw() - lb = cb.ax.yaxis.get_ticklabels(which='both') + lb = [l.get_text() for l in cb.ax.yaxis.get_ticklabels(which='both')] expected = [r'$\mathdefault{10^{4}}$', r'$\mathdefault{2\times10^{4}}$', r'$\mathdefault{3\times10^{4}}$', r'$\mathdefault{4\times10^{4}}$'] - for l, exp in zip(lb, expected): - assert l.get_text() == exp + for exp in expected: + assert exp in lb def test_colorbar_renorm(): @@ -482,16 +593,15 @@ def test_colorbar_renorm(): im = ax.imshow(z) cbar = fig.colorbar(im) np.testing.assert_allclose(cbar.ax.yaxis.get_majorticklocs(), - np.arange(0, 120000.1, 15000)) + np.arange(0, 120000.1, 20000)) cbar.set_ticks([1, 2, 3]) assert isinstance(cbar.locator, FixedLocator) norm = LogNorm(z.min(), z.max()) im.set_norm(norm) - assert isinstance(cbar.locator, _ColorbarLogLocator) np.testing.assert_allclose(cbar.ax.yaxis.get_majorticklocs(), - np.logspace(-8, 5, 14)) + np.logspace(-10, 7, 18)) # note that set_norm removes the FixedLocator... assert np.isclose(cbar.vmin, z.min()) cbar.set_ticks([1, 2, 3]) @@ -505,28 +615,29 @@ def test_colorbar_renorm(): assert np.isclose(cbar.vmax, z.max() * 1000) -def test_colorbar_format(): +@pytest.mark.parametrize('fmt', ['%4.2e', '{x:.2e}']) +def test_colorbar_format(fmt): # make sure that format is passed properly x, y = np.ogrid[-4:4:31j, -4:4:31j] z = 120000*np.exp(-x**2 - y**2) fig, ax = plt.subplots() im = ax.imshow(z) - cbar = fig.colorbar(im, format='%4.2e') + cbar = fig.colorbar(im, format=fmt) fig.canvas.draw() - assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '6.00e+04' + assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '8.00e+04' # make sure that if we change the clim of the mappable that the # formatting is *not* lost: im.set_clim([4, 200]) fig.canvas.draw() - assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '8.00e+01' + assert cbar.ax.yaxis.get_ticklabels()[4].get_text() == '2.00e+02' # but if we change the norm: im.set_norm(LogNorm(vmin=0.1, vmax=10)) fig.canvas.draw() assert (cbar.ax.yaxis.get_ticklabels()[0].get_text() == - r'$\mathdefault{10^{-1}}$') + '$\\mathdefault{10^{-2}}$') def test_colorbar_scale_reset(): @@ -552,7 +663,7 @@ def test_colorbar_get_ticks_2(): fig, ax = plt.subplots() pc = ax.pcolormesh([[.05, .95]]) cb = fig.colorbar(pc) - np.testing.assert_allclose(cb.get_ticks(), [0.2, 0.4, 0.6, 0.8]) + np.testing.assert_allclose(cb.get_ticks(), [0., 0.2, 0.4, 0.6, 0.8, 1.0]) def test_colorbar_inverted_ticks(): @@ -570,29 +681,32 @@ def test_colorbar_inverted_ticks(): cbar.minorticks_on() ticks = cbar.get_ticks() minorticks = cbar.get_ticks(minor=True) + assert isinstance(minorticks, np.ndarray) cbar.ax.invert_yaxis() np.testing.assert_allclose(ticks, cbar.get_ticks()) np.testing.assert_allclose(minorticks, cbar.get_ticks(minor=True)) -def test_extend_colorbar_customnorm(): - # This was a funny error with TwoSlopeNorm, maybe with other norms, - # when extend='both' - fig, (ax0, ax1) = plt.subplots(2, 1) - pcm = ax0.pcolormesh([[0]], norm=TwoSlopeNorm(vcenter=0., vmin=-2, vmax=1)) - cb = fig.colorbar(pcm, ax=ax0, extend='both') - np.testing.assert_allclose(cb.ax.get_position().extents, - [0.78375, 0.536364, 0.796147, 0.9], rtol=1e-3) - - def test_mappable_no_alpha(): fig, ax = plt.subplots() sm = cm.ScalarMappable(norm=mcolors.Normalize(), cmap='viridis') - fig.colorbar(sm) + fig.colorbar(sm, ax=ax) sm.set_cmap('plasma') plt.draw() +def test_mappable_2d_alpha(): + fig, ax = plt.subplots() + x = np.arange(1, 5).reshape(2, 2)/4 + pc = ax.pcolormesh(x, alpha=x) + cb = fig.colorbar(pc, ax=ax) + # The colorbar's alpha should be None and the mappable should still have + # the original alpha array + assert cb.alpha is None + assert pc.get_alpha() is x + fig.draw_without_rendering() + + def test_colorbar_label(): """ Test the label parameter. It should just be mapped to the xlabel/ylabel of @@ -614,6 +728,17 @@ def test_colorbar_label(): assert cbar3.ax.get_xlabel() == 'horizontal cbar' +@image_comparison(['colorbar_keeping_xlabel.png'], style='mpl20') +def test_keeping_xlabel(): + # github issue #23398 - xlabels being ignored in colorbar axis + arr = np.arange(25).reshape((5, 5)) + fig, ax = plt.subplots() + im = ax.imshow(arr) + cbar = plt.colorbar(im) + cbar.ax.set_xlabel('Visible Xlabel') + cbar.set_label('YLabel') + + @pytest.mark.parametrize("clim", [(-20000, 20000), (-32768, 0)]) def test_colorbar_int(clim): # Check that we cast to float early enough to not @@ -705,3 +830,388 @@ def test_anchored_cbar_position_using_specgrid(): np.testing.assert_allclose( [cx1, cx0], [x1 * shrink + (1 - shrink) * p0, p0 * (1 - shrink) + x0 * shrink]) + + +@image_comparison(['colorbar_change_lim_scale.png'], remove_text=True, + style='mpl20') +def test_colorbar_change_lim_scale(): + fig, ax = plt.subplots(1, 2, constrained_layout=True) + pc = ax[0].pcolormesh(np.arange(100).reshape(10, 10)+1) + cb = fig.colorbar(pc, ax=ax[0], extend='both') + cb.ax.set_yscale('log') + + pc = ax[1].pcolormesh(np.arange(100).reshape(10, 10)+1) + cb = fig.colorbar(pc, ax=ax[1], extend='both') + cb.ax.set_ylim([20, 90]) + + +@check_figures_equal(extensions=["png"]) +def test_axes_handles_same_functions(fig_ref, fig_test): + # prove that cax and cb.ax are functionally the same + for nn, fig in enumerate([fig_ref, fig_test]): + ax = fig.add_subplot() + pc = ax.pcolormesh(np.ones(300).reshape(10, 30)) + cax = fig.add_axes([0.9, 0.1, 0.03, 0.8]) + cb = fig.colorbar(pc, cax=cax) + if nn == 0: + caxx = cax + else: + caxx = cb.ax + caxx.set_yticks(np.arange(0, 20)) + caxx.set_yscale('log') + caxx.set_position([0.92, 0.1, 0.02, 0.7]) + + +def test_inset_colorbar_layout(): + fig, ax = plt.subplots(constrained_layout=True, figsize=(3, 6)) + pc = ax.imshow(np.arange(100).reshape(10, 10)) + cax = ax.inset_axes([1.02, 0.1, 0.03, 0.8]) + cb = fig.colorbar(pc, cax=cax) + + fig.draw_without_rendering() + # make sure this is in the figure. In the colorbar swapping + # it was being dropped from the list of children... + np.testing.assert_allclose(cb.ax.get_position().bounds, + [0.87, 0.342, 0.0237, 0.315], atol=0.01) + assert cb.ax in ax.child_axes + + +@image_comparison(['colorbar_twoslope.png'], remove_text=True, + style='mpl20') +def test_twoslope_colorbar(): + # Note that the second tick = 20, and should be in the middle + # of the colorbar (white) + # There should be no tick right at the bottom, nor at the top. + fig, ax = plt.subplots() + + norm = mcolors.TwoSlopeNorm(20, 5, 95) + pc = ax.pcolormesh(np.arange(1, 11), np.arange(1, 11), + np.arange(100).reshape(10, 10), + norm=norm, cmap='RdBu_r') + fig.colorbar(pc) + + +@check_figures_equal(extensions=["png"]) +def test_remove_cb_whose_mappable_has_no_figure(fig_ref, fig_test): + ax = fig_test.add_subplot() + cb = fig_test.colorbar(cm.ScalarMappable(), cax=ax) + cb.remove() + + +def test_aspects(): + fig, ax = plt.subplots(3, 2, figsize=(8, 8)) + aspects = [20, 20, 10] + extends = ['neither', 'both', 'both'] + cb = [[None, None, None], [None, None, None]] + for nn, orient in enumerate(['vertical', 'horizontal']): + for mm, (aspect, extend) in enumerate(zip(aspects, extends)): + pc = ax[mm, nn].pcolormesh(np.arange(100).reshape(10, 10)) + cb[nn][mm] = fig.colorbar(pc, ax=ax[mm, nn], orientation=orient, + aspect=aspect, extend=extend) + fig.draw_without_rendering() + # check the extends are right ratio: + np.testing.assert_almost_equal(cb[0][1].ax.get_position().height, + cb[0][0].ax.get_position().height * 0.9, + decimal=2) + # horizontal + np.testing.assert_almost_equal(cb[1][1].ax.get_position().width, + cb[1][0].ax.get_position().width * 0.9, + decimal=2) + # check correct aspect: + pos = cb[0][0].ax.get_position(original=False) + np.testing.assert_almost_equal(pos.height, pos.width * 20, decimal=2) + pos = cb[1][0].ax.get_position(original=False) + np.testing.assert_almost_equal(pos.height * 20, pos.width, decimal=2) + # check twice as wide if aspect is 10 instead of 20 + np.testing.assert_almost_equal( + cb[0][0].ax.get_position(original=False).width * 2, + cb[0][2].ax.get_position(original=False).width, decimal=2) + np.testing.assert_almost_equal( + cb[1][0].ax.get_position(original=False).height * 2, + cb[1][2].ax.get_position(original=False).height, decimal=2) + + +@image_comparison(['proportional_colorbars.png'], remove_text=True, + style='mpl20') +def test_proportional_colorbars(): + + x = y = np.arange(-3.0, 3.01, 0.025) + X, Y = np.meshgrid(x, y) + Z1 = np.exp(-X**2 - Y**2) + Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) + Z = (Z1 - Z2) * 2 + + levels = [-1.25, -0.5, -0.125, 0.125, 0.5, 1.25] + cmap = mcolors.ListedColormap( + ['0.3', '0.5', 'white', 'lightblue', 'steelblue']) + cmap.set_under('darkred') + cmap.set_over('crimson') + norm = mcolors.BoundaryNorm(levels, cmap.N) + + extends = ['neither', 'both'] + spacings = ['uniform', 'proportional'] + fig, axs = plt.subplots(2, 2) + for i in range(2): + for j in range(2): + CS3 = axs[i, j].contourf(X, Y, Z, levels, cmap=cmap, norm=norm, + extend=extends[i]) + fig.colorbar(CS3, spacing=spacings[j], ax=axs[i, j]) + + +@image_comparison(['extend_drawedges.png'], remove_text=True, style='mpl20') +def test_colorbar_extend_drawedges(): + params = [ + ('both', 1, [[[1.1, 0], [1.1, 1]], + [[2, 0], [2, 1]], + [[2.9, 0], [2.9, 1]]]), + ('min', 0, [[[1.1, 0], [1.1, 1]], + [[2, 0], [2, 1]]]), + ('max', 0, [[[2, 0], [2, 1]], + [[2.9, 0], [2.9, 1]]]), + ('neither', -1, [[[2, 0], [2, 1]]]), + ] + + plt.rcParams['axes.linewidth'] = 2 + + fig = plt.figure(figsize=(10, 4)) + subfigs = fig.subfigures(1, 2) + + for orientation, subfig in zip(['horizontal', 'vertical'], subfigs): + if orientation == 'horizontal': + axs = subfig.subplots(4, 1) + else: + axs = subfig.subplots(1, 4) + fig.subplots_adjust(left=0.05, bottom=0.05, right=0.95, top=0.95) + + for ax, (extend, coloroffset, res) in zip(axs, params): + cmap = mpl.colormaps["viridis"] + bounds = np.arange(5) + nb_colors = len(bounds) + coloroffset + colors = cmap(np.linspace(100, 255, nb_colors).astype(int)) + cmap, norm = mcolors.from_levels_and_colors(bounds, colors, + extend=extend) + + cbar = Colorbar(ax, cmap=cmap, norm=norm, orientation=orientation, + drawedges=True) + # Set limits such that only two colours are visible, and the + # dividers would be outside the Axes, to ensure that a) they are + # not drawn outside, and b) a divider still appears between the + # main colour and the extension. + if orientation == 'horizontal': + ax.set_xlim(1.1, 2.9) + else: + ax.set_ylim(1.1, 2.9) + res = np.array(res)[:, :, [1, 0]] + np.testing.assert_array_equal(cbar.dividers.get_segments(), res) + + +@image_comparison(['contourf_extend_patches.png'], remove_text=True, + style='mpl20') +def test_colorbar_contourf_extend_patches(): + params = [ + ('both', 5, ['\\', '//']), + ('min', 7, ['+']), + ('max', 2, ['|', '-', '/', '\\', '//']), + ('neither', 10, ['//', '\\', '||']), + ] + + plt.rcParams['axes.linewidth'] = 2 + + fig = plt.figure(figsize=(10, 4)) + subfigs = fig.subfigures(1, 2) + fig.subplots_adjust(left=0.05, bottom=0.05, right=0.95, top=0.95) + + x = np.linspace(-2, 3, 50) + y = np.linspace(-2, 3, 30) + z = np.cos(x[np.newaxis, :]) + np.sin(y[:, np.newaxis]) + + cmap = mpl.colormaps["viridis"] + for orientation, subfig in zip(['horizontal', 'vertical'], subfigs): + axs = subfig.subplots(2, 2).ravel() + for ax, (extend, levels, hatches) in zip(axs, params): + cs = ax.contourf(x, y, z, levels, hatches=hatches, + cmap=cmap, extend=extend) + subfig.colorbar(cs, ax=ax, orientation=orientation, fraction=0.4, + extendfrac=0.2, aspect=5) + + +def test_negative_boundarynorm(): + fig, ax = plt.subplots(figsize=(1, 3)) + cmap = mpl.colormaps["viridis"] + + clevs = np.arange(-94, -85) + norm = BoundaryNorm(clevs, cmap.N) + cb = fig.colorbar(cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax) + np.testing.assert_allclose(cb.ax.get_ylim(), [clevs[0], clevs[-1]]) + np.testing.assert_allclose(cb.ax.get_yticks(), clevs) + + clevs = np.arange(85, 94) + norm = BoundaryNorm(clevs, cmap.N) + cb = fig.colorbar(cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax) + np.testing.assert_allclose(cb.ax.get_ylim(), [clevs[0], clevs[-1]]) + np.testing.assert_allclose(cb.ax.get_yticks(), clevs) + + clevs = np.arange(-3, 3) + norm = BoundaryNorm(clevs, cmap.N) + cb = fig.colorbar(cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax) + np.testing.assert_allclose(cb.ax.get_ylim(), [clevs[0], clevs[-1]]) + np.testing.assert_allclose(cb.ax.get_yticks(), clevs) + + clevs = np.arange(-8, 1) + norm = BoundaryNorm(clevs, cmap.N) + cb = fig.colorbar(cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax) + np.testing.assert_allclose(cb.ax.get_ylim(), [clevs[0], clevs[-1]]) + np.testing.assert_allclose(cb.ax.get_yticks(), clevs) + + +def test_centerednorm(): + # Test default centered norm gets expanded with non-singular limits + # when plot data is all equal (autoscale halfrange == 0) + fig, ax = plt.subplots(figsize=(1, 3)) + + norm = mcolors.CenteredNorm() + mappable = ax.pcolormesh(np.zeros((3, 3)), norm=norm) + fig.colorbar(mappable) + assert (norm.vmin, norm.vmax) == (-0.1, 0.1) + + +@image_comparison(['nonorm_colorbars.svg'], style='mpl20') +def test_nonorm(): + plt.rcParams['svg.fonttype'] = 'none' + data = [1, 2, 3, 4, 5] + + fig, ax = plt.subplots(figsize=(6, 1)) + fig.subplots_adjust(bottom=0.5) + + norm = NoNorm(vmin=min(data), vmax=max(data)) + cmap = mpl.colormaps["viridis"].resampled(len(data)) + mappable = cm.ScalarMappable(norm=norm, cmap=cmap) + cbar = fig.colorbar(mappable, cax=ax, orientation="horizontal") + + +@image_comparison(['test_boundaries.png'], remove_text=True, + style='mpl20') +def test_boundaries(): + np.random.seed(seed=19680808) + fig, ax = plt.subplots(figsize=(2, 2)) + pc = ax.pcolormesh(np.random.randn(10, 10), cmap='RdBu_r') + cb = fig.colorbar(pc, ax=ax, boundaries=np.linspace(-3, 3, 7)) + + +def test_colorbar_no_warning_rcparams_grid_true(): + # github issue #21723 - If mpl style has 'axes.grid' = True, + # fig.colorbar raises a warning about Auto-removal of grids + # by pcolor() and pcolormesh(). This is fixed by PR #22216. + plt.rcParams['axes.grid'] = True + fig, ax = plt.subplots() + ax.grid(False) + im = ax.pcolormesh([0, 1], [0, 1], [[1]]) + # make sure that no warning is raised by fig.colorbar + fig.colorbar(im) + + +def test_colorbar_set_formatter_locator(): + # check that the locator properties echo what is on the axis: + fig, ax = plt.subplots() + pc = ax.pcolormesh(np.random.randn(10, 10)) + cb = fig.colorbar(pc) + cb.ax.yaxis.set_major_locator(FixedLocator(np.arange(10))) + cb.ax.yaxis.set_minor_locator(FixedLocator(np.arange(0, 10, 0.2))) + assert cb.locator is cb.ax.yaxis.get_major_locator() + assert cb.minorlocator is cb.ax.yaxis.get_minor_locator() + cb.ax.yaxis.set_major_formatter(LogFormatter()) + cb.ax.yaxis.set_minor_formatter(LogFormatter()) + assert cb.formatter is cb.ax.yaxis.get_major_formatter() + assert cb.minorformatter is cb.ax.yaxis.get_minor_formatter() + + # check that the setter works as expected: + loc = FixedLocator(np.arange(7)) + cb.locator = loc + assert cb.ax.yaxis.get_major_locator() is loc + loc = FixedLocator(np.arange(0, 7, 0.1)) + cb.minorlocator = loc + assert cb.ax.yaxis.get_minor_locator() is loc + fmt = LogFormatter() + cb.formatter = fmt + assert cb.ax.yaxis.get_major_formatter() is fmt + fmt = LogFormatter() + cb.minorformatter = fmt + assert cb.ax.yaxis.get_minor_formatter() is fmt + + +@image_comparison(['colorbar_extend_alpha.png'], remove_text=True, + savefig_kwarg={'dpi': 40}) +def test_colorbar_extend_alpha(): + fig, ax = plt.subplots() + im = ax.imshow([[0, 1], [2, 3]], alpha=0.3, interpolation="none") + fig.colorbar(im, extend='both', boundaries=[0.5, 1.5, 2.5]) + + +def test_offset_text_loc(): + plt.style.use('mpl20') + fig, ax = plt.subplots() + np.random.seed(seed=19680808) + pc = ax.pcolormesh(np.random.randn(10, 10)*1e6) + cb = fig.colorbar(pc, location='right', extend='max') + fig.draw_without_rendering() + # check that the offsetText is in the proper place above the + # colorbar axes. In this case the colorbar axes is the same + # height as the parent, so use the parents bbox. + assert cb.ax.yaxis.offsetText.get_position()[1] > ax.bbox.y1 + + +def test_title_text_loc(): + plt.style.use('mpl20') + fig, ax = plt.subplots() + np.random.seed(seed=19680808) + pc = ax.pcolormesh(np.random.randn(10, 10)) + cb = fig.colorbar(pc, location='right', extend='max') + cb.ax.set_title('Aardvark') + fig.draw_without_rendering() + # check that the title is in the proper place above the + # colorbar axes, including its extend triangles.... + assert (cb.ax.title.get_window_extent(fig.canvas.get_renderer()).ymax > + cb.ax.spines['outline'].get_window_extent().ymax) + + +@check_figures_equal(extensions=["png"]) +def test_passing_location(fig_ref, fig_test): + ax_ref = fig_ref.add_subplot() + im = ax_ref.imshow([[0, 1], [2, 3]]) + ax_ref.figure.colorbar(im, cax=ax_ref.inset_axes([0, 1.05, 1, 0.05]), + orientation="horizontal", ticklocation="top") + ax_test = fig_test.add_subplot() + im = ax_test.imshow([[0, 1], [2, 3]]) + ax_test.figure.colorbar(im, cax=ax_test.inset_axes([0, 1.05, 1, 0.05]), + location="top") + + +@pytest.mark.parametrize("kwargs,error,message", [ + ({'location': 'top', 'orientation': 'vertical'}, TypeError, + "location and orientation are mutually exclusive"), + ({'location': 'top', 'orientation': 'vertical', 'cax': True}, TypeError, + "location and orientation are mutually exclusive"), # Different to above + ({'ticklocation': 'top', 'orientation': 'vertical', 'cax': True}, + ValueError, "'top' is not a valid value for position"), + ({'location': 'top', 'extendfrac': (0, None)}, ValueError, + "invalid value for extendfrac"), + ]) +def test_colorbar_errors(kwargs, error, message): + fig, ax = plt.subplots() + im = ax.imshow([[0, 1], [2, 3]]) + if kwargs.get('cax', None) is True: + kwargs['cax'] = ax.inset_axes([0, 1.05, 1, 0.05]) + with pytest.raises(error, match=message): + fig.colorbar(im, **kwargs) + + +def test_colorbar_axes_parmeters(): + fig, ax = plt.subplots(2) + im = ax[0].imshow([[0, 1], [2, 3]]) + # colorbar should accept any form of axes sequence: + fig.colorbar(im, ax=ax) + fig.colorbar(im, ax=ax[0]) + fig.colorbar(im, ax=[_ax for _ax in ax]) + fig.colorbar(im, ax=(ax[0], ax[1])) + fig.colorbar(im, ax={i: _ax for i, _ax in enumerate(ax)}.values()) + fig.draw_without_rendering() diff --git a/lib/matplotlib/tests/test_colors.py b/lib/matplotlib/tests/test_colors.py index 04ad73bfc185..e40796caa7cc 100644 --- a/lib/matplotlib/tests/test_colors.py +++ b/lib/matplotlib/tests/test_colors.py @@ -1,5 +1,6 @@ import copy import itertools +import unittest.mock from io import BytesIO import numpy as np @@ -9,15 +10,14 @@ from numpy.testing import assert_array_equal, assert_array_almost_equal -from matplotlib import cycler +from matplotlib import cbook, cm, cycler import matplotlib +import matplotlib as mpl import matplotlib.colors as mcolors -import matplotlib.cm as cm import matplotlib.colorbar as mcolorbar -import matplotlib.cbook as cbook import matplotlib.pyplot as plt import matplotlib.scale as mscale -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import image_comparison, check_figures_equal @pytest.mark.parametrize('N, result', [ @@ -30,9 +30,16 @@ def test_create_lookup_table(N, result): assert_array_almost_equal(mcolors._create_lookup_table(N, data), result) -def test_resample(): +@pytest.mark.parametrize("dtype", [np.uint8, int, np.float16, float]) +def test_index_dtype(dtype): + # We use subtraction in the indexing, so need to verify that uint8 works + cm = mpl.colormaps["viridis"] + assert_array_equal(cm(dtype(0)), cm(0)) + + +def test_resampled(): """ - GitHub issue #6025 pointed to incorrect ListedColormap._resample; + GitHub issue #6025 pointed to incorrect ListedColormap.resampled; here we test the method for LinearSegmentedColormap as well. """ n = 101 @@ -48,8 +55,8 @@ def test_resample(): cmap.set_under('r') cmap.set_over('g') cmap.set_bad('b') - lsc3 = lsc._resample(3) - lc3 = lc._resample(3) + lsc3 = lsc.resampled(3) + lc3 = lc.resampled(3) expected = np.array([[0.0, 0.2, 1.0, 0.7], [0.5, 0.2, 0.5, 0.7], [1.0, 0.2, 0.0, 0.7]], float) @@ -65,79 +72,91 @@ def test_resample(): def test_register_cmap(): - new_cm = copy.copy(cm.get_cmap("viridis")) + new_cm = mpl.colormaps["viridis"] target = "viridis2" - cm.register_cmap(target, new_cm) - assert plt.get_cmap(target) == new_cm + with pytest.warns( + mpl.MatplotlibDeprecationWarning, + match=r"matplotlib\.colormaps\.register\(name\)" + ): + cm.register_cmap(target, new_cm) + assert mpl.colormaps[target] == new_cm with pytest.raises(ValueError, match="Arguments must include a name or a Colormap"): - cm.register_cmap() - - with pytest.warns(UserWarning): - cm.register_cmap(target, new_cm) - - cm.unregister_cmap(target) + with pytest.warns( + mpl.MatplotlibDeprecationWarning, + match=r"matplotlib\.colormaps\.register\(name\)" + ): + cm.register_cmap() + + with pytest.warns( + mpl.MatplotlibDeprecationWarning, + match=r"matplotlib\.colormaps\.unregister\(name\)" + ): + cm.unregister_cmap(target) with pytest.raises(ValueError, match=f'{target!r} is not a valid value for name;'): - cm.get_cmap(target) - # test that second time is error free - cm.unregister_cmap(target) - - with pytest.raises(ValueError, match="You must pass a Colormap instance."): - cm.register_cmap('nome', cmap='not a cmap') + with pytest.warns( + mpl.MatplotlibDeprecationWarning, + match=r"matplotlib\.colormaps\[name\]" + ): + cm.get_cmap(target) + with pytest.warns( + mpl.MatplotlibDeprecationWarning, + match=r"matplotlib\.colormaps\.unregister\(name\)" + ): + # test that second time is error free + cm.unregister_cmap(target) + + with pytest.raises(TypeError, match="'cmap' must be"): + with pytest.warns( + mpl.MatplotlibDeprecationWarning, + match=r"matplotlib\.colormaps\.register\(name\)" + ): + cm.register_cmap('nome', cmap='not a cmap') + + +def test_colormaps_get_cmap(): + cr = mpl.colormaps + + # check str, and Colormap pass + assert cr.get_cmap('plasma') == cr["plasma"] + assert cr.get_cmap(cr["magma"]) == cr["magma"] + + # check default + assert cr.get_cmap(None) == cr[mpl.rcParams['image.cmap']] + + # check ValueError on bad name + bad_cmap = 'AardvarksAreAwkward' + with pytest.raises(ValueError, match=bad_cmap): + cr.get_cmap(bad_cmap) + + # check TypeError on bad type + with pytest.raises(TypeError, match='object'): + cr.get_cmap(object()) def test_double_register_builtin_cmap(): name = "viridis" - match = f"Trying to re-register the builtin cmap {name!r}." + match = f"Re-registering the builtin cmap {name!r}." with pytest.raises(ValueError, match=match): - cm.register_cmap(name, cm.get_cmap(name)) + matplotlib.colormaps.register( + mpl.colormaps[name], name=name, force=True + ) + with pytest.raises(ValueError, match='A colormap named "viridis"'): + with pytest.warns(mpl.MatplotlibDeprecationWarning): + cm.register_cmap(name, mpl.colormaps[name]) with pytest.warns(UserWarning): - cm.register_cmap(name, cm.get_cmap(name), override_builtin=True) + # TODO is warning more than once! + cm.register_cmap(name, mpl.colormaps[name], override_builtin=True) def test_unregister_builtin_cmap(): name = "viridis" match = f'cannot unregister {name!r} which is a builtin colormap.' with pytest.raises(ValueError, match=match): - cm.unregister_cmap(name) - - -def test_colormap_global_set_warn(): - new_cm = plt.get_cmap('viridis') - # Store the old value so we don't override the state later on. - orig_cmap = copy.copy(new_cm) - with pytest.warns(cbook.MatplotlibDeprecationWarning, - match="You are modifying the state of a globally"): - # This should warn now because we've modified the global state - new_cm.set_under('k') - - # This shouldn't warn because it is a copy - copy.copy(new_cm).set_under('b') - - # Test that registering and then modifying warns - plt.register_cmap(name='test_cm', cmap=copy.copy(orig_cmap)) - new_cm = plt.get_cmap('test_cm') - with pytest.warns(cbook.MatplotlibDeprecationWarning, - match="You are modifying the state of a globally"): - # This should warn now because we've modified the global state - new_cm.set_under('k') - - # Re-register the original - with pytest.warns(UserWarning): - plt.register_cmap(cmap=orig_cmap, override_builtin=True) - - -def test_colormap_dict_deprecate(): - # Make sure we warn on get and set access into cmap_d - with pytest.warns(cbook.MatplotlibDeprecationWarning, - match="The global colormaps dictionary is no longer"): - cmap = plt.cm.cmap_d['viridis'] - - with pytest.warns(cbook.MatplotlibDeprecationWarning, - match="The global colormaps dictionary is no longer"): - plt.cm.cmap_d['test'] = cmap + with pytest.warns(mpl.MatplotlibDeprecationWarning): + cm.unregister_cmap(name) def test_colormap_copy(): @@ -162,13 +181,36 @@ def test_colormap_copy(): assert_array_equal(ret1, ret2) +def test_colormap_equals(): + cmap = mpl.colormaps["plasma"] + cm_copy = cmap.copy() + # different object id's + assert cm_copy is not cmap + # But the same data should be equal + assert cm_copy == cmap + # Change the copy + cm_copy.set_bad('y') + assert cm_copy != cmap + # Make sure we can compare different sizes without failure + cm_copy._lut = cm_copy._lut[:10, :] + assert cm_copy != cmap + # Test different names are not equal + cm_copy = cmap.copy() + cm_copy.name = "Test" + assert cm_copy != cmap + # Test colorbar extends + cm_copy = cmap.copy() + cm_copy.colorbar_extend = not cmap.colorbar_extend + assert cm_copy != cmap + + def test_colormap_endian(): """ GitHub issue #1005: a bug in putmask caused erroneous mapping of 1.0 when input from a non-native-byteorder array. """ - cmap = cm.get_cmap("jet") + cmap = mpl.colormaps["jet"] # Test under, over, and invalid along with values 0 and 1. a = [-0.5, 0, 0.5, 1, 1.5, np.nan] for dt in ["f2", "f4", "f8"]: @@ -183,7 +225,7 @@ def test_colormap_invalid(): rather than bad. This tests to make sure all invalid values (-inf, nan, inf) are mapped respectively to (under, bad, over). """ - cmap = cm.get_cmap("plasma") + cmap = mpl.colormaps["plasma"] x = np.array([-np.inf, -1, 0, np.nan, .7, 2, np.inf]) expected = np.array([[0.050383, 0.029803, 0.527975, 1.], @@ -208,7 +250,7 @@ def test_colormap_invalid(): # Test scalar representations assert_array_equal(cmap(-np.inf), cmap(0)) assert_array_equal(cmap(np.inf), cmap(1.0)) - assert_array_equal(cmap(np.nan), np.array([0., 0., 0., 0.])) + assert_array_equal(cmap(np.nan), [0., 0., 0., 0.]) def test_colormap_return_types(): @@ -216,7 +258,7 @@ def test_colormap_return_types(): Make sure that tuples are returned for scalar input and that the proper shapes are returned for ndarrays. """ - cmap = cm.get_cmap("plasma") + cmap = mpl.colormaps["plasma"] # Test return types and shapes # scalar input needs to return a tuple of length 4 assert isinstance(cmap(0.5), tuple) @@ -331,7 +373,7 @@ def test_BoundaryNorm(): # Testing extend keyword, with interpolation (large cmap) bounds = [1, 2, 3] - cmap = cm.get_cmap('viridis') + cmap = mpl.colormaps['viridis'] mynorm = mcolors.BoundaryNorm(bounds, cmap.N, extend='both') refnorm = mcolors.BoundaryNorm([0] + bounds + [4], cmap.N) x = np.random.randn(100) * 10 + 2 @@ -450,11 +492,25 @@ def test_CenteredNorm(): norm(np.linspace(-1.0, 0.0, 10)) assert norm.vmax == 1.0 assert norm.halfrange == 1.0 - # set vcenter to 1, which should double halfrange + # set vcenter to 1, which should move the center but leave the + # halfrange unchanged norm.vcenter = 1 - assert norm.vmin == -1.0 - assert norm.vmax == 3.0 - assert norm.halfrange == 2.0 + assert norm.vmin == 0 + assert norm.vmax == 2 + assert norm.halfrange == 1 + + # Check setting vmin directly updates the halfrange and vmax, but + # leaves vcenter alone + norm.vmin = -1 + assert norm.halfrange == 2 + assert norm.vmax == 3 + assert norm.vcenter == 1 + + # also check vmax updates + norm.vmax = 2 + assert norm.halfrange == 1 + assert norm.vmin == 0 + assert norm.vcenter == 1 @pytest.mark.parametrize("vmin,vmax", [[-1, 2], [3, 1]]) @@ -539,19 +595,20 @@ def test_Normalize(): # i.e. 127-(-128) here). vals = np.array([-128, 127], dtype=np.int8) norm = mcolors.Normalize(vals.min(), vals.max()) - assert_array_equal(np.asarray(norm(vals)), [0, 1]) + assert_array_equal(norm(vals), [0, 1]) # Don't lose precision on longdoubles (float128 on Linux): # for array inputs... vals = np.array([1.2345678901, 9.8765432109], dtype=np.longdouble) - norm = mcolors.Normalize(vals.min(), vals.max()) - assert_array_equal(np.asarray(norm(vals)), [0, 1]) + norm = mcolors.Normalize(vals[0], vals[1]) + assert norm(vals).dtype == np.longdouble + assert_array_equal(norm(vals), [0, 1]) # and for scalar ones. eps = np.finfo(np.longdouble).resolution norm = plt.Normalize(1, 1 + 100 * eps) # This returns exactly 0.5 when longdouble is extended precision (80-bit), # but only a value close to it when it is quadruple precision (128-bit). - assert 0 < norm(1 + 50 * eps) < 1 + assert_array_almost_equal(norm(1 + 50 * eps), 0.5, decimal=3) def test_FuncNorm(): @@ -717,10 +774,32 @@ def test_SymLogNorm_single_zero(): norm = mcolors.SymLogNorm(1e-5, vmin=-1, vmax=1, base=np.e) cbar = mcolorbar.ColorbarBase(fig.add_subplot(), norm=norm) ticks = cbar.get_ticks() - assert sum(ticks == 0) == 1 + assert np.count_nonzero(ticks == 0) <= 1 plt.close(fig) +class TestAsinhNorm: + """ + Tests for `~.colors.AsinhNorm` + """ + + def test_init(self): + norm0 = mcolors.AsinhNorm() + assert norm0.linear_width == 1 + + norm5 = mcolors.AsinhNorm(linear_width=5) + assert norm5.linear_width == 5 + + def test_norm(self): + norm = mcolors.AsinhNorm(2, vmin=-4, vmax=4) + vals = np.arange(-3.5, 3.5, 10) + normed_vals = norm(vals) + asinh2 = np.arcsinh(2) + + expected = (2 * np.arcsinh(vals / 2) + 2 * asinh2) / (4 * asinh2) + assert_array_almost_equal(normed_vals, expected) + + def _inverse_tester(norm_instance, vals): """ Checks if the inverse of the given normalization is working. @@ -779,12 +858,12 @@ def test_boundarynorm_and_colorbarbase(): # Set the colormap and bounds bounds = [-1, 2, 5, 7, 12, 15] - cmap = cm.get_cmap('viridis') + cmap = mpl.colormaps['viridis'] # Default behavior norm = mcolors.BoundaryNorm(bounds, cmap.N) cb1 = mcolorbar.ColorbarBase(ax1, cmap=cmap, norm=norm, extend='both', - orientation='horizontal') + orientation='horizontal', spacing='uniform') # New behavior norm = mcolors.BoundaryNorm(bounds, cmap.N, extend='both') cb2 = mcolorbar.ColorbarBase(ax2, cmap=cmap, norm=norm, @@ -1124,13 +1203,13 @@ def test_pandas_iterable(pd): assert_array_equal(cm1.colors, cm2.colors) -@pytest.mark.parametrize('name', sorted(plt.colormaps())) +@pytest.mark.parametrize('name', sorted(mpl.colormaps())) def test_colormap_reversing(name): """ Check the generated _lut data of a colormap and corresponding reversed colormap if they are almost the same. """ - cmap = plt.get_cmap(name) + cmap = mpl.colormaps[name] cmap_r = cmap.reversed() if not cmap_r._isinit: cmap._init() @@ -1142,6 +1221,15 @@ def test_colormap_reversing(name): assert_array_almost_equal(cmap(np.nan), cmap_r(np.nan)) +def test_has_alpha_channel(): + assert mcolors._has_alpha_channel((0, 0, 0, 0)) + assert mcolors._has_alpha_channel([1, 1, 1, 1]) + assert not mcolors._has_alpha_channel('blue') # 4-char string! + assert not mcolors._has_alpha_channel('0.25') + assert not mcolors._has_alpha_channel('r') + assert not mcolors._has_alpha_channel((1, 0, 0)) + + def test_cn(): matplotlib.rcParams['axes.prop_cycle'] = cycler('color', ['blue', 'r']) @@ -1199,8 +1287,7 @@ def test_to_rgba_array_single_str(): # single char color sequence is invalid with pytest.raises(ValueError, - match="Using a string of single character colors as " - "a color sequence is not supported."): + match="'rgb' is not a valid color value."): array = mcolors.to_rgba_array("rgb") @@ -1289,7 +1376,7 @@ def test_hex_shorthand_notation(): def test_repr_png(): - cmap = plt.get_cmap('viridis') + cmap = mpl.colormaps['viridis'] png = cmap._repr_png_() assert len(png) > 0 img = Image.open(BytesIO(png)) @@ -1302,7 +1389,7 @@ def test_repr_png(): def test_repr_html(): - cmap = plt.get_cmap('viridis') + cmap = mpl.colormaps['viridis'] html = cmap._repr_html_() assert len(html) > 0 png = cmap._repr_png_() @@ -1313,7 +1400,7 @@ def test_repr_html(): def test_get_under_over_bad(): - cmap = plt.get_cmap('viridis') + cmap = mpl.colormaps['viridis'] assert_array_equal(cmap.get_under(), cmap(-np.inf)) assert_array_equal(cmap.get_over(), cmap(np.inf)) assert_array_equal(cmap.get_bad(), cmap(np.nan)) @@ -1321,7 +1408,7 @@ def test_get_under_over_bad(): @pytest.mark.parametrize('kind', ('over', 'under', 'bad')) def test_non_mutable_get_values(kind): - cmap = copy.copy(plt.get_cmap('viridis')) + cmap = copy.copy(mpl.colormaps['viridis']) init_value = getattr(cmap, f'get_{kind}')() getattr(cmap, f'set_{kind}')('k') black_value = getattr(cmap, f'get_{kind}')() @@ -1330,7 +1417,7 @@ def test_non_mutable_get_values(kind): def test_colormap_alpha_array(): - cmap = plt.get_cmap('viridis') + cmap = mpl.colormaps['viridis'] vals = [-1, 0.5, 2] # under, valid, over with pytest.raises(ValueError, match="alpha is array-like but"): cmap(vals, alpha=[1, 1, 1, 1]) @@ -1342,7 +1429,7 @@ def test_colormap_alpha_array(): def test_colormap_bad_data_with_alpha(): - cmap = plt.get_cmap('viridis') + cmap = mpl.colormaps['viridis'] c = cmap(np.nan, alpha=0.5) assert c == (0, 0, 0, 0) c = cmap([0.5, np.nan], alpha=0.5) @@ -1383,6 +1470,130 @@ def test_norm_deepcopy(): norm = mcolors.Normalize() norm.vmin = 0.0002 norm2 = copy.deepcopy(norm) - assert isinstance(norm2._scale, mscale.LinearScale) + assert norm2._scale is None assert norm2.vmin == norm.vmin - assert norm2._scale is not norm._scale + + +def test_norm_callback(): + increment = unittest.mock.Mock(return_value=None) + + norm = mcolors.Normalize() + norm.callbacks.connect('changed', increment) + # Haven't updated anything, so call count should be 0 + assert increment.call_count == 0 + + # Now change vmin and vmax to test callbacks + norm.vmin = 1 + assert increment.call_count == 1 + norm.vmax = 5 + assert increment.call_count == 2 + # callback shouldn't be called if setting to the same value + norm.vmin = 1 + assert increment.call_count == 2 + norm.vmax = 5 + assert increment.call_count == 2 + + # We only want autoscale() calls to send out one update signal + increment.call_count = 0 + norm.autoscale([0, 1, 2]) + assert increment.call_count == 1 + + +def test_scalarmappable_norm_update(): + norm = mcolors.Normalize() + sm = matplotlib.cm.ScalarMappable(norm=norm, cmap='plasma') + # sm doesn't have a stale attribute at first, set it to False + sm.stale = False + # The mappable should be stale after updating vmin/vmax + norm.vmin = 5 + assert sm.stale + sm.stale = False + norm.vmax = 5 + assert sm.stale + sm.stale = False + norm.clip = True + assert sm.stale + # change to the CenteredNorm and TwoSlopeNorm to test those + # Also make sure that updating the norm directly and with + # set_norm both update the Norm callback + norm = mcolors.CenteredNorm() + sm.norm = norm + sm.stale = False + norm.vcenter = 1 + assert sm.stale + norm = mcolors.TwoSlopeNorm(vcenter=0, vmin=-1, vmax=1) + sm.set_norm(norm) + sm.stale = False + norm.vcenter = 1 + assert sm.stale + + +@check_figures_equal() +def test_norm_update_figs(fig_test, fig_ref): + ax_ref = fig_ref.add_subplot() + ax_test = fig_test.add_subplot() + + z = np.arange(100).reshape((10, 10)) + ax_ref.imshow(z, norm=mcolors.Normalize(10, 90)) + + # Create the norm beforehand with different limits and then update + # after adding to the plot + norm = mcolors.Normalize(0, 1) + ax_test.imshow(z, norm=norm) + # Force initial draw to make sure it isn't already stale + fig_test.canvas.draw() + norm.vmin, norm.vmax = 10, 90 + + +def test_make_norm_from_scale_name(): + logitnorm = mcolors.make_norm_from_scale( + mscale.LogitScale, mcolors.Normalize) + assert logitnorm.__name__ == logitnorm.__qualname__ == "LogitScaleNorm" + + +def test_color_sequences(): + # basic access + assert plt.color_sequences is matplotlib.color_sequences # same registry + assert list(plt.color_sequences) == [ + 'tab10', 'tab20', 'tab20b', 'tab20c', 'Pastel1', 'Pastel2', 'Paired', + 'Accent', 'Dark2', 'Set1', 'Set2', 'Set3'] + assert len(plt.color_sequences['tab10']) == 10 + assert len(plt.color_sequences['tab20']) == 20 + + tab_colors = [ + 'tab:blue', 'tab:orange', 'tab:green', 'tab:red', 'tab:purple', + 'tab:brown', 'tab:pink', 'tab:gray', 'tab:olive', 'tab:cyan'] + for seq_color, tab_color in zip(plt.color_sequences['tab10'], tab_colors): + assert mcolors.same_color(seq_color, tab_color) + + # registering + with pytest.raises(ValueError, match="reserved name"): + plt.color_sequences.register('tab10', ['r', 'g', 'b']) + with pytest.raises(ValueError, match="not a valid color specification"): + plt.color_sequences.register('invalid', ['not a color']) + + rgb_colors = ['r', 'g', 'b'] + plt.color_sequences.register('rgb', rgb_colors) + assert plt.color_sequences['rgb'] == ['r', 'g', 'b'] + # should not affect the registered sequence because input is copied + rgb_colors.append('c') + assert plt.color_sequences['rgb'] == ['r', 'g', 'b'] + # should not affect the registered sequence because returned list is a copy + plt.color_sequences['rgb'].append('c') + assert plt.color_sequences['rgb'] == ['r', 'g', 'b'] + + # unregister + plt.color_sequences.unregister('rgb') + with pytest.raises(KeyError): + plt.color_sequences['rgb'] # rgb is gone + plt.color_sequences.unregister('rgb') # multiple unregisters are ok + with pytest.raises(ValueError, match="Cannot unregister builtin"): + plt.color_sequences.unregister('tab10') + + +def test_cm_set_cmap_error(): + sm = cm.ScalarMappable() + # Pick a name we are pretty sure will never be a colormap name + bad_cmap = 'AardvarksAreAwkward' + with pytest.raises(ValueError, match=bad_cmap): + sm.set_cmap(bad_cmap) diff --git a/lib/matplotlib/tests/test_compare_images.py b/lib/matplotlib/tests/test_compare_images.py index 95173a8860a1..6023f3d05468 100644 --- a/lib/matplotlib/tests/test_compare_images.py +++ b/lib/matplotlib/tests/test_compare_images.py @@ -4,7 +4,7 @@ import pytest from pytest import approx -from matplotlib.testing.compare import compare_images, make_test_filename +from matplotlib.testing.compare import compare_images from matplotlib.testing.decorators import _image_directories @@ -42,7 +42,8 @@ # Now test the reverse comparison. ('all128.png', 'all127.png', 0, 1), ]) -def test_image_comparison_expect_rms(im1, im2, tol, expect_rms): +def test_image_comparison_expect_rms(im1, im2, tol, expect_rms, tmp_path, + monkeypatch): """ Compare two images, expecting a particular RMS error. @@ -54,16 +55,16 @@ def test_image_comparison_expect_rms(im1, im2, tol, expect_rms): succeed if compare_images succeeds. Otherwise, the test will succeed if compare_images fails and returns an RMS error almost equal to this value. """ + # Change the working directory using monkeypatch to use a temporary + # test specific directory + monkeypatch.chdir(tmp_path) baseline_dir, result_dir = map(Path, _image_directories(lambda: "dummy")) - # Copy both "baseline" and "test" image to result_dir, so that 1) - # compare_images writes the diff to result_dir, rather than to the source - # tree and 2) the baseline image doesn't appear missing to triage_tests.py. - result_im1 = make_test_filename(result_dir / im1, "expected") - shutil.copyfile(baseline_dir / im1, result_im1) + # Copy "test" image to result_dir, so that compare_images writes + # the diff to result_dir, rather than to the source tree result_im2 = result_dir / im1 shutil.copyfile(baseline_dir / im2, result_im2) results = compare_images( - result_im1, result_im2, tol=tol, in_decorator=True) + baseline_dir / im1, result_im2, tol=tol, in_decorator=True) if expect_rms is None: assert results is None diff --git a/lib/matplotlib/tests/test_constrainedlayout.py b/lib/matplotlib/tests/test_constrainedlayout.py index 117b221cc21a..b0833052ad6e 100644 --- a/lib/matplotlib/tests/test_constrainedlayout.py +++ b/lib/matplotlib/tests/test_constrainedlayout.py @@ -1,11 +1,11 @@ import numpy as np import pytest +import matplotlib as mpl from matplotlib.testing.decorators import image_comparison import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec import matplotlib.transforms as mtransforms -from matplotlib import ticker, rcParams +from matplotlib import gridspec, ticker def example_plot(ax, fontsize=12, nodec=False): @@ -16,8 +16,8 @@ def example_plot(ax, fontsize=12, nodec=False): ax.set_ylabel('y-label', fontsize=fontsize) ax.set_title('Title', fontsize=fontsize) else: - ax.set_xticklabels('') - ax.set_yticklabels('') + ax.set_xticklabels([]) + ax.set_yticklabels([]) def example_pcolor(ax, fontsize=12): @@ -36,7 +36,7 @@ def example_pcolor(ax, fontsize=12): @image_comparison(['constrained_layout1.png']) def test_constrained_layout1(): """Test constrained_layout for a single subplot""" - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout="constrained") ax = fig.add_subplot() example_plot(ax, fontsize=24) @@ -44,7 +44,7 @@ def test_constrained_layout1(): @image_comparison(['constrained_layout2.png']) def test_constrained_layout2(): """Test constrained_layout for 2x2 subplots""" - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: example_plot(ax, fontsize=24) @@ -52,10 +52,8 @@ def test_constrained_layout2(): @image_comparison(['constrained_layout3.png']) def test_constrained_layout3(): """Test constrained_layout for colorbars with subplots""" - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for nn, ax in enumerate(axs.flat): pcm = example_pcolor(ax, fontsize=24) if nn == 3: @@ -68,10 +66,8 @@ def test_constrained_layout3(): @image_comparison(['constrained_layout4.png']) def test_constrained_layout4(): """Test constrained_layout for a single colorbar with subplots""" - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax, fontsize=24) fig.colorbar(pcm, ax=axs, pad=0.01, shrink=0.6) @@ -83,10 +79,8 @@ def test_constrained_layout5(): Test constrained_layout for a single colorbar with subplots, colorbar bottom """ - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax, fontsize=24) fig.colorbar(pcm, ax=axs, @@ -100,7 +94,7 @@ def test_constrained_layout6(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout="constrained") gs = fig.add_gridspec(1, 2, figure=fig) gsl = gs[0].subgridspec(2, 2) gsr = gs[1].subgridspec(1, 2) @@ -121,29 +115,47 @@ def test_constrained_layout6(): ticks=ticker.MaxNLocator(nbins=5)) +def test_identical_subgridspec(): + + fig = plt.figure(constrained_layout=True) + + GS = fig.add_gridspec(2, 1) + + GSA = GS[0].subgridspec(1, 3) + GSB = GS[1].subgridspec(1, 3) + + axa = [] + axb = [] + for i in range(3): + axa += [fig.add_subplot(GSA[i])] + axb += [fig.add_subplot(GSB[i])] + + fig.draw_without_rendering() + # check first row above second + assert axa[0].get_position().y0 > axb[0].get_position().y1 + + def test_constrained_layout7(): """Test for proper warning if fig not set in GridSpec""" with pytest.warns( UserWarning, match=('There are no gridspecs with layoutgrids. ' 'Possibly did not call parent GridSpec with ' 'the "figure" keyword')): - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout="constrained") gs = gridspec.GridSpec(1, 2) gsl = gridspec.GridSpecFromSubplotSpec(2, 2, gs[0]) gsr = gridspec.GridSpecFromSubplotSpec(1, 2, gs[1]) for gs in gsl: fig.add_subplot(gs) # need to trigger a draw to get warning - fig.draw(fig.canvas.get_renderer()) + fig.draw_without_rendering() @image_comparison(['constrained_layout8.png']) def test_constrained_layout8(): """Test for gridspecs that are not completely full""" - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - fig = plt.figure(figsize=(10, 5), constrained_layout=True) + fig = plt.figure(figsize=(10, 5), layout="constrained") gs = gridspec.GridSpec(3, 5, figure=fig) axs = [] for j in [0, 1]: @@ -154,7 +166,7 @@ def test_constrained_layout8(): for i in ilist: ax = fig.add_subplot(gs[j, i]) axs += [ax] - pcm = example_pcolor(ax, fontsize=9) + example_pcolor(ax, fontsize=9) if i > 0: ax.set_ylabel('') if j < 1: @@ -170,10 +182,8 @@ def test_constrained_layout8(): @image_comparison(['constrained_layout9.png']) def test_constrained_layout9(): """Test for handling suptitle and for sharex and sharey""" - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - fig, axs = plt.subplots(2, 2, constrained_layout=True, + fig, axs = plt.subplots(2, 2, layout="constrained", sharex=False, sharey=False) for ax in axs.flat: pcm = example_pcolor(ax, fontsize=24) @@ -187,7 +197,7 @@ def test_constrained_layout9(): @image_comparison(['constrained_layout10.png']) def test_constrained_layout10(): """Test for handling legend outside axis""" - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: ax.plot(np.arange(12), label='This is a label') ax.legend(loc='center left', bbox_to_anchor=(0.8, 0.5)) @@ -196,10 +206,8 @@ def test_constrained_layout10(): @image_comparison(['constrained_layout11.png']) def test_constrained_layout11(): """Test for multiple nested gridspecs""" - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - fig = plt.figure(constrained_layout=True, figsize=(13, 3)) + fig = plt.figure(layout="constrained", figsize=(13, 3)) gs0 = gridspec.GridSpec(1, 2, figure=fig) gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0]) gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1]) @@ -218,10 +226,8 @@ def test_constrained_layout11(): @image_comparison(['constrained_layout11rat.png']) def test_constrained_layout11rat(): """Test for multiple nested gridspecs with width_ratios""" - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - fig = plt.figure(constrained_layout=True, figsize=(10, 3)) + fig = plt.figure(layout="constrained", figsize=(10, 3)) gs0 = gridspec.GridSpec(1, 2, figure=fig, width_ratios=[6, 1]) gsl = gridspec.GridSpecFromSubplotSpec(1, 2, gs0[0]) gsl0 = gridspec.GridSpecFromSubplotSpec(2, 2, gsl[1], height_ratios=[2, 1]) @@ -240,7 +246,7 @@ def test_constrained_layout11rat(): @image_comparison(['constrained_layout12.png']) def test_constrained_layout12(): """Test that very unbalanced labeling still works.""" - fig = plt.figure(constrained_layout=True, figsize=(6, 8)) + fig = plt.figure(layout="constrained", figsize=(6, 8)) gs0 = gridspec.GridSpec(6, 2, figure=fig) @@ -262,27 +268,23 @@ def test_constrained_layout12(): @image_comparison(['constrained_layout13.png'], tol=2.e-2) def test_constrained_layout13(): """Test that padding works.""" - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax, fontsize=12) fig.colorbar(pcm, ax=ax, shrink=0.6, aspect=20., pad=0.02) - fig.set_constrained_layout_pads(w_pad=24./72., h_pad=24./72.) + with pytest.raises(TypeError): + fig.get_layout_engine().set(wpad=1, hpad=2) + fig.get_layout_engine().set(w_pad=24./72., h_pad=24./72.) @image_comparison(['constrained_layout14.png']) def test_constrained_layout14(): """Test that padding works.""" - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax, fontsize=12) fig.colorbar(pcm, ax=ax, shrink=0.6, aspect=20., pad=0.02) - fig.set_constrained_layout_pads( + fig.get_layout_engine().set( w_pad=3./72., h_pad=3./72., hspace=0.2, wspace=0.2) @@ -290,7 +292,7 @@ def test_constrained_layout14(): @image_comparison(['constrained_layout15.png']) def test_constrained_layout15(): """Test that rcparams work.""" - rcParams['figure.constrained_layout.use'] = True + mpl.rcParams['figure.constrained_layout.use'] = True fig, axs = plt.subplots(2, 2) for ax in axs.flat: example_plot(ax, fontsize=12) @@ -299,7 +301,7 @@ def test_constrained_layout15(): @image_comparison(['constrained_layout16.png']) def test_constrained_layout16(): """Test ax.set_position.""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") example_plot(ax, fontsize=12) ax2 = fig.add_axes([0.2, 0.2, 0.4, 0.4]) @@ -307,7 +309,7 @@ def test_constrained_layout16(): @image_comparison(['constrained_layout17.png']) def test_constrained_layout17(): """Test uneven gridspecs""" - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout="constrained") gs = gridspec.GridSpec(3, 3, figure=fig) ax1 = fig.add_subplot(gs[0, 0]) @@ -323,23 +325,23 @@ def test_constrained_layout17(): def test_constrained_layout18(): """Test twinx""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") ax2 = ax.twinx() example_plot(ax) example_plot(ax2, fontsize=24) - fig.canvas.draw() + fig.draw_without_rendering() assert all(ax.get_position().extents == ax2.get_position().extents) def test_constrained_layout19(): """Test twiny""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") ax2 = ax.twiny() example_plot(ax) example_plot(ax2, fontsize=24) ax2.set_title('') ax.set_title('') - fig.canvas.draw() + fig.draw_without_rendering() assert all(ax.get_position().extents == ax2.get_position().extents) @@ -356,14 +358,14 @@ def test_constrained_layout20(): def test_constrained_layout21(): """#11035: repeated calls to suptitle should not alter the layout""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") fig.suptitle("Suptitle0") - fig.canvas.draw() + fig.draw_without_rendering() extents0 = np.copy(ax.get_position().extents) fig.suptitle("Suptitle1") - fig.canvas.draw() + fig.draw_without_rendering() extents1 = np.copy(ax.get_position().extents) np.testing.assert_allclose(extents0, extents1) @@ -371,13 +373,13 @@ def test_constrained_layout21(): def test_constrained_layout22(): """#11035: suptitle should not be include in CL if manually positioned""" - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") - fig.canvas.draw() + fig.draw_without_rendering() extents0 = np.copy(ax.get_position().extents) fig.suptitle("Suptitle", y=0.5) - fig.canvas.draw() + fig.draw_without_rendering() extents1 = np.copy(ax.get_position().extents) np.testing.assert_allclose(extents0, extents1) @@ -390,7 +392,7 @@ def test_constrained_layout23(): """ for i in range(2): - fig = plt.figure(constrained_layout=True, clear=True, num="123") + fig = plt.figure(layout="constrained", clear=True, num="123") gs = fig.add_gridspec(1, 2) sub = gs[0].subgridspec(2, 2) fig.suptitle("Suptitle{}".format(i)) @@ -406,7 +408,7 @@ def test_colorbar_location(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False - fig, axs = plt.subplots(4, 5, constrained_layout=True) + fig, axs = plt.subplots(4, 5, layout="constrained") for ax in axs.flat: pcm = example_pcolor(ax) ax.set_xlabel('') @@ -420,12 +422,12 @@ def test_colorbar_location(): def test_hidden_axes(): - # test that if we make an axes not visible that constrained_layout + # test that if we make an Axes not visible that constrained_layout # still works. Note the axes still takes space in the layout # (as does a gridspec slot that is empty) - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") axs[0, 1].set_visible(False) - fig.canvas.draw() + fig.draw_without_rendering() extents1 = np.copy(axs[0, 0].get_position().extents) np.testing.assert_allclose( @@ -434,7 +436,7 @@ def test_hidden_axes(): def test_colorbar_align(): for location in ['right', 'left', 'top', 'bottom']: - fig, axs = plt.subplots(2, 2, constrained_layout=True) + fig, axs = plt.subplots(2, 2, layout="constrained") cbs = [] for nn, ax in enumerate(axs.flat): ax.tick_params(direction='in') @@ -446,12 +448,12 @@ def test_colorbar_align(): if nn != 1: cb.ax.xaxis.set_ticks([]) cb.ax.yaxis.set_ticks([]) - ax.set_xticklabels('') - ax.set_yticklabels('') - fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.1, - wspace=0.1) + ax.set_xticklabels([]) + ax.set_yticklabels([]) + fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, + hspace=0.1, wspace=0.1) - fig.canvas.draw() + fig.draw_without_rendering() if location in ['left', 'right']: np.testing.assert_allclose(cbs[0].ax.get_position().x0, cbs[2].ax.get_position().x0) @@ -464,10 +466,9 @@ def test_colorbar_align(): cbs[3].ax.get_position().y0) -@image_comparison(['test_colorbars_no_overlapV.png'], - remove_text=False, style='mpl20') +@image_comparison(['test_colorbars_no_overlapV.png'], style='mpl20') def test_colorbars_no_overlapV(): - fig = plt.figure(figsize=(2, 4), constrained_layout=True) + fig = plt.figure(figsize=(2, 4), layout="constrained") axs = fig.subplots(2, 1, sharex=True, sharey=True) for ax in axs: ax.yaxis.set_major_formatter(ticker.NullFormatter()) @@ -477,10 +478,9 @@ def test_colorbars_no_overlapV(): fig.suptitle("foo") -@image_comparison(['test_colorbars_no_overlapH.png'], - remove_text=False, style='mpl20') +@image_comparison(['test_colorbars_no_overlapH.png'], style='mpl20') def test_colorbars_no_overlapH(): - fig = plt.figure(figsize=(4, 2), constrained_layout=True) + fig = plt.figure(figsize=(4, 2), layout="constrained") fig.suptitle("foo") axs = fig.subplots(1, 2, sharex=True, sharey=True) for ax in axs: @@ -491,17 +491,17 @@ def test_colorbars_no_overlapH(): def test_manually_set_position(): - fig, axs = plt.subplots(1, 2, constrained_layout=True) + fig, axs = plt.subplots(1, 2, layout="constrained") axs[0].set_position([0.2, 0.2, 0.3, 0.3]) - fig.canvas.draw() + fig.draw_without_rendering() pp = axs[0].get_position() np.testing.assert_allclose(pp, [[0.2, 0.2], [0.5, 0.5]]) - fig, axs = plt.subplots(1, 2, constrained_layout=True) + fig, axs = plt.subplots(1, 2, layout="constrained") axs[0].set_position([0.2, 0.2, 0.3, 0.3]) pc = axs[0].pcolormesh(np.random.rand(20, 20)) fig.colorbar(pc, ax=axs[0]) - fig.canvas.draw() + fig.draw_without_rendering() pp = axs[0].get_position() np.testing.assert_allclose(pp, [[0.2, 0.2], [0.44, 0.5]]) @@ -510,7 +510,7 @@ def test_manually_set_position(): remove_text=True, style='mpl20', savefig_kwarg={'bbox_inches': 'tight'}) def test_bboxtight(): - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") ax.set_aspect(1.) @@ -519,7 +519,7 @@ def test_bboxtight(): savefig_kwarg={'bbox_inches': mtransforms.Bbox([[0.5, 0], [2.5, 2]])}) def test_bbox(): - fig, ax = plt.subplots(constrained_layout=True) + fig, ax = plt.subplots(layout="constrained") ax.set_aspect(1.) @@ -530,7 +530,7 @@ def test_align_labels(): negative numbers, drives the non-negative subplots' y labels off the edge of the plot """ - fig, (ax3, ax1, ax2) = plt.subplots(3, 1, constrained_layout=True, + fig, (ax3, ax1, ax2) = plt.subplots(3, 1, layout="constrained", figsize=(6.4, 8), gridspec_kw={"height_ratios": (1, 1, 0.7)}) @@ -546,7 +546,7 @@ def test_align_labels(): fig.align_ylabels(axs=(ax3, ax1, ax2)) - fig.canvas.draw() + fig.draw_without_rendering() after_align = [ax1.yaxis.label.get_window_extent(), ax2.yaxis.label.get_window_extent(), ax3.yaxis.label.get_window_extent()] @@ -558,23 +558,123 @@ def test_align_labels(): def test_suplabels(): - fig, ax = plt.subplots(constrained_layout=True) - fig.canvas.draw() + fig, ax = plt.subplots(layout="constrained") + fig.draw_without_rendering() pos0 = ax.get_tightbbox(fig.canvas.get_renderer()) fig.supxlabel('Boo') fig.supylabel('Booy') - fig.canvas.draw() + fig.draw_without_rendering() pos = ax.get_tightbbox(fig.canvas.get_renderer()) assert pos.y0 > pos0.y0 + 10.0 assert pos.x0 > pos0.x0 + 10.0 - fig, ax = plt.subplots(constrained_layout=True) - fig.canvas.draw() + fig, ax = plt.subplots(layout="constrained") + fig.draw_without_rendering() pos0 = ax.get_tightbbox(fig.canvas.get_renderer()) # check that specifying x (y) doesn't ruin the layout fig.supxlabel('Boo', x=0.5) fig.supylabel('Boo', y=0.5) - fig.canvas.draw() + fig.draw_without_rendering() pos = ax.get_tightbbox(fig.canvas.get_renderer()) assert pos.y0 > pos0.y0 + 10.0 assert pos.x0 > pos0.x0 + 10.0 + + +def test_gridspec_addressing(): + fig = plt.figure() + gs = fig.add_gridspec(3, 3) + sp = fig.add_subplot(gs[0:, 1:]) + fig.draw_without_rendering() + + +def test_discouraged_api(): + fig, ax = plt.subplots(constrained_layout=True) + fig.draw_without_rendering() + + with pytest.warns(PendingDeprecationWarning, + match="will be deprecated"): + fig, ax = plt.subplots() + fig.set_constrained_layout(True) + fig.draw_without_rendering() + + with pytest.warns(PendingDeprecationWarning, + match="will be deprecated"): + fig, ax = plt.subplots() + fig.set_constrained_layout({'w_pad': 0.02, 'h_pad': 0.02}) + fig.draw_without_rendering() + + +def test_kwargs(): + fig, ax = plt.subplots(constrained_layout={'h_pad': 0.02}) + fig.draw_without_rendering() + + +def test_rect(): + fig, ax = plt.subplots(layout='constrained') + fig.get_layout_engine().set(rect=[0, 0, 0.5, 0.5]) + fig.draw_without_rendering() + ppos = ax.get_position() + assert ppos.x1 < 0.5 + assert ppos.y1 < 0.5 + + fig, ax = plt.subplots(layout='constrained') + fig.get_layout_engine().set(rect=[0.2, 0.2, 0.3, 0.3]) + fig.draw_without_rendering() + ppos = ax.get_position() + assert ppos.x1 < 0.5 + assert ppos.y1 < 0.5 + assert ppos.x0 > 0.2 + assert ppos.y0 > 0.2 + + +def test_compressed1(): + fig, axs = plt.subplots(3, 2, layout='compressed', + sharex=True, sharey=True) + for ax in axs.flat: + pc = ax.imshow(np.random.randn(20, 20)) + + fig.colorbar(pc, ax=axs) + fig.draw_without_rendering() + + pos = axs[0, 0].get_position() + np.testing.assert_allclose(pos.x0, 0.2344, atol=1e-3) + pos = axs[0, 1].get_position() + np.testing.assert_allclose(pos.x1, 0.7024, atol=1e-3) + + # wider than tall + fig, axs = plt.subplots(2, 3, layout='compressed', + sharex=True, sharey=True, figsize=(5, 4)) + for ax in axs.flat: + pc = ax.imshow(np.random.randn(20, 20)) + + fig.colorbar(pc, ax=axs) + fig.draw_without_rendering() + + pos = axs[0, 0].get_position() + np.testing.assert_allclose(pos.x0, 0.06195, atol=1e-3) + np.testing.assert_allclose(pos.y1, 0.8537, atol=1e-3) + pos = axs[1, 2].get_position() + np.testing.assert_allclose(pos.x1, 0.8618, atol=1e-3) + np.testing.assert_allclose(pos.y0, 0.1934, atol=1e-3) + + +@pytest.mark.parametrize('arg, state', [ + (True, True), + (False, False), + ({}, True), + ({'rect': None}, True) +]) +def test_set_constrained_layout(arg, state): + fig, ax = plt.subplots(constrained_layout=arg) + assert fig.get_constrained_layout() is state + + +def test_constrained_toggle(): + fig, ax = plt.subplots() + with pytest.warns(PendingDeprecationWarning): + fig.set_constrained_layout(True) + assert fig.get_constrained_layout() + fig.set_constrained_layout(False) + assert not fig.get_constrained_layout() + fig.set_constrained_layout(True) + assert fig.get_constrained_layout() diff --git a/lib/matplotlib/tests/test_contour.py b/lib/matplotlib/tests/test_contour.py index 58c15c9b34fa..41d4dc8501bd 100644 --- a/lib/matplotlib/tests/test_contour.py +++ b/lib/matplotlib/tests/test_contour.py @@ -2,12 +2,14 @@ import platform import re +import contourpy import numpy as np -from numpy.testing import assert_array_almost_equal +from numpy.testing import ( + assert_array_almost_equal, assert_array_almost_equal_nulp) import matplotlib as mpl from matplotlib.testing.decorators import image_comparison -from matplotlib import pyplot as plt, rc_context -from matplotlib.colors import LogNorm +from matplotlib import pyplot as plt, rc_context, ticker +from matplotlib.colors import LogNorm, same_color import pytest @@ -60,15 +62,16 @@ def test_contour_shape_error(args, message): ax.contour(*args) -def test_contour_empty_levels(): - - x = np.arange(9) - z = np.random.random((9, 9)) - +def test_contour_no_valid_levels(): fig, ax = plt.subplots() - with pytest.warns(UserWarning) as record: - ax.contour(x, x, z, levels=[]) - assert len(record) == 1 + # no warning for empty levels. + ax.contour(np.random.rand(9, 9), levels=[]) + # no warning if levels is given and is not within the range of z. + cs = ax.contour(np.arange(81).reshape((9, 9)), levels=[100]) + # ... and if fmt is given. + ax.clabel(cs, fmt={100: '%1.2f'}) + # no warning if z is uniform. + ax.contour(np.ones((9, 9))) def test_contour_Nlevels(): @@ -82,33 +85,6 @@ def test_contour_Nlevels(): assert (cs1.levels == cs2.levels).all() -def test_contour_badlevel_fmt(): - # Test edge case from https://github.com/matplotlib/matplotlib/issues/9742 - # User supplied fmt for each level as a dictionary, but Matplotlib changed - # the level to the minimum data value because no contours possible. - # This was fixed in https://github.com/matplotlib/matplotlib/pull/9743 - x = np.arange(9) - z = np.zeros((9, 9)) - - fig, ax = plt.subplots() - fmt = {1.: '%1.2f'} - with pytest.warns(UserWarning) as record: - cs = ax.contour(x, x, z, levels=[1.]) - ax.clabel(cs, fmt=fmt) - assert len(record) == 1 - - -def test_contour_uniform_z(): - - x = np.arange(9) - z = np.ones((9, 9)) - - fig, ax = plt.subplots() - with pytest.warns(UserWarning) as record: - ax.contour(x, x, z) - assert len(record) == 1 - - @image_comparison(['contour_manual_labels'], remove_text=True, style='mpl20') def test_contour_manual_labels(): x, y = np.meshgrid(np.arange(0, 10), np.arange(0, 10)) @@ -153,8 +129,29 @@ def test_given_colors_levels_and_extends(): plt.colorbar(c, ax=ax) -@image_comparison(['contour_datetime_axis.png'], - remove_text=False, style='mpl20') +@image_comparison(['contour_log_locator.svg'], style='mpl20', + remove_text=False) +def test_log_locator_levels(): + + fig, ax = plt.subplots() + + N = 100 + x = np.linspace(-3.0, 3.0, N) + y = np.linspace(-2.0, 2.0, N) + + X, Y = np.meshgrid(x, y) + + Z1 = np.exp(-X**2 - Y**2) + Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2) + data = Z1 + 50 * Z2 + + c = ax.contourf(data, locator=ticker.LogLocator()) + assert_array_almost_equal(c.levels, np.power(10.0, np.arange(-6, 3))) + cb = fig.colorbar(c, ax=ax) + assert_array_almost_equal(cb.ax.get_yticks(), c.levels) + + +@image_comparison(['contour_datetime_axis.png'], style='mpl20') def test_contour_datetime_axis(): fig = plt.figure() fig.subplots_adjust(hspace=0.4, top=0.98, bottom=.15) @@ -244,33 +241,6 @@ def test_contourf_symmetric_locator(): assert_array_almost_equal(cs.levels, np.linspace(-12, 12, 5)) -@pytest.mark.parametrize("args, cls, message", [ - ((), TypeError, - 'function takes exactly 6 arguments (0 given)'), - ((1, 2, 3, 4, 5, 6), ValueError, - 'Expected 2-dimensional array, got 0'), - (([[0]], [[0]], [[]], None, True, 0), ValueError, - 'x, y and z must all be 2D arrays with the same dimensions'), - (([[0]], [[0]], [[0]], None, True, 0), ValueError, - 'x, y and z must all be at least 2x2 arrays'), - ((*[np.arange(4).reshape((2, 2))] * 3, [[0]], True, 0), ValueError, - 'If mask is set it must be a 2D array with the same dimensions as x.'), -]) -def test_internal_cpp_api(args, cls, message): # Github issue 8197. - from matplotlib import _contour # noqa: ensure lazy-loaded module *is* loaded. - with pytest.raises(cls, match=re.escape(message)): - mpl._contour.QuadContourGenerator(*args) - - -def test_internal_cpp_api_2(): - from matplotlib import _contour # noqa: ensure lazy-loaded module *is* loaded. - arr = [[0, 1], [2, 3]] - qcg = mpl._contour.QuadContourGenerator(arr, arr, arr, None, True, 0) - with pytest.raises( - ValueError, match=r'filled contour levels must be increasing'): - qcg.create_filled_contour(1, 0) - - def test_circular_contour_warning(): # Check that almost circular contours don't throw a warning x, y = np.meshgrid(np.linspace(-2, 2, 4), np.linspace(-2, 2, 4)) @@ -305,8 +275,12 @@ def test_clabel_zorder(use_clabeltext, contour_zorder, clabel_zorder): assert clabel.get_zorder() == expected_clabel_zorder +# tol because ticks happen to fall on pixel boundaries so small +# floating point changes in tick location flip which pixel gets +# the tick. @image_comparison(['contour_log_extension.png'], - remove_text=True, style='mpl20') + remove_text=True, style='mpl20', + tol=1.444) def test_contourf_log_extension(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -336,14 +310,14 @@ def test_contourf_log_extension(): cb = plt.colorbar(c1, ax=ax1) assert cb.ax.get_ylim() == (1e-8, 1e10) cb = plt.colorbar(c2, ax=ax2) - assert cb.ax.get_ylim() == (1e-4, 1e6) + assert_array_almost_equal_nulp(cb.ax.get_ylim(), np.array((1e-4, 1e6))) cb = plt.colorbar(c3, ax=ax3) - assert_array_almost_equal( - cb.ax.get_ylim(), [3.162277660168379e-05, 3162277.660168383], 2) -@image_comparison(['contour_addlines.png'], - remove_text=True, style='mpl20', tol=0.03) +@image_comparison( + ['contour_addlines.png'], remove_text=True, style='mpl20', + tol=0.15 if platform.machine() in ('aarch64', 'ppc64le', 's390x') + else 0.03) # tolerance is because image changed minutely when tick finding on # colorbars was cleaned up... def test_contour_addlines(): @@ -398,3 +372,339 @@ def test_contour_linewidth( def test_label_nonagg(): # This should not crash even if the canvas doesn't have a get_renderer(). plt.clabel(plt.contour([[1, 2], [3, 4]])) + + +@image_comparison(baseline_images=['contour_closed_line_loop'], + extensions=['png'], remove_text=True) +def test_contour_closed_line_loop(): + # github issue 19568. + z = [[0, 0, 0], [0, 2, 0], [0, 0, 0], [2, 1, 2]] + + fig, ax = plt.subplots(figsize=(2, 2)) + ax.contour(z, [0.5], linewidths=[20], alpha=0.7) + ax.set_xlim(-0.1, 2.1) + ax.set_ylim(-0.1, 3.1) + + +def test_quadcontourset_reuse(): + # If QuadContourSet returned from one contour(f) call is passed as first + # argument to another the underlying C++ contour generator will be reused. + x, y = np.meshgrid([0.0, 1.0], [0.0, 1.0]) + z = x + y + fig, ax = plt.subplots() + qcs1 = ax.contourf(x, y, z) + qcs2 = ax.contour(x, y, z) + assert qcs2._contour_generator != qcs1._contour_generator + qcs3 = ax.contour(qcs1, z) + assert qcs3._contour_generator == qcs1._contour_generator + + +@image_comparison(baseline_images=['contour_manual'], + extensions=['png'], remove_text=True) +def test_contour_manual(): + # Manually specifying contour lines/polygons to plot. + from matplotlib.contour import ContourSet + + fig, ax = plt.subplots(figsize=(4, 4)) + cmap = 'viridis' + + # Segments only (no 'kind' codes). + lines0 = [[[2, 0], [1, 2], [1, 3]]] # Single line. + lines1 = [[[3, 0], [3, 2]], [[3, 3], [3, 4]]] # Two lines. + filled01 = [[[0, 0], [0, 4], [1, 3], [1, 2], [2, 0]]] + filled12 = [[[2, 0], [3, 0], [3, 2], [1, 3], [1, 2]], # Two polygons. + [[1, 4], [3, 4], [3, 3]]] + ContourSet(ax, [0, 1, 2], [filled01, filled12], filled=True, cmap=cmap) + ContourSet(ax, [1, 2], [lines0, lines1], linewidths=3, colors=['r', 'k']) + + # Segments and kind codes (1 = MOVETO, 2 = LINETO, 79 = CLOSEPOLY). + segs = [[[4, 0], [7, 0], [7, 3], [4, 3], [4, 0], + [5, 1], [5, 2], [6, 2], [6, 1], [5, 1]]] + kinds = [[1, 2, 2, 2, 79, 1, 2, 2, 2, 79]] # Polygon containing hole. + ContourSet(ax, [2, 3], [segs], [kinds], filled=True, cmap=cmap) + ContourSet(ax, [2], [segs], [kinds], colors='k', linewidths=3) + + +@image_comparison(baseline_images=['contour_line_start_on_corner_edge'], + extensions=['png'], remove_text=True) +def test_contour_line_start_on_corner_edge(): + fig, ax = plt.subplots(figsize=(6, 5)) + + x, y = np.meshgrid([0, 1, 2, 3, 4], [0, 1, 2]) + z = 1.2 - (x - 2)**2 + (y - 1)**2 + mask = np.zeros_like(z, dtype=bool) + mask[1, 1] = mask[1, 3] = True + z = np.ma.array(z, mask=mask) + + filled = ax.contourf(x, y, z, corner_mask=True) + cbar = fig.colorbar(filled) + lines = ax.contour(x, y, z, corner_mask=True, colors='k') + cbar.add_lines(lines) + + +def test_find_nearest_contour(): + xy = np.indices((15, 15)) + img = np.exp(-np.pi * (np.sum((xy - 5)**2, 0)/5.**2)) + cs = plt.contour(img, 10) + + nearest_contour = cs.find_nearest_contour(1, 1, pixel=False) + expected_nearest = (1, 0, 33, 1.965966, 1.965966, 1.866183) + assert_array_almost_equal(nearest_contour, expected_nearest) + + nearest_contour = cs.find_nearest_contour(8, 1, pixel=False) + expected_nearest = (1, 0, 5, 7.550173, 1.587542, 0.547550) + assert_array_almost_equal(nearest_contour, expected_nearest) + + nearest_contour = cs.find_nearest_contour(2, 5, pixel=False) + expected_nearest = (3, 0, 21, 1.884384, 5.023335, 0.013911) + assert_array_almost_equal(nearest_contour, expected_nearest) + + nearest_contour = cs.find_nearest_contour(2, 5, + indices=(5, 7), + pixel=False) + expected_nearest = (5, 0, 16, 2.628202, 5.0, 0.394638) + assert_array_almost_equal(nearest_contour, expected_nearest) + + +def test_find_nearest_contour_no_filled(): + xy = np.indices((15, 15)) + img = np.exp(-np.pi * (np.sum((xy - 5)**2, 0)/5.**2)) + cs = plt.contourf(img, 10) + + with pytest.raises(ValueError, + match="Method does not support filled contours."): + cs.find_nearest_contour(1, 1, pixel=False) + + with pytest.raises(ValueError, + match="Method does not support filled contours."): + cs.find_nearest_contour(1, 10, indices=(5, 7), pixel=False) + + with pytest.raises(ValueError, + match="Method does not support filled contours."): + cs.find_nearest_contour(2, 5, indices=(2, 7), pixel=True) + + +@mpl.style.context("default") +def test_contour_autolabel_beyond_powerlimits(): + ax = plt.figure().add_subplot() + cs = plt.contour(np.geomspace(1e-6, 1e-4, 100).reshape(10, 10), + levels=[.25e-5, 1e-5, 4e-5]) + ax.clabel(cs) + # Currently, the exponent is missing, but that may be fixed in the future. + assert {text.get_text() for text in ax.texts} == {"0.25", "1.00", "4.00"} + + +def test_contourf_legend_elements(): + from matplotlib.patches import Rectangle + x = np.arange(1, 10) + y = x.reshape(-1, 1) + h = x * y + + cs = plt.contourf(h, levels=[10, 30, 50], + colors=['#FFFF00', '#FF00FF', '#00FFFF'], + extend='both') + cs.cmap.set_over('red') + cs.cmap.set_under('blue') + cs.changed() + artists, labels = cs.legend_elements() + assert labels == ['$x \\leq -1e+250s$', + '$10.0 < x \\leq 30.0$', + '$30.0 < x \\leq 50.0$', + '$x > 1e+250s$'] + expected_colors = ('blue', '#FFFF00', '#FF00FF', 'red') + assert all(isinstance(a, Rectangle) for a in artists) + assert all(same_color(a.get_facecolor(), c) + for a, c in zip(artists, expected_colors)) + + +def test_contour_legend_elements(): + from matplotlib.collections import LineCollection + x = np.arange(1, 10) + y = x.reshape(-1, 1) + h = x * y + + colors = ['blue', '#00FF00', 'red'] + cs = plt.contour(h, levels=[10, 30, 50], + colors=colors, + extend='both') + artists, labels = cs.legend_elements() + assert labels == ['$x = 10.0$', '$x = 30.0$', '$x = 50.0$'] + assert all(isinstance(a, LineCollection) for a in artists) + assert all(same_color(a.get_color(), c) + for a, c in zip(artists, colors)) + + +@pytest.mark.parametrize( + "algorithm, klass", + [('mpl2005', contourpy.Mpl2005ContourGenerator), + ('mpl2014', contourpy.Mpl2014ContourGenerator), + ('serial', contourpy.SerialContourGenerator), + ('threaded', contourpy.ThreadedContourGenerator), + ('invalid', None)]) +def test_algorithm_name(algorithm, klass): + z = np.array([[1.0, 2.0], [3.0, 4.0]]) + if klass is not None: + cs = plt.contourf(z, algorithm=algorithm) + assert isinstance(cs._contour_generator, klass) + else: + with pytest.raises(ValueError): + plt.contourf(z, algorithm=algorithm) + + +@pytest.mark.parametrize( + "algorithm", ['mpl2005', 'mpl2014', 'serial', 'threaded']) +def test_algorithm_supports_corner_mask(algorithm): + z = np.array([[1.0, 2.0], [3.0, 4.0]]) + + # All algorithms support corner_mask=False + plt.contourf(z, algorithm=algorithm, corner_mask=False) + + # Only some algorithms support corner_mask=True + if algorithm != 'mpl2005': + plt.contourf(z, algorithm=algorithm, corner_mask=True) + else: + with pytest.raises(ValueError): + plt.contourf(z, algorithm=algorithm, corner_mask=True) + + +@image_comparison(baseline_images=['contour_all_algorithms'], + extensions=['png'], remove_text=True) +def test_all_algorithms(): + algorithms = ['mpl2005', 'mpl2014', 'serial', 'threaded'] + + rng = np.random.default_rng(2981) + x, y = np.meshgrid(np.linspace(0.0, 1.0, 10), np.linspace(0.0, 1.0, 6)) + z = np.sin(15*x)*np.cos(10*y) + rng.normal(scale=0.5, size=(6, 10)) + mask = np.zeros_like(z, dtype=bool) + mask[3, 7] = True + z = np.ma.array(z, mask=mask) + + _, axs = plt.subplots(2, 2) + for ax, algorithm in zip(axs.ravel(), algorithms): + ax.contourf(x, y, z, algorithm=algorithm) + ax.contour(x, y, z, algorithm=algorithm, colors='k') + ax.set_title(algorithm) + + +def test_subfigure_clabel(): + # Smoke test for gh#23173 + delta = 0.025 + x = np.arange(-3.0, 3.0, delta) + y = np.arange(-2.0, 2.0, delta) + X, Y = np.meshgrid(x, y) + Z1 = np.exp(-(X**2) - Y**2) + Z2 = np.exp(-((X - 1) ** 2) - (Y - 1) ** 2) + Z = (Z1 - Z2) * 2 + + fig = plt.figure() + figs = fig.subfigures(nrows=1, ncols=2) + + for f in figs: + ax = f.subplots() + CS = ax.contour(X, Y, Z) + ax.clabel(CS, inline=True, fontsize=10) + ax.set_title("Simplest default with labels") + + +@pytest.mark.parametrize( + "style", ['solid', 'dashed', 'dashdot', 'dotted']) +def test_linestyles(style): + delta = 0.025 + x = np.arange(-3.0, 3.0, delta) + y = np.arange(-2.0, 2.0, delta) + X, Y = np.meshgrid(x, y) + Z1 = np.exp(-X**2 - Y**2) + Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) + Z = (Z1 - Z2) * 2 + + # Positive contour defaults to solid + fig1, ax1 = plt.subplots() + CS1 = ax1.contour(X, Y, Z, 6, colors='k') + ax1.clabel(CS1, fontsize=9, inline=True) + ax1.set_title('Single color - positive contours solid (default)') + assert CS1.linestyles is None # default + + # Change linestyles using linestyles kwarg + fig2, ax2 = plt.subplots() + CS2 = ax2.contour(X, Y, Z, 6, colors='k', linestyles=style) + ax2.clabel(CS2, fontsize=9, inline=True) + ax2.set_title(f'Single color - positive contours {style}') + assert CS2.linestyles == style + + # Ensure linestyles do not change when negative_linestyles is defined + fig3, ax3 = plt.subplots() + CS3 = ax3.contour(X, Y, Z, 6, colors='k', linestyles=style, + negative_linestyles='dashdot') + ax3.clabel(CS3, fontsize=9, inline=True) + ax3.set_title(f'Single color - positive contours {style}') + assert CS3.linestyles == style + + +@pytest.mark.parametrize( + "style", ['solid', 'dashed', 'dashdot', 'dotted']) +def test_negative_linestyles(style): + delta = 0.025 + x = np.arange(-3.0, 3.0, delta) + y = np.arange(-2.0, 2.0, delta) + X, Y = np.meshgrid(x, y) + Z1 = np.exp(-X**2 - Y**2) + Z2 = np.exp(-(X - 1)**2 - (Y - 1)**2) + Z = (Z1 - Z2) * 2 + + # Negative contour defaults to dashed + fig1, ax1 = plt.subplots() + CS1 = ax1.contour(X, Y, Z, 6, colors='k') + ax1.clabel(CS1, fontsize=9, inline=True) + ax1.set_title('Single color - negative contours dashed (default)') + assert CS1.negative_linestyles == 'dashed' # default + + # Change negative_linestyles using rcParams + plt.rcParams['contour.negative_linestyle'] = style + fig2, ax2 = plt.subplots() + CS2 = ax2.contour(X, Y, Z, 6, colors='k') + ax2.clabel(CS2, fontsize=9, inline=True) + ax2.set_title(f'Single color - negative contours {style}' + '(using rcParams)') + assert CS2.negative_linestyles == style + + # Change negative_linestyles using negative_linestyles kwarg + fig3, ax3 = plt.subplots() + CS3 = ax3.contour(X, Y, Z, 6, colors='k', negative_linestyles=style) + ax3.clabel(CS3, fontsize=9, inline=True) + ax3.set_title(f'Single color - negative contours {style}') + assert CS3.negative_linestyles == style + + # Ensure negative_linestyles do not change when linestyles is defined + fig4, ax4 = plt.subplots() + CS4 = ax4.contour(X, Y, Z, 6, colors='k', linestyles='dashdot', + negative_linestyles=style) + ax4.clabel(CS4, fontsize=9, inline=True) + ax4.set_title(f'Single color - negative contours {style}') + assert CS4.negative_linestyles == style + + +def test_contour_remove(): + ax = plt.figure().add_subplot() + orig_children = ax.get_children() + cs = ax.contour(np.arange(16).reshape((4, 4))) + cs.clabel() + assert ax.get_children() != orig_children + cs.remove() + assert ax.get_children() == orig_children + + +def test_bool_autolevel(): + x, y = np.random.rand(2, 9) + z = (np.arange(9) % 2).reshape((3, 3)).astype(bool) + m = [[False, False, False], [False, True, False], [False, False, False]] + assert plt.contour(z.tolist()).levels.tolist() == [.5] + assert plt.contour(z).levels.tolist() == [.5] + assert plt.contour(np.ma.array(z, mask=m)).levels.tolist() == [.5] + assert plt.contourf(z.tolist()).levels.tolist() == [0, .5, 1] + assert plt.contourf(z).levels.tolist() == [0, .5, 1] + assert plt.contourf(np.ma.array(z, mask=m)).levels.tolist() == [0, .5, 1] + z = z.ravel() + assert plt.tricontour(x, y, z.tolist()).levels.tolist() == [.5] + assert plt.tricontour(x, y, z).levels.tolist() == [.5] + assert plt.tricontourf(x, y, z.tolist()).levels.tolist() == [0, .5, 1] + assert plt.tricontourf(x, y, z).levels.tolist() == [0, .5, 1] diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 81ce8c820594..cc9a83a5c5b6 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from matplotlib import rc_context +from matplotlib import _api, rc_context, style import matplotlib.dates as mdates import matplotlib.pyplot as plt from matplotlib.testing.decorators import image_comparison @@ -69,13 +69,75 @@ def test_date2num_NaT_scalar(units): assert np.isnan(tmpl) -@image_comparison(['date_empty.png']) +def test_date2num_masked(): + # Without tzinfo + base = datetime.datetime(2022, 12, 15) + dates = np.ma.array([base + datetime.timedelta(days=(2 * i)) + for i in range(7)], mask=[0, 1, 1, 0, 0, 0, 1]) + npdates = mdates.date2num(dates) + np.testing.assert_array_equal(np.ma.getmask(npdates), + (False, True, True, False, False, False, + True)) + + # With tzinfo + base = datetime.datetime(2022, 12, 15, tzinfo=mdates.UTC) + dates = np.ma.array([base + datetime.timedelta(days=(2 * i)) + for i in range(7)], mask=[0, 1, 1, 0, 0, 0, 1]) + npdates = mdates.date2num(dates) + np.testing.assert_array_equal(np.ma.getmask(npdates), + (False, True, True, False, False, False, + True)) + + def test_date_empty(): # make sure we do the right thing when told to plot dates even # if no date data has been presented, cf # http://sourceforge.net/tracker/?func=detail&aid=2850075&group_id=80706&atid=560720 fig, ax = plt.subplots() ax.xaxis_date() + fig.draw_without_rendering() + np.testing.assert_allclose(ax.get_xlim(), + [mdates.date2num(np.datetime64('1970-01-01')), + mdates.date2num(np.datetime64('1970-01-02'))]) + + mdates._reset_epoch_test_example() + mdates.set_epoch('0000-12-31') + fig, ax = plt.subplots() + ax.xaxis_date() + fig.draw_without_rendering() + np.testing.assert_allclose(ax.get_xlim(), + [mdates.date2num(np.datetime64('1970-01-01')), + mdates.date2num(np.datetime64('1970-01-02'))]) + mdates._reset_epoch_test_example() + + +def test_date_not_empty(): + fig = plt.figure() + ax = fig.add_subplot() + + ax.plot([50, 70], [1, 2]) + ax.xaxis.axis_date() + np.testing.assert_allclose(ax.get_xlim(), [50, 70]) + + +def test_axhline(): + # make sure that axhline doesn't set the xlimits... + fig, ax = plt.subplots() + ax.axhline(1.5) + ax.plot([np.datetime64('2016-01-01'), np.datetime64('2016-01-02')], [1, 2]) + np.testing.assert_allclose(ax.get_xlim(), + [mdates.date2num(np.datetime64('2016-01-01')), + mdates.date2num(np.datetime64('2016-01-02'))]) + + mdates._reset_epoch_test_example() + mdates.set_epoch('0000-12-31') + fig, ax = plt.subplots() + ax.axhline(1.5) + ax.plot([np.datetime64('2016-01-01'), np.datetime64('2016-01-02')], [1, 2]) + np.testing.assert_allclose(ax.get_xlim(), + [mdates.date2num(np.datetime64('2016-01-01')), + mdates.date2num(np.datetime64('2016-01-02'))]) + mdates._reset_epoch_test_example() @image_comparison(['date_axhspan.png']) @@ -139,8 +201,8 @@ def test_too_many_date_ticks(caplog): with pytest.warns(UserWarning) as rec: ax.set_xlim((t0, tf), auto=True) assert len(rec) == 1 - assert \ - 'Attempting to set identical left == right' in str(rec[0].message) + assert ('Attempting to set identical low and high xlims' + in str(rec[0].message)) ax.plot([], []) ax.xaxis.set_major_locator(mdates.DayLocator()) v = ax.xaxis.get_major_locator()() @@ -196,6 +258,18 @@ def test_RRuleLocator_dayrange(): # On success, no overflow error shall be thrown +def test_RRuleLocator_close_minmax(): + # if d1 and d2 are very close together, rrule cannot create + # reasonable tick intervals; ensure that this is handled properly + rrule = mdates.rrulewrapper(dateutil.rrule.SECONDLY, interval=5) + loc = mdates.RRuleLocator(rrule) + d1 = datetime.datetime(year=2020, month=1, day=1) + d2 = datetime.datetime(year=2020, month=1, day=1, microsecond=1) + expected = ['2020-01-01 00:00:00+00:00', + '2020-01-01 00:00:00.000001+00:00'] + assert list(map(str, mdates.num2date(loc.tick_values(d1, d2)))) == expected + + @image_comparison(['DateFormatter_fractionalSeconds.png']) def test_DateFormatter(): import matplotlib.testing.jpl_units as units @@ -268,19 +342,23 @@ def callable_formatting_function(dates, _): @pytest.mark.parametrize('delta, expected', [ (datetime.timedelta(weeks=52 * 200), - [r'$\mathdefault{%d}$' % (year,) for year in range(1990, 2171, 20)]), + [r'$\mathdefault{%d}$' % year for year in range(1990, 2171, 20)]), (datetime.timedelta(days=30), - [r'Jan$\mathdefault{ %02d 1990}$' % (day,) for day in range(1, 32, 3)]), + [r'$\mathdefault{1990{-}01{-}%02d}$' % day for day in range(1, 32, 3)]), (datetime.timedelta(hours=20), - [r'$\mathdefault{%02d:00:00}$' % (hour,) for hour in range(0, 21, 2)]), + [r'$\mathdefault{01{-}01\;%02d}$' % hour for hour in range(0, 21, 2)]), + (datetime.timedelta(minutes=10), + [r'$\mathdefault{01\;00{:}%02d}$' % minu for minu in range(0, 11)]), ]) def test_date_formatter_usetex(delta, expected): + style.use("default") + d1 = datetime.datetime(1990, 1, 1) d2 = d1 + delta locator = mdates.AutoDateLocator(interval_multiples=False) locator.create_dummy_axis() - locator.set_view_interval(mdates.date2num(d1), mdates.date2num(d2)) + locator.axis.set_view_interval(mdates.date2num(d1), mdates.date2num(d2)) formatter = mdates.AutoDateFormatter(locator, usetex=True) assert [formatter(loc) for loc in locator()] == expected @@ -298,9 +376,13 @@ def test_drange(): # dates from an half open interval [start, end) assert len(mdates.drange(start, end, delta)) == 24 + # Same if interval ends slightly earlier + end = end - datetime.timedelta(microseconds=1) + assert len(mdates.drange(start, end, delta)) == 24 + # if end is a little bit later, we expect the range to contain one element # more - end = end + datetime.timedelta(microseconds=1) + end = end + datetime.timedelta(microseconds=2) assert len(mdates.drange(start, end, delta)) == 25 # reset end @@ -319,8 +401,7 @@ def test_auto_date_locator(): def _create_auto_date_locator(date1, date2): locator = mdates.AutoDateLocator(interval_multiples=False) locator.create_dummy_axis() - locator.set_view_interval(mdates.date2num(date1), - mdates.date2num(date2)) + locator.axis.set_view_interval(*mdates.date2num([date1, date2])) return locator d1 = datetime.datetime(1990, 1, 1) @@ -385,14 +466,22 @@ def _create_auto_date_locator(date1, date2): locator = _create_auto_date_locator(d1, d2) assert list(map(str, mdates.num2date(locator()))) == expected + locator = mdates.AutoDateLocator(interval_multiples=False) + assert locator.maxticks == {0: 11, 1: 12, 3: 11, 4: 12, 5: 11, 6: 11, 7: 8} + + locator = mdates.AutoDateLocator(maxticks={dateutil.rrule.MONTHLY: 5}) + assert locator.maxticks == {0: 11, 1: 5, 3: 11, 4: 12, 5: 11, 6: 11, 7: 8} + + locator = mdates.AutoDateLocator(maxticks=5) + assert locator.maxticks == {0: 5, 1: 5, 3: 5, 4: 5, 5: 5, 6: 5, 7: 5} + @_new_epoch_decorator def test_auto_date_locator_intmult(): def _create_auto_date_locator(date1, date2): locator = mdates.AutoDateLocator(interval_multiples=True) locator.create_dummy_axis() - locator.set_view_interval(mdates.date2num(date1), - mdates.date2num(date2)) + locator.axis.set_view_interval(*mdates.date2num([date1, date2])) return locator results = ([datetime.timedelta(weeks=52 * 200), @@ -547,6 +636,53 @@ def test_concise_formatter_show_offset(t_delta, expected): assert formatter.get_offset() == expected +def test_concise_converter_stays(): + # This test demonstrates problems introduced by gh-23417 (reverted in gh-25278) + # In particular, downstream libraries like Pandas had their designated converters + # overridden by actions like setting xlim (or plotting additional points using + # stdlib/numpy dates and string date representation, which otherwise work fine with + # their date converters) + # While this is a bit of a toy example that would be unusual to see it demonstrates + # the same ideas (namely having a valid converter already applied that is desired) + # without introducing additional subclasses. + # See also discussion at gh-25219 for how Pandas was affected + x = [datetime.datetime(2000, 1, 1), datetime.datetime(2020, 2, 20)] + y = [0, 1] + fig, ax = plt.subplots() + ax.plot(x, y) + # Bypass Switchable date converter + ax.xaxis.converter = conv = mdates.ConciseDateConverter() + assert ax.xaxis.units is None + ax.set_xlim(*x) + assert ax.xaxis.converter == conv + + +def test_offset_changes(): + fig, ax = plt.subplots() + + d1 = datetime.datetime(1997, 1, 1) + d2 = d1 + datetime.timedelta(weeks=520) + + locator = mdates.AutoDateLocator() + formatter = mdates.ConciseDateFormatter(locator) + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + + ax.plot([d1, d2], [0, 0]) + fig.draw_without_rendering() + assert formatter.get_offset() == '' + ax.set_xlim(d1, d1 + datetime.timedelta(weeks=3)) + fig.draw_without_rendering() + assert formatter.get_offset() == '1997-Jan' + ax.set_xlim(d1 + datetime.timedelta(weeks=7), + d1 + datetime.timedelta(weeks=30)) + fig.draw_without_rendering() + assert formatter.get_offset() == '1997' + ax.set_xlim(d1, d1 + datetime.timedelta(weeks=520)) + fig.draw_without_rendering() + assert formatter.get_offset() == '' + + @pytest.mark.parametrize('t_delta, expected', [ (datetime.timedelta(weeks=52 * 200), ['$\\mathdefault{%d}$' % (t, ) for t in range(1980, 2201, 20)]), @@ -556,14 +692,14 @@ def test_concise_formatter_show_offset(t_delta, expected): '$\\mathdefault{25}$', '$\\mathdefault{29}$', 'Feb', '$\\mathdefault{05}$', '$\\mathdefault{09}$']), (datetime.timedelta(hours=40), - ['Jan$\\mathdefault{{-}01}$', '$\\mathdefault{04:00}$', - '$\\mathdefault{08:00}$', '$\\mathdefault{12:00}$', - '$\\mathdefault{16:00}$', '$\\mathdefault{20:00}$', - 'Jan$\\mathdefault{{-}02}$', '$\\mathdefault{04:00}$', - '$\\mathdefault{08:00}$', '$\\mathdefault{12:00}$', - '$\\mathdefault{16:00}$']), + ['Jan$\\mathdefault{{-}01}$', '$\\mathdefault{04{:}00}$', + '$\\mathdefault{08{:}00}$', '$\\mathdefault{12{:}00}$', + '$\\mathdefault{16{:}00}$', '$\\mathdefault{20{:}00}$', + 'Jan$\\mathdefault{{-}02}$', '$\\mathdefault{04{:}00}$', + '$\\mathdefault{08{:}00}$', '$\\mathdefault{12{:}00}$', + '$\\mathdefault{16{:}00}$']), (datetime.timedelta(seconds=2), - ['$\\mathdefault{59.5}$', '$\\mathdefault{00:00}$', + ['$\\mathdefault{59.5}$', '$\\mathdefault{00{:}00}$', '$\\mathdefault{00.5}$', '$\\mathdefault{01.0}$', '$\\mathdefault{01.5}$', '$\\mathdefault{02.0}$', '$\\mathdefault{02.5}$']), @@ -574,7 +710,7 @@ def test_concise_formatter_usetex(t_delta, expected): locator = mdates.AutoDateLocator(interval_multiples=True) locator.create_dummy_axis() - locator.set_view_interval(mdates.date2num(d1), mdates.date2num(d2)) + locator.axis.set_view_interval(mdates.date2num(d1), mdates.date2num(d2)) formatter = mdates.ConciseDateFormatter(locator, usetex=True) assert formatter.format_ticks(locator()) == expected @@ -732,8 +868,7 @@ def test_auto_date_locator_intmult_tz(): def _create_auto_date_locator(date1, date2, tz): locator = mdates.AutoDateLocator(interval_multiples=True, tz=tz) locator.create_dummy_axis() - locator.set_view_interval(mdates.date2num(date1), - mdates.date2num(date2)) + locator.axis.set_view_interval(*mdates.date2num([date1, date2])) return locator results = ([datetime.timedelta(weeks=52*200), @@ -822,7 +957,7 @@ def _test_date2num_dst(date_range, tz_convert): # Interval is 0b0.0000011 days, to prevent float rounding issues dtstart = datetime.datetime(2014, 3, 30, 0, 0, tzinfo=UTC) interval = datetime.timedelta(minutes=33, seconds=45) - interval_days = 0.0234375 # 2025 / 86400 seconds + interval_days = interval.seconds / 86400 N = 8 dt_utc = date_range(start=dtstart, freq=interval, periods=N) @@ -892,7 +1027,7 @@ def date_range(start, freq, periods): return [dtstart + (i * freq) for i in range(periods)] - # Define a tz_convert function that converts a list to a new time zone. + # Define a tz_convert function that converts a list to a new timezone. def tz_convert(dt_list, tzinfo): return [d.astimezone(tzinfo) for d in dt_list] @@ -930,6 +1065,20 @@ def attach_tz(dt, zi): _test_rrulewrapper(attach_tz, dateutil.tz.gettz) + SYD = dateutil.tz.gettz('Australia/Sydney') + dtstart = datetime.datetime(2017, 4, 1, 0) + dtend = datetime.datetime(2017, 4, 4, 0) + rule = mdates.rrulewrapper(freq=dateutil.rrule.DAILY, dtstart=dtstart, + tzinfo=SYD, until=dtend) + assert rule.after(dtstart) == datetime.datetime(2017, 4, 2, 0, 0, + tzinfo=SYD) + assert rule.before(dtend) == datetime.datetime(2017, 4, 3, 0, 0, + tzinfo=SYD) + + # Test parts of __getattr__ + assert rule._base_tzinfo == SYD + assert rule._interval == 1 + @pytest.mark.pytz def test_rrulewrapper_pytz(): @@ -951,8 +1100,8 @@ def test_yearlocator_pytz(): + datetime.timedelta(i) for i in range(2000)] locator = mdates.AutoDateLocator(interval_multiples=True, tz=tz) locator.create_dummy_axis() - locator.set_view_interval(mdates.date2num(x[0])-1.0, - mdates.date2num(x[-1])+1.0) + locator.axis.set_view_interval(mdates.date2num(x[0])-1.0, + mdates.date2num(x[-1])+1.0) t = np.array([733408.208333, 733773.208333, 734138.208333, 734503.208333, 734869.208333, 735234.208333, 735599.208333]) # convert to new epoch from old... @@ -964,6 +1113,54 @@ def test_yearlocator_pytz(): '2014-01-01 00:00:00-05:00', '2015-01-01 00:00:00-05:00'] st = list(map(str, mdates.num2date(locator(), tz=tz))) assert st == expected + assert np.allclose(locator.tick_values(x[0], x[1]), np.array( + [14610.20833333, 14610.33333333, 14610.45833333, 14610.58333333, + 14610.70833333, 14610.83333333, 14610.95833333, 14611.08333333, + 14611.20833333])) + assert np.allclose(locator.get_locator(x[1], x[0]).tick_values(x[0], x[1]), + np.array( + [14610.20833333, 14610.33333333, 14610.45833333, 14610.58333333, + 14610.70833333, 14610.83333333, 14610.95833333, 14611.08333333, + 14611.20833333])) + + +def test_YearLocator(): + def _create_year_locator(date1, date2, **kwargs): + locator = mdates.YearLocator(**kwargs) + locator.create_dummy_axis() + locator.axis.set_view_interval(mdates.date2num(date1), + mdates.date2num(date2)) + return locator + + d1 = datetime.datetime(1990, 1, 1) + results = ([datetime.timedelta(weeks=52 * 200), + {'base': 20, 'month': 1, 'day': 1}, + ['1980-01-01 00:00:00+00:00', '2000-01-01 00:00:00+00:00', + '2020-01-01 00:00:00+00:00', '2040-01-01 00:00:00+00:00', + '2060-01-01 00:00:00+00:00', '2080-01-01 00:00:00+00:00', + '2100-01-01 00:00:00+00:00', '2120-01-01 00:00:00+00:00', + '2140-01-01 00:00:00+00:00', '2160-01-01 00:00:00+00:00', + '2180-01-01 00:00:00+00:00', '2200-01-01 00:00:00+00:00'] + ], + [datetime.timedelta(weeks=52 * 200), + {'base': 20, 'month': 5, 'day': 16}, + ['1980-05-16 00:00:00+00:00', '2000-05-16 00:00:00+00:00', + '2020-05-16 00:00:00+00:00', '2040-05-16 00:00:00+00:00', + '2060-05-16 00:00:00+00:00', '2080-05-16 00:00:00+00:00', + '2100-05-16 00:00:00+00:00', '2120-05-16 00:00:00+00:00', + '2140-05-16 00:00:00+00:00', '2160-05-16 00:00:00+00:00', + '2180-05-16 00:00:00+00:00', '2200-05-16 00:00:00+00:00'] + ], + [datetime.timedelta(weeks=52 * 5), + {'base': 20, 'month': 9, 'day': 25}, + ['1980-09-25 00:00:00+00:00', '2000-09-25 00:00:00+00:00'] + ], + ) + + for delta, arguments, expected in results: + d2 = d1 + delta + locator = _create_year_locator(d1, d2, **arguments) + assert list(map(str, mdates.num2date(locator()))) == expected def test_DayLocator(): @@ -980,7 +1177,7 @@ def test_DayLocator(): def test_tz_utc(): dt = datetime.datetime(1970, 1, 1, tzinfo=mdates.UTC) - dt.tzname() + assert dt.tzname() == 'UTC' @pytest.mark.parametrize("x, tdelta", @@ -1038,9 +1235,9 @@ def test_warn_notintervals(): locator = mdates.AutoDateLocator(interval_multiples=False) locator.intervald[3] = [2] locator.create_dummy_axis() - locator.set_view_interval(mdates.date2num(dates[0]), - mdates.date2num(dates[-1])) - with pytest.warns(UserWarning, match="AutoDateLocator was unable") as rec: + locator.axis.set_view_interval(mdates.date2num(dates[0]), + mdates.date2num(dates[-1])) + with pytest.warns(UserWarning, match="AutoDateLocator was unable"): locs = locator() @@ -1061,7 +1258,7 @@ def test_change_converter(): fig.canvas.draw() assert ax.get_xticklabels()[0].get_text() == 'Jan 01 2020' assert ax.get_xticklabels()[1].get_text() == 'Jan 15 2020' - with pytest.warns(UserWarning) as rec: + with pytest.raises(ValueError): plt.rcParams['date.converter'] = 'boo' @@ -1084,27 +1281,142 @@ def test_change_interval_multiples(): assert ax.get_xticklabels()[1].get_text() == 'Feb 01 2020' -def test_epoch2num(): - mdates._reset_epoch_test_example() - mdates.set_epoch('0000-12-31') - assert mdates.epoch2num(86400) == 719164.0 - assert mdates.num2epoch(719165.0) == 86400 * 2 - # set back to the default - mdates._reset_epoch_test_example() - mdates.set_epoch('1970-01-01T00:00:00') - assert mdates.epoch2num(86400) == 1.0 - assert mdates.num2epoch(2.0) == 86400 * 2 +def test_julian2num(): + with pytest.warns(_api.MatplotlibDeprecationWarning): + mdates._reset_epoch_test_example() + mdates.set_epoch('0000-12-31') + # 2440587.5 is julian date for 1970-01-01T00:00:00 + # https://en.wikipedia.org/wiki/Julian_day + assert mdates.julian2num(2440588.5) == 719164.0 + assert mdates.num2julian(719165.0) == 2440589.5 + # set back to the default + mdates._reset_epoch_test_example() + mdates.set_epoch('1970-01-01T00:00:00') + assert mdates.julian2num(2440588.5) == 1.0 + assert mdates.num2julian(2.0) == 2440589.5 + + +def test_DateLocator(): + locator = mdates.DateLocator() + # Test nonsingular + assert locator.nonsingular(0, np.inf) == (0, 1) + assert locator.nonsingular(0, 1) == (0, 1) + assert locator.nonsingular(1, 0) == (0, 1) + assert locator.nonsingular(0, 0) == (-2, 2) + locator.create_dummy_axis() + # default values + assert locator.datalim_to_dt() == ( + datetime.datetime(1970, 1, 1, 0, 0, tzinfo=datetime.timezone.utc), + datetime.datetime(1970, 1, 2, 0, 0, tzinfo=datetime.timezone.utc)) + + # Check default is UTC + assert locator.tz == mdates.UTC + tz_str = 'Iceland' + iceland_tz = dateutil.tz.gettz(tz_str) + # Check not Iceland + assert locator.tz != iceland_tz + # Set it to to Iceland + locator.set_tzinfo('Iceland') + # Check now it is Iceland + assert locator.tz == iceland_tz + locator.create_dummy_axis() + locator.axis.set_data_interval(*mdates.date2num(["2022-01-10", + "2022-01-08"])) + assert locator.datalim_to_dt() == ( + datetime.datetime(2022, 1, 8, 0, 0, tzinfo=iceland_tz), + datetime.datetime(2022, 1, 10, 0, 0, tzinfo=iceland_tz)) + + # Set rcParam + plt.rcParams['timezone'] = tz_str + + # Create a new one in a similar way + locator = mdates.DateLocator() + # Check now it is Iceland + assert locator.tz == iceland_tz + + # Test invalid tz values + with pytest.raises(ValueError, match="Aiceland is not a valid timezone"): + mdates.DateLocator(tz="Aiceland") + with pytest.raises(TypeError, + match="tz must be string or tzinfo subclass."): + mdates.DateLocator(tz=1) + + +def test_datestr2num(): + assert mdates.datestr2num('2022-01-10') == 19002.0 + dt = datetime.date(year=2022, month=1, day=10) + assert mdates.datestr2num('2022-01', default=dt) == 19002.0 + assert np.all(mdates.datestr2num( + ['2022-01', '2022-02'], default=dt + ) == np.array([19002., 19033.])) + assert mdates.datestr2num([]).size == 0 + assert mdates.datestr2num([], datetime.date(year=2022, + month=1, day=10)).size == 0 + + +@pytest.mark.parametrize('kwarg', + ('formats', 'zero_formats', 'offset_formats')) +def test_concise_formatter_exceptions(kwarg): + locator = mdates.AutoDateLocator() + kwargs = {kwarg: ['', '%Y']} + match = f"{kwarg} argument must be a list" + with pytest.raises(ValueError, match=match): + mdates.ConciseDateFormatter(locator, **kwargs) -def test_julian2num(): - mdates._reset_epoch_test_example() - mdates.set_epoch('0000-12-31') - # 2440587.5 is julian date for 1970-01-01T00:00:00 - # https://en.wikipedia.org/wiki/Julian_day - assert mdates.julian2num(2440588.5) == 719164.0 - assert mdates.num2julian(719165.0) == 2440589.5 - # set back to the default - mdates._reset_epoch_test_example() - mdates.set_epoch('1970-01-01T00:00:00') - assert mdates.julian2num(2440588.5) == 1.0 - assert mdates.num2julian(2.0) == 2440589.5 +def test_concise_formatter_call(): + locator = mdates.AutoDateLocator() + formatter = mdates.ConciseDateFormatter(locator) + assert formatter(19002.0) == '2022' + assert formatter.format_data_short(19002.0) == '2022-01-10 00:00:00' + + +@pytest.mark.parametrize('span, expected_locator', + ((0.02, mdates.MinuteLocator), + (1, mdates.HourLocator), + (19, mdates.DayLocator), + (40, mdates.WeekdayLocator), + (200, mdates.MonthLocator), + (2000, mdates.YearLocator))) +def test_date_ticker_factory(span, expected_locator): + with pytest.warns(_api.MatplotlibDeprecationWarning): + locator, _ = mdates.date_ticker_factory(span) + assert isinstance(locator, expected_locator) + + +def test_datetime_masked(): + # make sure that all-masked data falls back to the viewlim + # set in convert.axisinfo.... + x = np.array([datetime.datetime(2017, 1, n) for n in range(1, 6)]) + y = np.array([1, 2, 3, 4, 5]) + m = np.ma.masked_greater(y, 0) + + fig, ax = plt.subplots() + ax.plot(x, m) + assert ax.get_xlim() == (0, 1) + + +@pytest.mark.parametrize('val', (-1000000, 10000000)) +def test_num2date_error(val): + with pytest.raises(ValueError, match=f"Date ordinal {val} converts"): + mdates.num2date(val) + + +def test_num2date_roundoff(): + assert mdates.num2date(100000.0000578702) == datetime.datetime( + 2243, 10, 17, 0, 0, 4, 999980, tzinfo=datetime.timezone.utc) + # Slightly larger, steps of 20 microseconds + assert mdates.num2date(100000.0000578703) == datetime.datetime( + 2243, 10, 17, 0, 0, 5, tzinfo=datetime.timezone.utc) + + +def test_DateFormatter_settz(): + time = mdates.date2num(datetime.datetime(2011, 1, 1, 0, 0, + tzinfo=mdates.UTC)) + formatter = mdates.DateFormatter('%Y-%b-%d %H:%M') + # Default UTC + assert formatter(time) == '2011-Jan-01 00:00' + + # Set tzinfo + formatter.set_tzinfo('Pacific/Kiritimati') + assert formatter(time) == '2011-Jan-01 14:00' diff --git a/lib/matplotlib/tests/test_determinism.py b/lib/matplotlib/tests/test_determinism.py index cce05f12dacd..fe0fb34e128a 100644 --- a/lib/matplotlib/tests/test_determinism.py +++ b/lib/matplotlib/tests/test_determinism.py @@ -11,14 +11,7 @@ import matplotlib as mpl import matplotlib.testing.compare from matplotlib import pyplot as plt - - -needs_ghostscript = pytest.mark.skipif( - "eps" not in mpl.testing.compare.converter, - reason="This test needs a ghostscript installation") -needs_usetex = pytest.mark.skipif( - not mpl.checkdep_usetex(True), - reason="This test needs a TeX installation") +from matplotlib.testing._markers import needs_ghostscript, needs_usetex def _save_figure(objects='mhi', fmt="pdf", usetex=False): diff --git a/lib/matplotlib/tests/test_doc.py b/lib/matplotlib/tests/test_doc.py new file mode 100644 index 000000000000..8a4df35179bc --- /dev/null +++ b/lib/matplotlib/tests/test_doc.py @@ -0,0 +1,34 @@ +import pytest + + +def test_sphinx_gallery_example_header(): + """ + We have copied EXAMPLE_HEADER and modified it to include meta keywords. + This test monitors that the version we have copied is still the same as + the EXAMPLE_HEADER in sphinx-gallery. If sphinx-gallery changes its + EXAMPLE_HEADER, this test will start to fail. In that case, please update + the monkey-patching of EXAMPLE_HEADER in conf.py. + """ + gen_rst = pytest.importorskip('sphinx_gallery.gen_rst') + + EXAMPLE_HEADER = """ +.. DO NOT EDIT. +.. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. +.. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: +.. "{0}" +.. LINE NUMBERS ARE GIVEN BELOW. + +.. only:: html + + .. note:: + :class: sphx-glr-download-link-note + + Click :ref:`here ` + to download the full example code{2} + +.. rst-class:: sphx-glr-example-title + +.. _sphx_glr_{1}: + +""" + assert gen_rst.EXAMPLE_HEADER == EXAMPLE_HEADER diff --git a/lib/matplotlib/tests/test_dviread.py b/lib/matplotlib/tests/test_dviread.py index d26beb02268e..7e10975f44d5 100644 --- a/lib/matplotlib/tests/test_dviread.py +++ b/lib/matplotlib/tests/test_dviread.py @@ -7,7 +7,7 @@ def test_PsfontsMap(monkeypatch): - monkeypatch.setattr(dr, 'find_tex_file', lambda x: x) + monkeypatch.setattr(dr, '_find_tex_file', lambda x: x) filename = str(Path(__file__).parent / 'baseline_images/dviread/test.map') fontmap = dr.PsfontsMap(filename) @@ -28,7 +28,7 @@ def test_PsfontsMap(monkeypatch): else: assert entry.filename == b'font%d.pfb' % n if n == 4: - assert entry.effects == {'slant': -0.1, 'extend': 2.2} + assert entry.effects == {'slant': -0.1, 'extend': 1.2} else: assert entry.effects == {} # Some special cases @@ -42,10 +42,22 @@ def test_PsfontsMap(monkeypatch): assert entry.filename == b'font8.pfb' assert entry.encoding is None entry = fontmap[b'TeXfont9'] + assert entry.psname == b'TeXfont9' assert entry.filename == b'/absolute/font9.pfb' + # First of duplicates only. + entry = fontmap[b'TeXfontA'] + assert entry.psname == b'PSfontA1' + # Slant/Extend only works for T1 fonts. + entry = fontmap[b'TeXfontB'] + assert entry.psname == b'PSfontB6' + # Subsetted TrueType must have encoding. + entry = fontmap[b'TeXfontC'] + assert entry.psname == b'PSfontC3' # Missing font - with pytest.raises(KeyError, match='no-such-font'): + with pytest.raises(LookupError, match='no-such-font'): fontmap[b'no-such-font'] + with pytest.raises(LookupError, match='%'): + fontmap[b'%'] @pytest.mark.skipif(shutil.which("kpsewhich") is None, diff --git a/lib/matplotlib/tests/test_figure.py b/lib/matplotlib/tests/test_figure.py index af800cd8872e..f3ece07660e3 100644 --- a/lib/matplotlib/tests/test_figure.py +++ b/lib/matplotlib/tests/test_figure.py @@ -1,29 +1,34 @@ -from contextlib import nullcontext +import copy from datetime import datetime import io from pathlib import Path +import pickle import platform +from threading import Timer from types import SimpleNamespace import warnings +import numpy as np +import pytest +from PIL import Image + import matplotlib as mpl -from matplotlib import cbook, rcParams +from matplotlib import gridspec from matplotlib.testing.decorators import image_comparison, check_figures_equal from matplotlib.axes import Axes -from matplotlib.figure import Figure +from matplotlib.figure import Figure, FigureBase +from matplotlib.layout_engine import (ConstrainedLayoutEngine, + TightLayoutEngine, + PlaceHolderLayoutEngine) from matplotlib.ticker import AutoMinorLocator, FixedFormatter, ScalarFormatter import matplotlib.pyplot as plt import matplotlib.dates as mdates -import matplotlib.gridspec as gridspec -from matplotlib.cbook import MatplotlibDeprecationWarning -import numpy as np -import pytest @image_comparison(['figure_align_labels'], extensions=['png', 'svg'], tol=0 if platform.machine() == 'x86_64' else 0.01) def test_align_labels(): - fig = plt.figure(tight_layout=True) + fig = plt.figure(layout='tight') gs = gridspec.GridSpec(3, 3) ax = fig.add_subplot(gs[0, :2]) @@ -61,6 +66,41 @@ def test_align_labels(): fig.align_labels() +def test_align_labels_stray_axes(): + fig, axs = plt.subplots(2, 2) + for nn, ax in enumerate(axs.flat): + ax.set_xlabel('Boo') + ax.set_xlabel('Who') + ax.plot(np.arange(4)**nn, np.arange(4)**nn) + fig.align_ylabels() + fig.align_xlabels() + fig.draw_without_rendering() + xn = np.zeros(4) + yn = np.zeros(4) + for nn, ax in enumerate(axs.flat): + yn[nn] = ax.xaxis.label.get_position()[1] + xn[nn] = ax.yaxis.label.get_position()[0] + np.testing.assert_allclose(xn[:2], xn[2:]) + np.testing.assert_allclose(yn[::2], yn[1::2]) + + fig, axs = plt.subplots(2, 2, constrained_layout=True) + for nn, ax in enumerate(axs.flat): + ax.set_xlabel('Boo') + ax.set_xlabel('Who') + pc = ax.pcolormesh(np.random.randn(10, 10)) + fig.colorbar(pc, ax=ax) + fig.align_ylabels() + fig.align_xlabels() + fig.draw_without_rendering() + xn = np.zeros(4) + yn = np.zeros(4) + for nn, ax in enumerate(axs.flat): + yn[nn] = ax.xaxis.label.get_position()[1] + xn[nn] = ax.yaxis.label.get_position()[0] + np.testing.assert_allclose(xn[:2], xn[2:]) + np.testing.assert_allclose(yn[::2], yn[1::2]) + + def test_figure_label(): # pyplot figure creation, selection, and closing with label/number/instance plt.close('all') @@ -150,66 +190,30 @@ def test_figure_legend(): def test_gca(): fig = plt.figure() - with pytest.warns(UserWarning): - # empty call to add_axes() will throw deprecation warning - assert fig.add_axes() is None - + # test that gca() picks up Axes created via add_axes() ax0 = fig.add_axes([0, 0, 1, 1]) - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - assert fig.gca(projection='rectilinear') is ax0 assert fig.gca() is ax0 - ax1 = fig.add_axes(rect=[0.1, 0.1, 0.8, 0.8]) - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - assert fig.gca(projection='rectilinear') is ax1 + # test that gca() picks up Axes created via add_subplot() + ax1 = fig.add_subplot(111) assert fig.gca() is ax1 - ax2 = fig.add_subplot(121, projection='polar') - assert fig.gca() is ax2 - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - assert fig.gca(polar=True) is ax2 - - ax3 = fig.add_subplot(122) - assert fig.gca() is ax3 - - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - assert fig.gca(polar=True) is ax3 - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - assert fig.gca(polar=True) is not ax2 - assert fig.gca().get_subplotspec().get_geometry() == (1, 2, 1, 1) - # add_axes on an existing Axes should not change stored order, but will # make it current. fig.add_axes(ax0) - assert fig.axes == [ax0, ax1, ax2, ax3] + assert fig.axes == [ax0, ax1] assert fig.gca() is ax0 + # sca() should not change stored order of Axes, which is order added. + fig.sca(ax0) + assert fig.axes == [ax0, ax1] + # add_subplot on an existing Axes should not change stored order, but will # make it current. - fig.add_subplot(ax2) - assert fig.axes == [ax0, ax1, ax2, ax3] - assert fig.gca() is ax2 - - fig.sca(ax1) - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - assert fig.gca(projection='rectilinear') is ax1 + fig.add_subplot(ax1) + assert fig.axes == [ax0, ax1] assert fig.gca() is ax1 - # sca() should not change stored order of Axes, which is order added. - assert fig.axes == [ax0, ax1, ax2, ax3] - def test_add_subplot_subclass(): fig = plt.figure() @@ -232,10 +236,15 @@ def test_add_subplot_invalid(): with pytest.raises(ValueError, match='Number of rows must be a positive integer'): fig.add_subplot(0, 2, 1) - with pytest.raises(ValueError, match='num must be 1 <= num <= 4'): + with pytest.raises(ValueError, match='num must be an integer with ' + '1 <= num <= 4'): fig.add_subplot(2, 2, 0) - with pytest.raises(ValueError, match='num must be 1 <= num <= 4'): + with pytest.raises(ValueError, match='num must be an integer with ' + '1 <= num <= 4'): fig.add_subplot(2, 2, 5) + with pytest.raises(ValueError, match='num must be an integer with ' + '1 <= num <= 4'): + fig.add_subplot(2, 2, 0.5) with pytest.raises(ValueError, match='must be a three-digit integer'): fig.add_subplot(42) @@ -248,17 +257,17 @@ def test_add_subplot_invalid(): with pytest.raises(TypeError, match='takes 1 or 3 positional arguments ' 'but 4 were given'): fig.add_subplot(1, 2, 3, 4) - with pytest.warns(cbook.MatplotlibDeprecationWarning, - match='Passing non-integers as three-element position ' - 'specification is deprecated'): + with pytest.raises(ValueError, + match="Number of rows must be a positive integer, " + "not '2'"): fig.add_subplot('2', 2, 1) - with pytest.warns(cbook.MatplotlibDeprecationWarning, - match='Passing non-integers as three-element position ' - 'specification is deprecated'): - fig.add_subplot(2.0, 2, 1) + with pytest.raises(ValueError, + match='Number of columns must be a positive integer, ' + 'not 2.0'): + fig.add_subplot(2, 2.0, 1) _, ax = plt.subplots() with pytest.raises(ValueError, - match='The Subplot must have been created in the ' + match='The Axes must have been created in the ' 'present figure'): fig.add_subplot(ax) @@ -267,7 +276,7 @@ def test_add_subplot_invalid(): def test_suptitle(): fig, _ = plt.subplots() fig.suptitle('hello', color='r') - fig.suptitle('title', color='g', rotation='30') + fig.suptitle('title', color='g', rotation=30) def test_suptitle_fontproperties(): @@ -295,7 +304,7 @@ def test_alpha(): def test_too_many_figures(): with pytest.warns(RuntimeWarning): - for i in range(rcParams['figure.max_open_warning'] + 1): + for i in range(mpl.rcParams['figure.max_open_warning'] + 1): plt.figure() @@ -312,7 +321,7 @@ def test_iterability_axes_argument(): class MyAxes(Axes): def __init__(self, *args, myclass=None, **kwargs): - return Axes.__init__(self, *args, **kwargs) + Axes.__init__(self, *args, **kwargs) class MyClass: @@ -370,7 +379,7 @@ def test_figaspect(): assert h / w == 1 -@pytest.mark.parametrize('which', [None, 'both', 'major', 'minor']) +@pytest.mark.parametrize('which', ['both', 'major', 'minor']) def test_autofmt_xdate(which): date = ['3 Jan 2013', '4 Jan 2013', '5 Jan 2013', '6 Jan 2013', '7 Jan 2013', '8 Jan 2013', '9 Jan 2013', '10 Jan 2013', @@ -399,11 +408,9 @@ def test_autofmt_xdate(which): 'FixedFormatter should only be used together with FixedLocator') ax.xaxis.set_minor_formatter(FixedFormatter(minors)) - with (pytest.warns(mpl.MatplotlibDeprecationWarning) if which is None else - nullcontext()): - fig.autofmt_xdate(0.2, angle, 'right', which) + fig.autofmt_xdate(0.2, angle, 'right', which) - if which in ('both', 'major', None): + if which in ('both', 'major'): for label in fig.axes[0].get_xticklabels(False, 'major'): assert int(label.get_rotation()) == angle @@ -412,14 +419,14 @@ def test_autofmt_xdate(which): assert int(label.get_rotation()) == angle -@pytest.mark.style('default') +@mpl.style.context('default') def test_change_dpi(): fig = plt.figure(figsize=(4, 4)) - fig.canvas.draw() + fig.draw_without_rendering() assert fig.canvas.renderer.height == 400 assert fig.canvas.renderer.width == 400 fig.dpi = 50 - fig.canvas.draw() + fig.draw_without_rendering() assert fig.canvas.renderer.height == 200 assert fig.canvas.renderer.width == 200 @@ -440,6 +447,10 @@ def test_invalid_figure_size(width, height): def test_invalid_figure_add_axes(): fig = plt.figure() + with pytest.raises(TypeError, + match="missing 1 required positional argument: 'rect'"): + fig.add_axes() + with pytest.raises(ValueError): fig.add_axes((.1, .1, .5, np.nan)) @@ -483,9 +494,8 @@ def test_savefig(): def test_savefig_warns(): fig = plt.figure() - msg = r'savefig\(\) got unexpected keyword argument "non_existent_kwarg"' for format in ['png', 'pdf', 'svg', 'tif', 'jpg']: - with pytest.warns(cbook.MatplotlibDeprecationWarning, match=msg): + with pytest.raises(TypeError): fig.savefig(io.BytesIO(), format=format, non_existent_kwarg=True) @@ -499,23 +509,122 @@ def test_savefig_backend(): fig.savefig("test.png", backend="pdf") +@pytest.mark.parametrize('backend', [ + pytest.param('Agg', marks=[pytest.mark.backend('Agg')]), + pytest.param('Cairo', marks=[pytest.mark.backend('Cairo')]), +]) +def test_savefig_pixel_ratio(backend): + fig, ax = plt.subplots() + ax.plot([1, 2, 3]) + with io.BytesIO() as buf: + fig.savefig(buf, format='png') + ratio1 = Image.open(buf) + ratio1.load() + + fig, ax = plt.subplots() + ax.plot([1, 2, 3]) + fig.canvas._set_device_pixel_ratio(2) + with io.BytesIO() as buf: + fig.savefig(buf, format='png') + ratio2 = Image.open(buf) + ratio2.load() + + assert ratio1 == ratio2 + + +def test_savefig_preserve_layout_engine(tmp_path): + fig = plt.figure(layout='compressed') + fig.savefig(tmp_path / 'foo.png', bbox_inches='tight') + + assert fig.get_layout_engine()._compress + + def test_figure_repr(): fig = plt.figure(figsize=(10, 20), dpi=10) assert repr(fig) == "
" -def test_warn_cl_plus_tl(): - fig, ax = plt.subplots(constrained_layout=True) +def test_valid_layouts(): + fig = Figure(layout=None) + assert not fig.get_tight_layout() + assert not fig.get_constrained_layout() + + fig = Figure(layout='tight') + assert fig.get_tight_layout() + assert not fig.get_constrained_layout() + + fig = Figure(layout='constrained') + assert not fig.get_tight_layout() + assert fig.get_constrained_layout() + + +def test_invalid_layouts(): + fig, ax = plt.subplots(layout="constrained") with pytest.warns(UserWarning): # this should warn, fig.subplots_adjust(top=0.8) - assert not(fig.get_constrained_layout()) + assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine) + + # Using layout + (tight|constrained)_layout warns, but the former takes + # precedence. + wst = "The Figure parameters 'layout' and 'tight_layout'" + with pytest.warns(UserWarning, match=wst): + fig = Figure(layout='tight', tight_layout=False) + assert isinstance(fig.get_layout_engine(), TightLayoutEngine) + wst = "The Figure parameters 'layout' and 'constrained_layout'" + with pytest.warns(UserWarning, match=wst): + fig = Figure(layout='constrained', constrained_layout=False) + assert not isinstance(fig.get_layout_engine(), TightLayoutEngine) + assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine) + + with pytest.raises(ValueError, + match="Invalid value for 'layout'"): + Figure(layout='foobar') + + # test that layouts can be swapped if no colorbar: + fig, ax = plt.subplots(layout="constrained") + fig.set_layout_engine("tight") + assert isinstance(fig.get_layout_engine(), TightLayoutEngine) + fig.set_layout_engine("constrained") + assert isinstance(fig.get_layout_engine(), ConstrainedLayoutEngine) + + # test that layouts cannot be swapped if there is a colorbar: + fig, ax = plt.subplots(layout="constrained") + pc = ax.pcolormesh(np.random.randn(2, 2)) + fig.colorbar(pc) + with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): + fig.set_layout_engine("tight") + fig.set_layout_engine("none") + with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): + fig.set_layout_engine("tight") + + fig, ax = plt.subplots(layout="tight") + pc = ax.pcolormesh(np.random.randn(2, 2)) + fig.colorbar(pc) + with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): + fig.set_layout_engine("constrained") + fig.set_layout_engine("none") + assert isinstance(fig.get_layout_engine(), PlaceHolderLayoutEngine) + + with pytest.raises(RuntimeError, match='Colorbar layout of new layout'): + fig.set_layout_engine("constrained") + + +@pytest.mark.parametrize('layout', ['constrained', 'compressed']) +def test_layout_change_warning(layout): + """ + Raise a warning when a previously assigned layout changes to tight using + plt.tight_layout(). + """ + fig, ax = plt.subplots(layout=layout) + with pytest.warns(UserWarning, match='The figure layout has changed to'): + plt.tight_layout() @check_figures_equal(extensions=["png", "pdf"]) def test_add_artist(fig_test, fig_ref): - fig_test.set_dpi(100) - fig_ref.set_dpi(100) + fig_test.dpi = 100 + fig_ref.dpi = 100 fig_test.subplots() l1 = plt.Line2D([.2, .7], [.7, .7], gid='l1') @@ -601,10 +710,98 @@ def test_removed_axis(): fig.canvas.draw() -@pytest.mark.style('mpl20') +@pytest.mark.parametrize('clear_meth', ['clear', 'clf']) +def test_figure_clear(clear_meth): + # we test the following figure clearing scenarios: + fig = plt.figure() + + # a) an empty figure + fig.clear() + assert fig.axes == [] + + # b) a figure with a single unnested axes + ax = fig.add_subplot(111) + getattr(fig, clear_meth)() + assert fig.axes == [] + + # c) a figure multiple unnested axes + axes = [fig.add_subplot(2, 1, i+1) for i in range(2)] + getattr(fig, clear_meth)() + assert fig.axes == [] + + # d) a figure with a subfigure + gs = fig.add_gridspec(ncols=2, nrows=1) + subfig = fig.add_subfigure(gs[0]) + subaxes = subfig.add_subplot(111) + getattr(fig, clear_meth)() + assert subfig not in fig.subfigs + assert fig.axes == [] + + # e) a figure with a subfigure and a subplot + subfig = fig.add_subfigure(gs[0]) + subaxes = subfig.add_subplot(111) + mainaxes = fig.add_subplot(gs[1]) + + # e.1) removing just the axes leaves the subplot + mainaxes.remove() + assert fig.axes == [subaxes] + + # e.2) removing just the subaxes leaves the subplot + # and subfigure + mainaxes = fig.add_subplot(gs[1]) + subaxes.remove() + assert fig.axes == [mainaxes] + assert subfig in fig.subfigs + + # e.3) clearing the subfigure leaves the subplot + subaxes = subfig.add_subplot(111) + assert mainaxes in fig.axes + assert subaxes in fig.axes + getattr(subfig, clear_meth)() + assert subfig in fig.subfigs + assert subaxes not in subfig.axes + assert subaxes not in fig.axes + assert mainaxes in fig.axes + + # e.4) clearing the whole thing + subaxes = subfig.add_subplot(111) + getattr(fig, clear_meth)() + assert fig.axes == [] + assert fig.subfigs == [] + + # f) multiple subfigures + subfigs = [fig.add_subfigure(gs[i]) for i in [0, 1]] + subaxes = [sfig.add_subplot(111) for sfig in subfigs] + assert all(ax in fig.axes for ax in subaxes) + assert all(sfig in fig.subfigs for sfig in subfigs) + + # f.1) clearing only one subfigure + getattr(subfigs[0], clear_meth)() + assert subaxes[0] not in fig.axes + assert subaxes[1] in fig.axes + assert subfigs[1] in fig.subfigs + + # f.2) clearing the whole thing + getattr(subfigs[1], clear_meth)() + subfigs = [fig.add_subfigure(gs[i]) for i in [0, 1]] + subaxes = [sfig.add_subplot(111) for sfig in subfigs] + assert all(ax in fig.axes for ax in subaxes) + assert all(sfig in fig.subfigs for sfig in subfigs) + getattr(fig, clear_meth)() + assert fig.subfigs == [] + assert fig.axes == [] + + +def test_clf_not_redefined(): + for klass in FigureBase.__subclasses__(): + # check that subclasses do not get redefined in our Figure subclasses + assert 'clf' not in klass.__dict__ + + +@mpl.style.context('mpl20') def test_picking_does_not_stale(): fig, ax = plt.subplots() - col = ax.scatter([0], [0], [1000], picker=True) + ax.scatter([0], [0], [1000], picker=True) fig.canvas.draw() assert not fig.stale @@ -658,7 +855,12 @@ def test_animated_with_canvas_change(fig_test, fig_ref): class TestSubplotMosaic: @check_figures_equal(extensions=["png"]) @pytest.mark.parametrize( - "x", [[["A", "A", "B"], ["C", "D", "B"]], [[1, 1, 2], [3, 4, 2]]] + "x", [ + [["A", "A", "B"], ["C", "D", "B"]], + [[1, 1, 2], [3, 4, 2]], + (("A", "A", "B"), ("C", "D", "B")), + ((1, 1, 2), (3, 4, 2)) + ] ) def test_basic(self, fig_test, fig_ref, x): grid_axes = fig_test.subplot_mosaic(x) @@ -688,8 +890,8 @@ def test_all_nested(self, fig_test, fig_ref): x = [["A", "B"], ["C", "D"]] y = [["E", "F"], ["G", "H"]] - fig_ref.set_constrained_layout(True) - fig_test.set_constrained_layout(True) + fig_ref.set_layout_engine("constrained") + fig_test.set_layout_engine("constrained") grid_axes = fig_test.subplot_mosaic([[x, y]]) for ax in grid_axes.values(): @@ -709,8 +911,8 @@ def test_all_nested(self, fig_test, fig_ref): @check_figures_equal(extensions=["png"]) def test_nested(self, fig_test, fig_ref): - fig_ref.set_constrained_layout(True) - fig_test.set_constrained_layout(True) + fig_ref.set_layout_engine("constrained") + fig_test.set_layout_engine("constrained") x = [["A", "B"], ["C", "D"]] @@ -748,6 +950,26 @@ def test_nested_tuple(self, fig_test, fig_ref): fig_ref.subplot_mosaic([["F"], [x]]) fig_test.subplot_mosaic([["F"], [xt]]) + def test_nested_width_ratios(self): + x = [["A", [["B"], + ["C"]]]] + width_ratios = [2, 1] + + fig, axd = plt.subplot_mosaic(x, width_ratios=width_ratios) + + assert axd["A"].get_gridspec().get_width_ratios() == width_ratios + assert axd["B"].get_gridspec().get_width_ratios() != width_ratios + + def test_nested_height_ratios(self): + x = [["A", [["B"], + ["C"]]], ["D", "D"]] + height_ratios = [1, 2] + + fig, axd = plt.subplot_mosaic(x, height_ratios=height_ratios) + + assert axd["D"].get_gridspec().get_height_ratios() == height_ratios + assert axd["B"].get_gridspec().get_height_ratios() != height_ratios + @check_figures_equal(extensions=["png"]) @pytest.mark.parametrize( "x, empty_sentinel", @@ -788,6 +1010,10 @@ def test_fail_list_of_str(self): plt.subplot_mosaic(['foo', 'bar']) with pytest.raises(ValueError, match='must be 2D'): plt.subplot_mosaic(['foo']) + with pytest.raises(ValueError, match='must be 2D'): + plt.subplot_mosaic([['foo', ('bar',)]]) + with pytest.raises(ValueError, match='must be 2D'): + plt.subplot_mosaic([['a', 'b'], [('a', 'b'), 'c']]) @check_figures_equal(extensions=["png"]) @pytest.mark.parametrize("subplot_kw", [{}, {"projection": "polar"}, None]) @@ -801,8 +1027,26 @@ def test_subplot_kw(self, fig_test, fig_ref, subplot_kw): axB = fig_ref.add_subplot(gs[0, 1], **subplot_kw) + @check_figures_equal(extensions=["png"]) + @pytest.mark.parametrize("multi_value", ['BC', tuple('BC')]) + def test_per_subplot_kw(self, fig_test, fig_ref, multi_value): + x = 'AB;CD' + grid_axes = fig_test.subplot_mosaic( + x, + subplot_kw={'facecolor': 'red'}, + per_subplot_kw={ + 'D': {'facecolor': 'blue'}, + multi_value: {'facecolor': 'green'}, + } + ) + + gs = fig_ref.add_gridspec(2, 2) + for color, spec in zip(['red', 'green', 'green', 'blue'], gs): + fig_ref.add_subplot(spec, facecolor=color) + def test_string_parser(self): normalize = Figure._normalize_grid_string + assert normalize('ABC') == [['A', 'B', 'C']] assert normalize('AB;CC') == [['A', 'B'], ['C', 'C']] assert normalize('AB;CC;DE') == [['A', 'B'], ['C', 'C'], ['D', 'E']] @@ -819,6 +1063,25 @@ def test_string_parser(self): DE """) == [['A', 'B'], ['C', 'C'], ['D', 'E']] + def test_per_subplot_kw_expander(self): + normalize = Figure._norm_per_subplot_kw + assert normalize({"A": {}, "B": {}}) == {"A": {}, "B": {}} + assert normalize({("A", "B"): {}}) == {"A": {}, "B": {}} + with pytest.raises( + ValueError, match=f'The key {"B"!r} appears multiple times' + ): + normalize({("A", "B"): {}, "B": {}}) + with pytest.raises( + ValueError, match=f'The key {"B"!r} appears multiple times' + ): + normalize({"B": {}, ("A", "B"): {}}) + + def test_extra_per_subplot_kw(self): + with pytest.raises( + ValueError, match=f'The keys {set("B")!r} are in' + ): + Figure().subplot_mosaic("A", per_subplot_kw={"B": {}}) + @check_figures_equal(extensions=["png"]) @pytest.mark.parametrize("str_pattern", ["AAA\nBBB", "\nAAA\nBBB\n", "ABC\nDEF"] @@ -883,6 +1146,20 @@ def test_nested_user_order(self): assert list(ax_dict) == list("ABCDEFGHI") assert list(fig.axes) == list(ax_dict.values()) + def test_share_all(self): + layout = [ + ["A", [["B", "C"], + ["D", "E"]]], + ["F", "G"], + [".", [["H", [["I"], + ["."]]]]] + ] + fig = plt.figure() + ax_dict = fig.subplot_mosaic(layout, sharex=True, sharey=True) + ax_dict["A"].set(xscale="log", yscale="logit") + assert all(ax.get_xscale() == "log" and ax.get_yscale() == "logit" + for ax in ax_dict.values()) + def test_reused_gridspec(): """Test that these all use the same gridspec""" @@ -900,11 +1177,10 @@ def test_reused_gridspec(): @image_comparison(['test_subfigure.png'], style='mpl20', - savefig_kwarg={'facecolor': 'teal'}, - remove_text=False) + savefig_kwarg={'facecolor': 'teal'}) def test_subfigure(): np.random.seed(19680801) - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout='constrained') sub = fig.subfigures(1, 2) axs = sub[0].subplots(2, 2) @@ -924,20 +1200,30 @@ def test_subfigure(): def test_subfigure_tightbbox(): # test that we can get the tightbbox with a subfigure... - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout='constrained') sub = fig.subfigures(1, 2) np.testing.assert_allclose( - fig.get_tightbbox(fig.canvas.get_renderer()).width, 0.1) + fig.get_tightbbox(fig.canvas.get_renderer()).width, + 8.0) + + +def test_subfigure_dpi(): + fig = plt.figure(dpi=100) + sub_fig = fig.subfigures() + assert sub_fig.get_dpi() == fig.get_dpi() + + sub_fig.set_dpi(200) + assert sub_fig.get_dpi() == 200 + assert fig.get_dpi() == 200 @image_comparison(['test_subfigure_ss.png'], style='mpl20', - savefig_kwarg={'facecolor': 'teal'}, - remove_text=False) + savefig_kwarg={'facecolor': 'teal'}, tol=0.02) def test_subfigure_ss(): # test assigning the subfigure via subplotspec np.random.seed(19680801) - fig = plt.figure(constrained_layout=True) + fig = plt.figure(layout='constrained') gs = fig.add_gridspec(1, 2) sub = fig.add_subfigure(gs[0], facecolor='pink') @@ -956,13 +1242,12 @@ def test_subfigure_ss(): @image_comparison(['test_subfigure_double.png'], style='mpl20', - savefig_kwarg={'facecolor': 'teal'}, - remove_text=False) + savefig_kwarg={'facecolor': 'teal'}) def test_subfigure_double(): # test assigning the subfigure via subplotspec np.random.seed(19680801) - fig = plt.figure(constrained_layout=True, figsize=(10, 8)) + fig = plt.figure(layout='constrained', figsize=(10, 8)) fig.suptitle('fig') @@ -1006,6 +1291,7 @@ def test_subfigure_spanning(): fig.add_subfigure(gs[0, 0]), fig.add_subfigure(gs[0:2, 1]), fig.add_subfigure(gs[2, 1:3]), + fig.add_subfigure(gs[0:, 1:]) ] w = 640 @@ -1019,6 +1305,12 @@ def test_subfigure_spanning(): np.testing.assert_allclose(sub_figs[2].bbox.min, [w / 3, 0]) np.testing.assert_allclose(sub_figs[2].bbox.max, [w, h / 3]) + # check here that slicing actually works. Last sub_fig + # with open slices failed, but only on draw... + for i in range(4): + sub_figs[i].add_subplot() + fig.draw_without_rendering() + @mpl.style.context('mpl20') def test_subfigure_ticks(): @@ -1039,11 +1331,11 @@ def test_subfigure_ticks(): ax2.scatter(x=[-126.5357270050049, 94.68456736755368], y=[1500, 3600]) ax3 = subfig_bl.add_subplot(gs[0, 3:14], sharey=ax1) - fig.set_dpi(120) - fig.canvas.draw() + fig.dpi = 120 + fig.draw_without_rendering() ticks120 = ax2.get_xticks() - fig.set_dpi(300) - fig.canvas.draw() + fig.dpi = 300 + fig.draw_without_rendering() ticks300 = ax2.get_xticks() np.testing.assert_allclose(ticks120, ticks300) @@ -1065,6 +1357,16 @@ def test_subfigure_scatter_size(): ax.scatter([3, 4, 5], [1, 2, 3], s=[20, 30, 40], marker='s', color='g') +def test_subfigure_pdf(): + fig = plt.figure(layout='constrained') + sub_fig = fig.subfigures() + ax = sub_fig.add_subplot(111) + b = ax.bar(1, 1) + ax.bar_label(b) + buffer = io.BytesIO() + fig.savefig(buffer, format='pdf') + + def test_add_subplot_kwargs(): # fig.add_subplot() always creates new axes, even if axes kwargs differ. fig = plt.figure() @@ -1113,3 +1415,97 @@ def test_add_axes_kwargs(): assert ax1.name == 'rectilinear' assert ax1 is not ax plt.close() + + +def test_ginput(recwarn): # recwarn undoes warn filters at exit. + warnings.filterwarnings("ignore", "cannot show the figure") + fig, ax = plt.subplots() + + def single_press(): + fig.canvas.button_press_event(*ax.transData.transform((.1, .2)), 1) + + Timer(.1, single_press).start() + assert fig.ginput() == [(.1, .2)] + + def multi_presses(): + fig.canvas.button_press_event(*ax.transData.transform((.1, .2)), 1) + fig.canvas.key_press_event("backspace") + fig.canvas.button_press_event(*ax.transData.transform((.3, .4)), 1) + fig.canvas.button_press_event(*ax.transData.transform((.5, .6)), 1) + fig.canvas.button_press_event(*ax.transData.transform((0, 0)), 2) + + Timer(.1, multi_presses).start() + np.testing.assert_allclose(fig.ginput(3), [(.3, .4), (.5, .6)]) + + +def test_waitforbuttonpress(recwarn): # recwarn undoes warn filters at exit. + warnings.filterwarnings("ignore", "cannot show the figure") + fig = plt.figure() + assert fig.waitforbuttonpress(timeout=.1) is None + Timer(.1, fig.canvas.key_press_event, ("z",)).start() + assert fig.waitforbuttonpress() is True + Timer(.1, fig.canvas.button_press_event, (0, 0, 1)).start() + assert fig.waitforbuttonpress() is False + + +def test_kwargs_pass(): + fig = Figure(label='whole Figure') + sub_fig = fig.subfigures(1, 1, label='sub figure') + + assert fig.get_label() == 'whole Figure' + assert sub_fig.get_label() == 'sub figure' + + +@check_figures_equal(extensions=["png"]) +def test_rcparams(fig_test, fig_ref): + fig_ref.supxlabel("xlabel", weight='bold', size=15) + fig_ref.supylabel("ylabel", weight='bold', size=15) + fig_ref.suptitle("Title", weight='light', size=20) + with mpl.rc_context({'figure.labelweight': 'bold', + 'figure.labelsize': 15, + 'figure.titleweight': 'light', + 'figure.titlesize': 20}): + fig_test.supxlabel("xlabel") + fig_test.supylabel("ylabel") + fig_test.suptitle("Title") + + +def test_deepcopy(): + fig1, ax = plt.subplots() + ax.plot([0, 1], [2, 3]) + ax.set_yscale('log') + + fig2 = copy.deepcopy(fig1) + + # Make sure it is a new object + assert fig2.axes[0] is not ax + # And that the axis scale got propagated + assert fig2.axes[0].get_yscale() == 'log' + # Update the deepcopy and check the original isn't modified + fig2.axes[0].set_yscale('linear') + assert ax.get_yscale() == 'log' + + # And test the limits of the axes don't get propagated + ax.set_xlim(1e-1, 1e2) + # Draw these to make sure limits are updated + fig1.draw_without_rendering() + fig2.draw_without_rendering() + + assert ax.get_xlim() == (1e-1, 1e2) + assert fig2.axes[0].get_xlim() == (0, 1) + + +def test_unpickle_with_device_pixel_ratio(): + fig = Figure(dpi=42) + fig.canvas._set_device_pixel_ratio(7) + assert fig.dpi == 42*7 + fig2 = pickle.loads(pickle.dumps(fig)) + assert fig2.dpi == 42 + + +def test_gridspec_no_mutate_input(): + gs = {'left': .1} + gs_orig = dict(gs) + plt.subplots(1, 2, width_ratios=[1, 2], gridspec_kw=gs) + assert gs == gs_orig + plt.subplot_mosaic('AB', width_ratios=[1, 2], gridspec_kw=gs) diff --git a/lib/matplotlib/tests/test_font_manager.py b/lib/matplotlib/tests/test_font_manager.py index 1e8252abb76c..3724db1e1b43 100644 --- a/lib/matplotlib/tests/test_font_manager.py +++ b/lib/matplotlib/tests/test_font_manager.py @@ -2,6 +2,7 @@ import multiprocessing import os from pathlib import Path +from PIL import Image import shutil import subprocess import sys @@ -11,9 +12,10 @@ import pytest from matplotlib.font_manager import ( - findfont, findSystemFonts, FontProperties, fontManager, json_dump, - json_load, get_font, get_fontconfig_fonts, is_opentype_cff_font, - MSUserFontDirectories, _call_fc_list) + findfont, findSystemFonts, FontEntry, FontProperties, fontManager, + json_dump, json_load, get_font, is_opentype_cff_font, + MSUserFontDirectories, _get_fontconfig_fonts, ft2font, + ttfFontProperty, cbook) from matplotlib import pyplot as plt, rc_context has_fclist = shutil.which('fc-list') is not None @@ -71,9 +73,10 @@ def test_otf(): assert res == is_opentype_cff_font(f.fname) -@pytest.mark.skipif(not has_fclist, reason='no fontconfig installed') +@pytest.mark.skipif(sys.platform == "win32" or not has_fclist, + reason='no fontconfig installed') def test_get_fontconfig_fonts(): - assert len(get_fontconfig_fonts()) > 1 + assert len(_get_fontconfig_fonts()) > 1 @pytest.mark.parametrize('factor', [2, 4, 6, 8]) @@ -101,7 +104,7 @@ def test_utf16m_sfnt(): entry = next(entry for entry in fontManager.ttflist if Path(entry.fname).name == "seguisbi.ttf") except StopIteration: - pytest.skip("Couldn't find font to test against.") + pytest.skip("Couldn't find seguisbi.ttf font to test against.") else: # Check that we successfully read "semibold" from the font's sfnt table # and set its weight accordingly. @@ -111,14 +114,25 @@ def test_utf16m_sfnt(): def test_find_ttc(): fp = FontProperties(family=["WenQuanYi Zen Hei"]) if Path(findfont(fp)).name != "wqy-zenhei.ttc": - pytest.skip("Font may be missing") - + pytest.skip("Font wqy-zenhei.ttc may be missing") fig, ax = plt.subplots() ax.text(.5, .5, "\N{KANGXI RADICAL DRAGON}", fontproperties=fp) for fmt in ["raw", "svg", "pdf", "ps"]: fig.savefig(BytesIO(), format=fmt) +def test_find_noto(): + fp = FontProperties(family=["Noto Sans CJK SC", "Noto Sans CJK JP"]) + name = Path(findfont(fp)).name + if name not in ("NotoSansCJKsc-Regular.otf", "NotoSansCJK-Regular.ttc"): + pytest.skip(f"Noto Sans CJK SC font may be missing (found {name})") + + fig, ax = plt.subplots() + ax.text(0.5, 0.5, 'Hello, 你好', fontproperties=fp) + for fmt in ["raw", "svg", "pdf", "ps"]: + fig.savefig(BytesIO(), format=fmt) + + def test_find_invalid(tmpdir): tmp_path = Path(tmpdir) @@ -133,11 +147,12 @@ def test_find_invalid(tmpdir): # Not really public, but get_font doesn't expose non-filename constructor. from matplotlib.ft2font import FT2Font - with pytest.raises(TypeError, match='path or binary-mode file'): + with pytest.raises(TypeError, match='font file or a binary-mode file'): FT2Font(StringIO()) -@pytest.mark.skipif(sys.platform != 'linux', reason='Linux only') +@pytest.mark.skipif(sys.platform != 'linux' or not has_fclist, + reason='only Linux with fontconfig installed') def test_user_fonts_linux(tmpdir, monkeypatch): font_test_file = 'mpltest.ttf' @@ -154,13 +169,29 @@ def test_user_fonts_linux(tmpdir, monkeypatch): with monkeypatch.context() as m: m.setenv('XDG_DATA_HOME', str(tmpdir)) - _call_fc_list.cache_clear() + _get_fontconfig_fonts.cache_clear() # Now, the font should be available fonts = findSystemFonts() assert any(font_test_file in font for font in fonts) # Make sure the temporary directory is no longer cached. - _call_fc_list.cache_clear() + _get_fontconfig_fonts.cache_clear() + + +def test_addfont_as_path(): + """Smoke test that addfont() accepts pathlib.Path.""" + font_test_file = 'mpltest.ttf' + path = Path(__file__).parent / font_test_file + try: + fontManager.addfont(path) + added, = [font for font in fontManager.ttflist + if font.fname.endswith(font_test_file)] + fontManager.ttflist.remove(added) + finally: + to_remove = [font for font in fontManager.ttflist + if font.fname.endswith(font_test_file)] + for font in to_remove: + fontManager.ttflist.remove(font) @pytest.mark.skipif(sys.platform != 'win32', reason='Windows only') @@ -169,7 +200,7 @@ def test_user_fonts_win32(): pytest.xfail("This test should only run on CI (appveyor or azure) " "as the developer's font directory should remain " "unchanged.") - + pytest.xfail("We need to update the registry for this test to work") font_test_file = 'mpltest.ttf' # Precondition: the test font should not be available @@ -255,3 +286,41 @@ def test_fontcache_thread_safe(): if proc.returncode: pytest.fail("The subprocess returned with non-zero exit status " f"{proc.returncode}.") + + +def test_fontentry_dataclass(): + fontent = FontEntry(name='font-name') + + png = fontent._repr_png_() + img = Image.open(BytesIO(png)) + assert img.width > 0 + assert img.height > 0 + + html = fontent._repr_html_() + assert html.startswith("
127: + assert Path(font.fname).name == found_file_name + else: + assert Path(font.fname).name == "DejaVuSans.ttf" diff --git a/lib/matplotlib/tests/test_getattr.py b/lib/matplotlib/tests/test_getattr.py new file mode 100644 index 000000000000..8fcb981746b2 --- /dev/null +++ b/lib/matplotlib/tests/test_getattr.py @@ -0,0 +1,34 @@ +from importlib import import_module +from pkgutil import walk_packages + +import matplotlib +import pytest + +# Get the names of all matplotlib submodules, +# except for the unit tests and private modules. +module_names = [ + m.name + for m in walk_packages( + path=matplotlib.__path__, prefix=f'{matplotlib.__name__}.' + ) + if not m.name.startswith(__package__) + and not any(x.startswith('_') for x in m.name.split('.')) +] + + +@pytest.mark.parametrize('module_name', module_names) +@pytest.mark.filterwarnings('ignore::DeprecationWarning') +def test_getattr(module_name): + """ + Test that __getattr__ methods raise AttributeError for unknown keys. + See #20822, #20855. + """ + try: + module = import_module(module_name) + except (ImportError, RuntimeError) as e: + # Skip modules that cannot be imported due to missing dependencies + pytest.skip(f'Cannot import {module_name} due to {e}') + + key = 'THIS_SYMBOL_SHOULD_NOT_EXIST' + if hasattr(module, key): + delattr(module, key) diff --git a/lib/matplotlib/tests/test_image.py b/lib/matplotlib/tests/test_image.py index c635564875de..76a622181ddf 100644 --- a/lib/matplotlib/tests/test_image.py +++ b/lib/matplotlib/tests/test_image.py @@ -11,8 +11,9 @@ from numpy.testing import assert_array_equal from PIL import Image +import matplotlib as mpl from matplotlib import ( - _api, colors, image as mimage, patches, pyplot as plt, style, rcParams) + colors, image as mimage, patches, pyplot as plt, style, rcParams) from matplotlib.image import (AxesImage, BboxImage, FigureImage, NonUniformImage, PcolorImage) from matplotlib.testing.decorators import check_figures_equal, image_comparison @@ -114,12 +115,12 @@ def test_imshow_antialiased(fig_test, fig_ref, A = np.random.rand(int(dpi * img_size), int(dpi * img_size)) for fig in [fig_test, fig_ref]: fig.set_size_inches(fig_size, fig_size) - axs = fig_test.subplots() - axs.set_position([0, 0, 1, 1]) - axs.imshow(A, interpolation='antialiased') - axs = fig_ref.subplots() - axs.set_position([0, 0, 1, 1]) - axs.imshow(A, interpolation=interpolation) + ax = fig_test.subplots() + ax.set_position([0, 0, 1, 1]) + ax.imshow(A, interpolation='antialiased') + ax = fig_ref.subplots() + ax.set_position([0, 0, 1, 1]) + ax.imshow(A, interpolation=interpolation) @check_figures_equal(extensions=['png']) @@ -130,14 +131,14 @@ def test_imshow_zoom(fig_test, fig_ref): A = np.random.rand(int(dpi * 3), int(dpi * 3)) for fig in [fig_test, fig_ref]: fig.set_size_inches(2.9, 2.9) - axs = fig_test.subplots() - axs.imshow(A, interpolation='antialiased') - axs.set_xlim([10, 20]) - axs.set_ylim([10, 20]) - axs = fig_ref.subplots() - axs.imshow(A, interpolation='nearest') - axs.set_xlim([10, 20]) - axs.set_ylim([10, 20]) + ax = fig_test.subplots() + ax.imshow(A, interpolation='antialiased') + ax.set_xlim([10, 20]) + ax.set_ylim([10, 20]) + ax = fig_ref.subplots() + ax.imshow(A, interpolation='nearest') + ax.set_xlim([10, 20]) + ax.set_ylim([10, 20]) @check_figures_equal() @@ -248,6 +249,7 @@ def test_imsave_pil_kwargs_tiff(): buf = io.BytesIO() pil_kwargs = {"description": "test image"} plt.imsave(buf, [[0, 1], [2, 3]], format="tiff", pil_kwargs=pil_kwargs) + assert len(pil_kwargs) == 1 im = Image.open(buf) tags = {TAGS[k].name: v for k, v in im.tag_v2.items()} assert tags["ImageDescription"] == "test image" @@ -286,11 +288,11 @@ def test_cursor_data(): # Hmm, something is wrong here... I get 0, not None... # But, this works further down in the tests with extents flipped - #x, y = 0.1, -0.1 - #xdisp, ydisp = ax.transData.transform([x, y]) - #event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) - #z = im.get_cursor_data(event) - #assert z is None, "Did not get None, got %d" % z + # x, y = 0.1, -0.1 + # xdisp, ydisp = ax.transData.transform([x, y]) + # event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) + # z = im.get_cursor_data(event) + # assert z is None, "Did not get None, got %d" % z ax.clear() # Now try with the extents flipped. @@ -336,11 +338,14 @@ def test_cursor_data(): @pytest.mark.parametrize( - "data, text_without_colorbar, text_with_colorbar", [ - ([[10001, 10000]], "[1e+04]", "[10001]"), - ([[.123, .987]], "[0.123]", "[0.123]"), -]) -def test_format_cursor_data(data, text_without_colorbar, text_with_colorbar): + "data, text", [ + ([[10001, 10000]], "[10001.000]"), + ([[.123, .987]], "[0.123]"), + ([[np.nan, 1, 2]], "[]"), + ([[1, 1+1e-15]], "[1.0000000000000000]"), + ([[-1, -1]], "[-1.0000000000000000]"), + ]) +def test_format_cursor_data(data, text): from matplotlib.backend_bases import MouseEvent fig, ax = plt.subplots() @@ -348,16 +353,7 @@ def test_format_cursor_data(data, text_without_colorbar, text_with_colorbar): xdisp, ydisp = ax.transData.transform([0, 0]) event = MouseEvent('motion_notify_event', fig.canvas, xdisp, ydisp) - assert im.get_cursor_data(event) == data[0][0] - assert im.format_cursor_data(im.get_cursor_data(event)) \ - == text_without_colorbar - - fig.colorbar(im) - fig.canvas.draw() # This is necessary to set up the colorbar formatter. - - assert im.get_cursor_data(event) == data[0][0] - assert im.format_cursor_data(im.get_cursor_data(event)) \ - == text_with_colorbar + assert im.format_cursor_data(im.get_cursor_data(event)) == text @image_comparison(['image_clip'], style='mpl20') @@ -506,6 +502,18 @@ def test_image_composite_alpha(): ax.set_ylim([5, 0]) +@check_figures_equal(extensions=["pdf"]) +def test_clip_path_disables_compositing(fig_test, fig_ref): + t = np.arange(9).reshape((3, 3)) + for fig in [fig_test, fig_ref]: + ax = fig.add_subplot() + ax.imshow(t, clip_path=(mpl.path.Path([(0, 0), (0, 1), (1, 0)]), + ax.transData)) + ax.imshow(t, clip_path=(mpl.path.Path([(1, 1), (1, 2), (2, 1)]), + ax.transData)) + fig_ref.suppressComposite = True + + @image_comparison(['rasterize_10dpi'], extensions=['pdf', 'svg'], remove_text=True, style='mpl20') def test_rasterize_dpi(): @@ -582,6 +590,20 @@ def test_get_window_extent_for_AxisImage(): assert_array_equal(im_bbox.get_points(), [[400, 200], [700, 900]]) + fig, ax = plt.subplots(figsize=(10, 10), dpi=100) + ax.set_position([0, 0, 1, 1]) + ax.set_xlim(1, 2) + ax.set_ylim(0, 1) + im_obj = ax.imshow( + im, extent=[0.4, 0.7, 0.2, 0.9], interpolation='nearest', + transform=ax.transAxes) + + fig.canvas.draw() + renderer = fig.canvas.renderer + im_bbox = im_obj.get_window_extent(renderer) + + assert_array_equal(im_bbox.get_points(), [[400, 200], [700, 900]]) + @image_comparison(['zoom_and_clip_upper_origin.png'], remove_text=True, style='mpl20') @@ -636,7 +658,7 @@ def test_jpeg_alpha(): # If this fails, there will be only one color (all black). If this # is working, we should have all 256 shades of grey represented. num_colors = len(image.getcolors(256)) - assert 175 <= num_colors <= 185 + assert 175 <= num_colors <= 210 # The fully transparent part should be red. corner_pixel = image.getpixel((0, 0)) assert corner_pixel == (254, 0, 0) @@ -714,7 +736,7 @@ def test_load_from_url(): url = ('file:' + ('///' if sys.platform == 'win32' else '') + path.resolve().as_posix()) - with _api.suppress_matplotlib_deprecation_warning(): + with pytest.raises(ValueError, match="Please open the URL"): plt.imread(url) with urllib.request.urlopen(url) as file: plt.imread(file) @@ -876,7 +898,7 @@ def test_imshow_endianess(): remove_text=True, style='mpl20') def test_imshow_masked_interpolation(): - cmap = plt.get_cmap('viridis').with_extremes(over='r', under='b', bad='k') + cmap = mpl.colormaps['viridis'].with_extremes(over='r', under='b', bad='k') N = 20 n = colors.Normalize(vmin=0, vmax=N*N-1) @@ -971,13 +993,20 @@ def test_imshow_bignumbers_real(): def test_empty_imshow(make_norm): fig, ax = plt.subplots() with pytest.warns(UserWarning, - match="Attempting to set identical left == right"): + match="Attempting to set identical low and high xlims"): im = ax.imshow([[]], norm=make_norm()) im.set_extent([-5, 5, -5, 5]) fig.canvas.draw() with pytest.raises(RuntimeError): - im.make_image(fig._cachedRenderer) + im.make_image(fig.canvas.get_renderer()) + + +def test_imshow_float16(): + fig, ax = plt.subplots() + ax.imshow(np.zeros((3, 3), dtype=np.float16)) + # Ensure that drawing doesn't cause crash. + fig.canvas.draw() def test_imshow_float128(): @@ -997,8 +1026,8 @@ def test_imshow_bool(): def test_full_invalid(): fig, ax = plt.subplots() ax.imshow(np.full((10, 10), np.nan)) - with pytest.warns(UserWarning): - fig.canvas.draw() + + fig.canvas.draw() @pytest.mark.parametrize("fmt,counted", @@ -1082,7 +1111,7 @@ def test_image_array_alpha(fig_test, fig_ref): zz = np.exp(- 3 * ((xx - 0.5) ** 2) + (yy - 0.7 ** 2)) alpha = zz / zz.max() - cmap = plt.get_cmap('viridis') + cmap = mpl.colormaps['viridis'] ax = fig_test.add_subplot() ax.imshow(zz, alpha=alpha, cmap=cmap, interpolation='nearest') @@ -1097,9 +1126,9 @@ def test_image_array_alpha_validation(): plt.imshow(np.zeros((2, 2)), alpha=[1, 1]) -@pytest.mark.style('mpl20') +@mpl.style.context('mpl20') def test_exact_vmin(): - cmap = copy(plt.cm.get_cmap("autumn_r")) + cmap = copy(mpl.colormaps["autumn_r"]) cmap.set_under(color="lightgrey") # make the image exactly 190 pixels wide @@ -1126,13 +1155,6 @@ def test_exact_vmin(): assert np.all(from_image == direct_computation) -@pytest.mark.network -@pytest.mark.flaky -def test_https_imread_smoketest(): - with _api.suppress_matplotlib_deprecation_warning(): - v = mimage.imread('https://matplotlib.org/1.5.0/_static/logo2.png') - - # A basic ndarray subclass that implements a quantity # It does not implement an entire unit system or all quantity math. # There is just enough implemented to test handling of ndarray @@ -1156,7 +1178,7 @@ def __getitem__(self, item): def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): func = getattr(ufunc, method) if "out" in kwargs: - raise NotImplementedError + return NotImplemented if len(inputs) == 1: i0 = inputs[0] unit = getattr(i0, "units", "dimensionless") @@ -1176,11 +1198,16 @@ def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): unit = f"{u0}*{u1}" elif ufunc == np.divide: unit = f"{u0}/({u1})" + elif ufunc in (np.greater, np.greater_equal, + np.equal, np.not_equal, + np.less, np.less_equal): + # Comparisons produce unitless booleans for output + unit = None else: - raise NotImplementedError + return NotImplemented out_arr = func(i0.view(np.ndarray), i1.view(np.ndarray), **kwargs) else: - raise NotImplementedError + return NotImplemented if unit is None: out_arr = np.array(out_arr) else: @@ -1222,7 +1249,7 @@ def test_norm_change(fig_test, fig_ref): masked_data = np.ma.array(data, mask=False) masked_data.mask[0:2, 0:2] = True - cmap = plt.get_cmap('viridis').with_extremes(under='w') + cmap = mpl.colormaps['viridis'].with_extremes(under='w') ax = fig_test.subplots() im = ax.imshow(data, norm=colors.LogNorm(vmin=0.5, vmax=1), @@ -1256,7 +1283,7 @@ def test_huge_range_log(fig_test, fig_ref, x): data[0:2, :] = 1000 ax = fig_ref.subplots() - cmap = plt.get_cmap('viridis').with_extremes(under='w') + cmap = mpl.colormaps['viridis'].with_extremes(under='w') ax.imshow(data, norm=colors.Normalize(vmin=1, vmax=data.max()), interpolation='nearest', cmap=cmap) @@ -1292,3 +1319,137 @@ def test_spy_box(fig_test, fig_ref): ax_ref[i].yaxis.set_major_locator( mticker.MaxNLocator(nbins=9, steps=[1, 2, 5, 10], integer=True) ) + + +@image_comparison(["nonuniform_and_pcolor.png"], style="mpl20") +def test_nonuniform_and_pcolor(): + axs = plt.figure(figsize=(3, 3)).subplots(3, sharex=True, sharey=True) + for ax, interpolation in zip(axs, ["nearest", "bilinear"]): + im = NonUniformImage(ax, interpolation=interpolation) + im.set_data(np.arange(3) ** 2, np.arange(3) ** 2, + np.arange(9).reshape((3, 3))) + ax.add_image(im) + axs[2].pcolorfast( # PcolorImage + np.arange(4) ** 2, np.arange(4) ** 2, np.arange(9).reshape((3, 3))) + for ax in axs: + ax.set_axis_off() + # NonUniformImage "leaks" out of extents, not PColorImage. + ax.set(xlim=(0, 10)) + + +@image_comparison( + ['rgba_antialias.png'], style='mpl20', remove_text=True, + tol=0.007 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) +def test_rgba_antialias(): + fig, axs = plt.subplots(2, 2, figsize=(3.5, 3.5), sharex=False, + sharey=False, constrained_layout=True) + N = 250 + aa = np.ones((N, N)) + aa[::2, :] = -1 + + x = np.arange(N) / N - 0.5 + y = np.arange(N) / N - 0.5 + + X, Y = np.meshgrid(x, y) + R = np.sqrt(X**2 + Y**2) + f0 = 10 + k = 75 + # aliased concentric circles + a = np.sin(np.pi * 2 * (f0 * R + k * R**2 / 2)) + + # stripes on lhs + a[:int(N/2), :][R[:int(N/2), :] < 0.4] = -1 + a[:int(N/2), :][R[:int(N/2), :] < 0.3] = 1 + aa[:, int(N/2):] = a[:, int(N/2):] + + # set some over/unders and NaNs + aa[20:50, 20:50] = np.NaN + aa[70:90, 70:90] = 1e6 + aa[70:90, 20:30] = -1e6 + aa[70:90, 195:215] = 1e6 + aa[20:30, 195:215] = -1e6 + + cmap = copy(plt.cm.RdBu_r) + cmap.set_over('yellow') + cmap.set_under('cyan') + + axs = axs.flatten() + # zoom in + axs[0].imshow(aa, interpolation='nearest', cmap=cmap, vmin=-1.2, vmax=1.2) + axs[0].set_xlim([N/2-25, N/2+25]) + axs[0].set_ylim([N/2+50, N/2-10]) + + # no anti-alias + axs[1].imshow(aa, interpolation='nearest', cmap=cmap, vmin=-1.2, vmax=1.2) + + # data antialias: Note no purples, and white in circle. Note + # that alternating red and blue stripes become white. + axs[2].imshow(aa, interpolation='antialiased', interpolation_stage='data', + cmap=cmap, vmin=-1.2, vmax=1.2) + + # rgba antialias: Note purples at boundary with circle. Note that + # alternating red and blue stripes become purple + axs[3].imshow(aa, interpolation='antialiased', interpolation_stage='rgba', + cmap=cmap, vmin=-1.2, vmax=1.2) + + +# We check for the warning with a draw() in the test, but we also need to +# filter the warning as it is emitted by the figure test decorator +@pytest.mark.filterwarnings(r'ignore:Data with more than .* ' + 'cannot be accurately displayed') +@pytest.mark.parametrize('origin', ['upper', 'lower']) +@pytest.mark.parametrize( + 'dim, size, msg', [['row', 2**23, r'2\*\*23 columns'], + ['col', 2**24, r'2\*\*24 rows']]) +@check_figures_equal(extensions=('png', )) +def test_large_image(fig_test, fig_ref, dim, size, msg, origin): + # Check that Matplotlib downsamples images that are too big for AGG + # See issue #19276. Currently the fix only works for png output but not + # pdf or svg output. + ax_test = fig_test.subplots() + ax_ref = fig_ref.subplots() + + array = np.zeros((1, size + 2)) + array[:, array.size // 2:] = 1 + if dim == 'col': + array = array.T + im = ax_test.imshow(array, vmin=0, vmax=1, + aspect='auto', extent=(0, 1, 0, 1), + interpolation='none', + origin=origin) + + with pytest.warns(UserWarning, + match=f'Data with more than {msg} cannot be ' + 'accurately displayed.'): + fig_test.canvas.draw() + + array = np.zeros((1, 2)) + array[:, 1] = 1 + if dim == 'col': + array = array.T + im = ax_ref.imshow(array, vmin=0, vmax=1, aspect='auto', + extent=(0, 1, 0, 1), + interpolation='none', + origin=origin) + + +@check_figures_equal(extensions=["png"]) +def test_str_norms(fig_test, fig_ref): + t = np.random.rand(10, 10) * .8 + .1 # between 0 and 1 + axts = fig_test.subplots(1, 5) + axts[0].imshow(t, norm="log") + axts[1].imshow(t, norm="log", vmin=.2) + axts[2].imshow(t, norm="symlog") + axts[3].imshow(t, norm="symlog", vmin=.3, vmax=.7) + axts[4].imshow(t, norm="logit", vmin=.3, vmax=.7) + axrs = fig_ref.subplots(1, 5) + axrs[0].imshow(t, norm=colors.LogNorm()) + axrs[1].imshow(t, norm=colors.LogNorm(vmin=.2)) + # same linthresh as SymmetricalLogScale's default. + axrs[2].imshow(t, norm=colors.SymLogNorm(linthresh=2)) + axrs[3].imshow(t, norm=colors.SymLogNorm(linthresh=2, vmin=.3, vmax=.7)) + axrs[4].imshow(t, norm="logit", clim=(.3, .7)) + + assert type(axts[0].images[0].norm) == colors.LogNorm # Exactly that class + with pytest.raises(ValueError): + axts[0].imshow(t, norm="foobar") diff --git a/lib/matplotlib/tests/test_legend.py b/lib/matplotlib/tests/test_legend.py index 782ef87853fe..a8d7fd107d8b 100644 --- a/lib/matplotlib/tests/test_legend.py +++ b/lib/matplotlib/tests/test_legend.py @@ -1,18 +1,24 @@ import collections import platform from unittest import mock +import warnings import numpy as np +from numpy.testing import assert_allclose import pytest -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import check_figures_equal, image_comparison +from matplotlib.testing._markers import needs_usetex import matplotlib.pyplot as plt import matplotlib as mpl +import matplotlib.patches as mpatches import matplotlib.transforms as mtransforms import matplotlib.collections as mcollections +import matplotlib.lines as mlines from matplotlib.legend_handler import HandlerTuple import matplotlib.legend as mlegend from matplotlib import rc_context +from matplotlib.font_manager import FontProperties def test_legend_ordereddict(): @@ -65,6 +71,60 @@ def test_legend_auto3(): ax.legend(loc='best') +def test_legend_auto4(): + """ + Check that the legend location with automatic placement is the same, + whatever the histogram type is. Related to issue #9580. + """ + # NB: barstacked is pointless with a single dataset. + fig, axs = plt.subplots(ncols=3, figsize=(6.4, 2.4)) + leg_bboxes = [] + for ax, ht in zip(axs.flat, ('bar', 'step', 'stepfilled')): + ax.set_title(ht) + # A high bar on the left but an even higher one on the right. + ax.hist([0] + 5*[9], bins=range(10), label="Legend", histtype=ht) + leg = ax.legend(loc="best") + fig.canvas.draw() + leg_bboxes.append( + leg.get_window_extent().transformed(ax.transAxes.inverted())) + + # The histogram type "bar" is assumed to be the correct reference. + assert_allclose(leg_bboxes[1].bounds, leg_bboxes[0].bounds) + assert_allclose(leg_bboxes[2].bounds, leg_bboxes[0].bounds) + + +def test_legend_auto5(): + """ + Check that the automatic placement handle a rather complex + case with non rectangular patch. Related to issue #9580. + """ + fig, axs = plt.subplots(ncols=2, figsize=(9.6, 4.8)) + + leg_bboxes = [] + for ax, loc in zip(axs.flat, ("center", "best")): + # An Ellipse patch at the top, a U-shaped Polygon patch at the + # bottom and a ring-like Wedge patch: the correct placement of + # the legend should be in the center. + for _patch in [ + mpatches.Ellipse( + xy=(0.5, 0.9), width=0.8, height=0.2, fc="C1"), + mpatches.Polygon(np.array([ + [0, 1], [0, 0], [1, 0], [1, 1], [0.9, 1.0], [0.9, 0.1], + [0.1, 0.1], [0.1, 1.0], [0.1, 1.0]]), fc="C1"), + mpatches.Wedge((0.5, 0.5), 0.5, 0, 360, width=0.05, fc="C0") + ]: + ax.add_patch(_patch) + + ax.plot([0.1, 0.9], [0.9, 0.9], label="A segment") # sthg to label + + leg = ax.legend(loc=loc) + fig.canvas.draw() + leg_bboxes.append( + leg.get_window_extent().transformed(ax.transAxes.inverted())) + + assert_allclose(leg_bboxes[1].bounds, leg_bboxes[0].bounds) + + @image_comparison(['legend_various_labels'], remove_text=True) def test_various_labels(): # tests all sorts of label types @@ -76,6 +136,20 @@ def test_various_labels(): ax.legend(numpoints=1, loc='best') +def test_legend_label_with_leading_underscore(): + """ + Test that artists with labels starting with an underscore are not added to + the legend, and that a warning is issued if one tries to add them + explicitly. + """ + fig, ax = plt.subplots() + line, = ax.plot([0, 1], label='_foo') + with pytest.warns(UserWarning, + match=r"starts with '_'.*excluded from the legend."): + legend = ax.legend(handles=[line]) + assert len(legend.legend_handles) == 0 + + @image_comparison(['legend_labels_first.png'], remove_text=True) def test_labels_first(): # test labels to left of markers @@ -126,12 +200,12 @@ def test_alpha_rcparam(): def test_fancy(): # using subplot triggers some offsetbox functionality untested elsewhere plt.subplot(121) - plt.scatter(np.arange(10), np.arange(10, 0, -1), label='XX\nXX') plt.plot([5] * 10, 'o--', label='XX') + plt.scatter(np.arange(10), np.arange(10, 0, -1), label='XX\nXX') plt.errorbar(np.arange(10), np.arange(10), xerr=0.5, yerr=0.5, label='XX') plt.legend(loc="center left", bbox_to_anchor=[1.0, 0.5], - ncol=2, shadow=True, title="My legend", numpoints=1) + ncols=2, shadow=True, title="My legend", numpoints=1) @image_comparison(['framealpha'], remove_text=True, @@ -173,7 +247,7 @@ def test_legend_expand(): ax.plot(x, x - 50, 'o', label='y=-1') l2 = ax.legend(loc='right', mode=mode) ax.add_artist(l2) - ax.legend(loc='lower left', mode=mode, ncol=2) + ax.legend(loc='lower left', mode=mode, ncols=2) @image_comparison(['hatching'], remove_text=True, style='default') @@ -220,6 +294,38 @@ def test_legend_remove(): assert ax.get_legend() is None +def test_reverse_legend_handles_and_labels(): + """Check that the legend handles and labels are reversed.""" + fig, ax = plt.subplots() + x = 1 + y = 1 + labels = ["First label", "Second label", "Third label"] + markers = ['.', ',', 'o'] + + ax.plot(x, y, markers[0], label=labels[0]) + ax.plot(x, y, markers[1], label=labels[1]) + ax.plot(x, y, markers[2], label=labels[2]) + leg = ax.legend(reverse=True) + actual_labels = [t.get_text() for t in leg.get_texts()] + actual_markers = [h.get_marker() for h in leg.legend_handles] + assert actual_labels == list(reversed(labels)) + assert actual_markers == list(reversed(markers)) + + +@check_figures_equal(extensions=["png"]) +def test_reverse_legend_display(fig_test, fig_ref): + """Check that the rendered legend entries are reversed""" + ax = fig_test.subplots() + ax.plot([1], 'ro', label="first") + ax.plot([2], 'bx', label="second") + ax.legend(reverse=True) + + ax = fig_ref.subplots() + ax.plot([2], 'bx', label="second") + ax.plot([1], 'ro', label="first") + ax.legend() + + class TestLegendFunction: # Tests the legend function on the Axes and pyplot. def test_legend_no_args(self): @@ -380,6 +486,47 @@ def test_warn_args_kwargs(self): "be discarded.") +def test_figure_legend_outside(): + todos = ['upper ' + pos for pos in ['left', 'center', 'right']] + todos += ['lower ' + pos for pos in ['left', 'center', 'right']] + todos += ['left ' + pos for pos in ['lower', 'center', 'upper']] + todos += ['right ' + pos for pos in ['lower', 'center', 'upper']] + + upperext = [20.347556, 27.722556, 790.583, 545.499] + lowerext = [20.347556, 71.056556, 790.583, 588.833] + leftext = [151.681556, 27.722556, 790.583, 588.833] + rightext = [20.347556, 27.722556, 659.249, 588.833] + axbb = [upperext, upperext, upperext, + lowerext, lowerext, lowerext, + leftext, leftext, leftext, + rightext, rightext, rightext] + + legbb = [[10., 555., 133., 590.], # upper left + [338.5, 555., 461.5, 590.], # upper center + [667, 555., 790., 590.], # upper right + [10., 10., 133., 45.], # lower left + [338.5, 10., 461.5, 45.], # lower center + [667., 10., 790., 45.], # lower right + [10., 10., 133., 45.], # left lower + [10., 282.5, 133., 317.5], # left center + [10., 555., 133., 590.], # left upper + [667, 10., 790., 45.], # right lower + [667., 282.5, 790., 317.5], # right center + [667., 555., 790., 590.]] # right upper + + for nn, todo in enumerate(todos): + print(todo) + fig, axs = plt.subplots(constrained_layout=True, dpi=100) + axs.plot(range(10), label='Boo1') + leg = fig.legend(loc='outside ' + todo) + fig.draw_without_rendering() + + assert_allclose(axs.get_window_extent().extents, + axbb[nn]) + assert_allclose(leg.get_window_extent().extents, + legbb[nn]) + + @image_comparison(['legend_stackplot.png']) def test_legend_stackplot(): """Test legend for PolyCollection using stackplot.""" @@ -476,11 +623,10 @@ def test_linecollection_scaled_dashes(): ax.add_collection(lc3) leg = ax.legend([lc1, lc2, lc3], ["line1", "line2", 'line 3']) - h1, h2, h3 = leg.legendHandles + h1, h2, h3 = leg.legend_handles for oh, lh in zip((lc1, lc2, lc3), (h1, h2, h3)): - assert oh.get_linestyles()[0][1] == lh._dashSeq - assert oh.get_linestyles()[0][0] == lh._dashOffset + assert oh.get_linestyles()[0] == lh._dash_pattern def test_handler_numpoints(): @@ -491,6 +637,22 @@ def test_handler_numpoints(): ax.legend(numpoints=0.5) +def test_text_nohandler_warning(): + """Test that Text artists with labels raise a warning""" + fig, ax = plt.subplots() + ax.text(x=0, y=0, s="text", label="label") + with pytest.warns(UserWarning) as record: + ax.legend() + assert len(record) == 1 + + # this should _not_ warn: + f, ax = plt.subplots() + ax.pcolormesh(np.random.uniform(0, 1, (10, 10))) + with warnings.catch_warnings(): + warnings.simplefilter("error") + ax.get_legend_handles_labels() + + def test_empty_bar_chart_with_legend(): """Test legend when bar chart is empty with a label.""" # related to issue #13003. Calling plt.legend() should not @@ -544,12 +706,61 @@ def test_window_extent_cached_renderer(): leg2.get_window_extent() -def test_legend_title_fontsize(): +def test_legend_title_fontprop_fontsize(): # test the title_fontsize kwarg + plt.plot(range(10)) + with pytest.raises(ValueError): + plt.legend(title='Aardvark', title_fontsize=22, + title_fontproperties={'family': 'serif', 'size': 22}) + + leg = plt.legend(title='Aardvark', title_fontproperties=FontProperties( + family='serif', size=22)) + assert leg.get_title().get_size() == 22 + + fig, axes = plt.subplots(2, 3, figsize=(10, 6)) + axes = axes.flat + axes[0].plot(range(10)) + leg0 = axes[0].legend(title='Aardvark', title_fontsize=22) + assert leg0.get_title().get_fontsize() == 22 + axes[1].plot(range(10)) + leg1 = axes[1].legend(title='Aardvark', + title_fontproperties={'family': 'serif', 'size': 22}) + assert leg1.get_title().get_fontsize() == 22 + axes[2].plot(range(10)) + mpl.rcParams['legend.title_fontsize'] = None + leg2 = axes[2].legend(title='Aardvark', + title_fontproperties={'family': 'serif'}) + assert leg2.get_title().get_fontsize() == mpl.rcParams['font.size'] + axes[3].plot(range(10)) + leg3 = axes[3].legend(title='Aardvark') + assert leg3.get_title().get_fontsize() == mpl.rcParams['font.size'] + axes[4].plot(range(10)) + mpl.rcParams['legend.title_fontsize'] = 20 + leg4 = axes[4].legend(title='Aardvark', + title_fontproperties={'family': 'serif'}) + assert leg4.get_title().get_fontsize() == 20 + axes[5].plot(range(10)) + leg5 = axes[5].legend(title='Aardvark') + assert leg5.get_title().get_fontsize() == 20 + + +@pytest.mark.parametrize('alignment', ('center', 'left', 'right')) +def test_legend_alignment(alignment): fig, ax = plt.subplots() - ax.plot(range(10)) - leg = ax.legend(title='Aardvark', title_fontsize=22) - assert leg.get_title().get_fontsize() == 22 + ax.plot(range(10), label='test') + leg = ax.legend(title="Aardvark", alignment=alignment) + assert leg.get_children()[0].align == alignment + assert leg.get_alignment() == alignment + + +@pytest.mark.parametrize('alignment', ('center', 'left', 'right')) +def test_legend_set_alignment(alignment): + fig, ax = plt.subplots() + ax.plot(range(10), label='test') + leg = ax.legend() + leg.set_alignment(alignment) + assert leg.get_children()[0].align == alignment + assert leg.get_alignment() == alignment @pytest.mark.parametrize('color', ('red', 'none', (.5, .5, .5))) @@ -589,6 +800,41 @@ def test_legend_labelcolor_linecolor(): assert mpl.colors.same_color(text.get_color(), color) +def test_legend_pathcollection_labelcolor_linecolor(): + # test the labelcolor for labelcolor='linecolor' on PathCollection + fig, ax = plt.subplots() + ax.scatter(np.arange(10), np.arange(10)*1, label='#1', c='r') + ax.scatter(np.arange(10), np.arange(10)*2, label='#2', c='g') + ax.scatter(np.arange(10), np.arange(10)*3, label='#3', c='b') + + leg = ax.legend(labelcolor='linecolor') + for text, color in zip(leg.get_texts(), ['r', 'g', 'b']): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_pathcollection_labelcolor_linecolor_iterable(): + # test the labelcolor for labelcolor='linecolor' on PathCollection + # with iterable colors + fig, ax = plt.subplots() + colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) + ax.scatter(np.arange(10), np.arange(10)*1, label='#1', c=colors) + + leg = ax.legend(labelcolor='linecolor') + text, = leg.get_texts() + assert mpl.colors.same_color(text.get_color(), 'black') + + +def test_legend_pathcollection_labelcolor_linecolor_cmap(): + # test the labelcolor for labelcolor='linecolor' on PathCollection + # with a colormap + fig, ax = plt.subplots() + ax.scatter(np.arange(10), np.arange(10), c=np.arange(10), label='#1') + + leg = ax.legend(labelcolor='linecolor') + text, = leg.get_texts() + assert mpl.colors.same_color(text.get_color(), 'black') + + def test_legend_labelcolor_markeredgecolor(): # test the labelcolor for labelcolor='markeredgecolor' fig, ax = plt.subplots() @@ -601,6 +847,49 @@ def test_legend_labelcolor_markeredgecolor(): assert mpl.colors.same_color(text.get_color(), color) +def test_legend_pathcollection_labelcolor_markeredgecolor(): + # test the labelcolor for labelcolor='markeredgecolor' on PathCollection + fig, ax = plt.subplots() + ax.scatter(np.arange(10), np.arange(10)*1, label='#1', edgecolor='r') + ax.scatter(np.arange(10), np.arange(10)*2, label='#2', edgecolor='g') + ax.scatter(np.arange(10), np.arange(10)*3, label='#3', edgecolor='b') + + leg = ax.legend(labelcolor='markeredgecolor') + for text, color in zip(leg.get_texts(), ['r', 'g', 'b']): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_pathcollection_labelcolor_markeredgecolor_iterable(): + # test the labelcolor for labelcolor='markeredgecolor' on PathCollection + # with iterable colors + fig, ax = plt.subplots() + colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) + ax.scatter(np.arange(10), np.arange(10)*1, label='#1', edgecolor=colors) + + leg = ax.legend(labelcolor='markeredgecolor') + for text, color in zip(leg.get_texts(), ['k']): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_pathcollection_labelcolor_markeredgecolor_cmap(): + # test the labelcolor for labelcolor='markeredgecolor' on PathCollection + # with a colormap + fig, ax = plt.subplots() + edgecolors = mpl.cm.viridis(np.random.rand(10)) + ax.scatter( + np.arange(10), + np.arange(10), + label='#1', + c=np.arange(10), + edgecolor=edgecolors, + cmap="Reds" + ) + + leg = ax.legend(labelcolor='markeredgecolor') + for text, color in zip(leg.get_texts(), ['k']): + assert mpl.colors.same_color(text.get_color(), color) + + def test_legend_labelcolor_markerfacecolor(): # test the labelcolor for labelcolor='markerfacecolor' fig, ax = plt.subplots() @@ -613,6 +902,127 @@ def test_legend_labelcolor_markerfacecolor(): assert mpl.colors.same_color(text.get_color(), color) +def test_legend_pathcollection_labelcolor_markerfacecolor(): + # test the labelcolor for labelcolor='markerfacecolor' on PathCollection + fig, ax = plt.subplots() + ax.scatter(np.arange(10), np.arange(10)*1, label='#1', facecolor='r') + ax.scatter(np.arange(10), np.arange(10)*2, label='#2', facecolor='g') + ax.scatter(np.arange(10), np.arange(10)*3, label='#3', facecolor='b') + + leg = ax.legend(labelcolor='markerfacecolor') + for text, color in zip(leg.get_texts(), ['r', 'g', 'b']): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_pathcollection_labelcolor_markerfacecolor_iterable(): + # test the labelcolor for labelcolor='markerfacecolor' on PathCollection + # with iterable colors + fig, ax = plt.subplots() + colors = np.random.default_rng().choice(['r', 'g', 'b'], 10) + ax.scatter(np.arange(10), np.arange(10)*1, label='#1', facecolor=colors) + + leg = ax.legend(labelcolor='markerfacecolor') + for text, color in zip(leg.get_texts(), ['k']): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_pathcollection_labelcolor_markfacecolor_cmap(): + # test the labelcolor for labelcolor='markerfacecolor' on PathCollection + # with colormaps + fig, ax = plt.subplots() + facecolors = mpl.cm.viridis(np.random.rand(10)) + ax.scatter( + np.arange(10), + np.arange(10), + label='#1', + c=np.arange(10), + facecolor=facecolors + ) + + leg = ax.legend(labelcolor='markerfacecolor') + for text, color in zip(leg.get_texts(), ['k']): + assert mpl.colors.same_color(text.get_color(), color) + + +@pytest.mark.parametrize('color', ('red', 'none', (.5, .5, .5))) +def test_legend_labelcolor_rcparam_single(color): + # test the rcParams legend.labelcolor for a single color + fig, ax = plt.subplots() + ax.plot(np.arange(10), np.arange(10)*1, label='#1') + ax.plot(np.arange(10), np.arange(10)*2, label='#2') + ax.plot(np.arange(10), np.arange(10)*3, label='#3') + + mpl.rcParams['legend.labelcolor'] = color + leg = ax.legend() + for text in leg.get_texts(): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_labelcolor_rcparam_linecolor(): + # test the rcParams legend.labelcolor for a linecolor + fig, ax = plt.subplots() + ax.plot(np.arange(10), np.arange(10)*1, label='#1', color='r') + ax.plot(np.arange(10), np.arange(10)*2, label='#2', color='g') + ax.plot(np.arange(10), np.arange(10)*3, label='#3', color='b') + + mpl.rcParams['legend.labelcolor'] = 'linecolor' + leg = ax.legend() + for text, color in zip(leg.get_texts(), ['r', 'g', 'b']): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_labelcolor_rcparam_markeredgecolor(): + # test the labelcolor for labelcolor='markeredgecolor' + fig, ax = plt.subplots() + ax.plot(np.arange(10), np.arange(10)*1, label='#1', markeredgecolor='r') + ax.plot(np.arange(10), np.arange(10)*2, label='#2', markeredgecolor='g') + ax.plot(np.arange(10), np.arange(10)*3, label='#3', markeredgecolor='b') + + mpl.rcParams['legend.labelcolor'] = 'markeredgecolor' + leg = ax.legend() + for text, color in zip(leg.get_texts(), ['r', 'g', 'b']): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_labelcolor_rcparam_markeredgecolor_short(): + # test the labelcolor for labelcolor='markeredgecolor' + fig, ax = plt.subplots() + ax.plot(np.arange(10), np.arange(10)*1, label='#1', markeredgecolor='r') + ax.plot(np.arange(10), np.arange(10)*2, label='#2', markeredgecolor='g') + ax.plot(np.arange(10), np.arange(10)*3, label='#3', markeredgecolor='b') + + mpl.rcParams['legend.labelcolor'] = 'mec' + leg = ax.legend() + for text, color in zip(leg.get_texts(), ['r', 'g', 'b']): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_labelcolor_rcparam_markerfacecolor(): + # test the labelcolor for labelcolor='markeredgecolor' + fig, ax = plt.subplots() + ax.plot(np.arange(10), np.arange(10)*1, label='#1', markerfacecolor='r') + ax.plot(np.arange(10), np.arange(10)*2, label='#2', markerfacecolor='g') + ax.plot(np.arange(10), np.arange(10)*3, label='#3', markerfacecolor='b') + + mpl.rcParams['legend.labelcolor'] = 'markerfacecolor' + leg = ax.legend() + for text, color in zip(leg.get_texts(), ['r', 'g', 'b']): + assert mpl.colors.same_color(text.get_color(), color) + + +def test_legend_labelcolor_rcparam_markerfacecolor_short(): + # test the labelcolor for labelcolor='markeredgecolor' + fig, ax = plt.subplots() + ax.plot(np.arange(10), np.arange(10)*1, label='#1', markerfacecolor='r') + ax.plot(np.arange(10), np.arange(10)*2, label='#2', markerfacecolor='g') + ax.plot(np.arange(10), np.arange(10)*3, label='#3', markerfacecolor='b') + + mpl.rcParams['legend.labelcolor'] = 'mfc' + leg = ax.legend() + for text, color in zip(leg.get_texts(), ['r', 'g', 'b']): + assert mpl.colors.same_color(text.get_color(), color) + + def test_get_set_draggable(): legend = plt.legend() assert not legend.get_draggable() @@ -622,18 +1032,24 @@ def test_get_set_draggable(): assert not legend.get_draggable() +@pytest.mark.parametrize('draggable', (True, False)) +def test_legend_draggable(draggable): + fig, ax = plt.subplots() + ax.plot(range(10), label='shabnams') + leg = ax.legend(draggable=draggable) + assert leg.get_draggable() is draggable + + def test_alpha_handles(): x, n, hh = plt.hist([1, 2, 3], alpha=0.25, label='data', color='red') legend = plt.legend() - for lh in legend.legendHandles: + for lh in legend.legend_handles: lh.set_alpha(1.0) assert lh.get_facecolor()[:-1] == hh[1].get_facecolor()[:-1] assert lh.get_edgecolor()[:-1] == hh[1].get_edgecolor()[:-1] -@pytest.mark.skipif( - not mpl.checkdep_usetex(True), - reason="This test needs a TeX installation") +@needs_usetex def test_usetex_no_warn(caplog): mpl.rcParams['font.family'] = 'serif' mpl.rcParams['font.serif'] = 'Computer Modern' @@ -724,7 +1140,7 @@ def test_plot_single_input_multiple_label(label_array): def test_plot_multiple_label_incorrect_length_exception(): - # check that excepton is raised if multiple labels + # check that exception is raised if multiple labels # are given, but number of on labels != number of lines with pytest.raises(ValueError): x = [1, 2, 3] @@ -742,3 +1158,64 @@ def test_legend_face_edgecolor(): ax.fill_between([0, 1, 2], [1, 2, 3], [2, 3, 4], facecolor='r', edgecolor='face', label='Fill') ax.legend() + + +def test_legend_text_axes(): + fig, ax = plt.subplots() + ax.plot([1, 2], [3, 4], label='line') + leg = ax.legend() + + assert leg.axes is ax + assert leg.get_texts()[0].axes is ax + + +def test_handlerline2d(): + # Test marker consistency for monolithic Line2D legend handler (#11357). + fig, ax = plt.subplots() + ax.scatter([0, 1], [0, 1], marker="v") + handles = [mlines.Line2D([0], [0], marker="v")] + leg = ax.legend(handles, ["Aardvark"], numpoints=1) + assert handles[0].get_marker() == leg.legend_handles[0].get_marker() + + +def test_subfigure_legend(): + # Test that legend can be added to subfigure (#20723) + subfig = plt.figure().subfigures() + ax = subfig.subplots() + ax.plot([0, 1], [0, 1], label="line") + leg = subfig.legend() + assert leg.figure is subfig + + +def test_setting_alpha_keeps_polycollection_color(): + pc = plt.fill_between([0, 1], [2, 3], color='#123456', label='label') + patch = plt.legend().get_patches()[0] + patch.set_alpha(0.5) + assert patch.get_facecolor()[:3] == tuple(pc.get_facecolor()[0][:3]) + assert patch.get_edgecolor()[:3] == tuple(pc.get_edgecolor()[0][:3]) + + +def test_legend_markers_from_line2d(): + # Test that markers can be copied for legend lines (#17960) + _markers = ['.', '*', 'v'] + fig, ax = plt.subplots() + lines = [mlines.Line2D([0], [0], ls='None', marker=mark) + for mark in _markers] + labels = ["foo", "bar", "xyzzy"] + markers = [line.get_marker() for line in lines] + legend = ax.legend(lines, labels) + + new_markers = [line.get_marker() for line in legend.get_lines()] + new_labels = [text.get_text() for text in legend.get_texts()] + + assert markers == new_markers == _markers + assert labels == new_labels + + +@check_figures_equal() +def test_ncol_ncols(fig_test, fig_ref): + # Test that both ncol and ncols work + strings = ["a", "b", "c", "d", "e", "f"] + ncols = 3 + fig_test.legend(strings, ncol=ncols) + fig_ref.legend(strings, ncols=ncols) diff --git a/lib/matplotlib/tests/test_lines.py b/lib/matplotlib/tests/test_lines.py index def26456bb36..b75d3c01b28e 100644 --- a/lib/matplotlib/tests/test_lines.py +++ b/lib/matplotlib/tests/test_lines.py @@ -3,6 +3,7 @@ """ import itertools +import platform import timeit from types import SimpleNamespace @@ -12,11 +13,14 @@ import pytest import matplotlib +import matplotlib as mpl import matplotlib.lines as mlines from matplotlib.markers import MarkerStyle from matplotlib.path import Path import matplotlib.pyplot as plt +import matplotlib.transforms as mtransforms from matplotlib.testing.decorators import image_comparison, check_figures_equal +from matplotlib._api.deprecation import MatplotlibDeprecationWarning def test_segment_hits(): @@ -81,6 +85,22 @@ def test_set_line_coll_dash(): ax.contour(np.random.randn(20, 30), linestyles=[(0, (3, 3))]) +def test_invalid_line_data(): + with pytest.raises(RuntimeError, match='xdata must be'): + mlines.Line2D(0, []) + with pytest.raises(RuntimeError, match='ydata must be'): + mlines.Line2D([], 1) + + line = mlines.Line2D([], []) + # when deprecation cycle is completed + # with pytest.raises(RuntimeError, match='x must be'): + with pytest.warns(MatplotlibDeprecationWarning): + line.set_xdata(0) + # with pytest.raises(RuntimeError, match='y must be'): + with pytest.warns(MatplotlibDeprecationWarning): + line.set_ydata(0) + + @image_comparison(['line_dashes'], remove_text=True) def test_line_dashes(): fig, ax = plt.subplots() @@ -107,7 +127,9 @@ def test_valid_colors(): def test_linestyle_variants(): fig, ax = plt.subplots() for ls in ["-", "solid", "--", "dashed", - "-.", "dashdot", ":", "dotted"]: + "-.", "dashdot", ":", "dotted", + (0, None), (0, ()), (0, []), # gh-22930 + ]: ax.plot(range(10), linestyle=ls) fig.canvas.draw() @@ -131,6 +153,17 @@ def test_drawstyle_variants(): ax.set(xlim=(0, 2), ylim=(0, 2)) +@check_figures_equal(extensions=('png',)) +def test_no_subslice_with_transform(fig_ref, fig_test): + ax = fig_ref.add_subplot() + x = np.arange(2000) + ax.plot(x + 2000, x) + + ax = fig_test.add_subplot() + t = mtransforms.Affine2D().translate(2000.0, 0.0) + ax.plot(x, x, transform=t+ax.transData) + + def test_valid_drawstyles(): line = mlines.Line2D([], []) with pytest.raises(ValueError): @@ -150,7 +183,9 @@ def test_set_drawstyle(): assert len(line.get_path().vertices) == len(x) -@image_comparison(['line_collection_dashes'], remove_text=True, style='mpl20') +@image_comparison( + ['line_collection_dashes'], remove_text=True, style='mpl20', + tol=0.65 if platform.machine() in ('aarch64', 'ppc64le', 's390x') else 0) def test_set_line_coll_dash_image(): fig, ax = plt.subplots() np.random.seed(0) @@ -167,7 +202,11 @@ def test_marker_fill_styles(): x = np.array([0, 9]) fig, ax = plt.subplots() - for j, marker in enumerate(mlines.Line2D.filled_markers): + # This hard-coded list of markers correspond to an earlier iteration of + # MarkerStyle.filled_markers; the value of that attribute has changed but + # we kept the old value here to not regenerate the baseline image. + # Replace with mlines.Line2D.filled_markers when the image is regenerated. + for j, marker in enumerate("ov^<>8sp*hHDdPX"): for i, fs in enumerate(mlines.Line2D.fillStyles): color = next(colors) ax.plot(j * 10 + x, y + i + .5 * (j % 2), @@ -217,28 +256,57 @@ def test_step_markers(fig_test, fig_ref): fig_ref.subplots().plot([0, 0, 1], [0, 1, 1], "-o", markevery=[0, 2]) +@pytest.mark.parametrize("parent", ["figure", "axes"]) @check_figures_equal(extensions=('png',)) -def test_markevery(fig_test, fig_ref): +def test_markevery(fig_test, fig_ref, parent): np.random.seed(42) - t = np.linspace(0, 3, 14) - y = np.random.rand(len(t)) + x = np.linspace(0, 1, 14) + y = np.random.rand(len(x)) - casesA = [None, 4, (2, 5), [1, 5, 11], - [0, -1], slice(5, 10, 2), 0.3, (0.3, 0.4), - np.arange(len(t))[y > 0.5]] - casesB = ["11111111111111", "10001000100010", "00100001000010", - "01000100000100", "10000000000001", "00000101010000", - "11011011011110", "01010011011101", "01110001110110"] + cases_test = [None, 4, (2, 5), [1, 5, 11], + [0, -1], slice(5, 10, 2), + np.arange(len(x))[y > 0.5], + 0.3, (0.3, 0.4)] + cases_ref = ["11111111111111", "10001000100010", "00100001000010", + "01000100000100", "10000000000001", "00000101010000", + "01110001110110", "11011011011110", "01010011011101"] - axsA = fig_ref.subplots(3, 3) - axsB = fig_test.subplots(3, 3) + if parent == "figure": + # float markevery ("relative to axes size") is not supported. + cases_test = cases_test[:-2] + cases_ref = cases_ref[:-2] - for ax, case in zip(axsA.flat, casesA): - ax.plot(t, y, "-gD", markevery=case) + def add_test(x, y, *, markevery): + fig_test.add_artist( + mlines.Line2D(x, y, marker="o", markevery=markevery)) - for ax, case in zip(axsB.flat, casesB): + def add_ref(x, y, *, markevery): + fig_ref.add_artist( + mlines.Line2D(x, y, marker="o", markevery=markevery)) + + elif parent == "axes": + axs_test = iter(fig_test.subplots(3, 3).flat) + axs_ref = iter(fig_ref.subplots(3, 3).flat) + + def add_test(x, y, *, markevery): + next(axs_test).plot(x, y, "-gD", markevery=markevery) + + def add_ref(x, y, *, markevery): + next(axs_ref).plot(x, y, "-gD", markevery=markevery) + + for case in cases_test: + add_test(x, y, markevery=case) + + for case in cases_ref: me = np.array(list(case)).astype(int).astype(bool) - ax.plot(t, y, "-gD", markevery=me) + add_ref(x, y, markevery=me) + + +def test_markevery_figure_line_unsupported_relsize(): + fig = plt.figure() + fig.add_artist(mlines.Line2D([0, 1], [0, 1], marker="o", markevery=.5)) + with pytest.raises(ValueError): + fig.canvas.draw() def test_marker_as_markerstyle(): @@ -253,7 +321,7 @@ def test_marker_as_markerstyle(): line.set_marker(MarkerStyle("o")) fig.canvas.draw() # test Path roundtrip - triangle1 = Path([[-1., -1.], [1., -1.], [0., 2.], [0., 0.]], closed=True) + triangle1 = Path._create_closed([[-1, -1], [1, -1], [0, 2]]) line2, = ax.plot([1, 3, 2], marker=MarkerStyle(triangle1), ms=22) line3, = ax.plot([0, 2, 1], marker=triangle1, ms=22) @@ -261,6 +329,17 @@ def test_marker_as_markerstyle(): assert_array_equal(line3.get_marker().vertices, triangle1.vertices) +@image_comparison(['striped_line.png'], remove_text=True, style='mpl20') +def test_striped_lines(): + rng = np.random.default_rng(19680801) + _, ax = plt.subplots() + ax.plot(rng.uniform(size=12), color='orange', gapcolor='blue', + linestyle='--', lw=5, label=' ') + ax.plot(rng.uniform(size=12), color='red', gapcolor='black', + linestyle=(0, (2, 5, 4, 2)), lw=5, label=' ', alpha=0.5) + ax.legend(handlelength=5) + + @check_figures_equal() def test_odd_dashes(fig_test, fig_ref): fig_test.add_subplot().plot([1, 2], dashes=[1, 2, 3]) @@ -291,3 +370,39 @@ def test_picking(): found, indices = l2.contains(mouse_event) assert found assert_array_equal(indices['ind'], [0]) + + +@check_figures_equal() +def test_input_copy(fig_test, fig_ref): + + t = np.arange(0, 6, 2) + l, = fig_test.add_subplot().plot(t, t, ".-") + t[:] = range(3) + # Trigger cache invalidation + l.set_drawstyle("steps") + fig_ref.add_subplot().plot([0, 2, 4], [0, 2, 4], ".-", drawstyle="steps") + + +@check_figures_equal(extensions=["png"]) +def test_markevery_prop_cycle(fig_test, fig_ref): + """Test that we can set markevery prop_cycle.""" + cases = [None, 8, (30, 8), [16, 24, 30], [0, -1], + slice(100, 200, 3), 0.1, 0.3, 1.5, + (0.0, 0.1), (0.45, 0.1)] + + cmap = mpl.colormaps['jet'] + colors = cmap(np.linspace(0.2, 0.8, len(cases))) + + x = np.linspace(-1, 1) + y = 5 * x**2 + + axs = fig_ref.add_subplot() + for i, markevery in enumerate(cases): + axs.plot(y - i, 'o-', markevery=markevery, color=colors[i]) + + matplotlib.rcParams['axes.prop_cycle'] = cycler(markevery=cases, + color=colors) + + ax = fig_test.add_subplot() + for i, _ in enumerate(cases): + ax.plot(y - i, 'o-') diff --git a/lib/matplotlib/tests/test_marker.py b/lib/matplotlib/tests/test_marker.py index f85d4ff467ef..f50f45bbd4ee 100644 --- a/lib/matplotlib/tests/test_marker.py +++ b/lib/matplotlib/tests/test_marker.py @@ -1,8 +1,10 @@ import numpy as np import matplotlib.pyplot as plt from matplotlib import markers +from matplotlib._api.deprecation import MatplotlibDeprecationWarning from matplotlib.path import Path from matplotlib.testing.decorators import check_figures_equal +from matplotlib.transforms import Affine2D import pytest @@ -18,7 +20,6 @@ def test_marker_fillstyle(): 'x', '', 'None', - None, r'$\frac{1}{2}$', "$\u266B$", 1, @@ -32,7 +33,6 @@ def test_marker_fillstyle(): (5, 0, 10), # a pentagon, rotated by 10 degrees (7, 1, 10), # a 7-pointed star, rotated by 10 degrees (5, 2, 10), # asterisk, rotated by 10 degrees - markers.MarkerStyle(), markers.MarkerStyle('o'), ]) def test_markers_valid(marker): @@ -40,6 +40,15 @@ def test_markers_valid(marker): markers.MarkerStyle(marker) +def test_deprecated_marker(): + with pytest.warns(MatplotlibDeprecationWarning): + ms = markers.MarkerStyle() + markers.MarkerStyle(ms) # No warning on copy. + with pytest.warns(MatplotlibDeprecationWarning): + ms = markers.MarkerStyle(None) + markers.MarkerStyle(ms) # No warning on copy. + + @pytest.mark.parametrize('marker', [ 'square', # arbitrary string np.array([[-0.5, 0, 1, 2, 3]]), # 1D array @@ -196,3 +205,99 @@ def test_marker_clipping(fig_ref, fig_test): ax_test.set(xlim=(-0.5, ncol), ylim=(-0.5, 2 * nrow)) ax_ref.axis('off') ax_test.axis('off') + + +def test_marker_init_transforms(): + """Test that initializing marker with transform is a simple addition.""" + marker = markers.MarkerStyle("o") + t = Affine2D().translate(1, 1) + t_marker = markers.MarkerStyle("o", transform=t) + assert marker.get_transform() + t == t_marker.get_transform() + + +def test_marker_init_joinstyle(): + marker = markers.MarkerStyle("*") + jstl = markers.JoinStyle.round + styled_marker = markers.MarkerStyle("*", joinstyle=jstl) + assert styled_marker.get_joinstyle() == jstl + assert marker.get_joinstyle() != jstl + + +def test_marker_init_captyle(): + marker = markers.MarkerStyle("*") + capstl = markers.CapStyle.round + styled_marker = markers.MarkerStyle("*", capstyle=capstl) + assert styled_marker.get_capstyle() == capstl + assert marker.get_capstyle() != capstl + + +@pytest.mark.parametrize("marker,transform,expected", [ + (markers.MarkerStyle("o"), Affine2D().translate(1, 1), + Affine2D().translate(1, 1)), + (markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)), + Affine2D().translate(1, 1), Affine2D().translate(2, 2)), + (markers.MarkerStyle("$|||$", transform=Affine2D().translate(1, 1)), + Affine2D().translate(1, 1), Affine2D().translate(2, 2)), + (markers.MarkerStyle( + markers.TICKLEFT, transform=Affine2D().translate(1, 1)), + Affine2D().translate(1, 1), Affine2D().translate(2, 2)), +]) +def test_marker_transformed(marker, transform, expected): + new_marker = marker.transformed(transform) + assert new_marker is not marker + assert new_marker.get_user_transform() == expected + assert marker._user_transform is not new_marker._user_transform + + +def test_marker_rotated_invalid(): + marker = markers.MarkerStyle("o") + with pytest.raises(ValueError): + new_marker = marker.rotated() + with pytest.raises(ValueError): + new_marker = marker.rotated(deg=10, rad=10) + + +@pytest.mark.parametrize("marker,deg,rad,expected", [ + (markers.MarkerStyle("o"), 10, None, Affine2D().rotate_deg(10)), + (markers.MarkerStyle("o"), None, 0.01, Affine2D().rotate(0.01)), + (markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)), + 10, None, Affine2D().translate(1, 1).rotate_deg(10)), + (markers.MarkerStyle("o", transform=Affine2D().translate(1, 1)), + None, 0.01, Affine2D().translate(1, 1).rotate(0.01)), + (markers.MarkerStyle("$|||$", transform=Affine2D().translate(1, 1)), + 10, None, Affine2D().translate(1, 1).rotate_deg(10)), + (markers.MarkerStyle( + markers.TICKLEFT, transform=Affine2D().translate(1, 1)), + 10, None, Affine2D().translate(1, 1).rotate_deg(10)), +]) +def test_marker_rotated(marker, deg, rad, expected): + new_marker = marker.rotated(deg=deg, rad=rad) + assert new_marker is not marker + assert new_marker.get_user_transform() == expected + assert marker._user_transform is not new_marker._user_transform + + +def test_marker_scaled(): + marker = markers.MarkerStyle("1") + new_marker = marker.scaled(2) + assert new_marker is not marker + assert new_marker.get_user_transform() == Affine2D().scale(2) + assert marker._user_transform is not new_marker._user_transform + + new_marker = marker.scaled(2, 3) + assert new_marker is not marker + assert new_marker.get_user_transform() == Affine2D().scale(2, 3) + assert marker._user_transform is not new_marker._user_transform + + marker = markers.MarkerStyle("1", transform=Affine2D().translate(1, 1)) + new_marker = marker.scaled(2) + assert new_marker is not marker + expected = Affine2D().translate(1, 1).scale(2) + assert new_marker.get_user_transform() == expected + assert marker._user_transform is not new_marker._user_transform + + +def test_alt_transform(): + m1 = markers.MarkerStyle("o", "left") + m2 = markers.MarkerStyle("o", "left", Affine2D().rotate_deg(90)) + assert m1.get_alt_transform().rotate_deg(90) == m2.get_alt_transform() diff --git a/lib/matplotlib/tests/test_mathtext.py b/lib/matplotlib/tests/test_mathtext.py index b5fd906d2f9c..2ee3e914d5f6 100644 --- a/lib/matplotlib/tests/test_mathtext.py +++ b/lib/matplotlib/tests/test_mathtext.py @@ -1,6 +1,9 @@ import io -import os +from pathlib import Path +import platform import re +import shlex +from xml.etree import ElementTree as ET import numpy as np import pytest @@ -8,13 +11,13 @@ import matplotlib as mpl from matplotlib.testing.decorators import check_figures_equal, image_comparison import matplotlib.pyplot as plt -from matplotlib import _api, mathtext +from matplotlib import mathtext, _mathtext # If test is removed, use None as placeholder math_tests = [ r'$a+b+\dot s+\dot{s}+\ldots$', - r'$x \doteq y$', + r'$x\hspace{-0.2}\doteq\hspace{-0.2}y$', r'\$100.00 $\alpha \_$', r'$\frac{\$100.00}{y}$', r'$x y$', @@ -45,7 +48,7 @@ r"$\arccos((x^i))$", r"$\gamma = \frac{x=\frac{6}{8}}{y} \delta$", r'$\limsup_{x\to\infty}$', - r'$\oint^\infty_0$', + None, r"$f'\quad f'''(x)\quad ''/\mathrm{yr}$", r'$\frac{x_2888}{y}$', r"$\sqrt[3]{\frac{X_2}{Y}}=5$", @@ -58,8 +61,8 @@ '$\\Gamma \\Delta \\Theta \\Lambda \\Xi \\Pi \\Sigma \\Upsilon \\Phi \\Psi \\Omega$', '$\\alpha \\beta \\gamma \\delta \\epsilon \\zeta \\eta \\theta \\iota \\lambda \\mu \\nu \\xi \\pi \\kappa \\rho \\sigma \\tau \\upsilon \\phi \\chi \\psi$', - # The examples prefixed by 'mmltt' are from the MathML torture test here: - # https://developer.mozilla.org/en-US/docs/Mozilla/MathML_Project/MathML_Torture_Test + # The following examples are from the MathML torture test here: + # https://www-archive.mozilla.org/projects/mathml/demo/texvsmml.xhtml r'${x}^{2}{y}^{2}$', r'${}_{2}F_{3}$', r'$\frac{x+{y}^{2}}{k+1}$', @@ -98,28 +101,35 @@ r"$? ! &$", # github issue #466 None, None, - r"$\left\Vert a \right\Vert \left\vert b \right\vert \left| a \right| \left\| b\right\| \Vert a \Vert \vert b \vert$", + r"$\left\Vert \frac{a}{b} \right\Vert \left\vert \frac{a}{b} \right\vert \left\| \frac{a}{b}\right\| \left| \frac{a}{b} \right| \Vert a \Vert \vert b \vert \| a \| | b |$", r'$\mathring{A} \AA$', r'$M \, M \thinspace M \/ M \> M \: M \; M \ M \enspace M \quad M \qquad M \! M$', - r'$\Cup$ $\Cap$ $\leftharpoonup$ $\barwedge$ $\rightharpoonup$', - r'$\dotplus$ $\doteq$ $\doteqdot$ $\ddots$', + r'$\Cap$ $\Cup$ $\leftharpoonup$ $\barwedge$ $\rightharpoonup$', + r'$\hspace{-0.2}\dotplus\hspace{-0.2}$ $\hspace{-0.2}\doteq\hspace{-0.2}$ $\hspace{-0.2}\doteqdot\hspace{-0.2}$ $\ddots$', r'$xyz^kx_kx^py^{p-2} d_i^jb_jc_kd x^j_i E^0 E^0_u$', # github issue #4873 r'${xyz}^k{x}_{k}{x}^{p}{y}^{p-2} {d}_{i}^{j}{b}_{j}{c}_{k}{d} {x}^{j}_{i}{E}^{0}{E}^0_u$', r'${\int}_x^x x\oint_x^x x\int_{X}^{X}x\int_x x \int^x x \int_{x} x\int^{x}{\int}_{x} x{\int}^{x}_{x}x$', r'testing$^{123}$', - ' '.join('$\\' + p + '$' for p in sorted(mathtext.Parser._accentprefixed)), + None, r'$6-2$; $-2$; $ -2$; ${-2}$; ${ -2}$; $20^{+3}_{-2}$', r'$\overline{\omega}^x \frac{1}{2}_0^x$', # github issue #5444 r'$,$ $.$ $1{,}234{, }567{ , }890$ and $1,234,567,890$', # github issue 5799 r'$\left(X\right)_{a}^{b}$', # github issue 7615 r'$\dfrac{\$100.00}{y}$', # github issue #1888 ] -# 'Lightweight' tests test only a single fontset (dejavusans, which is the +# 'svgastext' tests switch svg output to embed text as text (rather than as +# paths). +svgastext_math_tests = [ + r'$-$-', +] +# 'lightweight' tests test only a single fontset (dejavusans, which is the # default) and only png outputs, in order to minimize the size of baseline # images. lightweight_math_tests = [ r'$\sqrt[ab]{123}$', # github issue #8665 r'$x \overset{f}{\rightarrow} \overset{f}{x} \underset{xx}{ff} \overset{xx}{ff} \underset{f}{x} \underset{f}{\leftarrow} x$', # github issue #18241 + r'$\sum x\quad\sum^nx\quad\sum_nx\quad\sum_n^nx\quad\prod x\quad\prod^nx\quad\prod_nx\quad\prod_n^nx$', # GitHub issue 18085 + r'$1.$ $2.$ $19680801.$ $a.$ $b.$ $mpl.$', ] digits = "0123456789" @@ -189,7 +199,8 @@ def baseline_images(request, fontset, index, text): @pytest.mark.parametrize( 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif']) @pytest.mark.parametrize('baseline_images', ['mathtext'], indirect=True) -@image_comparison(baseline_images=None) +@image_comparison(baseline_images=None, + tol=0.011 if platform.machine() in ('ppc64le', 's390x') else 0) def test_mathtext_rendering(baseline_images, fontset, index, text): mpl.rcParams['mathtext.fontset'] = fontset fig = plt.figure(figsize=(5.25, 0.75)) @@ -197,6 +208,23 @@ def test_mathtext_rendering(baseline_images, fontset, index, text): horizontalalignment='center', verticalalignment='center') +@pytest.mark.parametrize('index, text', enumerate(svgastext_math_tests), + ids=range(len(svgastext_math_tests))) +@pytest.mark.parametrize('fontset', ['cm', 'dejavusans']) +@pytest.mark.parametrize('baseline_images', ['mathtext0'], indirect=True) +@image_comparison( + baseline_images=None, extensions=['svg'], + savefig_kwarg={'metadata': { # Minimize image size. + 'Creator': None, 'Date': None, 'Format': None, 'Type': None}}) +def test_mathtext_rendering_svgastext(baseline_images, fontset, index, text): + mpl.rcParams['mathtext.fontset'] = fontset + mpl.rcParams['svg.fonttype'] = 'none' # Minimize image size. + fig = plt.figure(figsize=(5.25, 0.75)) + fig.patch.set(visible=False) # Minimize image size. + fig.text(0.5, 0.5, text, + horizontalalignment='center', verticalalignment='center') + + @pytest.mark.parametrize('index, text', enumerate(lightweight_math_tests), ids=range(len(lightweight_math_tests))) @pytest.mark.parametrize('fontset', ['dejavusans']) @@ -213,7 +241,8 @@ def test_mathtext_rendering_lightweight(baseline_images, fontset, index, text): @pytest.mark.parametrize( 'fontset', ['cm', 'stix', 'stixsans', 'dejavusans', 'dejavuserif']) @pytest.mark.parametrize('baseline_images', ['mathfont'], indirect=True) -@image_comparison(baseline_images=None, extensions=['png']) +@image_comparison(baseline_images=None, extensions=['png'], + tol=0.011 if platform.machine() in ('ppc64le', 's390x') else 0) def test_mathfont_rendering(baseline_images, fontset, index, text): mpl.rcParams['mathtext.fontset'] = fontset fig = plt.figure(figsize=(5.25, 0.75)) @@ -221,6 +250,19 @@ def test_mathfont_rendering(baseline_images, fontset, index, text): horizontalalignment='center', verticalalignment='center') +@check_figures_equal(extensions=["png"]) +def test_short_long_accents(fig_test, fig_ref): + acc_map = _mathtext.Parser._accent_map + short_accs = [s for s in acc_map if len(s) == 1] + corresponding_long_accs = [] + for s in short_accs: + l, = [l for l in acc_map if len(l) > 1 and acc_map[l] == acc_map[s]] + corresponding_long_accs.append(l) + fig_test.text(0, .5, "$" + "".join(rf"\{s}a" for s in short_accs) + "$") + fig_ref.text( + 0, .5, "$" + "".join(fr"\{l} a" for l in corresponding_long_accs) + "$") + + def test_fontinfo(): fontpath = mpl.font_manager.findfont("DejaVu Sans") font = mpl.ft2font.FT2Font(fontpath) @@ -231,8 +273,10 @@ def test_fontinfo(): @pytest.mark.parametrize( 'math, msg', [ - (r'$\hspace{}$', r'Expected \hspace{n}'), - (r'$\hspace{foo}$', r'Expected \hspace{n}'), + (r'$\hspace{}$', r'Expected \hspace{space}'), + (r'$\hspace{foo}$', r'Expected \hspace{space}'), + (r'$\sinx$', r'Unknown symbol: \sinx'), + (r'$\dotx$', r'Unknown symbol: \dotx'), (r'$\frac$', r'Expected \frac{num}{den}'), (r'$\frac{}{}$', r'Expected \frac{num}{den}'), (r'$\binom$', r'Expected \binom{num}{den}'), @@ -243,20 +287,28 @@ def test_fontinfo(): r'Expected \genfrac{ldelim}{rdelim}{rulesize}{style}{num}{den}'), (r'$\sqrt$', r'Expected \sqrt{value}'), (r'$\sqrt f$', r'Expected \sqrt{value}'), - (r'$\overline$', r'Expected \overline{value}'), - (r'$\overline{}$', r'Expected \overline{value}'), + (r'$\overline$', r'Expected \overline{body}'), + (r'$\overline{}$', r'Expected \overline{body}'), (r'$\leftF$', r'Expected a delimiter'), (r'$\rightF$', r'Unknown symbol: \rightF'), (r'$\left(\right$', r'Expected a delimiter'), - (r'$\left($', r'Expected "\right"'), + # PyParsing 2 uses double quotes, PyParsing 3 uses single quotes and an + # extra backslash. + (r'$\left($', re.compile(r'Expected ("|\'\\)\\right["\']')), (r'$\dfrac$', r'Expected \dfrac{num}{den}'), (r'$\dfrac{}{}$', r'Expected \dfrac{num}{den}'), - (r'$\overset$', r'Expected \overset{body}{annotation}'), - (r'$\underset$', r'Expected \underset{body}{annotation}'), + (r'$\overset$', r'Expected \overset{annotation}{body}'), + (r'$\underset$', r'Expected \underset{annotation}{body}'), + (r'$\foo$', r'Unknown symbol: \foo'), + (r'$a^2^2$', r'Double superscript'), + (r'$a_2_2$', r'Double subscript'), + (r'$a^2_a^2$', r'Double superscript'), ], ids=[ 'hspace without value', 'hspace with invalid value', + 'function without space', + 'accent without space', 'frac without parameters', 'frac with empty parameters', 'binom without parameters', @@ -275,34 +327,36 @@ def test_fontinfo(): 'dfrac with empty parameters', 'overset without parameters', 'underset without parameters', + 'unknown symbol', + 'double superscript', + 'double subscript', + 'super on sub without braces' ] ) def test_mathtext_exceptions(math, msg): parser = mathtext.MathTextParser('agg') - - with pytest.raises(ValueError, match=re.escape(msg)): + match = re.escape(msg) if isinstance(msg, str) else msg + with pytest.raises(ValueError, match=match): parser.parse(math) -def test_single_minus_sign(): - plt.figure(figsize=(0.3, 0.3)) - plt.text(0.5, 0.5, '$-$') - plt.gca().spines[:].set_visible(False) - plt.gca().set_xticks([]) - plt.gca().set_yticks([]) +def test_get_unicode_index_exception(): + with pytest.raises(ValueError): + _mathtext.get_unicode_index(r'\foo') - buff = io.BytesIO() - plt.savefig(buff, format="rgba", dpi=1000) - array = np.frombuffer(buff.getvalue(), dtype=np.uint8) - # If this fails, it would be all white - assert not np.all(array == 0xff) +def test_single_minus_sign(): + fig = plt.figure() + fig.text(0.5, 0.5, '$-$') + fig.canvas.draw() + t = np.asarray(fig.canvas.renderer.buffer_rgba()) + assert (t != 0xff).any() # assert that canvas is not all white. @check_figures_equal(extensions=["png"]) def test_spaces(fig_test, fig_ref): - fig_test.subplots().set_title(r"$1\,2\>3\ 4$") - fig_ref.subplots().set_title(r"$1\/2\:3~4$") + fig_test.text(.5, .5, r"$1\,2\>3\ 4$") + fig_ref.text(.5, .5, r"$1\/2\:3~4$") @check_figures_equal(extensions=["png"]) @@ -315,6 +369,7 @@ def test_operator_space(fig_test, fig_ref): fig_test.text(0.1, 0.6, r"$\operatorname{op}[6]$") fig_test.text(0.1, 0.7, r"$\cos^2$") fig_test.text(0.1, 0.8, r"$\log_2$") + fig_test.text(0.1, 0.9, r"$\sin^2 \cos$") # GitHub issue #17852 fig_ref.text(0.1, 0.1, r"$\mathrm{log\,}6$") fig_ref.text(0.1, 0.2, r"$\mathrm{log}(6)$") @@ -324,6 +379,23 @@ def test_operator_space(fig_test, fig_ref): fig_ref.text(0.1, 0.6, r"$\mathrm{op}[6]$") fig_ref.text(0.1, 0.7, r"$\mathrm{cos}^2$") fig_ref.text(0.1, 0.8, r"$\mathrm{log}_2$") + fig_ref.text(0.1, 0.9, r"$\mathrm{sin}^2 \mathrm{\,cos}$") + + +@check_figures_equal(extensions=["png"]) +def test_inverted_delimiters(fig_test, fig_ref): + fig_test.text(.5, .5, r"$\left)\right($", math_fontfamily="dejavusans") + fig_ref.text(.5, .5, r"$)($", math_fontfamily="dejavusans") + + +@check_figures_equal(extensions=["png"]) +def test_genfrac_displaystyle(fig_test, fig_ref): + fig_test.text(0.1, 0.1, r"$\dfrac{2x}{3y}$") + + thickness = _mathtext.TruetypeFonts.get_underline_thickness( + None, None, fontsize=mpl.rcParams["font.size"], + dpi=mpl.rcParams["savefig.dpi"]) + fig_ref.text(0.1, 0.1, r"$\genfrac{}{}{%f}{0}{2x}{3y}$" % thickness) def test_mathtext_fallback_valid(): @@ -337,19 +409,13 @@ def test_mathtext_fallback_invalid(): mpl.rcParams['mathtext.fallback'] = fallback -def test_mathtext_fallback_to_cm_invalid(): - for fallback in [True, False]: - with pytest.warns(_api.MatplotlibDeprecationWarning): - mpl.rcParams['mathtext.fallback_to_cm'] = fallback - - @pytest.mark.parametrize( "fallback,fontlist", [("cm", ['DejaVu Sans', 'mpltest', 'STIXGeneral', 'cmr10', 'STIXGeneral']), ("stix", ['DejaVu Sans', 'mpltest', 'STIXGeneral'])]) def test_mathtext_fallback(fallback, fontlist): mpl.font_manager.fontManager.addfont( - os.path.join((os.path.dirname(os.path.realpath(__file__))), 'mpltest.ttf')) + str(Path(__file__).resolve().parent / 'mpltest.ttf')) mpl.rcParams["svg.fonttype"] = 'none' mpl.rcParams['mathtext.fontset'] = 'custom' mpl.rcParams['mathtext.rm'] = 'mpltest' @@ -363,24 +429,19 @@ def test_mathtext_fallback(fallback, fontlist): fig, ax = plt.subplots() fig.text(.5, .5, test_str, fontsize=40, ha='center') fig.savefig(buff, format="svg") - char_fonts = [ - line.split("font-family:")[-1].split(";")[0] - for line in str(buff.getvalue()).split(r"\n") if "tspan" in line - ] + tspans = (ET.fromstring(buff.getvalue()) + .findall(".//{http://www.w3.org/2000/svg}tspan[@style]")) + # Getting the last element of the style attrib is a close enough + # approximation for parsing the font property. + char_fonts = [shlex.split(tspan.attrib["style"])[-1] for tspan in tspans] assert char_fonts == fontlist - mpl.font_manager.fontManager.ttflist = mpl.font_manager.fontManager.ttflist[:-1] + mpl.font_manager.fontManager.ttflist.pop() def test_math_to_image(tmpdir): mathtext.math_to_image('$x^2$', str(tmpdir.join('example.png'))) mathtext.math_to_image('$x^2$', io.BytesIO()) - - -def test_mathtext_to_png(tmpdir): - with _api.suppress_matplotlib_deprecation_warning(): - mt = mathtext.MathTextParser('bitmap') - mt.to_png(str(tmpdir.join('example.png')), '$x^2$') - mt.to_png(io.BytesIO(), '$x^2$') + mathtext.math_to_image('$x^2$', io.BytesIO(), color='Maroon') @image_comparison(baseline_images=['math_fontfamily_image.png'], @@ -391,3 +452,54 @@ def test_math_fontfamily(): size=24, math_fontfamily='dejavusans') fig.text(0.2, 0.3, r"$This\ text\ should\ have\ another$", size=24, math_fontfamily='stix') + + +def test_default_math_fontfamily(): + mpl.rcParams['mathtext.fontset'] = 'cm' + test_str = r'abc$abc\alpha$' + fig, ax = plt.subplots() + + text1 = fig.text(0.1, 0.1, test_str, font='Arial') + prop1 = text1.get_fontproperties() + assert prop1.get_math_fontfamily() == 'cm' + text2 = fig.text(0.2, 0.2, test_str, fontproperties='Arial') + prop2 = text2.get_fontproperties() + assert prop2.get_math_fontfamily() == 'cm' + + fig.draw_without_rendering() + + +def test_argument_order(): + mpl.rcParams['mathtext.fontset'] = 'cm' + test_str = r'abc$abc\alpha$' + fig, ax = plt.subplots() + + text1 = fig.text(0.1, 0.1, test_str, + math_fontfamily='dejavusans', font='Arial') + prop1 = text1.get_fontproperties() + assert prop1.get_math_fontfamily() == 'dejavusans' + text2 = fig.text(0.2, 0.2, test_str, + math_fontfamily='dejavusans', fontproperties='Arial') + prop2 = text2.get_fontproperties() + assert prop2.get_math_fontfamily() == 'dejavusans' + text3 = fig.text(0.3, 0.3, test_str, + font='Arial', math_fontfamily='dejavusans') + prop3 = text3.get_fontproperties() + assert prop3.get_math_fontfamily() == 'dejavusans' + text4 = fig.text(0.4, 0.4, test_str, + fontproperties='Arial', math_fontfamily='dejavusans') + prop4 = text4.get_fontproperties() + assert prop4.get_math_fontfamily() == 'dejavusans' + + fig.draw_without_rendering() + + +def test_mathtext_cmr10_minus_sign(): + # cmr10 does not contain a minus sign and used to issue a warning + # RuntimeWarning: Glyph 8722 missing from current font. + mpl.rcParams['font.family'] = 'cmr10' + mpl.rcParams['axes.formatter.use_mathtext'] = True + fig, ax = plt.subplots() + ax.plot(range(-1, 1), range(-1, 1)) + # draw to make sure we have no warnings + fig.canvas.draw() diff --git a/lib/matplotlib/tests/test_matplotlib.py b/lib/matplotlib/tests/test_matplotlib.py index cad9433a562e..6f92b4ca0abd 100644 --- a/lib/matplotlib/tests/test_matplotlib.py +++ b/lib/matplotlib/tests/test_matplotlib.py @@ -7,6 +7,16 @@ import matplotlib +@pytest.mark.parametrize('version_str, version_tuple', [ + ('3.5.0', (3, 5, 0, 'final', 0)), + ('3.5.0rc2', (3, 5, 0, 'candidate', 2)), + ('3.5.0.dev820+g6768ef8c4c', (3, 5, 0, 'alpha', 820)), + ('3.5.0.post820+g6768ef8c4c', (3, 5, 1, 'alpha', 820)), +]) +def test_parse_to_version_info(version_str, version_tuple): + assert matplotlib._parse_to_version_info(version_str) == version_tuple + + @pytest.mark.skipif( os.name == "nt", reason="chmod() doesn't work as is on Windows") @pytest.mark.skipif(os.name != "nt" and os.geteuid() == 0, diff --git a/lib/matplotlib/tests/test_mlab.py b/lib/matplotlib/tests/test_mlab.py index 5976fb32fc2e..9cd1b44cc1e2 100644 --- a/lib/matplotlib/tests/test_mlab.py +++ b/lib/matplotlib/tests/test_mlab.py @@ -3,7 +3,7 @@ import numpy as np import pytest -import matplotlib.mlab as mlab +from matplotlib import mlab, _api class TestStride: @@ -13,6 +13,11 @@ def get_base(self, x): y = y.base return y + @pytest.fixture(autouse=True) + def stride_is_deprecated(self): + with _api.suppress_matplotlib_deprecation_warning(): + yield + def calc_window_target(self, x, NFFT, noverlap=0, axis=0): """ This is an adaptation of the original window extraction algorithm. @@ -92,7 +97,7 @@ def test_window(): class TestDetrend: - def setup(self): + def setup_method(self): np.random.seed(0) n = 1000 x = np.linspace(0., 100, n) @@ -262,7 +267,7 @@ def test_detrend_linear_2d(self): ([], 255, 33, -1, -1, None), ([], 256, 128, -1, 256, 256), ([], None, -1, 32, -1, -1), - ], + ], ids=[ 'nosig', 'Fs4', @@ -354,7 +359,7 @@ def stim(self, request, fstims, iscomplex, sides, len_x, NFFT_density, num=pad_to_spectrum_real // 2 + 1) else: # frequencies for specgram, psd, and csd - # need to handle even and odd differentl + # need to handle even and odd differently if pad_to_density_real % 2: freqs_density = np.linspace(-Fs / 2, Fs / 2, num=2 * pad_to_density_real, @@ -610,7 +615,7 @@ def test_psd_window_hanning(self): noverlap=0, sides=self.sides, window=mlab.window_none) - spec_c *= len(ycontrol1)/(np.abs(windowVals)**2).sum() + spec_c *= len(ycontrol1)/(windowVals**2).sum() assert_array_equal(fsp_g, fsp_c) assert_array_equal(fsp_b, fsp_c) assert_allclose(spec_g, spec_c, atol=1e-08) @@ -657,7 +662,7 @@ def test_psd_window_hanning_detrend_linear(self): noverlap=0, sides=self.sides, window=mlab.window_none) - spec_c *= len(ycontrol1)/(np.abs(windowVals)**2).sum() + spec_c *= len(ycontrol1)/(windowVals**2).sum() assert_array_equal(fsp_g, fsp_c) assert_array_equal(fsp_b, fsp_c) assert_allclose(spec_g, spec_c, atol=1e-08) @@ -665,6 +670,33 @@ def test_psd_window_hanning_detrend_linear(self): with pytest.raises(AssertionError): assert_allclose(spec_b, spec_c, atol=1e-08) + def test_psd_window_flattop(self): + # flattop window + # adaption from https://github.com/scipy/scipy/blob\ + # /v1.10.0/scipy/signal/windows/_windows.py#L562-L622 + a = [0.21557895, 0.41663158, 0.277263158, 0.083578947, 0.006947368] + fac = np.linspace(-np.pi, np.pi, self.NFFT_density_real) + win = np.zeros(self.NFFT_density_real) + for k in range(len(a)): + win += a[k] * np.cos(k * fac) + + spec, fsp = mlab.psd(x=self.y, + NFFT=self.NFFT_density, + Fs=self.Fs, + noverlap=0, + sides=self.sides, + window=win, + scale_by_freq=False) + spec_a, fsp_a = mlab.psd(x=self.y, + NFFT=self.NFFT_density, + Fs=self.Fs, + noverlap=0, + sides=self.sides, + window=win) + assert_allclose(spec*win.sum()**2, + spec_a*self.Fs*(win**2).sum(), + atol=1e-08) + def test_psd_windowarray(self): freqs = self.freqs_density spec, fsp = mlab.psd(x=self.y, @@ -858,11 +890,11 @@ def test_cohere(): assert np.isreal(np.mean(cohsq)) -#***************************************************************** -# These Tests where taken from SCIPY with some minor modifications +# ***************************************************************** +# These Tests were taken from SCIPY with some minor modifications # this can be retrieved from: # https://github.com/scipy/scipy/blob/master/scipy/stats/tests/test_kdeoth.py -#***************************************************************** +# ***************************************************************** class TestGaussianKDE: diff --git a/lib/matplotlib/tests/test_nbagg_01.ipynb b/lib/matplotlib/tests/test_nbagg_01.ipynb index c8839afe8ddd..8505e057fdc3 100644 --- a/lib/matplotlib/tests/test_nbagg_01.ipynb +++ b/lib/matplotlib/tests/test_nbagg_01.ipynb @@ -473,7 +473,7 @@ " };\n", "}\n", "\n", - "// from http://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", + "// from https://stackoverflow.com/questions/1114465/getting-mouse-location-in-canvas\n", "mpl.findpos = function(e) {\n", " //this section is from http://www.quirksmode.org/js/events_properties.html\n", " var targ;\n", @@ -498,7 +498,7 @@ "/*\n", " * return a copy of an object with only non-object keys\n", " * we need this to avoid circular references\n", - " * http://stackoverflow.com/a/24161582/3208463\n", + " * https://stackoverflow.com/a/24161582/3208463\n", " */\n", "function simpleKeys (original) {\n", " return Object.keys(original).reduce(function (obj, key) {\n", diff --git a/lib/matplotlib/tests/test_offsetbox.py b/lib/matplotlib/tests/test_offsetbox.py index 72fdbdbf2e3b..a0116d5dfcd9 100644 --- a/lib/matplotlib/tests/test_offsetbox.py +++ b/lib/matplotlib/tests/test_offsetbox.py @@ -9,11 +9,11 @@ import matplotlib.pyplot as plt import matplotlib.patches as mpatches import matplotlib.lines as mlines -from matplotlib.backend_bases import MouseButton +from matplotlib.backend_bases import MouseButton, MouseEvent from matplotlib.offsetbox import ( - AnchoredOffsetbox, AnnotationBbox, AnchoredText, DrawingArea, - OffsetImage, TextArea, _get_packed_offsets) + AnchoredOffsetbox, AnnotationBbox, AnchoredText, DrawingArea, OffsetBox, + OffsetImage, PaddedBox, TextArea, _get_packed_offsets, HPacker, VPacker) @image_comparison(['offsetbox_clipping'], remove_text=True) @@ -117,76 +117,74 @@ def test_expand_with_tight_layout(): d2 = [2, 1] ax.plot(d1, label='series 1') ax.plot(d2, label='series 2') - ax.legend(ncol=2, mode='expand') + ax.legend(ncols=2, mode='expand') fig.tight_layout() # where the crash used to happen -@pytest.mark.parametrize('wd_list', - ([(150, 1)], [(150, 1)]*3, [(0.1, 1)], [(0.1, 1)]*2)) +@pytest.mark.parametrize('widths', + ([150], [150, 150, 150], [0.1], [0.1, 0.1])) @pytest.mark.parametrize('total', (250, 100, 0, -1, None)) @pytest.mark.parametrize('sep', (250, 1, 0, -1)) @pytest.mark.parametrize('mode', ("expand", "fixed", "equal")) -def test_get_packed_offsets(wd_list, total, sep, mode): +def test_get_packed_offsets(widths, total, sep, mode): # Check a (rather arbitrary) set of parameters due to successive similar # issue tickets (at least #10476 and #10784) related to corner cases # triggered inside this function when calling higher-level functions # (e.g. `Axes.legend`). # These are just some additional smoke tests. The output is untested. - _get_packed_offsets(wd_list, total, sep, mode=mode) + _get_packed_offsets(widths, total, sep, mode=mode) _Params = namedtuple('_params', 'wd_list, total, sep, expected') -@pytest.mark.parametrize('wd_list, total, sep, expected', [ +@pytest.mark.parametrize('widths, total, sep, expected', [ _Params( # total=None - [(3, 0), (1, 0), (2, 0)], total=None, sep=1, expected=(8, [0, 4, 6])), + [3, 1, 2], total=None, sep=1, expected=(8, [0, 4, 6])), _Params( # total larger than required - [(3, 0), (1, 0), (2, 0)], total=10, sep=1, expected=(10, [0, 4, 6])), + [3, 1, 2], total=10, sep=1, expected=(10, [0, 4, 6])), _Params( # total smaller than required - [(3, 0), (1, 0), (2, 0)], total=5, sep=1, expected=(5, [0, 4, 6])), + [3, 1, 2], total=5, sep=1, expected=(5, [0, 4, 6])), ]) -def test_get_packed_offsets_fixed(wd_list, total, sep, expected): - result = _get_packed_offsets(wd_list, total, sep, mode='fixed') +def test_get_packed_offsets_fixed(widths, total, sep, expected): + result = _get_packed_offsets(widths, total, sep, mode='fixed') assert result[0] == expected[0] assert_allclose(result[1], expected[1]) -@pytest.mark.parametrize('wd_list, total, sep, expected', [ +@pytest.mark.parametrize('widths, total, sep, expected', [ _Params( # total=None (implicit 1) - [(.1, 0)] * 3, total=None, sep=None, expected=(1, [0, .45, .9])), + [.1, .1, .1], total=None, sep=None, expected=(1, [0, .45, .9])), _Params( # total larger than sum of widths - [(3, 0), (1, 0), (2, 0)], total=10, sep=1, expected=(10, [0, 5, 8])), + [3, 1, 2], total=10, sep=1, expected=(10, [0, 5, 8])), _Params( # total smaller sum of widths: overlapping boxes - [(3, 0), (1, 0), (2, 0)], total=5, sep=1, expected=(5, [0, 2.5, 3])), + [3, 1, 2], total=5, sep=1, expected=(5, [0, 2.5, 3])), ]) -def test_get_packed_offsets_expand(wd_list, total, sep, expected): - result = _get_packed_offsets(wd_list, total, sep, mode='expand') +def test_get_packed_offsets_expand(widths, total, sep, expected): + result = _get_packed_offsets(widths, total, sep, mode='expand') assert result[0] == expected[0] assert_allclose(result[1], expected[1]) -@pytest.mark.parametrize('wd_list, total, sep, expected', [ +@pytest.mark.parametrize('widths, total, sep, expected', [ _Params( # total larger than required - [(3, 0), (2, 0), (1, 0)], total=6, sep=None, expected=(6, [0, 2, 4])), + [3, 2, 1], total=6, sep=None, expected=(6, [0, 2, 4])), _Params( # total smaller sum of widths: overlapping boxes - [(3, 0), (2, 0), (1, 0), (.5, 0)], total=2, sep=None, - expected=(2, [0, 0.5, 1, 1.5])), + [3, 2, 1, .5], total=2, sep=None, expected=(2, [0, 0.5, 1, 1.5])), _Params( # total larger than required - [(.5, 0), (1, 0), (.2, 0)], total=None, sep=1, - expected=(6, [0, 2, 4])), + [.5, 1, .2], total=None, sep=1, expected=(6, [0, 2, 4])), # the case total=None, sep=None is tested separately below ]) -def test_get_packed_offsets_equal(wd_list, total, sep, expected): - result = _get_packed_offsets(wd_list, total, sep, mode='equal') +def test_get_packed_offsets_equal(widths, total, sep, expected): + result = _get_packed_offsets(widths, total, sep, mode='equal') assert result[0] == expected[0] assert_allclose(result[1], expected[1]) def test_get_packed_offsets_equal_total_none_sep_none(): with pytest.raises(ValueError): - _get_packed_offsets([(1, 0)] * 3, total=None, sep=None, mode='equal') + _get_packed_offsets([1, 1, 1], total=None, sep=None, mode='equal') @pytest.mark.parametrize('child_type', ['draw', 'image', 'text']) @@ -228,7 +226,8 @@ def test_picking(child_type, boxcoords): x, y = ax.transAxes.transform_point((0.5, 0.5)) fig.canvas.draw() calls.clear() - fig.canvas.button_press_event(x, y, MouseButton.LEFT) + MouseEvent( + "button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process() assert len(calls) == 1 and calls[0].artist == ab # Annotation should *not* be picked by an event at its original center @@ -237,7 +236,8 @@ def test_picking(child_type, boxcoords): ax.set_ylim(-1, 0) fig.canvas.draw() calls.clear() - fig.canvas.button_press_event(x, y, MouseButton.LEFT) + MouseEvent( + "button_press_event", fig.canvas, x, y, MouseButton.LEFT)._process() assert len(calls) == 0 @@ -321,3 +321,75 @@ def test_annotationbbox_extents(): fig.canvas.draw() fig.tight_layout() fig.canvas.draw() + + +def test_zorder(): + assert OffsetBox(zorder=42).zorder == 42 + + +def test_arrowprops_copied(): + da = DrawingArea(20, 20, 0, 0, clip=True) + arrowprops = {"arrowstyle": "->", "relpos": (.3, .7)} + ab = AnnotationBbox(da, [.5, .5], xybox=(-0.2, 0.5), xycoords='data', + boxcoords="axes fraction", box_alignment=(0., .5), + arrowprops=arrowprops) + assert ab.arrowprops is not ab + assert arrowprops["relpos"] == (.3, .7) + + +@pytest.mark.parametrize("align", ["baseline", "bottom", "top", + "left", "right", "center"]) +def test_packers(align): + # set the DPI to match points to make the math easier below + fig = plt.figure(dpi=72) + renderer = fig.canvas.get_renderer() + + x1, y1 = 10, 30 + x2, y2 = 20, 60 + r1 = DrawingArea(x1, y1) + r2 = DrawingArea(x2, y2) + + # HPacker + hpacker = HPacker(children=[r1, r2], align=align) + hpacker.draw(renderer) + bbox = hpacker.get_bbox(renderer) + px, py = hpacker.get_offset(bbox, renderer) + # width, height, xdescent, ydescent + assert_allclose(bbox.bounds, (0, 0, x1 + x2, max(y1, y2))) + # internal element placement + if align in ("baseline", "left", "bottom"): + y_height = 0 + elif align in ("right", "top"): + y_height = y2 - y1 + elif align == "center": + y_height = (y2 - y1) / 2 + # x-offsets, y-offsets + assert_allclose([child.get_offset() for child in hpacker.get_children()], + [(px, py + y_height), (px + x1, py)]) + + # VPacker + vpacker = VPacker(children=[r1, r2], align=align) + vpacker.draw(renderer) + bbox = vpacker.get_bbox(renderer) + px, py = vpacker.get_offset(bbox, renderer) + # width, height, xdescent, ydescent + assert_allclose(bbox.bounds, (0, -max(y1, y2), max(x1, x2), y1 + y2)) + # internal element placement + if align in ("baseline", "left", "bottom"): + x_height = 0 + elif align in ("right", "top"): + x_height = x2 - x1 + elif align == "center": + x_height = (x2 - x1) / 2 + # x-offsets, y-offsets + assert_allclose([child.get_offset() for child in vpacker.get_children()], + [(px + x_height, py), (px, py - y2)]) + + +def test_paddedbox(): + # smoke test paddedbox for correct default value + fig, ax = plt.subplots() + at = AnchoredText("foo", 'upper left') + pb = PaddedBox(at, patch_attrs={'facecolor': 'r'}, draw_frame=True) + ax.add_artist(pb) + fig.draw_without_rendering() diff --git a/lib/matplotlib/tests/test_patches.py b/lib/matplotlib/tests/test_patches.py index 4eacbac89126..45bd6b4b06fc 100644 --- a/lib/matplotlib/tests/test_patches.py +++ b/lib/matplotlib/tests/test_patches.py @@ -5,13 +5,15 @@ from numpy.testing import assert_almost_equal, assert_array_equal import pytest -from matplotlib.patches import Patch, Polygon, Rectangle, FancyArrowPatch +import matplotlib as mpl +from matplotlib.patches import (Annulus, Ellipse, Patch, Polygon, Rectangle, + FancyArrowPatch, FancyArrow, BoxStyle, Arc) from matplotlib.testing.decorators import image_comparison, check_figures_equal from matplotlib.transforms import Bbox import matplotlib.pyplot as plt from matplotlib import ( collections as mcollections, colors as mcolors, patches as mpatches, - path as mpath, style as mstyle, transforms as mtransforms, rcParams) + path as mpath, transforms as mtransforms, rcParams) import sys on_win = (sys.platform == 'win32') @@ -29,6 +31,7 @@ def test_Polygon_close(): # start with open path and close it: p = Polygon(xy, closed=True) + assert p.get_closed() assert_array_equal(p.get_xy(), xyclosed) p.set_xy(xy) assert_array_equal(p.get_xy(), xyclosed) @@ -41,6 +44,7 @@ def test_Polygon_close(): # start with open path and leave it open: p = Polygon(xy, closed=False) + assert not p.get_closed() assert_array_equal(p.get_xy(), xy) p.set_xy(xy) assert_array_equal(p.get_xy(), xy) @@ -52,6 +56,54 @@ def test_Polygon_close(): assert_array_equal(p.get_xy(), xyclosed) +def test_corner_center(): + loc = [10, 20] + width = 1 + height = 2 + + # Rectangle + # No rotation + corners = ((10, 20), (11, 20), (11, 22), (10, 22)) + rect = Rectangle(loc, width, height) + assert_array_equal(rect.get_corners(), corners) + assert_array_equal(rect.get_center(), (10.5, 21)) + + # 90 deg rotation + corners_rot = ((10, 20), (10, 21), (8, 21), (8, 20)) + rect.set_angle(90) + assert_array_equal(rect.get_corners(), corners_rot) + assert_array_equal(rect.get_center(), (9, 20.5)) + + # Rotation not a multiple of 90 deg + theta = 33 + t = mtransforms.Affine2D().rotate_around(*loc, np.deg2rad(theta)) + corners_rot = t.transform(corners) + rect.set_angle(theta) + assert_almost_equal(rect.get_corners(), corners_rot) + + # Ellipse + loc = [loc[0] + width / 2, + loc[1] + height / 2] + ellipse = Ellipse(loc, width, height) + + # No rotation + assert_array_equal(ellipse.get_corners(), corners) + + # 90 deg rotation + corners_rot = ((11.5, 20.5), (11.5, 21.5), (9.5, 21.5), (9.5, 20.5)) + ellipse.set_angle(90) + assert_array_equal(ellipse.get_corners(), corners_rot) + # Rotation shouldn't change ellipse center + assert_array_equal(ellipse.get_center(), loc) + + # Rotation not a multiple of 90 deg + theta = 33 + t = mtransforms.Affine2D().rotate_around(*loc, np.deg2rad(theta)) + corners_rot = t.transform(corners) + ellipse.set_angle(theta) + assert_almost_equal(ellipse.get_corners(), corners_rot) + + def test_rotate_rect(): loc = np.asarray([1.0, 2.0]) width = 2 @@ -76,6 +128,61 @@ def test_rotate_rect(): assert_almost_equal(rect1.get_verts(), new_verts) +@check_figures_equal(extensions=['png']) +def test_rotate_rect_draw(fig_test, fig_ref): + ax_test = fig_test.add_subplot() + ax_ref = fig_ref.add_subplot() + + loc = (0, 0) + width, height = (1, 1) + angle = 30 + rect_ref = Rectangle(loc, width, height, angle=angle) + ax_ref.add_patch(rect_ref) + assert rect_ref.get_angle() == angle + + # Check that when the angle is updated after adding to an Axes, that the + # patch is marked stale and redrawn in the correct location + rect_test = Rectangle(loc, width, height) + assert rect_test.get_angle() == 0 + ax_test.add_patch(rect_test) + rect_test.set_angle(angle) + assert rect_test.get_angle() == angle + + +@check_figures_equal(extensions=['png']) +def test_dash_offset_patch_draw(fig_test, fig_ref): + ax_test = fig_test.add_subplot() + ax_ref = fig_ref.add_subplot() + + loc = (0.1, 0.1) + width, height = (0.8, 0.8) + rect_ref = Rectangle(loc, width, height, linewidth=3, edgecolor='b', + linestyle=(0, [6, 6])) + # fill the line gaps using a linestyle (0, [0, 6, 6, 0]), which is + # equivalent to (6, [6, 6]) but has 0 dash offset + rect_ref2 = Rectangle(loc, width, height, linewidth=3, edgecolor='r', + linestyle=(0, [0, 6, 6, 0])) + assert rect_ref.get_linestyle() == (0, [6, 6]) + assert rect_ref2.get_linestyle() == (0, [0, 6, 6, 0]) + + ax_ref.add_patch(rect_ref) + ax_ref.add_patch(rect_ref2) + + # Check that the dash offset of the rect is the same if we pass it in the + # init method and if we create two rects with appropriate onoff sequence + # of linestyle. + + rect_test = Rectangle(loc, width, height, linewidth=3, edgecolor='b', + linestyle=(0, [6, 6])) + rect_test2 = Rectangle(loc, width, height, linewidth=3, edgecolor='r', + linestyle=(6, [6, 6])) + assert rect_test.get_linestyle() == (0, [6, 6]) + assert rect_test2.get_linestyle() == (6, [6, 6]) + + ax_test.add_patch(rect_test) + ax_test.add_patch(rect_test2) + + def test_negative_rect(): # These two rectangles have the same vertices, but starting from a # different point. (We also drop the last vertex, which is a duplicate.) @@ -127,18 +234,18 @@ def test_patch_alpha_coloring(): cut_star2 = mpath.Path(verts + 1, codes) ax = plt.axes() - patch = mpatches.PathPatch(cut_star1, - linewidth=5, linestyle='dashdot', - facecolor=(1, 0, 0, 0.5), - edgecolor=(0, 0, 1, 0.75)) - ax.add_patch(patch) - col = mcollections.PathCollection([cut_star2], linewidth=5, linestyles='dashdot', facecolor=(1, 0, 0, 0.5), edgecolor=(0, 0, 1, 0.75)) ax.add_collection(col) + patch = mpatches.PathPatch(cut_star1, + linewidth=5, linestyle='dashdot', + facecolor=(1, 0, 0, 0.5), + edgecolor=(0, 0, 1, 0.75)) + ax.add_patch(patch) + ax.set_xlim([-1, 2]) ax.set_ylim([-1, 2]) @@ -157,13 +264,6 @@ def test_patch_alpha_override(): cut_star2 = mpath.Path(verts + 1, codes) ax = plt.axes() - patch = mpatches.PathPatch(cut_star1, - linewidth=5, linestyle='dashdot', - alpha=0.25, - facecolor=(1, 0, 0, 0.5), - edgecolor=(0, 0, 1, 0.75)) - ax.add_patch(patch) - col = mcollections.PathCollection([cut_star2], linewidth=5, linestyles='dashdot', alpha=0.25, @@ -171,11 +271,18 @@ def test_patch_alpha_override(): edgecolor=(0, 0, 1, 0.75)) ax.add_collection(col) + patch = mpatches.PathPatch(cut_star1, + linewidth=5, linestyle='dashdot', + alpha=0.25, + facecolor=(1, 0, 0, 0.5), + edgecolor=(0, 0, 1, 0.75)) + ax.add_patch(patch) + ax.set_xlim([-1, 2]) ax.set_ylim([-1, 2]) -@pytest.mark.style('default') +@mpl.style.context('default') def test_patch_color_none(): # Make sure the alpha kwarg does not override 'none' facecolor. # Addresses issue #7478. @@ -196,18 +303,18 @@ def test_patch_custom_linestyle(): cut_star2 = mpath.Path(verts + 1, codes) ax = plt.axes() - patch = mpatches.PathPatch( - cut_star1, - linewidth=5, linestyle=(0, (5, 7, 10, 7)), - facecolor=(1, 0, 0), edgecolor=(0, 0, 1)) - ax.add_patch(patch) - col = mcollections.PathCollection( [cut_star2], linewidth=5, linestyles=[(0, (5, 7, 10, 7))], facecolor=(1, 0, 0), edgecolor=(0, 0, 1)) ax.add_collection(col) + patch = mpatches.PathPatch( + cut_star1, + linewidth=5, linestyle=(0, (5, 7, 10, 7)), + facecolor=(1, 0, 0), edgecolor=(0, 0, 1)) + ax.add_patch(patch) + ax.set_xlim([-1, 2]) ax.set_ylim([-1, 2]) @@ -333,6 +440,10 @@ def test_patch_str(): expected = 'Arc(xy=(1, 2), width=3, height=4, angle=5, theta1=6, theta2=7)' assert str(p) == expected + p = mpatches.Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2) + expected = "Annulus(xy=(1, 2), r=(3, 4), width=1, angle=2)" + assert str(p) == expected + p = mpatches.RegularPolygon((1, 2), 20, radius=5) assert str(p) == "RegularPolygon((1, 2), 20, radius=5, orientation=0)" @@ -384,7 +495,7 @@ def test_multi_color_hatch(): ax.autoscale(False) for i in range(5): - with mstyle.context({'hatch.color': 'C{}'.format(i)}): + with mpl.style.context({'hatch.color': 'C{}'.format(i)}): r = Rectangle((i - .8 / 2, 5), .8, 1, hatch='//', fc='none') ax.add_patch(r) @@ -460,7 +571,7 @@ def test_datetime_datetime_fails(): from datetime import datetime start = datetime(2017, 1, 1, 0, 0, 0) - dt_delta = datetime(1970, 1, 5) # Will be 5 days if units are done wrong + dt_delta = datetime(1970, 1, 5) # Will be 5 days if units are done wrong. with pytest.raises(TypeError): mpatches.Rectangle((start, 0), dt_delta, 1) @@ -470,7 +581,7 @@ def test_datetime_datetime_fails(): def test_contains_point(): - ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0, 0) + ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0) points = [(0.0, 0.5), (0.2, 0.5), (0.25, 0.5), (0.5, 0.5)] path = ell.get_path() transform = ell.get_transform() @@ -483,7 +594,7 @@ def test_contains_point(): def test_contains_points(): - ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0, 0) + ell = mpatches.Ellipse((0.5, 0.5), 0.5, 1.0) points = [(0.0, 0.5), (0.2, 0.5), (0.25, 0.5), (0.5, 0.5)] path = ell.get_path() transform = ell.get_transform() @@ -525,7 +636,37 @@ def test_fancyarrow_units(): dtime = datetime(2000, 1, 1) fig, ax = plt.subplots() arrow = FancyArrowPatch((0, dtime), (0.01, dtime)) - ax.add_patch(arrow) + + +def test_fancyarrow_setdata(): + fig, ax = plt.subplots() + arrow = ax.arrow(0, 0, 10, 10, head_length=5, head_width=1, width=.5) + expected1 = np.array( + [[13.54, 13.54], + [10.35, 9.65], + [10.18, 9.82], + [0.18, -0.18], + [-0.18, 0.18], + [9.82, 10.18], + [9.65, 10.35], + [13.54, 13.54]] + ) + assert np.allclose(expected1, np.round(arrow.verts, 2)) + + expected2 = np.array( + [[16.71, 16.71], + [16.71, 15.29], + [16.71, 15.29], + [1.71, 0.29], + [0.29, 1.71], + [15.29, 16.71], + [15.29, 16.71], + [16.71, 16.71]] + ) + arrow.set_data( + x=1, y=1, dx=15, dy=15, width=2, head_width=2, head_length=1 + ) + assert np.allclose(expected2, np.round(arrow.verts, 2)) @image_comparison(["large_arc.svg"], style="mpl20") @@ -535,7 +676,7 @@ def test_large_arc(): y = -2115 diameter = 4261 for ax in [ax1, ax2]: - a = mpatches.Arc((x, y), diameter, diameter, lw=2, color='k') + a = Arc((x, y), diameter, diameter, lw=2, color='k') ax.add_patch(a) ax.set_axis_off() ax.set_aspect('equal') @@ -562,7 +703,7 @@ def test_rotated_arcs(): for prescale, centers in zip((1 - .0001, (1 - .0001) / np.sqrt(2)), (on_axis_centers, diag_centers)): for j, (x_sign, y_sign) in enumerate(centers, start=k): - a = mpatches.Arc( + a = Arc( (x_sign * scale * prescale, y_sign * scale * prescale), scale * sx, @@ -585,6 +726,78 @@ def test_rotated_arcs(): ax.set_aspect("equal") +def test_fancyarrow_shape_error(): + with pytest.raises(ValueError, match="Got unknown shape: 'foo'"): + FancyArrow(0, 0, 0.2, 0.2, shape='foo') + + +@pytest.mark.parametrize('fmt, match', ( + ("foo", "Unknown style: 'foo'"), + ("Round,foo", "Incorrect style argument: 'Round,foo'"), +)) +def test_boxstyle_errors(fmt, match): + with pytest.raises(ValueError, match=match): + BoxStyle(fmt) + + +@image_comparison(baseline_images=['annulus'], extensions=['png']) +def test_annulus(): + + fig, ax = plt.subplots() + cir = Annulus((0.5, 0.5), 0.2, 0.05, fc='g') # circular annulus + ell = Annulus((0.5, 0.5), (0.5, 0.3), 0.1, 45, # elliptical + fc='m', ec='b', alpha=0.5, hatch='xxx') + ax.add_patch(cir) + ax.add_patch(ell) + ax.set_aspect('equal') + + +@image_comparison(baseline_images=['annulus'], extensions=['png']) +def test_annulus_setters(): + + fig, ax = plt.subplots() + cir = Annulus((0., 0.), 0.2, 0.01, fc='g') # circular annulus + ell = Annulus((0., 0.), (1, 2), 0.1, 0, # elliptical + fc='m', ec='b', alpha=0.5, hatch='xxx') + ax.add_patch(cir) + ax.add_patch(ell) + ax.set_aspect('equal') + + cir.center = (0.5, 0.5) + cir.radii = 0.2 + cir.width = 0.05 + + ell.center = (0.5, 0.5) + ell.radii = (0.5, 0.3) + ell.width = 0.1 + ell.angle = 45 + + +@image_comparison(baseline_images=['annulus'], extensions=['png']) +def test_annulus_setters2(): + + fig, ax = plt.subplots() + cir = Annulus((0., 0.), 0.2, 0.01, fc='g') # circular annulus + ell = Annulus((0., 0.), (1, 2), 0.1, 0, # elliptical + fc='m', ec='b', alpha=0.5, hatch='xxx') + ax.add_patch(cir) + ax.add_patch(ell) + ax.set_aspect('equal') + + cir.center = (0.5, 0.5) + cir.set_semimajor(0.2) + cir.set_semiminor(0.2) + assert cir.radii == (0.2, 0.2) + cir.width = 0.05 + + ell.center = (0.5, 0.5) + ell.set_semimajor(0.5) + ell.set_semiminor(0.3) + assert ell.radii == (0.5, 0.3) + ell.width = 0.1 + ell.angle = 45 + + def test_degenerate_polygon(): point = [0, 0] correct_extents = Bbox([point, point]).extents @@ -629,3 +842,41 @@ def test_default_capstyle(): def test_default_joinstyle(): patch = Patch() assert patch.get_joinstyle() == 'miter' + + +@image_comparison(["autoscale_arc"], extensions=['png', 'svg'], + style="mpl20", remove_text=True) +def test_autoscale_arc(): + fig, axs = plt.subplots(1, 3, figsize=(4, 1)) + arc_lists = ( + [Arc((0, 0), 1, 1, theta1=0, theta2=90)], + [Arc((0.5, 0.5), 1.5, 0.5, theta1=10, theta2=20)], + [Arc((0.5, 0.5), 1.5, 0.5, theta1=10, theta2=20), + Arc((0.5, 0.5), 2.5, 0.5, theta1=110, theta2=120), + Arc((0.5, 0.5), 3.5, 0.5, theta1=210, theta2=220), + Arc((0.5, 0.5), 4.5, 0.5, theta1=310, theta2=320)]) + + for ax, arcs in zip(axs, arc_lists): + for arc in arcs: + ax.add_patch(arc) + ax.autoscale() + + +@check_figures_equal(extensions=["png", 'svg', 'pdf', 'eps']) +def test_arc_in_collection(fig_test, fig_ref): + arc1 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20) + arc2 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20) + col = mcollections.PatchCollection(patches=[arc2], facecolors='none', + edgecolors='k') + fig_ref.subplots().add_patch(arc1) + fig_test.subplots().add_collection(col) + + +@check_figures_equal(extensions=["png", 'svg', 'pdf', 'eps']) +def test_modifying_arc(fig_test, fig_ref): + arc1 = Arc([.5, .5], .5, 1, theta1=0, theta2=60, angle=20) + arc2 = Arc([.5, .5], 1.5, 1, theta1=0, theta2=60, angle=10) + fig_ref.subplots().add_patch(arc1) + fig_test.subplots().add_patch(arc2) + arc2.set_width(.5) + arc2.set_angle(20) diff --git a/lib/matplotlib/tests/test_path.py b/lib/matplotlib/tests/test_path.py index ed818257bb17..8cc4905287e9 100644 --- a/lib/matplotlib/tests/test_path.py +++ b/lib/matplotlib/tests/test_path.py @@ -1,4 +1,3 @@ -import copy import re import numpy as np @@ -54,9 +53,7 @@ def test_path_exceptions(): def test_point_in_path(): # Test #1787 - verts2 = [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)] - - path = Path(verts2, closed=True) + path = Path._create_closed([(0, 0), (0, 1), (1, 1), (1, 0)]) points = [(0.5, 0.5), (1.5, 0.5)] ret = path.contains_points(points) assert ret.dtype == 'bool' @@ -333,8 +330,28 @@ def test_path_deepcopy(): codes = [Path.MOVETO, Path.LINETO] path1 = Path(verts) path2 = Path(verts, codes) - copy.deepcopy(path1) - copy.deepcopy(path2) + path1_copy = path1.deepcopy() + path2_copy = path2.deepcopy() + assert path1 is not path1_copy + assert path1.vertices is not path1_copy.vertices + assert path2 is not path2_copy + assert path2.vertices is not path2_copy.vertices + assert path2.codes is not path2_copy.codes + + +def test_path_shallowcopy(): + # Should not raise any error + verts = [[0, 0], [1, 1]] + codes = [Path.MOVETO, Path.LINETO] + path1 = Path(verts) + path2 = Path(verts, codes) + path1_copy = path1.copy() + path2_copy = path2.copy() + assert path1 is not path1_copy + assert path1.vertices is path1_copy.vertices + assert path2 is not path2_copy + assert path2.vertices is path2_copy.vertices + assert path2.codes is path2_copy.codes @pytest.mark.parametrize('phi', np.concatenate([ diff --git a/lib/matplotlib/tests/test_patheffects.py b/lib/matplotlib/tests/test_patheffects.py index 34462fa0d179..6e09f4e37d6d 100644 --- a/lib/matplotlib/tests/test_patheffects.py +++ b/lib/matplotlib/tests/test_patheffects.py @@ -136,7 +136,8 @@ def test_collection(): 'edgecolor': 'blue'}) -@image_comparison(['tickedstroke'], remove_text=True, extensions=['png']) +@image_comparison(['tickedstroke'], remove_text=True, extensions=['png'], + tol=0.22) # Increased tolerance due to fixed clipping. def test_tickedstroke(): fig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(12, 4)) path = Path.unit_circle() @@ -188,3 +189,16 @@ def test_tickedstroke(): ax3.set_xlim(0, 4) ax3.set_ylim(0, 4) + + +@image_comparison(['spaces_and_newlines.png'], remove_text=True) +def test_patheffects_spaces_and_newlines(): + ax = plt.subplot() + s1 = " " + s2 = "\nNewline also causes problems" + text1 = ax.text(0.5, 0.75, s1, ha='center', va='center', size=20, + bbox={'color': 'salmon'}) + text2 = ax.text(0.5, 0.25, s2, ha='center', va='center', size=20, + bbox={'color': 'thistle'}) + text1.set_path_effects([path_effects.Normal()]) + text2.set_path_effects([path_effects.Normal()]) diff --git a/lib/matplotlib/tests/test_pickle.py b/lib/matplotlib/tests/test_pickle.py index 5c169cb23303..a31927d59634 100644 --- a/lib/matplotlib/tests/test_pickle.py +++ b/lib/matplotlib/tests/test_pickle.py @@ -1,15 +1,21 @@ from io import BytesIO +import ast import pickle +import pickletools import numpy as np import pytest +import matplotlib as mpl from matplotlib import cm -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing import subprocess_run_helper +from matplotlib.testing.decorators import check_figures_equal from matplotlib.dates import rrulewrapper +from matplotlib.lines import VertexSelector import matplotlib.pyplot as plt import matplotlib.transforms as mtransforms import matplotlib.figure as mfigure +from mpl_toolkits.axes_grid1 import parasite_axes def test_simple(): @@ -39,13 +45,9 @@ def test_simple(): pickle.dump(fig, BytesIO(), pickle.HIGHEST_PROTOCOL) -@image_comparison( - ['multi_pickle.png'], remove_text=True, style='mpl20', tol=0.082) -def test_complete(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - - fig = plt.figure('Figure with a label?', figsize=(10, 6)) +def _generate_complete_test_figure(fig_ref): + fig_ref.set_size_inches((10, 6)) + plt.figure(fig_ref) plt.suptitle('Can you fit any more in a figure?') @@ -82,31 +84,83 @@ def test_complete(): plt.quiver(x, y, u, v) plt.subplot(3, 3, 8) - plt.scatter(x, x**2, label='$x^2$') + plt.scatter(x, x ** 2, label='$x^2$') plt.legend(loc='upper left') plt.subplot(3, 3, 9) plt.errorbar(x, x * -0.5, xerr=0.2, yerr=0.4) + plt.legend(draggable=True) + - # +@mpl.style.context("default") +@check_figures_equal(extensions=["png"]) +def test_complete(fig_test, fig_ref): + _generate_complete_test_figure(fig_ref) # plotting is done, now test its pickle-ability - # - result_fh = BytesIO() - pickle.dump(fig, result_fh, pickle.HIGHEST_PROTOCOL) + pkl = pickle.dumps(fig_ref, pickle.HIGHEST_PROTOCOL) + # FigureCanvasAgg is picklable and GUI canvases are generally not, but there should + # be no reference to the canvas in the pickle stream in either case. In order to + # keep the test independent of GUI toolkits, run it with Agg and check that there's + # no reference to FigureCanvasAgg in the pickle stream. + assert "FigureCanvasAgg" not in [arg for op, arg, pos in pickletools.genops(pkl)] + loaded = pickle.loads(pkl) + loaded.canvas.draw() + + fig_test.set_size_inches(loaded.get_size_inches()) + fig_test.figimage(loaded.canvas.renderer.buffer_rgba()) + + plt.close(loaded) + + +def _pickle_load_subprocess(): + import os + import pickle + + path = os.environ['PICKLE_FILE_PATH'] + + with open(path, 'rb') as blob: + fig = pickle.load(blob) + + print(str(pickle.dumps(fig))) + + +@mpl.style.context("default") +@check_figures_equal(extensions=['png']) +def test_pickle_load_from_subprocess(fig_test, fig_ref, tmp_path): + _generate_complete_test_figure(fig_ref) + + fp = tmp_path / 'sinus.pickle' + assert not fp.exists() - plt.close('all') + with fp.open('wb') as file: + pickle.dump(fig_ref, file, pickle.HIGHEST_PROTOCOL) + assert fp.exists() - # make doubly sure that there are no figures left - assert plt._pylab_helpers.Gcf.figs == {} + proc = subprocess_run_helper( + _pickle_load_subprocess, + timeout=60, + extra_env={'PICKLE_FILE_PATH': str(fp)} + ) - # wind back the fh and load in the figure - result_fh.seek(0) - fig = pickle.load(result_fh) + loaded_fig = pickle.loads(ast.literal_eval(proc.stdout)) - # make sure there is now a figure manager - assert plt._pylab_helpers.Gcf.figs != {} + loaded_fig.canvas.draw() - assert fig.get_label() == 'Figure with a label?' + fig_test.set_size_inches(loaded_fig.get_size_inches()) + fig_test.figimage(loaded_fig.canvas.renderer.buffer_rgba()) + + plt.close(loaded_fig) + + +def test_gcf(): + fig = plt.figure("a label") + buf = BytesIO() + pickle.dump(fig, buf, pickle.HIGHEST_PROTOCOL) + plt.close("all") + assert plt._pylab_helpers.Gcf.figs == {} # No figures must be left. + fig = pickle.loads(buf.getbuffer()) + assert plt._pylab_helpers.Gcf.figs != {} # A manager is there again. + assert fig.get_label() == "a label" def test_no_pyplot(): @@ -199,7 +253,7 @@ def test_inset_and_secondary(): pickle.loads(pickle.dumps(fig)) -@pytest.mark.parametrize("cmap", cm._cmap_registry.values()) +@pytest.mark.parametrize("cmap", cm._colormaps.values()) def test_cmap(cmap): pickle.dumps(cmap) @@ -212,3 +266,25 @@ def test_unpickle_canvas(): out.seek(0) fig2 = pickle.load(out) assert fig2.canvas is not None + + +def test_mpl_toolkits(): + ax = parasite_axes.host_axes([0, 0, 1, 1]) + assert type(pickle.loads(pickle.dumps(ax))) == parasite_axes.HostAxes + + +def test_standard_norm(): + assert type(pickle.loads(pickle.dumps(mpl.colors.LogNorm()))) \ + == mpl.colors.LogNorm + + +def test_dynamic_norm(): + logit_norm_instance = mpl.colors.make_norm_from_scale( + mpl.scale.LogitScale, mpl.colors.Normalize)() + assert type(pickle.loads(pickle.dumps(logit_norm_instance))) \ + == type(logit_norm_instance) + + +def test_vertexselector(): + line, = plt.plot([0, 1], picker=True) + pickle.loads(pickle.dumps(VertexSelector(line))) diff --git a/lib/matplotlib/tests/test_png.py b/lib/matplotlib/tests/test_png.py index 133d3954452b..646db60cd0ae 100644 --- a/lib/matplotlib/tests/test_png.py +++ b/lib/matplotlib/tests/test_png.py @@ -4,8 +4,7 @@ import pytest from matplotlib.testing.decorators import image_comparison -from matplotlib import pyplot as plt -import matplotlib.cm as cm +from matplotlib import cm, pyplot as plt @image_comparison(['pngsuite.png'], tol=0.03) @@ -27,18 +26,17 @@ def test_pngsuite(): plt.gca().set_xlim(0, len(files)) -def test_truncated_file(tmpdir): - d = tmpdir.mkdir('test') - fname = str(d.join('test.png')) - fname_t = str(d.join('test_truncated.png')) - plt.savefig(fname) - with open(fname, 'rb') as fin: +def test_truncated_file(tmp_path): + path = tmp_path / 'test.png' + path_t = tmp_path / 'test_truncated.png' + plt.savefig(path) + with open(path, 'rb') as fin: buf = fin.read() - with open(fname_t, 'wb') as fout: + with open(path_t, 'wb') as fout: fout.write(buf[:20]) with pytest.raises(Exception): - plt.imread(fname_t) + plt.imread(path_t) def test_truncated_buffer(): diff --git a/lib/matplotlib/tests/test_polar.py b/lib/matplotlib/tests/test_polar.py index c614eff027b5..1f8e6a75baca 100644 --- a/lib/matplotlib/tests/test_polar.py +++ b/lib/matplotlib/tests/test_polar.py @@ -249,6 +249,8 @@ def test_polar_theta_limits(): direction=DIRECTIONS[i % len(DIRECTIONS)], rotation='auto') ax.yaxis.set_tick_params(label2On=True, rotation='auto') + ax.xaxis.get_major_locator().base.set_params( # backcompat + steps=[1, 2, 2.5, 5, 10]) @check_figures_equal(extensions=["png"]) @@ -289,6 +291,13 @@ def test_polar_no_data(): assert ax.get_rmin() == 0 and ax.get_rmax() == 1 +def test_polar_default_log_lims(): + plt.subplot(projection='polar') + ax = plt.gca() + ax.set_rscale('log') + assert ax.get_rmin() > 0 + + def test_polar_not_datalim_adjustable(): ax = plt.figure().add_subplot(projection="polar") with pytest.raises(ValueError): @@ -320,15 +329,13 @@ def test_polar_interpolation_steps_constant_r(fig_test, fig_ref): # Check that an extra half-turn doesn't make any difference -- modulo # antialiasing, which we disable here. p1 = (fig_test.add_subplot(121, projection="polar") - .bar([0], [1], 3*np.pi, edgecolor="none")) + .bar([0], [1], 3*np.pi, edgecolor="none", antialiased=False)) p2 = (fig_test.add_subplot(122, projection="polar") - .bar([0], [1], -3*np.pi, edgecolor="none")) + .bar([0], [1], -3*np.pi, edgecolor="none", antialiased=False)) p3 = (fig_ref.add_subplot(121, projection="polar") - .bar([0], [1], 2*np.pi, edgecolor="none")) + .bar([0], [1], 2*np.pi, edgecolor="none", antialiased=False)) p4 = (fig_ref.add_subplot(122, projection="polar") - .bar([0], [1], -2*np.pi, edgecolor="none")) - for p in [p1, p2, p3, p4]: - plt.setp(p, antialiased=False) + .bar([0], [1], -2*np.pi, edgecolor="none", antialiased=False)) @check_figures_equal(extensions=["png"]) @@ -357,3 +364,85 @@ def test_thetalim_args(): assert tuple(np.radians((ax.get_thetamin(), ax.get_thetamax()))) == (0, 1) ax.set_thetalim((2, 3)) assert tuple(np.radians((ax.get_thetamin(), ax.get_thetamax()))) == (2, 3) + + +def test_default_thetalocator(): + # Ideally we would check AAAABBC, but the smallest axes currently puts a + # single tick at 150° because MaxNLocator doesn't have a way to accept 15° + # while rejecting 150°. + fig, axs = plt.subplot_mosaic( + "AAAABB.", subplot_kw={"projection": "polar"}) + for ax in axs.values(): + ax.set_thetalim(0, np.pi) + for ax in axs.values(): + ticklocs = np.degrees(ax.xaxis.get_majorticklocs()).tolist() + assert pytest.approx(90) in ticklocs + assert pytest.approx(100) not in ticklocs + + +def test_axvspan(): + ax = plt.subplot(projection="polar") + span = ax.axvspan(0, np.pi/4) + assert span.get_path()._interpolation_steps > 1 + + +@check_figures_equal(extensions=["png"]) +def test_remove_shared_polar(fig_ref, fig_test): + # Removing shared polar axes used to crash. Test removing them, keeping in + # both cases just the lower left axes of a grid to avoid running into a + # separate issue (now being fixed) of ticklabel visibility for shared axes. + axs = fig_ref.subplots( + 2, 2, sharex=True, subplot_kw={"projection": "polar"}) + for i in [0, 1, 3]: + axs.flat[i].remove() + axs = fig_test.subplots( + 2, 2, sharey=True, subplot_kw={"projection": "polar"}) + for i in [0, 1, 3]: + axs.flat[i].remove() + + +def test_shared_polar_keeps_ticklabels(): + fig, axs = plt.subplots( + 2, 2, subplot_kw={"projection": "polar"}, sharex=True, sharey=True) + fig.canvas.draw() + assert axs[0, 1].xaxis.majorTicks[0].get_visible() + assert axs[0, 1].yaxis.majorTicks[0].get_visible() + fig, axs = plt.subplot_mosaic( + "ab\ncd", subplot_kw={"projection": "polar"}, sharex=True, sharey=True) + fig.canvas.draw() + assert axs["b"].xaxis.majorTicks[0].get_visible() + assert axs["b"].yaxis.majorTicks[0].get_visible() + + +def test_axvline_axvspan_do_not_modify_rlims(): + ax = plt.subplot(projection="polar") + ax.axvspan(0, 1) + ax.axvline(.5) + ax.plot([.1, .2]) + assert ax.get_ylim() == (0, .2) + + +def test_cursor_precision(): + ax = plt.subplot(projection="polar") + # Higher radii correspond to higher theta-precisions. + assert ax.format_coord(0, 0) == "θ=0π (0°), r=0.000" + assert ax.format_coord(0, .1) == "θ=0.00π (0°), r=0.100" + assert ax.format_coord(0, 1) == "θ=0.000π (0.0°), r=1.000" + assert ax.format_coord(1, 0) == "θ=0.3π (57°), r=0.000" + assert ax.format_coord(1, .1) == "θ=0.32π (57°), r=0.100" + assert ax.format_coord(1, 1) == "θ=0.318π (57.3°), r=1.000" + assert ax.format_coord(2, 0) == "θ=0.6π (115°), r=0.000" + assert ax.format_coord(2, .1) == "θ=0.64π (115°), r=0.100" + assert ax.format_coord(2, 1) == "θ=0.637π (114.6°), r=1.000" + + +@image_comparison(['polar_log.png'], style='default') +def test_polar_log(): + fig = plt.figure() + ax = fig.add_subplot(polar=True) + + ax.set_rscale('log') + ax.set_rlim(1, 1000) + + n = 100 + ax.plot(np.linspace(0, 2 * np.pi, n), np.logspace(0, 2, n)) diff --git a/lib/matplotlib/tests/test_preprocess_data.py b/lib/matplotlib/tests/test_preprocess_data.py index 1f4707679508..a95a72e7f78d 100644 --- a/lib/matplotlib/tests/test_preprocess_data.py +++ b/lib/matplotlib/tests/test_preprocess_data.py @@ -1,4 +1,6 @@ import re +import subprocess +import sys import numpy as np import pytest @@ -78,7 +80,7 @@ def test_function_call_without_data(func): def test_function_call_with_dict_input(func): """Tests with dict input, unpacking via preprocess_pipeline""" data = {'a': 1, 'b': 2} - assert(func(None, data.keys(), data.values()) == + assert (func(None, data.keys(), data.values()) == "x: ['a', 'b'], y: [1, 2], ls: x, w: xyz, label: None") @@ -197,35 +199,70 @@ def func(ax, x, y, z=1): def test_docstring_addition(): @_preprocess_data() def funcy(ax, *args, **kwargs): - """Funcy does nothing""" + """ + Parameters + ---------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + """ - assert re.search(r"every other argument", funcy.__doc__) - assert not re.search(r"the following arguments", funcy.__doc__) + assert re.search(r"all parameters also accept a string", funcy.__doc__) + assert not re.search(r"the following parameters", funcy.__doc__) @_preprocess_data(replace_names=[]) def funcy(ax, x, y, z, bar=None): - """Funcy does nothing""" + """ + Parameters + ---------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + """ - assert not re.search(r"every other argument", funcy.__doc__) - assert not re.search(r"the following arguments", funcy.__doc__) + assert not re.search(r"all parameters also accept a string", funcy.__doc__) + assert not re.search(r"the following parameters", funcy.__doc__) @_preprocess_data(replace_names=["bar"]) def funcy(ax, x, y, z, bar=None): - """Funcy does nothing""" - - assert not re.search(r"every other argument", funcy.__doc__) - assert not re.search(r"the following arguments .*: \*bar\*\.", + """ + Parameters + ---------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + """ + + assert not re.search(r"all parameters also accept a string", funcy.__doc__) + assert not re.search(r"the following parameters .*: \*bar\*\.", funcy.__doc__) @_preprocess_data(replace_names=["x", "t"]) def funcy(ax, x, y, z, t=None): - """Funcy does nothing""" - - assert not re.search(r"every other argument", funcy.__doc__) - assert not re.search(r"the following arguments .*: \*x\*, \*t\*\.", + """ + Parameters + ---------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + """ + + assert not re.search(r"all parameters also accept a string", funcy.__doc__) + assert not re.search(r"the following parameters .*: \*x\*, \*t\*\.", funcy.__doc__) +def test_data_parameter_replacement(): + """ + Test that the docstring contains the correct *data* parameter stub + for all methods that we run _preprocess_data() on. + """ + program = ( + "import logging; " + "logging.basicConfig(level=logging.DEBUG); " + "import matplotlib.pyplot as plt" + ) + cmd = [sys.executable, "-c", program] + completed_proc = subprocess.run(cmd, text=True, capture_output=True) + assert 'data parameter docstring error' not in completed_proc.stderr + + class TestPlotTypes: plotters = [Axes.scatter, Axes.bar, Axes.plot] diff --git a/lib/matplotlib/tests/test_pyplot.py b/lib/matplotlib/tests/test_pyplot.py index c2c71d586715..95e3174d8ae8 100644 --- a/lib/matplotlib/tests/test_pyplot.py +++ b/lib/matplotlib/tests/test_pyplot.py @@ -1,4 +1,6 @@ import difflib + +import numpy as np import subprocess import sys from pathlib import Path @@ -7,7 +9,7 @@ import matplotlib as mpl from matplotlib import pyplot as plt -from matplotlib.cbook import MatplotlibDeprecationWarning +from matplotlib._api import MatplotlibDeprecationWarning def test_pyplot_up_to_date(tmpdir): @@ -205,8 +207,8 @@ def test_subplot_replace_projection(): ax = plt.subplot(1, 2, 1) ax1 = plt.subplot(1, 2, 1) ax2 = plt.subplot(1, 2, 2) - # This will delete ax / ax1 as they fully overlap - ax3 = plt.subplot(1, 2, 1, projection='polar') + with pytest.warns(MatplotlibDeprecationWarning): + ax3 = plt.subplot(1, 2, 1, projection='polar') ax4 = plt.subplot(1, 2, 1, projection='polar') assert ax is not None assert ax1 is ax @@ -227,12 +229,13 @@ def test_subplot_kwarg_collision(): ax1 = plt.subplot(projection='polar', theta_offset=0) ax2 = plt.subplot(projection='polar', theta_offset=0) assert ax1 is ax2 + ax1.remove() ax3 = plt.subplot(projection='polar', theta_offset=1) assert ax1 is not ax3 assert ax1 not in plt.gcf().axes -def test_gca_kwargs(): +def test_gca(): # plt.gca() returns an existing axes, unless there were no axes. plt.figure() ax = plt.gca() @@ -241,38 +244,16 @@ def test_gca_kwargs(): assert ax1 is ax plt.close() - # plt.gca() raises a DeprecationWarning if called with kwargs. - plt.figure() - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - ax = plt.gca(projection='polar') - ax1 = plt.gca() - assert ax is not None - assert ax1 is ax - assert ax1.name == 'polar' - plt.close() - - # plt.gca() ignores keyword arguments if an axes already exists. - plt.figure() - ax = plt.gca() - with pytest.warns( - MatplotlibDeprecationWarning, - match=r'Calling gca\(\) with keyword arguments was deprecated'): - ax1 = plt.gca(projection='polar') - assert ax is not None - assert ax1 is ax - assert ax1.name == 'rectilinear' - plt.close() - def test_subplot_projection_reuse(): - # create an axes + # create an Axes ax1 = plt.subplot(111) # check that it is current assert ax1 is plt.gca() # make sure we get it back if we ask again assert ax1 is plt.subplot(111) + # remove it + ax1.remove() # create a polar plot ax2 = plt.subplot(111, projection='polar') assert ax2 is plt.gca() @@ -280,6 +261,7 @@ def test_subplot_projection_reuse(): assert ax1 not in plt.gcf().axes # assert we get it back if no extra parameters passed assert ax2 is plt.subplot(111) + ax2.remove() # now check explicitly setting the projection to rectilinear # makes a new axes ax3 = plt.subplot(111, projection='rectilinear') @@ -301,12 +283,175 @@ def test_subplot_polar_normalization(): def test_subplot_change_projection(): + created_axes = set() ax = plt.subplot() + created_axes.add(ax) projections = ('aitoff', 'hammer', 'lambert', 'mollweide', 'polar', 'rectilinear', '3d') for proj in projections: - ax_next = plt.subplot(projection=proj) - assert ax_next is plt.subplot() - assert ax_next.name == proj - assert ax is not ax_next - ax = ax_next + ax.remove() + ax = plt.subplot(projection=proj) + assert ax is plt.subplot() + assert ax.name == proj + created_axes.add(ax) + # Check that each call created a new Axes. + assert len(created_axes) == 1 + len(projections) + + +def test_polar_second_call(): + # the first call creates the axes with polar projection + ln1, = plt.polar(0., 1., 'ro') + assert isinstance(ln1, mpl.lines.Line2D) + # the second call should reuse the existing axes + ln2, = plt.polar(1.57, .5, 'bo') + assert isinstance(ln2, mpl.lines.Line2D) + assert ln1.axes is ln2.axes + + +def test_fallback_position(): + # check that position kwarg works if rect not supplied + axref = plt.axes([0.2, 0.2, 0.5, 0.5]) + axtest = plt.axes(position=[0.2, 0.2, 0.5, 0.5]) + np.testing.assert_allclose(axtest.bbox.get_points(), + axref.bbox.get_points()) + + # check that position kwarg ignored if rect is supplied + axref = plt.axes([0.2, 0.2, 0.5, 0.5]) + axtest = plt.axes([0.2, 0.2, 0.5, 0.5], position=[0.1, 0.1, 0.8, 0.8]) + np.testing.assert_allclose(axtest.bbox.get_points(), + axref.bbox.get_points()) + + +def test_set_current_figure_via_subfigure(): + fig1 = plt.figure() + subfigs = fig1.subfigures(2) + + plt.figure() + assert plt.gcf() != fig1 + + current = plt.figure(subfigs[1]) + assert plt.gcf() == fig1 + assert current == fig1 + + +def test_set_current_axes_on_subfigure(): + fig = plt.figure() + subfigs = fig.subfigures(2) + + ax = subfigs[0].subplots(1, squeeze=True) + subfigs[1].subplots(1, squeeze=True) + + assert plt.gca() != ax + plt.sca(ax) + assert plt.gca() == ax + + +def test_pylab_integration(): + IPython = pytest.importorskip("IPython") + mpl.testing.subprocess_run_helper( + IPython.start_ipython, + "--pylab", + "-c", + ";".join(( + "import matplotlib.pyplot as plt", + "assert plt._REPL_DISPLAYHOOK == plt._ReplDisplayHook.IPYTHON", + )), + timeout=60, + ) + + +def test_doc_pyplot_summary(): + """Test that pyplot_summary lists all the plot functions.""" + pyplot_docs = Path(__file__).parent / '../../../doc/api/pyplot_summary.rst' + if not pyplot_docs.exists(): + pytest.skip("Documentation sources not available") + + def extract_documented_functions(lines): + """ + Return a list of all the functions that are mentioned in the + autosummary blocks contained in *lines*. + + An autosummary block looks like this:: + + .. autosummary:: + :toctree: _as_gen + :template: autosummary.rst + :nosignatures: + + plot + plot_date + + """ + functions = [] + in_autosummary = False + for line in lines: + if not in_autosummary: + if line.startswith(".. autosummary::"): + in_autosummary = True + else: + if not line or line.startswith(" :"): + # empty line or autosummary parameter + continue + if not line[0].isspace(): + # no more indentation: end of autosummary block + in_autosummary = False + continue + functions.append(line.strip()) + return functions + + lines = pyplot_docs.read_text().split("\n") + doc_functions = set(extract_documented_functions(lines)) + plot_commands = set(plt._get_pyplot_commands()) + missing = plot_commands.difference(doc_functions) + if missing: + raise AssertionError( + f"The following pyplot functions are not listed in the " + f"documentation. Please add them to doc/api/pyplot_summary.rst: " + f"{missing!r}") + extra = doc_functions.difference(plot_commands) + if extra: + raise AssertionError( + f"The following functions are listed in the pyplot documentation, " + f"but they do not exist in pyplot. " + f"Please remove them from doc/api/pyplot_summary.rst: {extra!r}") + + +def test_minor_ticks(): + plt.figure() + plt.plot(np.arange(1, 10)) + tick_pos, tick_labels = plt.xticks(minor=True) + assert np.all(tick_labels == np.array([], dtype=np.float64)) + assert tick_labels == [] + + plt.yticks(ticks=[3.5, 6.5], labels=["a", "b"], minor=True) + ax = plt.gca() + tick_pos = ax.get_yticks(minor=True) + tick_labels = ax.get_yticklabels(minor=True) + assert np.all(tick_pos == np.array([3.5, 6.5])) + assert [l.get_text() for l in tick_labels] == ['a', 'b'] + + +def test_switch_backend_no_close(): + plt.switch_backend('agg') + fig = plt.figure() + fig = plt.figure() + assert len(plt.get_fignums()) == 2 + plt.switch_backend('agg') + assert len(plt.get_fignums()) == 2 + plt.switch_backend('svg') + assert len(plt.get_fignums()) == 0 + + +def figure_hook_example(figure): + figure._test_was_here = True + + +def test_figure_hook(): + + test_rc = { + 'figure.hooks': ['matplotlib.tests.test_pyplot:figure_hook_example'] + } + with mpl.rc_context(test_rc): + fig = plt.figure() + + assert fig._test_was_here diff --git a/lib/matplotlib/tests/test_quiver.py b/lib/matplotlib/tests/test_quiver.py index d7a848f61bcc..6e032a54422a 100644 --- a/lib/matplotlib/tests/test_quiver.py +++ b/lib/matplotlib/tests/test_quiver.py @@ -1,20 +1,25 @@ +import platform +import sys + import numpy as np import pytest -import sys + from matplotlib import pyplot as plt from matplotlib.testing.decorators import image_comparison -def draw_quiver(ax, **kw): +def draw_quiver(ax, **kwargs): X, Y = np.meshgrid(np.arange(0, 2 * np.pi, 1), np.arange(0, 2 * np.pi, 1)) U = np.cos(X) V = np.sin(Y) - Q = ax.quiver(U, V, **kw) + Q = ax.quiver(U, V, **kwargs) return Q +@pytest.mark.skipif(platform.python_implementation() != 'CPython', + reason='Requires CPython') def test_quiver_memory_leak(): fig, ax = plt.subplots() @@ -27,6 +32,8 @@ def test_quiver_memory_leak(): assert sys.getrefcount(ttX) == 2 +@pytest.mark.skipif(platform.python_implementation() != 'CPython', + reason='Requires CPython') def test_quiver_key_memory_leak(): fig, ax = plt.subplots() @@ -44,11 +51,11 @@ def test_quiver_number_of_args(): X = [1, 2] with pytest.raises( TypeError, - match='takes 2-5 positional arguments but 1 were given'): + match='takes from 2 to 5 positional arguments but 1 were given'): plt.quiver(X) with pytest.raises( TypeError, - match='takes 2-5 positional arguments but 6 were given'): + match='takes from 2 to 5 positional arguments but 6 were given'): plt.quiver(X, X, X, X, X, X) @@ -84,7 +91,7 @@ def test_no_warnings(): def test_zero_headlength(): # Based on report by Doug McNeil: - # http://matplotlib.1069221.n5.nabble.com/quiver-warnings-td28107.html + # https://discourse.matplotlib.org/t/quiver-warnings/16722 fig, ax = plt.subplots() X, Y = np.meshgrid(np.arange(10), np.arange(10)) U, V = np.cos(X), np.sin(Y) diff --git a/lib/matplotlib/tests/test_rcparams.py b/lib/matplotlib/tests/test_rcparams.py index 4705b975d60a..c17e88aee1ac 100644 --- a/lib/matplotlib/tests/test_rcparams.py +++ b/lib/matplotlib/tests/test_rcparams.py @@ -1,7 +1,7 @@ -from collections import OrderedDict import copy import os from pathlib import Path +import re import subprocess import sys from unittest import mock @@ -16,11 +16,12 @@ import numpy as np from matplotlib.rcsetup import ( validate_bool, - validate_bool_maybe_none, validate_color, validate_colorlist, + _validate_color_or_linecolor, validate_cycler, validate_float, + validate_fontstretch, validate_fontweight, validate_hatch, validate_hist_bins, @@ -39,7 +40,7 @@ def test_rcparams(tmpdir): linewidth = mpl.rcParams['lines.linewidth'] rcpath = Path(tmpdir) / 'test_rcparams.rc' - rcpath.write_text('lines.linewidth: 33') + rcpath.write_text('lines.linewidth: 33', encoding='utf-8') # test context given dictionary with mpl.rc_context(rc={'text.usetex': not usetex}): @@ -119,8 +120,6 @@ def test_rcparams_init(): def test_Bug_2543(): # Test that it possible to add all values to itself / deepcopy - # This was not possible because validate_bool_maybe_none did not - # accept None as an argument. # https://github.com/matplotlib/matplotlib/issues/2543 # We filter warnings at this stage since a number of them are raised # for deprecated rcparams as they should. We don't want these in the @@ -132,11 +131,6 @@ def test_Bug_2543(): mpl.rcParams[key] = _copy[key] with mpl.rc_context(): copy.deepcopy(mpl.rcParams) - # real test is that this does not raise - assert validate_bool_maybe_none(None) is None - assert validate_bool_maybe_none("none") is None - with pytest.raises(ValueError): - validate_bool_maybe_none("blah") with pytest.raises(ValueError): validate_bool(None) with pytest.raises(ValueError): @@ -198,7 +192,7 @@ def test_axes_titlecolor_rcparams(): def test_Issue_1713(tmpdir): rcpath = Path(tmpdir) / 'test_rcparams.rc' - rcpath.write_text('timezone: UTC', encoding='UTF-32-BE') + rcpath.write_text('timezone: UTC', encoding='utf-8') with mock.patch('locale.getpreferredencoding', return_value='UTF-32-BE'): rc = mpl.rc_params_from_file(rcpath, True, False) assert rc.get('timezone') == 'UTC' @@ -233,8 +227,6 @@ def generate_validator_testcases(valid): (('a', 'b'), ['a', 'b']), (iter(['a', 'b']), ['a', 'b']), (np.array(['a', 'b']), ['a', 'b']), - ((1, 2), ['1', '2']), - (np.array([1, 2]), ['1', '2']), ), 'fail': ((set(), ValueError), (1, ValueError), @@ -286,6 +278,17 @@ def generate_validator_testcases(valid): ('cycler("bleh, [])', ValueError), # syntax error ('Cycler("linewidth", [1, 2, 3])', ValueError), # only 'cycler()' function is allowed + # do not allow dunder in string literals + ("cycler('c', [j.__class__(j) for j in ['r', 'b']])", + ValueError), + ("cycler('c', [j. __class__(j) for j in ['r', 'b']])", + ValueError), + ("cycler('c', [j.\t__class__(j) for j in ['r', 'b']])", + ValueError), + ("cycler('c', [j.\u000c__class__(j) for j in ['r', 'b']])", + ValueError), + ("cycler('c', [j.__class__(j).lower() for j in ['r', 'b']])", + ValueError), ('1 + 2', ValueError), # doesn't produce a Cycler object ('os.system("echo Gotcha")', ValueError), # os not available ('import os', ValueError), # should not be able to import @@ -338,6 +341,17 @@ def generate_validator_testcases(valid): ('(0, 1, "0.5")', ValueError), # last one not a float ), }, + {'validator': _validate_color_or_linecolor, + 'success': (('linecolor', 'linecolor'), + ('markerfacecolor', 'markerfacecolor'), + ('mfc', 'markerfacecolor'), + ('markeredgecolor', 'markeredgecolor'), + ('mec', 'markeredgecolor') + ), + 'fail': (('line', ValueError), + ('marker', ValueError) + ) + }, {'validator': validate_hist_bins, 'success': (('auto', 'auto'), ('fd', 'fd'), @@ -398,6 +412,7 @@ def generate_validator_testcases(valid): ([1, 2, 3], ValueError), # sequence with odd length (1.23, ValueError), # not a sequence (("a", [1, 2]), ValueError), # wrong explicit offset + ((None, [1, 2]), ValueError), # wrong explicit offset ((1, [1, 2, 3]), ValueError), # odd length sequence (([1, 2], 1), ValueError), # inverted offset/onoff ) @@ -454,12 +469,39 @@ def test_validate_fontweight(weight, parsed_weight): assert validate_fontweight(weight) == parsed_weight +@pytest.mark.parametrize('stretch, parsed_stretch', [ + ('expanded', 'expanded'), + ('EXPANDED', ValueError), # stretch is case-sensitive + (100, 100), + ('100', 100), + (np.array(100), 100), + # fractional fontweights are not defined. This should actually raise a + # ValueError, but historically did not. + (20.6, 20), + ('20.6', ValueError), + ([100], ValueError), +]) +def test_validate_fontstretch(stretch, parsed_stretch): + if parsed_stretch is ValueError: + with pytest.raises(ValueError): + validate_fontstretch(stretch) + else: + assert validate_fontstretch(stretch) == parsed_stretch + + def test_keymaps(): key_list = [k for k in mpl.rcParams if 'keymap' in k] for k in key_list: assert isinstance(mpl.rcParams[k], list) +def test_no_backend_reset_rccontext(): + assert mpl.rcParams['backend'] != 'module://aardvark' + with mpl.rc_context(): + mpl.rcParams['backend'] = 'module://aardvark' + assert mpl.rcParams['backend'] == 'module://aardvark' + + def test_rcparams_reset_after_fail(): # There was previously a bug that meant that if rc_context failed and # raised an exception due to issues in the supplied rc parameters, the @@ -467,8 +509,7 @@ def test_rcparams_reset_after_fail(): with mpl.rc_context(rc={'text.usetex': False}): assert mpl.rcParams['text.usetex'] is False with pytest.raises(KeyError): - with mpl.rc_context(rc=OrderedDict([('text.usetex', True), - ('test.blah', True)])): + with mpl.rc_context(rc={'text.usetex': True, 'test.blah': True}): pass assert mpl.rcParams['text.usetex'] is False @@ -481,11 +522,12 @@ def test_backend_fallback_headless(tmpdir): with pytest.raises(subprocess.CalledProcessError): subprocess.run( [sys.executable, "-c", - ("import matplotlib;" + - "matplotlib.use('tkagg');" + - "import matplotlib.pyplot") + "import matplotlib;" + "matplotlib.use('tkagg');" + "import matplotlib.pyplot;" + "matplotlib.pyplot.plot(42);" ], - env=env, check=True) + env=env, check=True, stderr=subprocess.DEVNULL) @pytest.mark.skipif( @@ -496,8 +538,62 @@ def test_backend_fallback_headful(tmpdir): env = {**os.environ, "MPLBACKEND": "", "MPLCONFIGDIR": str(tmpdir)} backend = subprocess.check_output( [sys.executable, "-c", - "import matplotlib.pyplot; print(matplotlib.get_backend())"], + "import matplotlib as mpl; " + "sentinel = mpl.rcsetup._auto_backend_sentinel; " + # Check that access on another instance does not resolve the sentinel. + "assert mpl.RcParams({'backend': sentinel})['backend'] == sentinel; " + "assert mpl.rcParams._get('backend') == sentinel; " + "import matplotlib.pyplot; " + "print(matplotlib.get_backend())"], env=env, universal_newlines=True) # The actual backend will depend on what's installed, but at least tkagg is # present. assert backend.strip().lower() != "agg" + + +def test_deprecation(monkeypatch): + monkeypatch.setitem( + mpl._deprecated_map, "patch.linewidth", + ("0.0", "axes.linewidth", lambda old: 2 * old, lambda new: new / 2)) + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert mpl.rcParams["patch.linewidth"] \ + == mpl.rcParams["axes.linewidth"] / 2 + with pytest.warns(_api.MatplotlibDeprecationWarning): + mpl.rcParams["patch.linewidth"] = 1 + assert mpl.rcParams["axes.linewidth"] == 2 + + monkeypatch.setitem( + mpl._deprecated_ignore_map, "patch.edgecolor", + ("0.0", "axes.edgecolor")) + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert mpl.rcParams["patch.edgecolor"] \ + == mpl.rcParams["axes.edgecolor"] + with pytest.warns(_api.MatplotlibDeprecationWarning): + mpl.rcParams["patch.edgecolor"] = "#abcd" + assert mpl.rcParams["axes.edgecolor"] != "#abcd" + + monkeypatch.setitem( + mpl._deprecated_ignore_map, "patch.force_edgecolor", + ("0.0", None)) + with pytest.warns(_api.MatplotlibDeprecationWarning): + assert mpl.rcParams["patch.force_edgecolor"] is None + + monkeypatch.setitem( + mpl._deprecated_remain_as_none, "svg.hashsalt", + ("0.0",)) + with pytest.warns(_api.MatplotlibDeprecationWarning): + mpl.rcParams["svg.hashsalt"] = "foobar" + assert mpl.rcParams["svg.hashsalt"] == "foobar" # Doesn't warn. + mpl.rcParams["svg.hashsalt"] = None # Doesn't warn. + + mpl.rcParams.update(mpl.rcParams.copy()) # Doesn't warn. + # Note that the warning suppression actually arises from the + # iteration over the updater rcParams being protected by + # suppress_matplotlib_deprecation_warning, rather than any explicit check. + + +def test_rcparams_legend_loc(): + value = (0.9, .7) + match_str = f"{value} is not a valid value for legend.loc;" + with pytest.raises(ValueError, match=re.escape(match_str)): + mpl.RcParams({'legend.loc': value}) diff --git a/lib/matplotlib/tests/test_sankey.py b/lib/matplotlib/tests/test_sankey.py index 1851525bd4e2..cbb7f516a65c 100644 --- a/lib/matplotlib/tests/test_sankey.py +++ b/lib/matplotlib/tests/test_sankey.py @@ -1,4 +1,8 @@ +import pytest +from numpy.testing import assert_allclose, assert_array_equal + from matplotlib.sankey import Sankey +from matplotlib.testing.decorators import check_figures_equal def test_sankey(): @@ -22,3 +26,80 @@ def show_three_decimal_places(value): format=show_three_decimal_places) assert s.diagrams[0].texts[0].get_text() == 'First\n0.250' + + +@pytest.mark.parametrize('kwargs, msg', ( + ({'gap': -1}, "'gap' is negative"), + ({'gap': 1, 'radius': 2}, "'radius' is greater than 'gap'"), + ({'head_angle': -1}, "'head_angle' is negative"), + ({'tolerance': -1}, "'tolerance' is negative"), + ({'flows': [1, -1], 'orientations': [-1, 0, 1]}, + r"The shapes of 'flows' \(2,\) and 'orientations'"), + ({'flows': [1, -1], 'labels': ['a', 'b', 'c']}, + r"The shapes of 'flows' \(2,\) and 'labels'"), + )) +def test_sankey_errors(kwargs, msg): + with pytest.raises(ValueError, match=msg): + Sankey(**kwargs) + + +@pytest.mark.parametrize('kwargs, msg', ( + ({'trunklength': -1}, "'trunklength' is negative"), + ({'flows': [0.2, 0.3], 'prior': 0}, "The scaled sum of the connected"), + ({'prior': -1}, "The index of the prior diagram is negative"), + ({'prior': 1}, "The index of the prior diagram is 1"), + ({'connect': (-1, 1), 'prior': 0}, "At least one of the connection"), + ({'connect': (2, 1), 'prior': 0}, "The connection index to the source"), + ({'connect': (1, 3), 'prior': 0}, "The connection index to this dia"), + ({'connect': (1, 1), 'prior': 0, 'flows': [-0.2, 0.2], + 'orientations': [2]}, "The value of orientations"), + ({'connect': (1, 1), 'prior': 0, 'flows': [-0.2, 0.2], + 'pathlengths': [2]}, "The lengths of 'flows'"), + )) +def test_sankey_add_errors(kwargs, msg): + sankey = Sankey() + with pytest.raises(ValueError, match=msg): + sankey.add(flows=[0.2, -0.2]) + sankey.add(**kwargs) + + +def test_sankey2(): + s = Sankey(flows=[0.25, -0.25, 0.5, -0.5], labels=['Foo'], + orientations=[-1], unit='Bar') + sf = s.finish() + assert_array_equal(sf[0].flows, [0.25, -0.25, 0.5, -0.5]) + assert sf[0].angles == [1, 3, 1, 3] + assert all([text.get_text()[0:3] == 'Foo' for text in sf[0].texts]) + assert all([text.get_text()[-3:] == 'Bar' for text in sf[0].texts]) + assert sf[0].text.get_text() == '' + assert_allclose(sf[0].tips, + [(-1.375, -0.52011255), + (1.375, -0.75506044), + (-0.75, -0.41522509), + (0.75, -0.8599479)]) + + s = Sankey(flows=[0.25, -0.25, 0, 0.5, -0.5], labels=['Foo'], + orientations=[-1], unit='Bar') + sf = s.finish() + assert_array_equal(sf[0].flows, [0.25, -0.25, 0, 0.5, -0.5]) + assert sf[0].angles == [1, 3, None, 1, 3] + assert_allclose(sf[0].tips, + [(-1.375, -0.52011255), + (1.375, -0.75506044), + (0, 0), + (-0.75, -0.41522509), + (0.75, -0.8599479)]) + + +@check_figures_equal(extensions=['png']) +def test_sankey3(fig_test, fig_ref): + ax_test = fig_test.gca() + s_test = Sankey(ax=ax_test, flows=[0.25, -0.25, -0.25, 0.25, 0.5, -0.5], + orientations=[1, -1, 1, -1, 0, 0]) + s_test.finish() + + ax_ref = fig_ref.gca() + s_ref = Sankey(ax=ax_ref) + s_ref.add(flows=[0.25, -0.25, -0.25, 0.25, 0.5, -0.5], + orientations=[1, -1, 1, -1, 0, 0]) + s_ref.finish() diff --git a/lib/matplotlib/tests/test_scale.py b/lib/matplotlib/tests/test_scale.py index 8fba86d2e82e..3b70d1e9d31d 100644 --- a/lib/matplotlib/tests/test_scale.py +++ b/lib/matplotlib/tests/test_scale.py @@ -2,9 +2,11 @@ import matplotlib.pyplot as plt from matplotlib.scale import ( + AsinhScale, AsinhTransform, LogTransform, InvertedLogTransform, SymmetricalLogTransform) import matplotlib.scale as mscale +from matplotlib.ticker import AsinhLocator, LogFormatterSciNotation from matplotlib.testing.decorators import check_figures_equal, image_comparison import numpy as np @@ -53,6 +55,20 @@ def test_symlog_mask_nan(): assert type(out) == type(x) +def test_symlog_linthresh(): + np.random.seed(19680801) + x = np.random.random(100) + y = np.random.random(100) + + fig, ax = plt.subplots() + plt.plot(x, y, 'o') + ax.set_xscale('symlog') + ax.set_yscale('symlog') + + with pytest.warns(UserWarning, match="All values .* of linthresh"): + fig.canvas.draw() + + @image_comparison(['logit_scales.png'], remove_text=True) def test_logit_scales(): fig, ax = plt.subplots() @@ -219,3 +235,75 @@ def test_scale_deepcopy(): sc2 = copy.deepcopy(sc) assert str(sc.get_transform()) == str(sc2.get_transform()) assert sc._transform is not sc2._transform + + +class TestAsinhScale: + def test_transforms(self): + a0 = 17.0 + a = np.linspace(-50, 50, 100) + + forward = AsinhTransform(a0) + inverse = forward.inverted() + invinv = inverse.inverted() + + a_forward = forward.transform_non_affine(a) + a_inverted = inverse.transform_non_affine(a_forward) + assert_allclose(a_inverted, a) + + a_invinv = invinv.transform_non_affine(a) + assert_allclose(a_invinv, a0 * np.arcsinh(a / a0)) + + def test_init(self): + fig, ax = plt.subplots() + + s = AsinhScale(axis=None, linear_width=23.0) + assert s.linear_width == 23 + assert s._base == 10 + assert s._subs == (2, 5) + + tx = s.get_transform() + assert isinstance(tx, AsinhTransform) + assert tx.linear_width == s.linear_width + + def test_base_init(self): + fig, ax = plt.subplots() + + s3 = AsinhScale(axis=None, base=3) + assert s3._base == 3 + assert s3._subs == (2,) + + s7 = AsinhScale(axis=None, base=7, subs=(2, 4)) + assert s7._base == 7 + assert s7._subs == (2, 4) + + def test_fmtloc(self): + class DummyAxis: + def __init__(self): + self.fields = {} + def set(self, **kwargs): + self.fields.update(**kwargs) + def set_major_formatter(self, f): + self.fields['major_formatter'] = f + + ax0 = DummyAxis() + s0 = AsinhScale(axis=ax0, base=0) + s0.set_default_locators_and_formatters(ax0) + assert isinstance(ax0.fields['major_locator'], AsinhLocator) + assert isinstance(ax0.fields['major_formatter'], str) + + ax5 = DummyAxis() + s7 = AsinhScale(axis=ax5, base=5) + s7.set_default_locators_and_formatters(ax5) + assert isinstance(ax5.fields['major_locator'], AsinhLocator) + assert isinstance(ax5.fields['major_formatter'], + LogFormatterSciNotation) + + def test_bad_scale(self): + fig, ax = plt.subplots() + + with pytest.raises(ValueError): + AsinhScale(axis=None, linear_width=0) + with pytest.raises(ValueError): + AsinhScale(axis=None, linear_width=-1) + s0 = AsinhScale(axis=None, ) + s1 = AsinhScale(axis=None, linear_width=3.0) diff --git a/lib/matplotlib/tests/test_simplification.py b/lib/matplotlib/tests/test_simplification.py index 07287618f26c..446fc92993e7 100644 --- a/lib/matplotlib/tests/test_simplification.py +++ b/lib/matplotlib/tests/test_simplification.py @@ -6,7 +6,8 @@ import pytest -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import ( + check_figures_equal, image_comparison, remove_ticks_and_titles) import matplotlib.pyplot as plt from matplotlib import patches, transforms @@ -47,6 +48,29 @@ def test_diamond(): ax.set_ylim(-0.6, 0.6) +def test_clipping_out_of_bounds(): + # Should work on a Path *without* codes. + path = Path([(0, 0), (1, 2), (2, 1)]) + simplified = path.cleaned(clip=(10, 10, 20, 20)) + assert_array_equal(simplified.vertices, [(0, 0)]) + assert simplified.codes == [Path.STOP] + + # Should work on a Path *with* codes, and no curves. + path = Path([(0, 0), (1, 2), (2, 1)], + [Path.MOVETO, Path.LINETO, Path.LINETO]) + simplified = path.cleaned(clip=(10, 10, 20, 20)) + assert_array_equal(simplified.vertices, [(0, 0)]) + assert simplified.codes == [Path.STOP] + + # A Path with curves does not do any clipping yet. + path = Path([(0, 0), (1, 2), (2, 3)], + [Path.MOVETO, Path.CURVE3, Path.CURVE3]) + simplified = path.cleaned() + simplified_clipped = path.cleaned(clip=(10, 10, 20, 20)) + assert_array_equal(simplified.vertices, simplified_clipped.vertices) + assert_array_equal(simplified.codes, simplified_clipped.codes) + + def test_noise(): np.random.seed(0) x = np.random.uniform(size=50000) * 50 @@ -207,7 +231,7 @@ def test_sine_plus_noise(): assert simplified.vertices.size == 25240 -@image_comparison(['simplify_curve'], remove_text=True) +@image_comparison(['simplify_curve'], remove_text=True, tol=0.017) def test_simplify_curve(): pp1 = patches.PathPatch( Path([(0, 0), (1, 0), (1, 1), (np.nan, 1), (0, 0), (2, 0), (2, 2), @@ -222,6 +246,155 @@ def test_simplify_curve(): ax.set_ylim((0, 2)) +@check_figures_equal() +def test_closed_path_nan_removal(fig_test, fig_ref): + ax_test = fig_test.subplots(2, 2).flatten() + ax_ref = fig_ref.subplots(2, 2).flatten() + + # NaN on the first point also removes the last point, because it's closed. + path = Path( + [[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, -3]], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]) + ax_test[0].add_patch(patches.PathPatch(path, facecolor='none')) + path = Path( + [[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, np.nan]], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO]) + ax_ref[0].add_patch(patches.PathPatch(path, facecolor='none')) + + # NaN on second-last point should not re-close. + path = Path( + [[-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]) + ax_test[0].add_patch(patches.PathPatch(path, facecolor='none')) + path = Path( + [[-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO]) + ax_ref[0].add_patch(patches.PathPatch(path, facecolor='none')) + + # Test multiple loops in a single path (with same paths as above). + path = Path( + [[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, -3], + [-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY, + Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.CLOSEPOLY]) + ax_test[1].add_patch(patches.PathPatch(path, facecolor='none')) + path = Path( + [[-3, np.nan], [3, -3], [3, 3], [-3, 3], [-3, np.nan], + [-2, -2], [2, -2], [2, 2], [-2, np.nan], [-2, -2]], + [Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO, + Path.MOVETO, Path.LINETO, Path.LINETO, Path.LINETO, Path.LINETO]) + ax_ref[1].add_patch(patches.PathPatch(path, facecolor='none')) + + # NaN in first point of CURVE3 should not re-close, and hide entire curve. + path = Path( + [[-1, -1], [1, -1], [1, np.nan], [0, 1], [-1, 1], [-1, -1]], + [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO, + Path.CLOSEPOLY]) + ax_test[2].add_patch(patches.PathPatch(path, facecolor='none')) + path = Path( + [[-1, -1], [1, -1], [1, np.nan], [0, 1], [-1, 1], [-1, -1]], + [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO, + Path.CLOSEPOLY]) + ax_ref[2].add_patch(patches.PathPatch(path, facecolor='none')) + + # NaN in second point of CURVE3 should not re-close, and hide entire curve + # plus next line segment. + path = Path( + [[-3, -3], [3, -3], [3, 0], [0, np.nan], [-3, 3], [-3, -3]], + [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO, + Path.LINETO]) + ax_test[2].add_patch(patches.PathPatch(path, facecolor='none')) + path = Path( + [[-3, -3], [3, -3], [3, 0], [0, np.nan], [-3, 3], [-3, -3]], + [Path.MOVETO, Path.LINETO, Path.CURVE3, Path.CURVE3, Path.LINETO, + Path.LINETO]) + ax_ref[2].add_patch(patches.PathPatch(path, facecolor='none')) + + # NaN in first point of CURVE4 should not re-close, and hide entire curve. + path = Path( + [[-1, -1], [1, -1], [1, np.nan], [0, 0], [0, 1], [-1, 1], [-1, -1]], + [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, + Path.LINETO, Path.CLOSEPOLY]) + ax_test[3].add_patch(patches.PathPatch(path, facecolor='none')) + path = Path( + [[-1, -1], [1, -1], [1, np.nan], [0, 0], [0, 1], [-1, 1], [-1, -1]], + [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, + Path.LINETO, Path.CLOSEPOLY]) + ax_ref[3].add_patch(patches.PathPatch(path, facecolor='none')) + + # NaN in second point of CURVE4 should not re-close, and hide entire curve. + path = Path( + [[-2, -2], [2, -2], [2, 0], [0, np.nan], [0, 2], [-2, 2], [-2, -2]], + [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, + Path.LINETO, Path.LINETO]) + ax_test[3].add_patch(patches.PathPatch(path, facecolor='none')) + path = Path( + [[-2, -2], [2, -2], [2, 0], [0, np.nan], [0, 2], [-2, 2], [-2, -2]], + [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, + Path.LINETO, Path.LINETO]) + ax_ref[3].add_patch(patches.PathPatch(path, facecolor='none')) + + # NaN in third point of CURVE4 should not re-close, and hide entire curve + # plus next line segment. + path = Path( + [[-3, -3], [3, -3], [3, 0], [0, 0], [0, np.nan], [-3, 3], [-3, -3]], + [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, + Path.LINETO, Path.LINETO]) + ax_test[3].add_patch(patches.PathPatch(path, facecolor='none')) + path = Path( + [[-3, -3], [3, -3], [3, 0], [0, 0], [0, np.nan], [-3, 3], [-3, -3]], + [Path.MOVETO, Path.LINETO, Path.CURVE4, Path.CURVE4, Path.CURVE4, + Path.LINETO, Path.LINETO]) + ax_ref[3].add_patch(patches.PathPatch(path, facecolor='none')) + + # Keep everything clean. + for ax in [*ax_test.flat, *ax_ref.flat]: + ax.set(xlim=(-3.5, 3.5), ylim=(-3.5, 3.5)) + remove_ticks_and_titles(fig_test) + remove_ticks_and_titles(fig_ref) + + +@check_figures_equal() +def test_closed_path_clipping(fig_test, fig_ref): + vertices = [] + for roll in range(8): + offset = 0.1 * roll + 0.1 + + # A U-like pattern. + pattern = [ + [-0.5, 1.5], [-0.5, -0.5], [1.5, -0.5], [1.5, 1.5], # Outer square + # With a notch in the top. + [1 - offset / 2, 1.5], [1 - offset / 2, offset], + [offset / 2, offset], [offset / 2, 1.5], + ] + + # Place the initial/final point anywhere in/out of the clipping area. + pattern = np.roll(pattern, roll, axis=0) + pattern = np.concatenate((pattern, pattern[:1, :])) + + vertices.append(pattern) + + # Multiple subpaths are used here to ensure they aren't broken by closed + # loop clipping. + codes = np.full(len(vertices[0]), Path.LINETO) + codes[0] = Path.MOVETO + codes[-1] = Path.CLOSEPOLY + codes = np.tile(codes, len(vertices)) + vertices = np.concatenate(vertices) + + fig_test.set_size_inches((5, 5)) + path = Path(vertices, codes) + fig_test.add_artist(patches.PathPatch(path, facecolor='none')) + + # For reference, we draw the same thing, but unclosed by using a line to + # the last point only. + fig_ref.set_size_inches((5, 5)) + codes = codes.copy() + codes[codes == Path.CLOSEPOLY] = Path.LINETO + path = Path(vertices, codes) + fig_ref.add_artist(patches.PathPatch(path, facecolor='none')) + + @image_comparison(['hatch_simplify'], remove_text=True) def test_hatch(): fig, ax = plt.subplots() @@ -282,8 +455,8 @@ def test_start_with_moveto(): def test_throw_rendering_complexity_exceeded(): plt.rcParams['path.simplify'] = False - xx = np.arange(200000) - yy = np.random.rand(200000) + xx = np.arange(2_000_000) + yy = np.random.rand(2_000_000) yy[1000] = np.nan fig, ax = plt.subplots() diff --git a/lib/matplotlib/tests/test_sphinxext.py b/lib/matplotlib/tests/test_sphinxext.py index 5d48a9817c83..41575d3a3ce1 100644 --- a/lib/matplotlib/tests/test_sphinxext.py +++ b/lib/matplotlib/tests/test_sphinxext.py @@ -3,23 +3,22 @@ import filecmp import os from pathlib import Path +import shutil from subprocess import Popen, PIPE import sys import pytest -pytest.importorskip('sphinx') +pytest.importorskip('sphinx', + minversion=None if sys.version_info < (3, 10) else '4.1.3') -def test_tinypages(tmpdir): - tmp_path = Path(tmpdir) - html_dir = tmp_path / 'html' - doctree_dir = tmp_path / 'doctrees' +def build_sphinx_html(source_dir, doctree_dir, html_dir, extra_args=None): # Build the pages with warnings turned into errors + extra_args = [] if extra_args is None else extra_args cmd = [sys.executable, '-msphinx', '-W', '-b', 'html', - '-d', str(doctree_dir), - str(Path(__file__).parent / 'tinypages'), str(html_dir)] + '-d', str(doctree_dir), str(source_dir), str(html_dir), *extra_args] proc = Popen(cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, env={**os.environ, "MPLBACKEND": ""}) out, err = proc.communicate() @@ -31,8 +30,34 @@ def test_tinypages(tmpdir): assert html_dir.is_dir() + +def test_tinypages(tmp_path): + shutil.copytree(Path(__file__).parent / 'tinypages', tmp_path, + dirs_exist_ok=True) + html_dir = tmp_path / '_build' / 'html' + img_dir = html_dir / '_images' + doctree_dir = tmp_path / 'doctrees' + # Build the pages with warnings turned into errors + cmd = [sys.executable, '-msphinx', '-W', '-b', 'html', + '-d', str(doctree_dir), + str(Path(__file__).parent / 'tinypages'), str(html_dir)] + # On CI, gcov emits warnings (due to agg headers being included with the + # same name in multiple extension modules -- but we don't care about their + # coverage anyways); hide them using GCOV_ERROR_FILE. + proc = Popen( + cmd, stdout=PIPE, stderr=PIPE, universal_newlines=True, + env={**os.environ, "MPLBACKEND": "", "GCOV_ERROR_FILE": os.devnull}) + out, err = proc.communicate() + + # Build the pages with warnings turned into errors + build_sphinx_html(tmp_path, doctree_dir, html_dir) + def plot_file(num): - return html_dir / f'some_plots-{num}.png' + return img_dir / f'some_plots-{num}.png' + + def plot_directive_file(num): + # This is always next to the doctree dir. + return doctree_dir.parent / 'plot_directive' / f'some_plots-{num}.png' range_10, range_6, range_4 = [plot_file(i) for i in range(1, 4)] # Plot 5 is range(6) plot @@ -48,10 +73,11 @@ def plot_file(num): assert filecmp.cmp(range_4, plot_file(13)) # Plot 14 has included source html_contents = (html_dir / 'some_plots.html').read_bytes() + assert b'# Only a comment' in html_contents # check plot defined in external file. - assert filecmp.cmp(range_4, html_dir / 'range4.png') - assert filecmp.cmp(range_6, html_dir / 'range6.png') + assert filecmp.cmp(range_4, img_dir / 'range4.png') + assert filecmp.cmp(range_6, img_dir / 'range6_range6.png') # check if figure caption made it into html file assert b'This is the caption for plot 15.' in html_contents # check if figure caption using :caption: made it into html file @@ -62,3 +88,93 @@ def plot_file(num): assert b'plot-directive my-class my-other-class' in html_contents # check that the multi-image caption is applied twice assert html_contents.count(b'This caption applies to both plots.') == 2 + # Plot 21 is range(6) plot via an include directive. But because some of + # the previous plots are repeated, the argument to plot_file() is only 17. + assert filecmp.cmp(range_6, plot_file(17)) + # plot 22 is from the range6.py file again, but a different function + assert filecmp.cmp(range_10, img_dir / 'range6_range10.png') + + # Modify the included plot + contents = (tmp_path / 'included_plot_21.rst').read_bytes() + contents = contents.replace(b'plt.plot(range(6))', b'plt.plot(range(4))') + (tmp_path / 'included_plot_21.rst').write_bytes(contents) + # Build the pages again and check that the modified file was updated + modification_times = [plot_directive_file(i).stat().st_mtime + for i in (1, 2, 3, 5)] + build_sphinx_html(tmp_path, doctree_dir, html_dir) + assert filecmp.cmp(range_4, plot_file(17)) + # Check that the plots in the plot_directive folder weren't changed. + # (plot_directive_file(1) won't be modified, but it will be copied to html/ + # upon compilation, so plot_file(1) will be modified) + assert plot_directive_file(1).stat().st_mtime == modification_times[0] + assert plot_directive_file(2).stat().st_mtime == modification_times[1] + assert plot_directive_file(3).stat().st_mtime == modification_times[2] + assert filecmp.cmp(range_10, plot_file(1)) + assert filecmp.cmp(range_6, plot_file(2)) + assert filecmp.cmp(range_4, plot_file(3)) + # Make sure that figures marked with context are re-created (but that the + # contents are the same) + assert plot_directive_file(5).stat().st_mtime > modification_times[3] + assert filecmp.cmp(range_6, plot_file(5)) + + +def test_plot_html_show_source_link(tmp_path): + parent = Path(__file__).parent + shutil.copyfile(parent / 'tinypages/conf.py', tmp_path / 'conf.py') + shutil.copytree(parent / 'tinypages/_static', tmp_path / '_static') + doctree_dir = tmp_path / 'doctrees' + (tmp_path / 'index.rst').write_text(""" +.. plot:: + + plt.plot(range(2)) +""") + # Make sure source scripts are created by default + html_dir1 = tmp_path / '_build' / 'html1' + build_sphinx_html(tmp_path, doctree_dir, html_dir1) + assert len(list(html_dir1.glob("**/index-1.py"))) == 1 + # Make sure source scripts are NOT created when + # plot_html_show_source_link` is False + html_dir2 = tmp_path / '_build' / 'html2' + build_sphinx_html(tmp_path, doctree_dir, html_dir2, + extra_args=['-D', 'plot_html_show_source_link=0']) + assert len(list(html_dir2.glob("**/index-1.py"))) == 0 + + +@pytest.mark.parametrize('plot_html_show_source_link', [0, 1]) +def test_show_source_link_true(tmp_path, plot_html_show_source_link): + # Test that a source link is generated if :show-source-link: is true, + # whether or not plot_html_show_source_link is true. + parent = Path(__file__).parent + shutil.copyfile(parent / 'tinypages/conf.py', tmp_path / 'conf.py') + shutil.copytree(parent / 'tinypages/_static', tmp_path / '_static') + doctree_dir = tmp_path / 'doctrees' + (tmp_path / 'index.rst').write_text(""" +.. plot:: + :show-source-link: true + + plt.plot(range(2)) +""") + html_dir = tmp_path / '_build' / 'html' + build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=[ + '-D', f'plot_html_show_source_link={plot_html_show_source_link}']) + assert len(list(html_dir.glob("**/index-1.py"))) == 1 + + +@pytest.mark.parametrize('plot_html_show_source_link', [0, 1]) +def test_show_source_link_false(tmp_path, plot_html_show_source_link): + # Test that a source link is NOT generated if :show-source-link: is false, + # whether or not plot_html_show_source_link is true. + parent = Path(__file__).parent + shutil.copyfile(parent / 'tinypages/conf.py', tmp_path / 'conf.py') + shutil.copytree(parent / 'tinypages/_static', tmp_path / '_static') + doctree_dir = tmp_path / 'doctrees' + (tmp_path / 'index.rst').write_text(""" +.. plot:: + :show-source-link: false + + plt.plot(range(2)) +""") + html_dir = tmp_path / '_build' / 'html' + build_sphinx_html(tmp_path, doctree_dir, html_dir, extra_args=[ + '-D', f'plot_html_show_source_link={plot_html_show_source_link}']) + assert len(list(html_dir.glob("**/index-1.py"))) == 0 diff --git a/lib/matplotlib/tests/test_spines.py b/lib/matplotlib/tests/test_spines.py index 589badc310d9..89bc0c872de5 100644 --- a/lib/matplotlib/tests/test_spines.py +++ b/lib/matplotlib/tests/test_spines.py @@ -35,6 +35,8 @@ def set_val(self, val): spines[:].set_val('y') assert all(spine.val == 'y' for spine in spines.values()) + with pytest.raises(AttributeError, match='foo'): + spines.foo with pytest.raises(KeyError, match='foo'): spines['foo'] with pytest.raises(KeyError, match='foo, bar'): @@ -132,3 +134,17 @@ def test_label_without_ticks(): spine.get_path()).get_extents() assert ax.xaxis.label.get_position()[1] < spinebbox.ymin, \ "X-Axis label not below the spine" + + +@image_comparison(['black_axes']) +def test_spines_black_axes(): + # GitHub #18804 + plt.rcParams["savefig.pad_inches"] = 0 + plt.rcParams["savefig.bbox"] = 'tight' + fig = plt.figure(0, figsize=(4, 4)) + ax = fig.add_axes((0, 0, 1, 1)) + ax.set_xticklabels([]) + ax.set_yticklabels([]) + ax.set_xticks([]) + ax.set_yticks([]) + ax.set_facecolor((0, 0, 0)) diff --git a/lib/matplotlib/tests/test_streamplot.py b/lib/matplotlib/tests/test_streamplot.py index 3ed111881782..10a64f1d6968 100644 --- a/lib/matplotlib/tests/test_streamplot.py +++ b/lib/matplotlib/tests/test_streamplot.py @@ -1,5 +1,3 @@ -import sys - import numpy as np from numpy.testing import assert_array_almost_equal import pytest @@ -8,19 +6,15 @@ import matplotlib.transforms as mtransforms -on_win = (sys.platform == 'win32') -on_mac = (sys.platform == 'darwin') - - def velocity_field(): - Y, X = np.mgrid[-3:3:100j, -3:3:100j] + Y, X = np.mgrid[-3:3:100j, -3:3:200j] U = -1 - X**2 + Y V = 1 + X - Y**2 return X, Y, U, V def swirl_velocity_field(): - x = np.linspace(-3., 3., 100) + x = np.linspace(-3., 3., 200) y = np.linspace(-3., 3., 100) X, Y = np.meshgrid(x, y) a = 0.1 @@ -29,61 +23,51 @@ def swirl_velocity_field(): return x, y, U, V -@image_comparison(['streamplot_startpoints'], remove_text=True, style='mpl20') +@image_comparison(['streamplot_startpoints'], remove_text=True, style='mpl20', + extensions=['png']) def test_startpoints(): X, Y, U, V = velocity_field() - start_x = np.linspace(X.min(), X.max(), 10) - start_y = np.linspace(Y.min(), Y.max(), 10) - start_points = np.column_stack([start_x, start_y]) + start_x, start_y = np.meshgrid(np.linspace(X.min(), X.max(), 5), + np.linspace(Y.min(), Y.max(), 5)) + start_points = np.column_stack([start_x.ravel(), start_y.ravel()]) plt.streamplot(X, Y, U, V, start_points=start_points) plt.plot(start_x, start_y, 'ok') -@image_comparison(['streamplot_colormap'], - tol=.04, remove_text=True, style='mpl20') +@image_comparison(['streamplot_colormap'], remove_text=True, style='mpl20', + tol=0.022) def test_colormap(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - X, Y, U, V = velocity_field() plt.streamplot(X, Y, U, V, color=U, density=0.6, linewidth=2, cmap=plt.cm.autumn) plt.colorbar() -@image_comparison(['streamplot_linewidth'], remove_text=True, style='mpl20') +@image_comparison(['streamplot_linewidth'], remove_text=True, style='mpl20', + tol=0.002) def test_linewidth(): X, Y, U, V = velocity_field() speed = np.hypot(U, V) lw = 5 * speed / speed.max() - # Compatibility for old test image - df = 25 / 30 ax = plt.figure().subplots() - ax.set(xlim=(-3.0, 2.9999999999999947), - ylim=(-3.0000000000000004, 2.9999999999999947)) - ax.streamplot(X, Y, U, V, density=[0.5 * df, 1. * df], color='k', - linewidth=lw) + ax.streamplot(X, Y, U, V, density=[0.5, 1], color='k', linewidth=lw) @image_comparison(['streamplot_masks_and_nans'], - remove_text=True, style='mpl20', tol=0.04 if on_win else 0) + remove_text=True, style='mpl20') def test_masks_and_nans(): X, Y, U, V = velocity_field() mask = np.zeros(U.shape, dtype=bool) - mask[40:60, 40:60] = 1 - U[:20, :20] = np.nan + mask[40:60, 80:120] = 1 + U[:20, :40] = np.nan U = np.ma.array(U, mask=mask) - # Compatibility for old test image ax = plt.figure().subplots() - ax.set(xlim=(-3.0, 2.9999999999999947), - ylim=(-3.0000000000000004, 2.9999999999999947)) with np.errstate(invalid='ignore'): ax.streamplot(X, Y, U, V, color=U, cmap=plt.cm.Blues) @image_comparison(['streamplot_maxlength.png'], - remove_text=True, style='mpl20', - tol=0.002 if on_mac else 0) + remove_text=True, style='mpl20', tol=0.302) def test_maxlength(): x, y, U, V = swirl_velocity_field() ax = plt.figure().subplots() @@ -94,8 +78,20 @@ def test_maxlength(): ax.set(xlim=(None, 3.2555988021882305), ylim=(None, 3.078326760195413)) +@image_comparison(['streamplot_maxlength_no_broken.png'], + remove_text=True, style='mpl20', tol=0.302) +def test_maxlength_no_broken(): + x, y, U, V = swirl_velocity_field() + ax = plt.figure().subplots() + ax.streamplot(x, y, U, V, maxlength=10., start_points=[[0., 1.5]], + linewidth=2, density=2, broken_streamlines=False) + assert ax.get_xlim()[-1] == ax.get_ylim()[-1] == 3 + # Compatibility for old test image + ax.set(xlim=(None, 3.2555988021882305), ylim=(None, 3.078326760195413)) + + @image_comparison(['streamplot_direction.png'], - remove_text=True, style='mpl20') + remove_text=True, style='mpl20', tol=0.073) def test_direction(): x, y, U, V = swirl_velocity_field() plt.streamplot(x, y, U, V, integration_direction='backward', @@ -161,3 +157,13 @@ def test_streamplot_grid(): with pytest.raises(ValueError, match="'y' must be strictly increasing"): plt.streamplot(x, y, u, v) + + +def test_streamplot_inputs(): # test no exception occurs. + # fully-masked + plt.streamplot(np.arange(3), np.arange(3), + np.full((3, 3), np.nan), np.full((3, 3), np.nan), + color=np.random.rand(3, 3)) + # array-likes + plt.streamplot(range(3), range(3), + np.random.rand(3, 3), np.random.rand(3, 3)) diff --git a/lib/matplotlib/tests/test_style.py b/lib/matplotlib/tests/test_style.py index 538c89e838c5..07233ef9d01f 100644 --- a/lib/matplotlib/tests/test_style.py +++ b/lib/matplotlib/tests/test_style.py @@ -1,10 +1,9 @@ -from collections import OrderedDict from contextlib import contextmanager -import gc from pathlib import Path from tempfile import TemporaryDirectory import sys +import numpy as np import pytest import matplotlib as mpl @@ -27,7 +26,8 @@ def temp_style(style_name, settings=None): with TemporaryDirectory() as tmpdir: # Write style settings to file in the tmpdir. Path(tmpdir, temp_file).write_text( - "\n".join("{}: {}".format(k, v) for k, v in settings.items())) + "\n".join("{}: {}".format(k, v) for k, v in settings.items()), + encoding="utf-8") # Add tmpdir to style path and reload so we can access this style. USER_LIBRARY_PATHS.append(tmpdir) style.reload_library() @@ -60,7 +60,7 @@ def test_use(): def test_use_url(tmpdir): path = Path(tmpdir, 'file') - path.write_text('axes.facecolor: adeade') + path.write_text('axes.facecolor: adeade', encoding='utf-8') with temp_style('test', DUMMY_SETTINGS): url = ('file:' + ('///' if sys.platform == 'win32' else '') @@ -73,7 +73,7 @@ def test_single_path(tmpdir): mpl.rcParams[PARAM] = 'gray' temp_file = f'text.{STYLE_EXTENSION}' path = Path(tmpdir, temp_file) - path.write_text(f'{PARAM} : {VALUE}') + path.write_text(f'{PARAM} : {VALUE}', encoding='utf-8') with style.context(path): assert mpl.rcParams[PARAM] == VALUE assert mpl.rcParams[PARAM] == 'gray' @@ -138,10 +138,9 @@ def test_context_with_union_of_dict_and_namedstyle(): def test_context_with_badparam(): original_value = 'gray' other_value = 'blue' - d = OrderedDict([(PARAM, original_value), ('badparam', None)]) with style.context({PARAM: other_value}): assert mpl.rcParams[PARAM] == other_value - x = style.context([d]) + x = style.context({PARAM: original_value, 'badparam': None}) with pytest.raises(KeyError): with x: pass @@ -167,7 +166,7 @@ def test_xkcd_no_cm(): assert mpl.rcParams["path.sketch"] is None plt.xkcd() assert mpl.rcParams["path.sketch"] == (1, 100, 2) - gc.collect() + np.testing.break_cycles() assert mpl.rcParams["path.sketch"] == (1, 100, 2) @@ -176,3 +175,35 @@ def test_xkcd_cm(): with plt.xkcd(): assert mpl.rcParams["path.sketch"] == (1, 100, 2) assert mpl.rcParams["path.sketch"] is None + + +def test_deprecated_seaborn_styles(): + with mpl.style.context("seaborn-v0_8-bright"): + seaborn_bright = mpl.rcParams.copy() + assert mpl.rcParams != seaborn_bright + with pytest.warns(mpl._api.MatplotlibDeprecationWarning): + mpl.style.use("seaborn-bright") + assert mpl.rcParams == seaborn_bright + with pytest.warns(mpl._api.MatplotlibDeprecationWarning): + mpl.style.library["seaborn-bright"] + + +def test_up_to_date_blacklist(): + assert mpl.style.core.STYLE_BLACKLIST <= {*mpl.rcsetup._validators} + + +def test_style_from_module(tmp_path, monkeypatch): + monkeypatch.syspath_prepend(tmp_path) + monkeypatch.chdir(tmp_path) + pkg_path = tmp_path / "mpl_test_style_pkg" + pkg_path.mkdir() + (pkg_path / "test_style.mplstyle").write_text( + "lines.linewidth: 42", encoding="utf-8") + pkg_path.with_suffix(".mplstyle").write_text( + "lines.linewidth: 84", encoding="utf-8") + mpl.style.use("mpl_test_style_pkg.test_style") + assert mpl.rcParams["lines.linewidth"] == 42 + mpl.style.use("mpl_test_style_pkg.mplstyle") + assert mpl.rcParams["lines.linewidth"] == 84 + mpl.style.use("./mpl_test_style_pkg.mplstyle") + assert mpl.rcParams["lines.linewidth"] == 84 diff --git a/lib/matplotlib/tests/test_subplots.py b/lib/matplotlib/tests/test_subplots.py index 7c71067322ae..462dc55d8a8d 100644 --- a/lib/matplotlib/tests/test_subplots.py +++ b/lib/matplotlib/tests/test_subplots.py @@ -3,9 +3,9 @@ import numpy as np import pytest +from matplotlib.axes import Axes, SubplotBase import matplotlib.pyplot as plt -from matplotlib.testing.decorators import image_comparison -import matplotlib.axes as maxes +from matplotlib.testing.decorators import check_figures_equal, image_comparison def check_shared(axs, x_shared, y_shared): @@ -19,9 +19,7 @@ def check_shared(axs, x_shared, y_shared): enumerate(zip("xy", [x_shared, y_shared]))): if i2 <= i1: continue - assert \ - (getattr(axs[0], "_shared_{}_axes".format(name)).joined(ax1, ax2) - == shared[i1, i2]), \ + assert axs[0]._shared_axes[name].joined(ax1, ax2) == shared[i1, i2], \ "axes %i and %i incorrectly %ssharing %s axis" % ( i1, i2, "not " if shared[i1, i2] else "", name) @@ -34,6 +32,12 @@ def check_visible(axs, x_visible, y_visible): for l in ax.get_yticklabels() + [ax.yaxis.offsetText]: assert l.get_visible() == vy, \ f"Visibility of y axis #{i} is incorrectly {vy}" + # axis label "visibility" is toggled by label_outer by resetting the + # label to empty, but it can also be empty to start with. + if not vx: + assert ax.get_xlabel() == "" + if not vy: + assert ax.get_ylabel() == "" def test_shared(): @@ -80,7 +84,7 @@ def test_shared(): plt.close(f) # test all option combinations - ops = [False, True, 'all', 'none', 'row', 'col'] + ops = [False, True, 'all', 'none', 'row', 'col', 0, 1] for xo in ops: for yo in ops: f, ((a1, a2), (a3, a4)) = plt.subplots(2, 2, sharex=xo, sharey=yo) @@ -93,6 +97,7 @@ def test_shared(): f, ((a1, a2), (a3, a4)) = plt.subplots(2, 2, sharex=True, sharey=True) axs = [a1, a2, a3, a4] for ax in axs: + ax.set(xlabel="foo", ylabel="bar") ax.label_outer() check_visible(axs, [False, False, True, True], [True, False, True, False]) @@ -117,6 +122,12 @@ def test_label_outer_span(): fig.axes, [False, True, False, True], [True, True, False, False]) +def test_label_outer_non_gridspec(): + ax = plt.axes([0, 0, 1, 1]) + ax.label_outer() # Does nothing. + check_visible([ax], [True], [True]) + + def test_shared_and_moved(): # test if sharey is on, but then tick_left is called that labels don't # re-appear. Seaborn does this just to be sure yaxis is on left... @@ -137,20 +148,9 @@ def test_exceptions(): plt.subplots(2, 2, sharex='blah') with pytest.raises(ValueError): plt.subplots(2, 2, sharey='blah') - # We filter warnings in this test which are genuine since - # the point of this test is to ensure that this raises. - with pytest.warns(UserWarning, match='.*sharex argument to subplots'), \ - pytest.raises(ValueError): - plt.subplots(2, 2, -1) - with pytest.warns(UserWarning, match='.*sharex argument to subplots'), \ - pytest.raises(ValueError): - plt.subplots(2, 2, 0) - with pytest.warns(UserWarning, match='.*sharex argument to subplots'), \ - pytest.raises(ValueError): - plt.subplots(2, 2, 5) - - -@image_comparison(['subplots_offset_text'], remove_text=False) + + +@image_comparison(['subplots_offset_text']) def test_subplots_offsettext(): x = np.arange(0, 1e10, 1e9) y = np.arange(0, 100, 10)+1e4 @@ -165,7 +165,7 @@ def test_subplots_offsettext(): @pytest.mark.parametrize("bottom", [True, False]) @pytest.mark.parametrize("left", [True, False]) @pytest.mark.parametrize("right", [True, False]) -def test_subplots_hide_labels(top, bottom, left, right): +def test_subplots_hide_ticklabels(top, bottom, left, right): # Ideally, we would also test offset-text visibility (and remove # test_subplots_offsettext), but currently, setting rcParams fails to move # the offset texts as well. @@ -183,6 +183,23 @@ def test_subplots_hide_labels(top, bottom, left, right): assert yright == (right and j == 2) +@pytest.mark.parametrize("xlabel_position", ["bottom", "top"]) +@pytest.mark.parametrize("ylabel_position", ["left", "right"]) +def test_subplots_hide_axislabels(xlabel_position, ylabel_position): + axs = plt.figure().subplots(3, 3, sharex=True, sharey=True) + for (i, j), ax in np.ndenumerate(axs): + ax.set(xlabel="foo", ylabel="bar") + ax.xaxis.set_label_position(xlabel_position) + ax.yaxis.set_label_position(ylabel_position) + ax.label_outer() + assert bool(ax.get_xlabel()) == ( + xlabel_position == "bottom" and i == 2 + or xlabel_position == "top" and i == 0) + assert bool(ax.get_ylabel()) == ( + ylabel_position == "left" and j == 0 + or ylabel_position == "right" and j == 2) + + def test_get_gridspec(): # ahem, pretty trivial, but... fig, ax = plt.subplots() @@ -198,6 +215,48 @@ def test_dont_mutate_kwargs(): assert gridspec_kw == {'width_ratios': [1, 2]} -def test_subplot_factory_reapplication(): - assert maxes.subplot_class_factory(maxes.Axes) is maxes.Subplot - assert maxes.subplot_class_factory(maxes.Subplot) is maxes.Subplot +@pytest.mark.parametrize("width_ratios", [None, [1, 3, 2]]) +@pytest.mark.parametrize("height_ratios", [None, [1, 2]]) +@check_figures_equal(extensions=['png']) +def test_width_and_height_ratios(fig_test, fig_ref, + height_ratios, width_ratios): + fig_test.subplots(2, 3, height_ratios=height_ratios, + width_ratios=width_ratios) + fig_ref.subplots(2, 3, gridspec_kw={ + 'height_ratios': height_ratios, + 'width_ratios': width_ratios}) + + +@pytest.mark.parametrize("width_ratios", [None, [1, 3, 2]]) +@pytest.mark.parametrize("height_ratios", [None, [1, 2]]) +@check_figures_equal(extensions=['png']) +def test_width_and_height_ratios_mosaic(fig_test, fig_ref, + height_ratios, width_ratios): + mosaic_spec = [['A', 'B', 'B'], ['A', 'C', 'D']] + fig_test.subplot_mosaic(mosaic_spec, height_ratios=height_ratios, + width_ratios=width_ratios) + fig_ref.subplot_mosaic(mosaic_spec, gridspec_kw={ + 'height_ratios': height_ratios, + 'width_ratios': width_ratios}) + + +@pytest.mark.parametrize('method,args', [ + ('subplots', (2, 3)), + ('subplot_mosaic', ('abc;def', )) + ] +) +def test_ratio_overlapping_kws(method, args): + with pytest.raises(ValueError, match='height_ratios'): + getattr(plt, method)(*args, height_ratios=[1, 2], + gridspec_kw={'height_ratios': [1, 2]}) + with pytest.raises(ValueError, match='width_ratios'): + getattr(plt, method)(*args, width_ratios=[1, 2, 3], + gridspec_kw={'width_ratios': [1, 2, 3]}) + + +def test_old_subplot_compat(): + fig = plt.figure() + assert isinstance(fig.add_subplot(), SubplotBase) + assert not isinstance(fig.add_axes(rect=[0, 0, 1, 1]), SubplotBase) + with pytest.raises(TypeError): + Axes(fig, [0, 0, 1, 1], rect=[0, 0, 1, 1]) diff --git a/lib/matplotlib/tests/test_table.py b/lib/matplotlib/tests/test_table.py index d0537c3b2ddf..9b2cb96ea037 100644 --- a/lib/matplotlib/tests/test_table.py +++ b/lib/matplotlib/tests/test_table.py @@ -1,9 +1,10 @@ import matplotlib.pyplot as plt import numpy as np -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import image_comparison, check_figures_equal from matplotlib.table import CustomCell, Table from matplotlib.path import Path +from matplotlib.transforms import Bbox def test_non_square(): @@ -149,7 +150,7 @@ def test_auto_column(): tb2.set_fontsize(12) tb2.auto_set_column_width((-1, 0, 1)) - #3 single inputs + # 3 single inputs ax3 = fig.add_subplot(4, 1, 3) ax3.axis('off') tb3 = ax3.table( @@ -164,7 +165,7 @@ def test_auto_column(): tb3.auto_set_column_width(0) tb3.auto_set_column_width(1) - #4 non integer iterable input + # 4 non integer iterable input ax4 = fig.add_subplot(4, 1, 4) ax4.axis('off') tb4 = ax4.table( @@ -194,3 +195,30 @@ def test_table_cells(): # properties and setp table.properties() plt.setp(table) + + +@check_figures_equal(extensions=["png"]) +def test_table_bbox(fig_test, fig_ref): + data = [[2, 3], + [4, 5]] + + col_labels = ('Foo', 'Bar') + row_labels = ('Ada', 'Bob') + + cell_text = [[f"{x}" for x in row] for row in data] + + ax_list = fig_test.subplots() + ax_list.table(cellText=cell_text, + rowLabels=row_labels, + colLabels=col_labels, + loc='center', + bbox=[0.1, 0.2, 0.8, 0.6] + ) + + ax_bbox = fig_ref.subplots() + ax_bbox.table(cellText=cell_text, + rowLabels=row_labels, + colLabels=col_labels, + loc='center', + bbox=Bbox.from_extents(0.1, 0.2, 0.9, 0.8) + ) diff --git a/lib/matplotlib/tests/test_texmanager.py b/lib/matplotlib/tests/test_texmanager.py index 170a8362e287..fbff21144e60 100644 --- a/lib/matplotlib/tests/test_texmanager.py +++ b/lib/matplotlib/tests/test_texmanager.py @@ -1,18 +1,74 @@ +import os +from pathlib import Path +import re +import subprocess +import sys + import matplotlib.pyplot as plt from matplotlib.texmanager import TexManager +from matplotlib.testing._markers import needs_usetex +import pytest def test_fontconfig_preamble(): - """ - Test that the preamble is included in _fontconfig - """ + """Test that the preamble is included in the source.""" plt.rcParams['text.usetex'] = True - tm1 = TexManager() - font_config1 = tm1.get_font_config() - + src1 = TexManager()._get_tex_source("", fontsize=12) plt.rcParams['text.latex.preamble'] = '\\usepackage{txfonts}' - tm2 = TexManager() - font_config2 = tm2.get_font_config() + src2 = TexManager()._get_tex_source("", fontsize=12) + + assert src1 != src2 + + +@pytest.mark.parametrize( + "rc, preamble, family", [ + ({"font.family": "sans-serif", "font.sans-serif": "helvetica"}, + r"\usepackage{helvet}", r"\sffamily"), + ({"font.family": "serif", "font.serif": "palatino"}, + r"\usepackage{mathpazo}", r"\rmfamily"), + ({"font.family": "cursive", "font.cursive": "zapf chancery"}, + r"\usepackage{chancery}", r"\rmfamily"), + ({"font.family": "monospace", "font.monospace": "courier"}, + r"\usepackage{courier}", r"\ttfamily"), + ({"font.family": "helvetica"}, r"\usepackage{helvet}", r"\sffamily"), + ({"font.family": "palatino"}, r"\usepackage{mathpazo}", r"\rmfamily"), + ({"font.family": "zapf chancery"}, + r"\usepackage{chancery}", r"\rmfamily"), + ({"font.family": "courier"}, r"\usepackage{courier}", r"\ttfamily") + ]) +def test_font_selection(rc, preamble, family): + plt.rcParams.update(rc) + tm = TexManager() + src = Path(tm.make_tex("hello, world", fontsize=12)).read_text() + assert preamble in src + assert [*re.findall(r"\\\w+family", src)] == [family] + + +@needs_usetex +def test_unicode_characters(): + # Smoke test to see that Unicode characters does not cause issues + # See #23019 + plt.rcParams['text.usetex'] = True + fig, ax = plt.subplots() + ax.set_ylabel('\\textit{Velocity (\N{DEGREE SIGN}/sec)}') + ax.set_xlabel('\N{VULGAR FRACTION ONE QUARTER}Öøæ') + fig.canvas.draw() + + # But not all characters. + # Should raise RuntimeError, not UnicodeDecodeError + with pytest.raises(RuntimeError): + ax.set_title('\N{SNOWMAN}') + fig.canvas.draw() + - assert font_config1 != font_config2 +@needs_usetex +def test_openin_any_paranoid(): + completed = subprocess.run( + [sys.executable, "-c", + 'import matplotlib.pyplot as plt;' + 'plt.rcParams.update({"text.usetex": True});' + 'plt.title("paranoid");' + 'plt.show(block=False);'], + env={**os.environ, 'openin_any': 'p'}, check=True, capture_output=True) + assert completed.stderr == b"" diff --git a/lib/matplotlib/tests/test_text.py b/lib/matplotlib/tests/test_text.py index 435da825a2cd..5c431b4e8303 100644 --- a/lib/matplotlib/tests/test_text.py +++ b/lib/matplotlib/tests/test_text.py @@ -13,14 +13,10 @@ import matplotlib.pyplot as plt import matplotlib.transforms as mtransforms from matplotlib.testing.decorators import check_figures_equal, image_comparison +from matplotlib.testing._markers import needs_usetex from matplotlib.text import Text -needs_usetex = pytest.mark.skipif( - not mpl.checkdep_usetex(True), - reason="This test needs a TeX installation") - - @image_comparison(['font_styles']) def test_font_styles(): @@ -43,11 +39,16 @@ def find_matplotlib_font(**kw): style="normal", variant="normal", size=14) - ax.annotate( + a = ax.annotate( "Normal Font", (0.1, 0.1), xycoords='axes fraction', fontproperties=normal_font) + assert a.get_fontname() == 'DejaVu Sans' + assert a.get_fontstyle() == 'normal' + assert a.get_fontvariant() == 'normal' + assert a.get_weight() == 'normal' + assert a.get_stretch() == 'normal' bold_font = find_matplotlib_font( family="Foo", @@ -199,7 +200,7 @@ def test_antialiasing(): def test_afm_kerning(): fn = mpl.font_manager.findfont("Helvetica", fontext="afm") with open(fn, 'rb') as fh: - afm = mpl.afm.AFM(fh) + afm = mpl._afm.AFM(fh) assert afm.string_width_height('VAVAVAVAVAVA') == (7174.0, 718) @@ -240,12 +241,27 @@ def test_annotation_contains(): fig, ax = plt.subplots() ann = ax.annotate( "hello", xy=(.4, .4), xytext=(.6, .6), arrowprops={"arrowstyle": "->"}) - fig.canvas.draw() # Needed for the same reason as in test_contains. + fig.canvas.draw() # Needed for the same reason as in test_contains. event = MouseEvent( "button_press_event", fig.canvas, *ax.transData.transform((.5, .6))) assert ann.contains(event) == (False, {}) +@pytest.mark.parametrize('err, xycoords, match', ( + (RuntimeError, print, "Unknown return type"), + (RuntimeError, [0, 0], r"Unknown coordinate type: \[0, 0\]"), + (ValueError, "foo", "'foo' is not a recognized coordinate"), + (ValueError, "foo bar", "'foo bar' is not a recognized coordinate"), + (ValueError, "offset foo", "xycoords cannot be an offset coordinate"), + (ValueError, "axes foo", "'foo' is not a recognized unit"), +)) +def test_annotate_errors(err, xycoords, match): + fig, ax = plt.subplots() + with pytest.raises(err, match=match): + ax.annotate('xy', (0, 0), xytext=(0.5, 0.5), xycoords=xycoords) + fig.canvas.draw() + + @image_comparison(['titles']) def test_titles(): # left and right side titles @@ -323,6 +339,32 @@ def test_set_position(): assert a + shift_val == b +def test_char_index_at(): + fig = plt.figure() + text = fig.text(0.1, 0.9, "") + + text.set_text("i") + bbox = text.get_window_extent() + size_i = bbox.x1 - bbox.x0 + + text.set_text("m") + bbox = text.get_window_extent() + size_m = bbox.x1 - bbox.x0 + + text.set_text("iiiimmmm") + bbox = text.get_window_extent() + origin = bbox.x0 + + assert text._char_index_at(origin - size_i) == 0 # left of first char + assert text._char_index_at(origin) == 0 + assert text._char_index_at(origin + 0.499*size_i) == 0 + assert text._char_index_at(origin + 0.501*size_i) == 1 + assert text._char_index_at(origin + size_i*3) == 3 + assert text._char_index_at(origin + size_i*4 + size_m*3) == 7 + assert text._char_index_at(origin + size_i*4 + size_m*4) == 8 + assert text._char_index_at(origin + size_i*4 + size_m*10) == 8 + + @pytest.mark.parametrize('text', ['', 'O'], ids=['empty', 'non-empty']) def test_non_default_dpi(text): fig, ax = plt.subplots() @@ -340,33 +382,32 @@ def test_non_default_dpi(text): def test_get_rotation_string(): - assert mpl.text.get_rotation('horizontal') == 0. - assert mpl.text.get_rotation('vertical') == 90. - assert mpl.text.get_rotation('15.') == 15. + assert Text(rotation='horizontal').get_rotation() == 0. + assert Text(rotation='vertical').get_rotation() == 90. def test_get_rotation_float(): for i in [15., 16.70, 77.4]: - assert mpl.text.get_rotation(i) == i + assert Text(rotation=i).get_rotation() == i def test_get_rotation_int(): for i in [67, 16, 41]: - assert mpl.text.get_rotation(i) == float(i) + assert Text(rotation=i).get_rotation() == float(i) def test_get_rotation_raises(): with pytest.raises(ValueError): - mpl.text.get_rotation('hozirontal') + Text(rotation='hozirontal') def test_get_rotation_none(): - assert mpl.text.get_rotation(None) == 0.0 + assert Text(rotation=None).get_rotation() == 0.0 def test_get_rotation_mod360(): for i, j in zip([360., 377., 720+177.2], [0., 17., 177.2]): - assert_almost_equal(mpl.text.get_rotation(i), j) + assert_almost_equal(Text(rotation=i).get_rotation(), j) @pytest.mark.parametrize("ha", ["center", "right", "left"]) @@ -504,8 +545,8 @@ def test_two_2line_texts(spacing1, spacing2): fig = plt.figure() renderer = fig.canvas.get_renderer() - text1 = plt.text(0.25, 0.5, text_string, linespacing=spacing1) - text2 = plt.text(0.25, 0.5, text_string, linespacing=spacing2) + text1 = fig.text(0.25, 0.5, text_string, linespacing=spacing1) + text2 = fig.text(0.25, 0.5, text_string, linespacing=spacing2) fig.canvas.draw() box1 = text1.get_window_extent(renderer=renderer) @@ -519,6 +560,11 @@ def test_two_2line_texts(spacing1, spacing2): assert box1.height != box2.height +def test_validate_linespacing(): + with pytest.raises(TypeError): + plt.text(.25, .5, "foo", linespacing="abc") + + def test_nonfinite_pos(): fig, ax = plt.subplots() ax.text(0, np.nan, 'nan') @@ -636,12 +682,12 @@ def test_large_subscript_title(): ax = axs[0] ax.set_title(r'$\sum_{i} x_i$') ax.set_title('New way', loc='left') - ax.set_xticklabels('') + ax.set_xticklabels([]) ax = axs[1] ax.set_title(r'$\sum_{i} x_i$', y=1.01) ax.set_title('Old Way', loc='left') - ax.set_xticklabels('') + ax.set_xticklabels([]) def test_wrap(): @@ -655,6 +701,22 @@ def test_wrap(): 'times.') +def test_get_window_extent_wrapped(): + # Test that a long title that wraps to two lines has the same vertical + # extent as an explicit two line title. + + fig1 = plt.figure(figsize=(3, 3)) + fig1.suptitle("suptitle that is clearly too long in this case", wrap=True) + window_extent_test = fig1._suptitle.get_window_extent() + + fig2 = plt.figure(figsize=(3, 3)) + fig2.suptitle("suptitle that is clearly\ntoo long in this case") + window_extent_ref = fig2._suptitle.get_window_extent() + + assert window_extent_test.y0 == window_extent_ref.y0 + assert window_extent_test.y1 == window_extent_ref.y1 + + def test_long_word_wrap(): fig = plt.figure(figsize=(6, 4)) text = fig.text(9.5, 8, 'Alonglineoftexttowrap', wrap=True) @@ -711,6 +773,14 @@ def test_update_mutate_input(): assert inp['bbox'] == cache['bbox'] +@pytest.mark.parametrize('rotation', ['invalid string', [90]]) +def test_invalid_rotation_values(rotation): + with pytest.raises( + ValueError, + match=("rotation must be 'vertical', 'horizontal' or a number")): + Text(0, 0, 'foo', rotation=rotation) + + def test_invalid_color(): with pytest.raises(ValueError): plt.figtext(.5, .5, "foo", c="foobar") @@ -720,3 +790,84 @@ def test_invalid_color(): def test_pdf_kerning(): plt.figure() plt.figtext(0.1, 0.5, "ATATATATATATATATATA", size=30) + + +def test_unsupported_script(recwarn): + fig = plt.figure() + fig.text(.5, .5, "\N{BENGALI DIGIT ZERO}") + fig.canvas.draw() + assert all(isinstance(warn.message, UserWarning) for warn in recwarn) + assert ( + [warn.message.args for warn in recwarn] == + [(r"Glyph 2534 (\N{BENGALI DIGIT ZERO}) missing from current font.",), + (r"Matplotlib currently does not support Bengali natively.",)]) + + +def test_parse_math(): + fig, ax = plt.subplots() + ax.text(0, 0, r"$ \wrong{math} $", parse_math=False) + fig.canvas.draw() + + ax.text(0, 0, r"$ \wrong{math} $", parse_math=True) + with pytest.raises(ValueError, match='Unknown symbol'): + fig.canvas.draw() + + +def test_parse_math_rcparams(): + # Default is True + fig, ax = plt.subplots() + ax.text(0, 0, r"$ \wrong{math} $") + with pytest.raises(ValueError, match='Unknown symbol'): + fig.canvas.draw() + + # Setting rcParams to False + with mpl.rc_context({'text.parse_math': False}): + fig, ax = plt.subplots() + ax.text(0, 0, r"$ \wrong{math} $") + fig.canvas.draw() + + +@image_comparison(['text_pdf_font42_kerning.pdf'], style='mpl20') +def test_pdf_font42_kerning(): + plt.rcParams['pdf.fonttype'] = 42 + plt.figure() + plt.figtext(0.1, 0.5, "ATAVATAVATAVATAVATA", size=30) + + +@image_comparison(['text_pdf_chars_beyond_bmp.pdf'], style='mpl20') +def test_pdf_chars_beyond_bmp(): + plt.rcParams['pdf.fonttype'] = 42 + plt.rcParams['mathtext.fontset'] = 'stixsans' + plt.figure() + plt.figtext(0.1, 0.5, "Mass $m$ \U00010308", size=30) + + +@needs_usetex +def test_metrics_cache(): + mpl.text._get_text_metrics_with_cache_impl.cache_clear() + + fig = plt.figure() + fig.text(.3, .5, "foo\nbar") + fig.text(.3, .5, "foo\nbar", usetex=True) + fig.text(.5, .5, "foo\nbar", usetex=True) + fig.canvas.draw() + renderer = fig._get_renderer() + ys = {} # mapping of strings to where they were drawn in y with draw_tex. + + def call(*args, **kwargs): + renderer, x, y, s, *_ = args + ys.setdefault(s, set()).add(y) + + renderer.draw_tex = call + fig.canvas.draw() + assert [*ys] == ["foo", "bar"] + # Check that both TeX strings were drawn with the same y-position for both + # single-line substrings. Previously, there used to be an incorrect cache + # collision with the non-TeX string (drawn first here) whose metrics would + # get incorrectly reused by the first TeX string. + assert len(ys["foo"]) == len(ys["bar"]) == 1 + + info = mpl.text._get_text_metrics_with_cache_impl.cache_info() + # Every string gets a miss for the first layouting (extents), then a hit + # when drawing, but "foo\nbar" gets two hits as it's drawn twice. + assert info.hits > info.misses diff --git a/lib/matplotlib/tests/test_textpath.py b/lib/matplotlib/tests/test_textpath.py new file mode 100644 index 000000000000..e421d2623cad --- /dev/null +++ b/lib/matplotlib/tests/test_textpath.py @@ -0,0 +1,10 @@ +import copy + +from matplotlib.textpath import TextPath + + +def test_copy(): + tp = TextPath((0, 0), ".") + assert copy.deepcopy(tp).vertices is not tp.vertices + assert (copy.deepcopy(tp).vertices == tp.vertices).all() + assert copy.copy(tp).vertices is tp.vertices diff --git a/lib/matplotlib/tests/test_ticker.py b/lib/matplotlib/tests/test_ticker.py index 6138757447d7..2bea8c999067 100644 --- a/lib/matplotlib/tests/test_ticker.py +++ b/lib/matplotlib/tests/test_ticker.py @@ -1,6 +1,7 @@ from contextlib import nullcontext import itertools import locale +import logging import re import numpy as np @@ -8,7 +9,6 @@ import pytest import matplotlib as mpl -from matplotlib import _api import matplotlib.pyplot as plt import matplotlib.ticker as mticker @@ -208,6 +208,15 @@ def test_basic(self): test_value = np.array([0.5, 1., 2., 4., 8., 16., 32., 64., 128., 256.]) assert_almost_equal(loc.tick_values(1, 100), test_value) + def test_polar_axes(self): + """ + Polar axes have a different ticking logic. + """ + fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}) + ax.set_yscale('log') + ax.set_ylim(1, 100) + assert_array_equal(ax.get_yticks(), [10, 100, 1000]) + def test_switch_to_autolocator(self): loc = mticker.LogLocator(subs="all") assert_array_equal(loc.tick_values(0.45, 0.55), @@ -444,19 +453,110 @@ def test_set_params(self): assert sym.numticks == 8 -class TestIndexFormatter: - @pytest.mark.parametrize('x, label', [(-2, ''), - (-1, 'label0'), - (0, 'label0'), - (0.5, 'label1'), - (1, 'label1'), - (1.5, 'label2'), - (2, 'label2'), - (2.5, '')]) - def test_formatting(self, x, label): - with _api.suppress_matplotlib_deprecation_warning(): - formatter = mticker.IndexFormatter(['label0', 'label1', 'label2']) - assert formatter(x) == label +class TestAsinhLocator: + def test_init(self): + lctr = mticker.AsinhLocator(linear_width=2.718, numticks=19) + assert lctr.linear_width == 2.718 + assert lctr.numticks == 19 + assert lctr.base == 10 + + def test_set_params(self): + lctr = mticker.AsinhLocator(linear_width=5, + numticks=17, symthresh=0.125, + base=4, subs=(2.5, 3.25)) + assert lctr.numticks == 17 + assert lctr.symthresh == 0.125 + assert lctr.base == 4 + assert lctr.subs == (2.5, 3.25) + + lctr.set_params(numticks=23) + assert lctr.numticks == 23 + lctr.set_params(None) + assert lctr.numticks == 23 + + lctr.set_params(symthresh=0.5) + assert lctr.symthresh == 0.5 + lctr.set_params(symthresh=None) + assert lctr.symthresh == 0.5 + + lctr.set_params(base=7) + assert lctr.base == 7 + lctr.set_params(base=None) + assert lctr.base == 7 + + lctr.set_params(subs=(2, 4.125)) + assert lctr.subs == (2, 4.125) + lctr.set_params(subs=None) + assert lctr.subs == (2, 4.125) + lctr.set_params(subs=[]) + assert lctr.subs is None + + def test_linear_values(self): + lctr = mticker.AsinhLocator(linear_width=100, numticks=11, base=0) + + assert_almost_equal(lctr.tick_values(-1, 1), + np.arange(-1, 1.01, 0.2)) + assert_almost_equal(lctr.tick_values(-0.1, 0.1), + np.arange(-0.1, 0.101, 0.02)) + assert_almost_equal(lctr.tick_values(-0.01, 0.01), + np.arange(-0.01, 0.0101, 0.002)) + + def test_wide_values(self): + lctr = mticker.AsinhLocator(linear_width=0.1, numticks=11, base=0) + + assert_almost_equal(lctr.tick_values(-100, 100), + [-100, -20, -5, -1, -0.2, + 0, 0.2, 1, 5, 20, 100]) + assert_almost_equal(lctr.tick_values(-1000, 1000), + [-1000, -100, -20, -3, -0.4, + 0, 0.4, 3, 20, 100, 1000]) + + def test_near_zero(self): + """Check that manually injected zero will supersede nearby tick""" + lctr = mticker.AsinhLocator(linear_width=100, numticks=3, base=0) + + assert_almost_equal(lctr.tick_values(-1.1, 0.9), [-1.0, 0.0, 0.9]) + + def test_fallback(self): + lctr = mticker.AsinhLocator(1.0, numticks=11) + + assert_almost_equal(lctr.tick_values(101, 102), + np.arange(101, 102.01, 0.1)) + + def test_symmetrizing(self): + class DummyAxis: + bounds = (-1, 1) + @classmethod + def get_view_interval(cls): return cls.bounds + + lctr = mticker.AsinhLocator(linear_width=1, numticks=3, + symthresh=0.25, base=0) + lctr.axis = DummyAxis + + DummyAxis.bounds = (-1, 2) + assert_almost_equal(lctr(), [-1, 0, 2]) + + DummyAxis.bounds = (-1, 0.9) + assert_almost_equal(lctr(), [-1, 0, 1]) + + DummyAxis.bounds = (-0.85, 1.05) + assert_almost_equal(lctr(), [-1, 0, 1]) + + DummyAxis.bounds = (1, 1.1) + assert_almost_equal(lctr(), [1, 1.05, 1.1]) + + def test_base_rounding(self): + lctr10 = mticker.AsinhLocator(linear_width=1, numticks=8, + base=10, subs=(1, 3, 5)) + assert_almost_equal(lctr10.tick_values(-110, 110), + [-500, -300, -100, -50, -30, -10, -5, -3, -1, + -0.5, -0.3, -0.1, 0, 0.1, 0.3, 0.5, + 1, 3, 5, 10, 30, 50, 100, 300, 500]) + + lctr5 = mticker.AsinhLocator(linear_width=1, numticks=20, base=5) + assert_almost_equal(lctr5.tick_values(-1050, 1050), + [-625, -125, -25, -5, -1, -0.2, 0, + 0.2, 1, 5, 25, 125, 625]) class TestScalarFormatter: @@ -495,6 +595,8 @@ class TestScalarFormatter: use_offset_data = [True, False] + useMathText_data = [True, False] + # (sci_type, scilimits, lim, orderOfMag, fewticks) scilimits_data = [ (False, (0, 0), (10.0, 20.0), 0, False), @@ -516,6 +618,13 @@ class TestScalarFormatter: [12.3, "12.300"], ] + format_data = [ + (.1, "1e-1"), + (.11, "1.1e-1"), + (1e8, "1e8"), + (1.1e8, "1.1e8"), + ] + @pytest.mark.parametrize('unicode_minus, result', [(True, "\N{MINUS SIGN}1"), (False, "-1")]) def test_unicode_minus(self, unicode_minus, result): @@ -546,6 +655,19 @@ def test_use_offset(self, use_offset): with mpl.rc_context({'axes.formatter.useoffset': use_offset}): tmp_form = mticker.ScalarFormatter() assert use_offset == tmp_form.get_useOffset() + assert tmp_form.offset == 0 + + @pytest.mark.parametrize('use_math_text', useMathText_data) + def test_useMathText(self, use_math_text): + with mpl.rc_context({'axes.formatter.use_mathtext': use_math_text}): + tmp_form = mticker.ScalarFormatter() + assert use_math_text == tmp_form.get_useMathText() + + def test_set_use_offset_float(self): + tmp_form = mticker.ScalarFormatter() + tmp_form.set_useOffset(0.5) + assert not tmp_form.get_useOffset() + assert tmp_form.offset == 0.5 def test_use_locale(self): conv = locale.localeconv() @@ -558,7 +680,7 @@ def test_use_locale(self): assert tmp_form.get_useLocale() tmp_form.create_dummy_axis() - tmp_form.set_bounds(0, 10) + tmp_form.axis.set_data_interval(0, 10) tmp_form.set_locs([1, 2, 3]) assert sep in tmp_form(1e9) @@ -577,6 +699,12 @@ def test_scilimits(self, sci_type, scilimits, lim, orderOfMag, fewticks): tmp_form.set_locs(ax.yaxis.get_majorticklocs()) assert orderOfMag == tmp_form.orderOfMagnitude + @pytest.mark.parametrize('value, expected', format_data) + def test_format_data(self, value, expected): + mpl.rcParams['axes.unicode_minus'] = False + sf = mticker.ScalarFormatter() + assert sf.format_data(value) == expected + @pytest.mark.parametrize('data, expected', cursor_data) def test_cursor_precision(self, data, expected): fig, ax = plt.subplots() @@ -589,9 +717,46 @@ def test_cursor_dummy_axis(self, data, expected): # Issue #17624 sf = mticker.ScalarFormatter() sf.create_dummy_axis() - sf.set_bounds(0, 10) + sf.axis.set_view_interval(0, 10) fmt = sf.format_data_short assert fmt(data) == expected + assert sf.axis.get_tick_space() == 9 + assert sf.axis.get_minpos() == 0 + + def test_mathtext_ticks(self): + mpl.rcParams.update({ + 'font.family': 'serif', + 'font.serif': 'cmr10', + 'axes.formatter.use_mathtext': False + }) + + with pytest.warns(UserWarning, match='cmr10 font should ideally'): + fig, ax = plt.subplots() + ax.set_xticks([-1, 0, 1]) + fig.canvas.draw() + + def test_cmr10_substitutions(self, caplog): + mpl.rcParams.update({ + 'font.family': 'cmr10', + 'mathtext.fontset': 'cm', + 'axes.formatter.use_mathtext': True, + }) + + # Test that it does not log a warning about missing glyphs. + with caplog.at_level(logging.WARNING, logger='matplotlib.mathtext'): + fig, ax = plt.subplots() + ax.plot([-0.03, 0.05], [40, 0.05]) + ax.set_yscale('log') + yticks = [0.02, 0.3, 4, 50] + formatter = mticker.LogFormatterSciNotation() + ax.set_yticks(yticks, map(formatter, yticks)) + fig.canvas.draw() + assert not caplog.text + + def test_empty_locs(self): + sf = mticker.ScalarFormatter() + sf.set_locs([]) + assert sf(0.5) == '' class FakeAxis: @@ -628,6 +793,7 @@ def test_basic(self, labelOnlyBase, base, exponent, locs, positions, formatter.axis = FakeAxis(1, base**exponent) vals = base**locs labels = [formatter(x, pos) for (x, pos) in zip(vals, positions)] + expected = [label.replace('-', '\N{Minus Sign}') for label in expected] assert labels == expected def test_blank(self): @@ -676,7 +842,7 @@ class TestLogFormatterSciNotation: (10, 500000, '$\\mathdefault{5\\times10^{5}}$'), ] - @pytest.mark.style('default') + @mpl.style.context('default') @pytest.mark.parametrize('base, value, expected', test_data) def test_basic(self, base, value, expected): formatter = mticker.LogFormatterSciNotation(base=base) @@ -837,7 +1003,7 @@ def _sub_labels(self, axis, subs=()): label_test = [fmt(x) != '' for x in minor_tlocs] assert label_test == label_expected - @pytest.mark.style('default') + @mpl.style.context('default') def test_sublabel(self): # test label locator fig, ax = plt.subplots() @@ -1171,7 +1337,7 @@ def test_params(self, unicode_minus, input, expected): assert _formatter(input) == _exp_output # Test several non default separators: no separator, a narrow - # no-break space (unicode character) and an extravagant string. + # no-break space (Unicode character) and an extravagant string. for _sep in ("", "\N{NARROW NO-BREAK SPACE}", "@_@"): # Case 2: unit=UNIT and sep=_sep. # Replace the default space separator from the reference case @@ -1368,14 +1534,30 @@ def test_remove_overlap(remove_overlapping_locs, expected_num): def test_bad_locator_subs(sub): ll = mticker.LogLocator() with pytest.raises(ValueError): - ll.subs(sub) + ll.set_params(subs=sub) @pytest.mark.parametrize('numticks', [1, 2, 3, 9]) -@pytest.mark.style('default') +@mpl.style.context('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() + + +def test_NullFormatter(): + formatter = mticker.NullFormatter() + assert formatter(1.0) == '' + assert formatter.format_data(1.0) == '' + assert formatter.format_data_short(1.0) == '' + + +@pytest.mark.parametrize('formatter', ( + mticker.FuncFormatter(lambda a: f'val: {a}'), + mticker.FixedFormatter(('foo', 'bar')))) +def test_set_offset_string(formatter): + assert formatter.get_offset() == '' + formatter.set_offset_string('mpl') + assert formatter.get_offset() == 'mpl' diff --git a/lib/matplotlib/tests/test_tightlayout.py b/lib/matplotlib/tests/test_tightlayout.py index 23d363b5083b..968f0da7b514 100644 --- a/lib/matplotlib/tests/test_tightlayout.py +++ b/lib/matplotlib/tests/test_tightlayout.py @@ -134,9 +134,10 @@ def test_tight_layout7(): def test_tight_layout8(): """Test automatic use of tight_layout.""" fig = plt.figure() - fig.set_tight_layout({'pad': .1}) + fig.set_layout_engine(layout='tight', pad=0.1) ax = fig.add_subplot() example_plot(ax, fontsize=24) + fig.draw_without_rendering() @image_comparison(['tight_layout9']) @@ -329,3 +330,64 @@ def __init__(self, *args, **kwargs): monkeypatch.setattr(mpl.backend_bases.RendererBase, "__init__", __init__) fig, ax = plt.subplots() fig.tight_layout() + + +def test_manual_colorbar(): + # This should warn, but not raise + fig, axes = plt.subplots(1, 2) + pts = axes[1].scatter([0, 1], [0, 1], c=[1, 5]) + ax_rect = axes[1].get_position() + cax = fig.add_axes( + [ax_rect.x1 + 0.005, ax_rect.y0, 0.015, ax_rect.height] + ) + fig.colorbar(pts, cax=cax) + with pytest.warns(UserWarning, match="This figure includes Axes"): + fig.tight_layout() + + +def test_clipped_to_axes(): + # Ensure that _fully_clipped_to_axes() returns True under default + # conditions for all projection types. Axes.get_tightbbox() + # uses this to skip artists in layout calculations. + arr = np.arange(100).reshape((10, 10)) + fig = plt.figure(figsize=(6, 2)) + ax1 = fig.add_subplot(131, projection='rectilinear') + ax2 = fig.add_subplot(132, projection='mollweide') + ax3 = fig.add_subplot(133, projection='polar') + for ax in (ax1, ax2, ax3): + # Default conditions (clipped by ax.bbox or ax.patch) + ax.grid(False) + h, = ax.plot(arr[:, 0]) + m = ax.pcolor(arr) + assert h._fully_clipped_to_axes() + assert m._fully_clipped_to_axes() + # Non-default conditions (not clipped by ax.patch) + rect = Rectangle((0, 0), 0.5, 0.5, transform=ax.transAxes) + h.set_clip_path(rect) + m.set_clip_path(rect.get_path(), rect.get_transform()) + assert not h._fully_clipped_to_axes() + assert not m._fully_clipped_to_axes() + + +def test_tight_pads(): + fig, ax = plt.subplots() + with pytest.warns(PendingDeprecationWarning, + match='will be deprecated'): + fig.set_tight_layout({'pad': 0.15}) + fig.draw_without_rendering() + + +def test_tight_kwargs(): + fig, ax = plt.subplots(tight_layout={'pad': 0.15}) + fig.draw_without_rendering() + + +def test_tight_toggle(): + fig, ax = plt.subplots() + with pytest.warns(PendingDeprecationWarning): + fig.set_tight_layout(True) + assert fig.get_tight_layout() + fig.set_tight_layout(False) + assert not fig.get_tight_layout() + fig.set_tight_layout(True) + assert fig.get_tight_layout() diff --git a/lib/matplotlib/tests/test_transforms.py b/lib/matplotlib/tests/test_transforms.py index ad572fe5287a..064a7240c39d 100644 --- a/lib/matplotlib/tests/test_transforms.py +++ b/lib/matplotlib/tests/test_transforms.py @@ -10,7 +10,7 @@ import matplotlib.patches as mpatches import matplotlib.transforms as mtransforms from matplotlib.path import Path -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import image_comparison, check_figures_equal def test_non_affine_caching(): @@ -69,16 +69,13 @@ def _as_mpl_transform(self, axes): mtransforms.Affine2D().scale(10).get_matrix()) -@image_comparison(['pre_transform_data'], - tol=0.08, remove_text=True, style='mpl20') +@image_comparison(['pre_transform_data'], remove_text=True, style='mpl20', + tol=0.05) def test_pre_transform_plotting(): # a catch-all for as many as possible plot layouts which handle # pre-transforming the data NOTE: The axis range is important in this # plot. It should be x10 what the data suggests it should be - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - ax = plt.axes() times10 = mtransforms.Affine2D().scale(10) @@ -97,9 +94,8 @@ def test_pre_transform_plotting(): u = 2*np.sin(x) + np.cos(y[:, np.newaxis]) v = np.sin(x) - np.cos(y[:, np.newaxis]) - df = 25. / 30. # Compatibility factor for old test image ax.streamplot(x, y, u, v, transform=times10 + ax.transData, - density=(df, df), linewidth=u**2 + v**2) + linewidth=np.hypot(u, v)) # reduce the vector data down a bit for barb and quiver plotting x, y = x[::3], y[::3] @@ -195,8 +191,7 @@ def test_affine_inverted_invalidated(): def test_clipping_of_log(): # issue 804 - path = Path([(0.2, -99), (0.4, -99), (0.4, 20), (0.2, 20), (0.2, -99)], - closed=True) + path = Path._create_closed([(0.2, -99), (0.4, -99), (0.4, 20), (0.2, 20)]) # something like this happens in plotting logarithmic histograms trans = mtransforms.BlendedGenericTransform( mtransforms.Affine2D(), scale.LogTransform(10, 'clip')) @@ -205,7 +200,7 @@ def test_clipping_of_log(): clip=(0, 0, 100, 100), simplify=False) tpoints, tcodes = zip(*result) - assert_allclose(tcodes, path.codes) + assert_allclose(tcodes, path.codes[:-1]) # No longer closed. class NonAffineForTest(mtransforms.Transform): @@ -427,7 +422,7 @@ def test_pathc_extents_non_affine(self): ax = plt.axes() offset = mtransforms.Affine2D().translate(10, 10) na_offset = NonAffineForTest(mtransforms.Affine2D().translate(10, 10)) - pth = Path(np.array([[0, 0], [0, 10], [10, 10], [10, 0]])) + pth = Path([[0, 0], [0, 10], [10, 10], [10, 0]]) patch = mpatches.PathPatch(pth, transform=offset + na_offset + ax.transData) ax.add_patch(patch) @@ -437,7 +432,7 @@ def test_pathc_extents_non_affine(self): def test_pathc_extents_affine(self): ax = plt.axes() offset = mtransforms.Affine2D().translate(10, 10) - pth = Path(np.array([[0, 0], [0, 10], [10, 10], [10, 0]])) + pth = Path([[0, 0], [0, 10], [10, 10], [10, 0]]) patch = mpatches.PathPatch(pth, transform=offset + ax.transData) ax.add_patch(patch) expected_data_lim = np.array([[0., 0.], [10., 10.]]) + 10 @@ -460,6 +455,12 @@ def assert_bbox_eq(bbox1, bbox2): assert_array_equal(bbox1.bounds, bbox2.bounds) +def test_bbox_frozen_copies_minpos(): + bbox = mtransforms.Bbox.from_extents(0.0, 0.0, 1.0, 1.0, minpos=1.0) + frozen = bbox.frozen() + assert_array_equal(frozen.minpos, bbox.minpos) + + def test_bbox_intersection(): bbox_from_ext = mtransforms.Bbox.from_extents inter = mtransforms.Bbox.intersection @@ -506,16 +507,10 @@ def test_str_transform(): IdentityTransform(), IdentityTransform())), CompositeAffine2D( - Affine2D( - [[1. 0. 0.] - [0. 1. 0.] - [0. 0. 1.]]), - Affine2D( - [[1. 0. 0.] - [0. 1. 0.] - [0. 0. 1.]]))), + Affine2D().scale(1.0), + Affine2D().scale(1.0))), PolarTransform( - PolarAxesSubplot(0.125,0.1;0.775x0.8), + PolarAxes(0.125,0.1;0.775x0.8), use_rmin=True, _apply_theta_transforms=False)), CompositeGenericTransform( @@ -535,14 +530,8 @@ def test_str_transform(): TransformedBbox( Bbox(x0=0.0, y0=0.0, x1=6.283185307179586, y1=1.0), CompositeAffine2D( - Affine2D( - [[1. 0. 0.] - [0. 1. 0.] - [0. 0. 1.]]), - Affine2D( - [[1. 0. 0.] - [0. 1. 0.] - [0. 0. 1.]]))), + Affine2D().scale(1.0), + Affine2D().scale(1.0))), LockableBbox( Bbox(x0=0.0, y0=0.0, x1=6.283185307179586, y1=1.0), [[-- --] @@ -553,10 +542,7 @@ def test_str_transform(): BboxTransformTo( TransformedBbox( Bbox(x0=0.0, y0=0.0, x1=8.0, y1=6.0), - Affine2D( - [[80. 0. 0.] - [ 0. 80. 0.] - [ 0. 0. 1.]])))))))""" + Affine2D().scale(80.0)))))))""" def test_transform_single_point(): @@ -736,3 +722,25 @@ def test_deepcopy(): b1.translate(3, 4) assert not s._invalid assert (s.get_matrix() == a.get_matrix()).all() + + +def test_transformwrapper(): + t = mtransforms.TransformWrapper(mtransforms.Affine2D()) + with pytest.raises(ValueError, match=( + r"The input and output dims of the new child \(1, 1\) " + r"do not match those of current child \(2, 2\)")): + t.set(scale.LogTransform(10)) + + +@check_figures_equal(extensions=["png"]) +def test_scale_swapping(fig_test, fig_ref): + np.random.seed(19680801) + samples = np.random.normal(size=10) + x = np.linspace(-5, 5, 10) + + for fig, log_state in zip([fig_test, fig_ref], [True, False]): + ax = fig.subplots() + ax.hist(samples, log=log_state, density=True) + ax.plot(x, np.exp(-(x**2) / 2) / np.sqrt(2 * np.pi)) + fig.canvas.draw() + ax.set_yscale('linear') diff --git a/lib/matplotlib/tests/test_triangulation.py b/lib/matplotlib/tests/test_triangulation.py index dacbfd39c154..c292d82812d3 100644 --- a/lib/matplotlib/tests/test_triangulation.py +++ b/lib/matplotlib/tests/test_triangulation.py @@ -5,11 +5,94 @@ import pytest import matplotlib as mpl -import matplotlib.cm as cm import matplotlib.pyplot as plt import matplotlib.tri as mtri from matplotlib.path import Path -from matplotlib.testing.decorators import image_comparison +from matplotlib.testing.decorators import image_comparison, check_figures_equal + + +class TestTriangulationParams: + x = [-1, 0, 1, 0] + y = [0, -1, 0, 1] + triangles = [[0, 1, 2], [0, 2, 3]] + mask = [False, True] + + @pytest.mark.parametrize('args, kwargs, expected', [ + ([x, y], {}, [x, y, None, None]), + ([x, y, triangles], {}, [x, y, triangles, None]), + ([x, y], dict(triangles=triangles), [x, y, triangles, None]), + ([x, y], dict(mask=mask), [x, y, None, mask]), + ([x, y, triangles], dict(mask=mask), [x, y, triangles, mask]), + ([x, y], dict(triangles=triangles, mask=mask), [x, y, triangles, mask]) + ]) + def test_extract_triangulation_params(self, args, kwargs, expected): + other_args = [1, 2] + other_kwargs = {'a': 3, 'b': '4'} + x_, y_, triangles_, mask_, args_, kwargs_ = \ + mtri.Triangulation._extract_triangulation_params( + args + other_args, {**kwargs, **other_kwargs}) + x, y, triangles, mask = expected + assert x_ is x + assert y_ is y + assert_array_equal(triangles_, triangles) + assert mask_ is mask + assert args_ == other_args + assert kwargs_ == other_kwargs + + +def test_extract_triangulation_positional_mask(): + # mask cannot be passed positionally + mask = [True] + args = [[0, 2, 1], [0, 0, 1], [[0, 1, 2]], mask] + x_, y_, triangles_, mask_, args_, kwargs_ = \ + mtri.Triangulation._extract_triangulation_params(args, {}) + assert mask_ is None + assert args_ == [mask] + # the positional mask must be caught downstream because this must pass + # unknown args through + + +def test_triangulation_init(): + x = [-1, 0, 1, 0] + y = [0, -1, 0, 1] + with pytest.raises(ValueError, match="x and y must be equal-length"): + mtri.Triangulation(x, [1, 2]) + with pytest.raises( + ValueError, + match=r"triangles must be a \(N, 3\) int array, but found shape " + r"\(3,\)"): + mtri.Triangulation(x, y, [0, 1, 2]) + with pytest.raises( + ValueError, + match=r"triangles must be a \(N, 3\) int array, not 'other'"): + mtri.Triangulation(x, y, 'other') + with pytest.raises(ValueError, match="found value 99"): + mtri.Triangulation(x, y, [[0, 1, 99]]) + with pytest.raises(ValueError, match="found value -1"): + mtri.Triangulation(x, y, [[0, 1, -1]]) + + +def test_triangulation_set_mask(): + x = [-1, 0, 1, 0] + y = [0, -1, 0, 1] + triangles = [[0, 1, 2], [2, 3, 0]] + triang = mtri.Triangulation(x, y, triangles) + + # Check neighbors, which forces creation of C++ triangulation + assert_array_equal(triang.neighbors, [[-1, -1, 1], [-1, -1, 0]]) + + # Set mask + triang.set_mask([False, True]) + assert_array_equal(triang.mask, [False, True]) + + # Reset mask + triang.set_mask(None) + assert triang.mask is None + + msg = r"mask array must have same length as triangles array" + for mask in ([False, True, False], [False], [True], False, True): + with pytest.raises(ValueError, match=msg): + triang.set_mask(mask) def test_delaunay(): @@ -177,6 +260,59 @@ def test_tripcolor(): plt.title('facecolors') +def test_tripcolor_color(): + x = [-1, 0, 1, 0] + y = [0, -1, 0, 1] + fig, ax = plt.subplots() + with pytest.raises(TypeError, match=r"tripcolor\(\) missing 1 required "): + ax.tripcolor(x, y) + with pytest.raises(ValueError, match="The length of c must match either"): + ax.tripcolor(x, y, [1, 2, 3]) + with pytest.raises(ValueError, + match="length of facecolors must match .* triangles"): + ax.tripcolor(x, y, facecolors=[1, 2, 3, 4]) + with pytest.raises(ValueError, + match="'gouraud' .* at the points.* not at the faces"): + ax.tripcolor(x, y, facecolors=[1, 2], shading='gouraud') + with pytest.raises(ValueError, + match="'gouraud' .* at the points.* not at the faces"): + ax.tripcolor(x, y, [1, 2], shading='gouraud') # faces + with pytest.raises(TypeError, + match="positional.*'c'.*keyword-only.*'facecolors'"): + ax.tripcolor(x, y, C=[1, 2, 3, 4]) + + # smoke test for valid color specifications (via C or facecolors) + ax.tripcolor(x, y, [1, 2, 3, 4]) # edges + ax.tripcolor(x, y, [1, 2, 3, 4], shading='gouraud') # edges + ax.tripcolor(x, y, [1, 2]) # faces + ax.tripcolor(x, y, facecolors=[1, 2]) # faces + + +def test_tripcolor_clim(): + np.random.seed(19680801) + a, b, c = np.random.rand(10), np.random.rand(10), np.random.rand(10) + + ax = plt.figure().add_subplot() + clim = (0.25, 0.75) + norm = ax.tripcolor(a, b, c, clim=clim).norm + assert (norm.vmin, norm.vmax) == clim + + +def test_tripcolor_warnings(): + x = [-1, 0, 1, 0] + y = [0, -1, 0, 1] + c = [0.4, 0.5] + fig, ax = plt.subplots() + # additional parameters + with pytest.warns(DeprecationWarning, match="Additional positional param"): + ax.tripcolor(x, y, c, 'unused_positional') + # facecolors takes precedence over c + with pytest.warns(UserWarning, match="Positional parameter c .*no effect"): + ax.tripcolor(x, y, c, facecolors=c) + with pytest.warns(UserWarning, match="Positional parameter c .*no effect"): + ax.tripcolor(x, y, 'interpreted as c', facecolors=c) + + def test_no_modify(): # Test that Triangulation does not modify triangles array passed to it. triangles = np.array([[3, 2, 0], [3, 1, 0]], dtype=np.int32) @@ -501,15 +637,15 @@ def poisson_sparse_matrix(n, m): # Instantiating a sparse Poisson matrix of size 48 x 48: (n, m) = (12, 4) - mat = mtri.triinterpolate._Sparse_Matrix_coo(*poisson_sparse_matrix(n, m)) + mat = mtri._triinterpolate._Sparse_Matrix_coo(*poisson_sparse_matrix(n, m)) mat.compress_csc() mat_dense = mat.to_dense() # Testing a sparse solve for all 48 basis vector for itest in range(n*m): b = np.zeros(n*m, dtype=np.float64) b[itest] = 1. - x, _ = mtri.triinterpolate._cg(A=mat, b=b, x0=np.zeros(n*m), - tol=1.e-10) + x, _ = mtri._triinterpolate._cg(A=mat, b=b, x0=np.zeros(n*m), + tol=1.e-10) assert_array_almost_equal(np.dot(mat_dense, x), b) # 2) Same matrix with inserting 2 rows - cols with null diag terms @@ -522,16 +658,16 @@ def poisson_sparse_matrix(n, m): rows = np.concatenate([rows, [i_zero, i_zero-1, j_zero, j_zero-1]]) cols = np.concatenate([cols, [i_zero-1, i_zero, j_zero-1, j_zero]]) vals = np.concatenate([vals, [1., 1., 1., 1.]]) - mat = mtri.triinterpolate._Sparse_Matrix_coo(vals, rows, cols, - (n*m + 2, n*m + 2)) + mat = mtri._triinterpolate._Sparse_Matrix_coo(vals, rows, cols, + (n*m + 2, n*m + 2)) mat.compress_csc() mat_dense = mat.to_dense() # Testing a sparse solve for all 50 basis vec for itest in range(n*m + 2): b = np.zeros(n*m + 2, dtype=np.float64) b[itest] = 1. - x, _ = mtri.triinterpolate._cg(A=mat, b=b, x0=np.ones(n*m + 2), - tol=1.e-10) + x, _ = mtri._triinterpolate._cg(A=mat, b=b, x0=np.ones(n * m + 2), + tol=1.e-10) assert_array_almost_equal(np.dot(mat_dense, x), b) # 3) Now a simple test that summation of duplicate (i.e. with same rows, @@ -542,7 +678,7 @@ def poisson_sparse_matrix(n, m): cols = np.array([0, 1, 2, 1, 1, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2], dtype=np.int32) dim = (3, 3) - mat = mtri.triinterpolate._Sparse_Matrix_coo(vals, rows, cols, dim) + mat = mtri._triinterpolate._Sparse_Matrix_coo(vals, rows, cols, dim) mat.compress_csc() mat_dense = mat.to_dense() assert_array_almost_equal(mat_dense, np.array([ @@ -565,7 +701,7 @@ def test_triinterpcubic_geom_weights(): y_rot = -np.sin(theta)*x + np.cos(theta)*y triang = mtri.Triangulation(x_rot, y_rot, triangles) cubic_geom = mtri.CubicTriInterpolator(triang, z, kind='geom') - dof_estimator = mtri.triinterpolate._DOF_estimator_geom(cubic_geom) + dof_estimator = mtri._triinterpolate._DOF_estimator_geom(cubic_geom) weights = dof_estimator.compute_geom_weights() # Testing for the 4 possibilities... sum_w[0, :] = np.sum(weights, 1) - 1 @@ -811,7 +947,7 @@ def dipole_potential(x, y): plt.triplot(triang, color='0.8') levels = np.arange(0., 1., 0.01) - cmap = cm.get_cmap(name='hot', lut=None) + cmap = mpl.colormaps['hot'] plt.tricontour(tri_refi, z_test_refi, levels=levels, cmap=cmap, linewidths=[2.0, 1.0, 1.0, 1.0]) # Plots direction of the electrical vector field @@ -831,8 +967,7 @@ def test_tritools(): mask = np.array([False, False, True], dtype=bool) triang = mtri.Triangulation(x, y, triangles, mask=mask) analyser = mtri.TriAnalyzer(triang) - assert_array_almost_equal(analyser.scale_factors, - np.array([1., 1./(1.+0.5*np.sqrt(3.))])) + assert_array_almost_equal(analyser.scale_factors, [1, 1/(1+3**.5/2)]) assert_array_almost_equal( analyser.circle_ratios(rescale=False), np.ma.masked_array([0.5, 1./(1.+np.sqrt(2.)), np.nan], mask)) @@ -1054,55 +1189,62 @@ def test_internal_cpp_api(): # C++ Triangulation. with pytest.raises( TypeError, - match=r'function takes exactly 7 arguments \(0 given\)'): + match=r'__init__\(\): incompatible constructor arguments.'): mpl._tri.Triangulation() with pytest.raises( ValueError, match=r'x and y must be 1D arrays of the same length'): - mpl._tri.Triangulation([], [1], [[]], None, None, None, False) + mpl._tri.Triangulation([], [1], [[]], (), (), (), False) x = [0, 1, 1] y = [0, 0, 1] with pytest.raises( ValueError, match=r'triangles must be a 2D array of shape \(\?,3\)'): - mpl._tri.Triangulation(x, y, [[0, 1]], None, None, None, False) + mpl._tri.Triangulation(x, y, [[0, 1]], (), (), (), False) tris = [[0, 1, 2]] with pytest.raises( ValueError, match=r'mask must be a 1D array with the same length as the ' r'triangles array'): - mpl._tri.Triangulation(x, y, tris, [0, 1], None, None, False) + mpl._tri.Triangulation(x, y, tris, [0, 1], (), (), False) with pytest.raises( ValueError, match=r'edges must be a 2D array with shape \(\?,2\)'): - mpl._tri.Triangulation(x, y, tris, None, [[1]], None, False) + mpl._tri.Triangulation(x, y, tris, (), [[1]], (), False) with pytest.raises( ValueError, match=r'neighbors must be a 2D array with the same shape as the ' r'triangles array'): - mpl._tri.Triangulation(x, y, tris, None, None, [[-1]], False) + mpl._tri.Triangulation(x, y, tris, (), (), [[-1]], False) - triang = mpl._tri.Triangulation(x, y, tris, None, None, None, False) + triang = mpl._tri.Triangulation(x, y, tris, (), (), (), False) with pytest.raises( ValueError, - match=r'z array must have same length as triangulation x and y ' - r'array'): + match=r'z must be a 1D array with the same length as the ' + r'triangulation x and y arrays'): triang.calculate_plane_coefficients([]) - with pytest.raises( - ValueError, - match=r'mask must be a 1D array with the same length as the ' - r'triangles array'): - triang.set_mask([0, 1]) + for mask in ([0, 1], None): + with pytest.raises( + ValueError, + match=r'mask must be a 1D array with the same length as the ' + r'triangles array'): + triang.set_mask(mask) + + triang.set_mask([True]) + assert_array_equal(triang.get_edges(), np.empty((0, 2))) + + triang.set_mask(()) # Equivalent to Python Triangulation mask=None + assert_array_equal(triang.get_edges(), [[1, 0], [2, 0], [2, 1]]) # C++ TriContourGenerator. with pytest.raises( TypeError, - match=r'function takes exactly 2 arguments \(0 given\)'): + match=r'__init__\(\): incompatible constructor arguments.'): mpl._tri.TriContourGenerator() with pytest.raises( @@ -1120,7 +1262,8 @@ def test_internal_cpp_api(): # C++ TrapezoidMapTriFinder. with pytest.raises( - TypeError, match=r'function takes exactly 1 argument \(0 given\)'): + TypeError, + match=r'__init__\(\): incompatible constructor arguments.'): mpl._tri.TrapezoidMapTriFinder() trifinder = mpl._tri.TrapezoidMapTriFinder(triang) @@ -1163,3 +1306,38 @@ def test_tricontour_non_finite_z(): with pytest.raises(ValueError, match='z must not contain masked points ' 'within the triangulation'): plt.tricontourf(triang, np.ma.array([0, 1, 2, 3], mask=[1, 0, 0, 0])) + + +def test_tricontourset_reuse(): + # If TriContourSet returned from one tricontour(f) call is passed as first + # argument to another the underlying C++ contour generator will be reused. + x = [0.0, 0.5, 1.0] + y = [0.0, 1.0, 0.0] + z = [1.0, 2.0, 3.0] + fig, ax = plt.subplots() + tcs1 = ax.tricontourf(x, y, z) + tcs2 = ax.tricontour(x, y, z) + assert tcs2._contour_generator != tcs1._contour_generator + tcs3 = ax.tricontour(tcs1, z) + assert tcs3._contour_generator == tcs1._contour_generator + + +@check_figures_equal() +def test_triplot_with_ls(fig_test, fig_ref): + x = [0, 2, 1] + y = [0, 0, 1] + data = [[0, 1, 2]] + fig_test.subplots().triplot(x, y, data, ls='--') + fig_ref.subplots().triplot(x, y, data, linestyle='--') + + +def test_triplot_label(): + x = [0, 2, 1] + y = [0, 0, 1] + data = [[0, 1, 2]] + fig, ax = plt.subplots() + lines, markers = ax.triplot(x, y, data, label='label') + handles, labels = ax.get_legend_handles_labels() + assert labels == ['label'] + assert len(handles) == 1 + assert handles[0] is lines diff --git a/lib/matplotlib/tests/test_type1font.py b/lib/matplotlib/tests/test_type1font.py index 8800e184b3b7..1e173d5ea84d 100644 --- a/lib/matplotlib/tests/test_type1font.py +++ b/lib/matplotlib/tests/test_type1font.py @@ -1,6 +1,7 @@ -import matplotlib.type1font as t1f +import matplotlib._type1font as t1f import os.path import difflib +import pytest def test_Type1Font(): @@ -13,8 +14,35 @@ def test_Type1Font(): assert font.parts[0] == rawdata[0x0006:0x10c5] assert font.parts[1] == rawdata[0x10cb:0x897f] assert font.parts[2] == rawdata[0x8985:0x8ba6] - assert font.parts[1:] == slanted.parts[1:] - assert font.parts[1:] == condensed.parts[1:] + assert font.decrypted.startswith(b'dup\n/Private 18 dict dup begin') + assert font.decrypted.endswith(b'mark currentfile closefile\n') + assert slanted.decrypted.startswith(b'dup\n/Private 18 dict dup begin') + assert slanted.decrypted.endswith(b'mark currentfile closefile\n') + assert b'UniqueID 5000793' in font.parts[0] + assert b'UniqueID 5000793' in font.decrypted + assert font._pos['UniqueID'] == [(797, 818), (4483, 4504)] + + len0 = len(font.parts[0]) + for key in font._pos.keys(): + for pos0, pos1 in font._pos[key]: + if pos0 < len0: + data = font.parts[0][pos0:pos1] + else: + data = font.decrypted[pos0-len0:pos1-len0] + assert data.startswith(f'/{key}'.encode('ascii')) + assert {'FontType', 'FontMatrix', 'PaintType', 'ItalicAngle', 'RD' + } < set(font._pos.keys()) + + assert b'UniqueID 5000793' not in slanted.parts[0] + assert b'UniqueID 5000793' not in slanted.decrypted + assert 'UniqueID' not in slanted._pos + assert font.prop['Weight'] == 'Medium' + assert not font.prop['isFixedPitch'] + assert font.prop['ItalicAngle'] == 0 + assert slanted.prop['ItalicAngle'] == -45 + assert font.prop['Encoding'][5] == 'Pi' + assert isinstance(font.prop['CharStrings']['Pi'], bytes) + assert font._abbr['ND'] == 'ND' differ = difflib.Differ() diff = list(differ.compare( @@ -22,14 +50,13 @@ def test_Type1Font(): slanted.parts[0].decode('latin-1').splitlines())) for line in ( # Removes UniqueID - '- FontDirectory/CMR10 known{/CMR10 findfont dup/UniqueID known{dup', - '+ FontDirectory/CMR10 known{/CMR10 findfont dup', + '- /UniqueID 5000793 def', # Changes the font name - '- /FontName /CMR10 def', - '+ /FontName /CMR10_Slant_1000 def', + '- /FontName /CMR10 def', + '+ /FontName/CMR10_Slant_1000 def', # Alters FontMatrix '- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def', - '+ /FontMatrix [0.001 0 0.001 0.001 0 0]readonly def', + '+ /FontMatrix [0.001 0 0.001 0.001 0 0] readonly def', # Alters ItalicAngle '- /ItalicAngle 0 def', '+ /ItalicAngle -45.0 def'): @@ -40,17 +67,73 @@ def test_Type1Font(): condensed.parts[0].decode('latin-1').splitlines())) for line in ( # Removes UniqueID - '- FontDirectory/CMR10 known{/CMR10 findfont dup/UniqueID known{dup', - '+ FontDirectory/CMR10 known{/CMR10 findfont dup', + '- /UniqueID 5000793 def', # Changes the font name - '- /FontName /CMR10 def', - '+ /FontName /CMR10_Extend_500 def', + '- /FontName /CMR10 def', + '+ /FontName/CMR10_Extend_500 def', # Alters FontMatrix '- /FontMatrix [0.001 0 0 0.001 0 0 ]readonly def', - '+ /FontMatrix [0.0005 0 0 0.001 0 0]readonly def'): + '+ /FontMatrix [0.0005 0 0 0.001 0 0] readonly def'): assert line in diff, 'diff to condensed font must contain %s' % line +def test_Type1Font_2(): + filename = os.path.join(os.path.dirname(__file__), + 'Courier10PitchBT-Bold.pfb') + font = t1f.Type1Font(filename) + assert font.prop['Weight'] == 'Bold' + assert font.prop['isFixedPitch'] + assert font.prop['Encoding'][65] == 'A' # the font uses StandardEncoding + (pos0, pos1), = font._pos['Encoding'] + assert font.parts[0][pos0:pos1] == b'/Encoding StandardEncoding' + assert font._abbr['ND'] == '|-' + + +def test_tokenize(): + data = (b'1234/abc false -9.81 Foo <<[0 1 2]<0 1ef a\t>>>\n' + b'(string with(nested\t\\) par)ens\\\\)') + # 1 2 x 2 xx1 + # 1 and 2 are matching parens, x means escaped character + n, w, num, kw, d = 'name', 'whitespace', 'number', 'keyword', 'delimiter' + b, s = 'boolean', 'string' + correct = [ + (num, 1234), (n, 'abc'), (w, ' '), (b, False), (w, ' '), (num, -9.81), + (w, ' '), (kw, 'Foo'), (w, ' '), (d, '<<'), (d, '['), (num, 0), + (w, ' '), (num, 1), (w, ' '), (num, 2), (d, ']'), (s, b'\x01\xef\xa0'), + (d, '>>'), (w, '\n'), (s, 'string with(nested\t) par)ens\\') + ] + correct_no_ws = [x for x in correct if x[0] != w] + + def convert(tokens): + return [(t.kind, t.value()) for t in tokens] + + assert convert(t1f._tokenize(data, False)) == correct + assert convert(t1f._tokenize(data, True)) == correct_no_ws + + def bin_after(n): + tokens = t1f._tokenize(data, True) + result = [] + for _ in range(n): + result.append(next(tokens)) + result.append(tokens.send(10)) + return convert(result) + + for n in range(1, len(correct_no_ws)): + result = bin_after(n) + assert result[:-1] == correct_no_ws[:n] + assert result[-1][0] == 'binary' + assert isinstance(result[-1][1], bytes) + + +def test_tokenize_errors(): + with pytest.raises(ValueError): + list(t1f._tokenize(b'1234 (this (string) is unterminated\\)', True)) + with pytest.raises(ValueError): + list(t1f._tokenize(b'/Foo<01234', True)) + with pytest.raises(ValueError): + list(t1f._tokenize(b'/Foo<01234abcg>/Bar', True)) + + def test_overprecision(): # We used to output too many digits in FontMatrix entries and # ItalicAngle, which could make Type-1 parsers unhappy. @@ -67,3 +150,11 @@ def test_overprecision(): assert matrix == '0.001 0 0.000167 0.001 0 0' # and here we had -9.48090361795083 assert angle == '-9.4809' + + +def test_encrypt_decrypt_roundtrip(): + data = b'this is my plaintext \0\1\2\3' + encrypted = t1f.Type1Font._encrypt(data, 'eexec') + decrypted = t1f.Type1Font._decrypt(encrypted, 'eexec') + assert encrypted != decrypted + assert data == decrypted diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py index 3f40a99a2f5a..d3b8c5a71643 100644 --- a/lib/matplotlib/tests/test_units.py +++ b/lib/matplotlib/tests/test_units.py @@ -26,6 +26,9 @@ def to(self, new_units): else: return Quantity(self.magnitude, self.units) + def __copy__(self): + return Quantity(self.magnitude, self.units) + def __getattr__(self, attr): return getattr(self.magnitude, attr) @@ -67,14 +70,15 @@ def default_units(value, axis): return None qc.convert = MagicMock(side_effect=convert) - qc.axisinfo = MagicMock(side_effect=lambda u, a: munits.AxisInfo(label=u)) + qc.axisinfo = MagicMock(side_effect=lambda u, a: + munits.AxisInfo(label=u, default_limits=(0, 100))) qc.default_units = MagicMock(side_effect=default_units) return qc # Tests that the conversion machinery works properly for classes that # work as a facade over numpy arrays (like pint) -@image_comparison(['plot_pint.png'], remove_text=False, style='mpl20', +@image_comparison(['plot_pint.png'], style='mpl20', tol=0 if platform.machine() == 'x86_64' else 0.01) def test_numpy_facade(quantity_converter): # use former defaults to match existing baseline image @@ -166,6 +170,14 @@ def test_scatter_element0_masked(): fig.canvas.draw() +def test_errorbar_mixed_units(): + x = np.arange(10) + y = [datetime(2020, 5, i * 2 + 1) for i in x] + fig, ax = plt.subplots() + ax.errorbar(x, y, timedelta(days=0.5)) + fig.canvas.draw() + + @check_figures_equal(extensions=["png"]) def test_subclass(fig_test, fig_ref): class subdate(datetime): @@ -211,3 +223,63 @@ def test_shared_axis_categorical(): ax2.plot(d2.keys(), d2.values()) ax1.xaxis.set_units(UnitData(["c", "d"])) assert "c" in ax2.xaxis.get_units()._mapping.keys() + + +def test_empty_default_limits(quantity_converter): + munits.registry[Quantity] = quantity_converter + fig, ax1 = plt.subplots() + ax1.xaxis.update_units(Quantity([10], "miles")) + fig.draw_without_rendering() + assert ax1.get_xlim() == (0, 100) + ax1.yaxis.update_units(Quantity([10], "miles")) + fig.draw_without_rendering() + assert ax1.get_ylim() == (0, 100) + + fig, ax = plt.subplots() + ax.axhline(30) + ax.plot(Quantity(np.arange(0, 3), "miles"), + Quantity(np.arange(0, 6, 2), "feet")) + fig.draw_without_rendering() + assert ax.get_xlim() == (0, 2) + assert ax.get_ylim() == (0, 30) + + fig, ax = plt.subplots() + ax.axvline(30) + ax.plot(Quantity(np.arange(0, 3), "miles"), + Quantity(np.arange(0, 6, 2), "feet")) + fig.draw_without_rendering() + assert ax.get_xlim() == (0, 30) + assert ax.get_ylim() == (0, 4) + + fig, ax = plt.subplots() + ax.xaxis.update_units(Quantity([10], "miles")) + ax.axhline(30) + fig.draw_without_rendering() + assert ax.get_xlim() == (0, 100) + assert ax.get_ylim() == (28.5, 31.5) + + fig, ax = plt.subplots() + ax.yaxis.update_units(Quantity([10], "miles")) + ax.axvline(30) + fig.draw_without_rendering() + assert ax.get_ylim() == (0, 100) + assert ax.get_xlim() == (28.5, 31.5) + + +# test array-like objects... +class Kernel: + def __init__(self, array): + self._array = np.asanyarray(array) + + def __array__(self): + return self._array + + @property + def shape(self): + return self._array.shape + + +def test_plot_kernel(): + # just a smoketest that fail + kernel = Kernel([1, 2, 3, 4, 5]) + plt.plot(kernel) diff --git a/lib/matplotlib/tests/test_usetex.py b/lib/matplotlib/tests/test_usetex.py index 2d79e155e72e..0f01ebaffb56 100644 --- a/lib/matplotlib/tests/test_usetex.py +++ b/lib/matplotlib/tests/test_usetex.py @@ -1,14 +1,17 @@ +from tempfile import TemporaryFile + import numpy as np import pytest import matplotlib as mpl +from matplotlib import dviread from matplotlib.testing import _has_tex_package from matplotlib.testing.decorators import check_figures_equal, image_comparison +from matplotlib.testing._markers import needs_usetex import matplotlib.pyplot as plt -if not mpl.checkdep_usetex(True): - pytestmark = pytest.mark.skip('Missing TeX of Ghostscript or dvipng') +pytestmark = needs_usetex @image_comparison( @@ -61,6 +64,21 @@ def test_mathdefault(): fig.canvas.draw() +@image_comparison(['eqnarray.png']) +def test_multiline_eqnarray(): + text = ( + r'\begin{eqnarray*}' + r'foo\\' + r'bar\\' + r'baz\\' + r'\end{eqnarray*}' + ) + + fig = plt.figure(figsize=(1, 1)) + fig.text(0.5, 0.5, text, usetex=True, + horizontalalignment='center', verticalalignment='center') + + @pytest.mark.parametrize("fontsize", [8, 10, 12]) def test_minus_no_descent(fontsize): # Test special-casing of minus descent in DviFont._height_depth_of, by @@ -71,7 +89,7 @@ def test_minus_no_descent(fontsize): heights = {} fig = plt.figure() for vals in [(1,), (-1,), (-1, 1)]: - fig.clf() + fig.clear() for x in vals: fig.text(.5, .5, f"${x}$", usetex=True) fig.canvas.draw() @@ -81,16 +99,18 @@ def test_minus_no_descent(fontsize): assert len({*heights.values()}) == 1 -@pytest.mark.skipif(not _has_tex_package('xcolor'), - reason='xcolor is not available') -def test_usetex_xcolor(): +@pytest.mark.parametrize('pkg', ['xcolor', 'chemformula']) +def test_usetex_packages(pkg): + if not _has_tex_package(pkg): + pytest.skip(f'{pkg} is not available') mpl.rcParams['text.usetex'] = True fig = plt.figure() text = fig.text(0.5, 0.5, "Some text 0123456789") fig.canvas.draw() - mpl.rcParams['text.latex.preamble'] = r'\usepackage[dvipsnames]{xcolor}' + mpl.rcParams['text.latex.preamble'] = ( + r'\PassOptionsToPackage{dvipsnames}{xcolor}\usepackage{%s}' % pkg) fig = plt.figure() text2 = fig.text(0.5, 0.5, "Some text 0123456789") fig.canvas.draw() @@ -98,8 +118,38 @@ def test_usetex_xcolor(): text.get_window_extent()) -def test_textcomp_full(): - plt.rcParams["text.latex.preamble"] = r"\usepackage[full]{textcomp}" +@pytest.mark.parametrize( + "preamble", + [r"\usepackage[full]{textcomp}", r"\usepackage{underscore}"], +) +def test_latex_pkg_already_loaded(preamble): + plt.rcParams["text.latex.preamble"] = preamble fig = plt.figure() fig.text(.5, .5, "hello, world", usetex=True) fig.canvas.draw() + + +def test_usetex_with_underscore(): + plt.rcParams["text.usetex"] = True + df = {"a_b": range(5)[::-1], "c": range(5)} + fig, ax = plt.subplots() + ax.plot("c", "a_b", data=df) + ax.legend() + ax.text(0, 0, "foo_bar", usetex=True) + plt.draw() + + +@pytest.mark.flaky(reruns=3) # Tends to hit a TeX cache lock on AppVeyor. +@pytest.mark.parametrize("fmt", ["pdf", "svg"]) +def test_missing_psfont(fmt, monkeypatch): + """An error is raised if a TeX font lacks a Type-1 equivalent""" + monkeypatch.setattr( + dviread.PsfontsMap, '__getitem__', + lambda self, k: dviread.PsFont( + texname=b'texfont', psname=b'Some Font', + effects=None, encoding=None, filename=None)) + mpl.rcParams['text.usetex'] = True + fig, ax = plt.subplots() + ax.text(0.5, 0.5, 'hello') + with TemporaryFile() as tmpfile, pytest.raises(ValueError): + fig.savefig(tmpfile, format=fmt) diff --git a/lib/matplotlib/tests/test_widgets.py b/lib/matplotlib/tests/test_widgets.py index 2b81dbccc219..2f9572822f64 100644 --- a/lib/matplotlib/tests/test_widgets.py +++ b/lib/matplotlib/tests/test_widgets.py @@ -1,23 +1,76 @@ +import functools +import io +from unittest import mock + +from matplotlib._api.deprecation import MatplotlibDeprecationWarning +from matplotlib.backend_bases import MouseEvent import matplotlib.colors as mcolors import matplotlib.widgets as widgets import matplotlib.pyplot as plt -from matplotlib.testing.decorators import image_comparison -from matplotlib.testing.widgets import do_event, get_ax, mock_event +from matplotlib.patches import Rectangle +from matplotlib.lines import Line2D +from matplotlib.testing.decorators import check_figures_equal, image_comparison +from matplotlib.testing.widgets import (click_and_drag, do_event, get_ax, + mock_event, noop) +import numpy as np from numpy.testing import assert_allclose import pytest -def check_rectangle(**kwargs): - ax = get_ax() +@pytest.fixture +def ax(): + return get_ax() - def onselect(epress, erelease): - ax._got_onselect = True - assert epress.xdata == 100 - assert epress.ydata == 100 - assert erelease.xdata == 199 - assert erelease.ydata == 199 + +def test_save_blitted_widget_as_pdf(): + from matplotlib.widgets import CheckButtons, RadioButtons + from matplotlib.cbook import _get_running_interactive_framework + if _get_running_interactive_framework() not in ['headless', None]: + pytest.xfail("Callback exceptions are not raised otherwise.") + + fig, ax = plt.subplots( + nrows=2, ncols=2, figsize=(5, 2), width_ratios=[1, 2] + ) + default_rb = RadioButtons(ax[0, 0], ['Apples', 'Oranges']) + styled_rb = RadioButtons( + ax[0, 1], ['Apples', 'Oranges'], + label_props={'color': ['red', 'orange'], + 'fontsize': [16, 20]}, + radio_props={'edgecolor': ['red', 'orange'], + 'facecolor': ['mistyrose', 'peachpuff']} + ) + + default_cb = CheckButtons(ax[1, 0], ['Apples', 'Oranges'], + actives=[True, True]) + styled_cb = CheckButtons( + ax[1, 1], ['Apples', 'Oranges'], + actives=[True, True], + label_props={'color': ['red', 'orange'], + 'fontsize': [16, 20]}, + frame_props={'edgecolor': ['red', 'orange'], + 'facecolor': ['mistyrose', 'peachpuff']}, + check_props={'color': ['darkred', 'darkorange']} + ) + + ax[0, 0].set_title('Default') + ax[0, 1].set_title('Stylized') + # force an Agg render + fig.canvas.draw() + # force a pdf save + with io.BytesIO() as result_after: + fig.savefig(result_after, format='pdf') + + +@pytest.mark.parametrize('kwargs', [ + dict(), + dict(useblit=True, button=1), + dict(minspanx=10, minspany=10, spancoords='pixels'), + dict(props=dict(fill=True)), +]) +def test_rectangle_selector(ax, kwargs): + onselect = mock.Mock(spec=noop, return_value=None) tool = widgets.RectangleSelector(ax, onselect, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) @@ -32,173 +85,909 @@ def onselect(epress, erelease): [100, 199, 199, 100, 100]], err_msg=tool.geometry) - assert ax._got_onselect - - -def test_rectangle_selector(): - check_rectangle() - check_rectangle(drawtype='line', useblit=False) - check_rectangle(useblit=True, button=1) - check_rectangle(drawtype='none', minspanx=10, minspany=10) - check_rectangle(minspanx=10, minspany=10, spancoords='pixels') - check_rectangle(rectprops=dict(fill=True)) + onselect.assert_called_once() + (epress, erelease), kwargs = onselect.call_args + assert epress.xdata == 100 + assert epress.ydata == 100 + assert erelease.xdata == 199 + assert erelease.ydata == 199 + assert kwargs == {} + + +@pytest.mark.parametrize('spancoords', ['data', 'pixels']) +@pytest.mark.parametrize('minspanx, x1', [[0, 10], [1, 10.5], [1, 11]]) +@pytest.mark.parametrize('minspany, y1', [[0, 10], [1, 10.5], [1, 11]]) +def test_rectangle_minspan(ax, spancoords, minspanx, x1, minspany, y1): + + onselect = mock.Mock(spec=noop, return_value=None) + + x0, y0 = (10, 10) + if spancoords == 'pixels': + minspanx, minspany = (ax.transData.transform((x1, y1)) - + ax.transData.transform((x0, y0))) + + tool = widgets.RectangleSelector(ax, onselect, interactive=True, + spancoords=spancoords, + minspanx=minspanx, minspany=minspany) + # Too small to create a selector + click_and_drag(tool, start=(x0, x1), end=(y0, y1)) + assert not tool._selection_completed + onselect.assert_not_called() + + click_and_drag(tool, start=(20, 20), end=(30, 30)) + assert tool._selection_completed + onselect.assert_called_once() + + # Too small to create a selector. Should clear existing selector, and + # trigger onselect because there was a preexisting selector + onselect.reset_mock() + click_and_drag(tool, start=(x0, y0), end=(x1, y1)) + assert not tool._selection_completed + onselect.assert_called_once() + (epress, erelease), kwargs = onselect.call_args + assert epress.xdata == x0 + assert epress.ydata == y0 + assert erelease.xdata == x1 + assert erelease.ydata == y1 + assert kwargs == {} + + +def test_deprecation_selector_visible_attribute(ax): + tool = widgets.RectangleSelector(ax, lambda *args: None) + + assert tool.get_visible() + + with pytest.warns( + MatplotlibDeprecationWarning, + match="was deprecated in Matplotlib 3.6"): + tool.visible = False + assert not tool.get_visible() + + +@pytest.mark.parametrize('drag_from_anywhere, new_center', + [[True, (60, 75)], + [False, (30, 20)]]) +def test_rectangle_drag(ax, drag_from_anywhere, new_center): + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + drag_from_anywhere=drag_from_anywhere) + # Create rectangle + click_and_drag(tool, start=(0, 10), end=(100, 120)) + assert tool.center == (50, 65) + # Drag inside rectangle, but away from centre handle + # + # If drag_from_anywhere == True, this will move the rectangle by (10, 10), + # giving it a new center of (60, 75) + # + # If drag_from_anywhere == False, this will create a new rectangle with + # center (30, 20) + click_and_drag(tool, start=(25, 15), end=(35, 25)) + assert tool.center == new_center + # Check that in both cases, dragging outside the rectangle draws a new + # rectangle + click_and_drag(tool, start=(175, 185), end=(185, 195)) + assert tool.center == (180, 190) + + +def test_rectangle_selector_set_props_handle_props(ax): + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + props=dict(facecolor='b', alpha=0.2), + handle_props=dict(alpha=0.5)) + # Create rectangle + click_and_drag(tool, start=(0, 10), end=(100, 120)) + + artist = tool._selection_artist + assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2) + tool.set_props(facecolor='r', alpha=0.3) + assert artist.get_facecolor() == mcolors.to_rgba('r', alpha=0.3) + + for artist in tool._handles_artists: + assert artist.get_markeredgecolor() == 'black' + assert artist.get_alpha() == 0.5 + tool.set_handle_props(markeredgecolor='r', alpha=0.3) + for artist in tool._handles_artists: + assert artist.get_markeredgecolor() == 'r' + assert artist.get_alpha() == 0.3 + + +def test_rectangle_resize(ax): + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + # Create rectangle + click_and_drag(tool, start=(0, 10), end=(100, 120)) + assert tool.extents == (0.0, 100.0, 10.0, 120.0) + + # resize NE handle + extents = tool.extents + xdata, ydata = extents[1], extents[3] + xdata_new, ydata_new = xdata + 10, ydata + 5 + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert tool.extents == (extents[0], xdata_new, extents[2], ydata_new) + + # resize E handle + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdata_new, ydata_new = xdata + 10, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert tool.extents == (extents[0], xdata_new, extents[2], extents[3]) + + # resize W handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdata_new, ydata_new = xdata + 15, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert tool.extents == (xdata_new, extents[1], extents[2], extents[3]) + + # resize SW handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + xdata_new, ydata_new = xdata + 20, ydata + 25 + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert tool.extents == (xdata_new, extents[1], ydata_new, extents[3]) + + +def test_rectangle_add_state(ax): + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + # Create rectangle + click_and_drag(tool, start=(70, 65), end=(125, 130)) + with pytest.raises(ValueError): + tool.add_state('unsupported_state') -def test_ellipse(): + with pytest.raises(ValueError): + tool.add_state('clear') + tool.add_state('move') + tool.add_state('square') + tool.add_state('center') + + +@pytest.mark.parametrize('add_state', [True, False]) +def test_rectangle_resize_center(ax, add_state): + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + # Create rectangle + click_and_drag(tool, start=(70, 65), end=(125, 130)) + assert tool.extents == (70.0, 125.0, 65.0, 130.0) + + if add_state: + tool.add_state('center') + use_key = None + else: + use_key = 'control' + + # resize NE handle + extents = tool.extents + xdata, ydata = extents[1], extents[3] + xdiff, ydiff = 10, 5 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (extents[0] - xdiff, xdata_new, + extents[2] - ydiff, ydata_new) + + # resize E handle + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 10 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (extents[0] - xdiff, xdata_new, + extents[2], extents[3]) + + # resize E handle negative diff + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -20 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (extents[0] - xdiff, xdata_new, + extents[2], extents[3]) + + # resize W handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 15 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (xdata_new, extents[1] - xdiff, + extents[2], extents[3]) + + # resize W handle negative diff + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -25 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (xdata_new, extents[1] - xdiff, + extents[2], extents[3]) + + # resize SW handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + xdiff, ydiff = 20, 25 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (xdata_new, extents[1] - xdiff, + ydata_new, extents[3] - ydiff) + + +@pytest.mark.parametrize('add_state', [True, False]) +def test_rectangle_resize_square(ax, add_state): + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + # Create rectangle + click_and_drag(tool, start=(70, 65), end=(120, 115)) + assert tool.extents == (70.0, 120.0, 65.0, 115.0) + + if add_state: + tool.add_state('square') + use_key = None + else: + use_key = 'shift' + + # resize NE handle + extents = tool.extents + xdata, ydata = extents[1], extents[3] + xdiff, ydiff = 10, 5 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (extents[0], xdata_new, + extents[2], extents[3] + xdiff) + + # resize E handle + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 10 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (extents[0], xdata_new, + extents[2], extents[3] + xdiff) + + # resize E handle negative diff + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -20 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (extents[0], xdata_new, + extents[2], extents[3] + xdiff) + + # resize W handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 15 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (xdata_new, extents[1], + extents[2], extents[3] - xdiff) + + # resize W handle negative diff + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -25 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (xdata_new, extents[1], + extents[2], extents[3] - xdiff) + + # resize SW handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + xdiff, ydiff = 20, 25 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new), + key=use_key) + assert tool.extents == (extents[0] + ydiff, extents[1], + ydata_new, extents[3]) + + +def test_rectangle_resize_square_center(ax): + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + # Create rectangle + click_and_drag(tool, start=(70, 65), end=(120, 115)) + tool.add_state('square') + tool.add_state('center') + assert_allclose(tool.extents, (70.0, 120.0, 65.0, 115.0)) + + # resize NE handle + extents = tool.extents + xdata, ydata = extents[1], extents[3] + xdiff, ydiff = 10, 5 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert_allclose(tool.extents, (extents[0] - xdiff, xdata_new, + extents[2] - xdiff, extents[3] + xdiff)) + + # resize E handle + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 10 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert_allclose(tool.extents, (extents[0] - xdiff, xdata_new, + extents[2] - xdiff, extents[3] + xdiff)) + + # resize E handle negative diff + extents = tool.extents + xdata, ydata = extents[1], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -20 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert_allclose(tool.extents, (extents[0] - xdiff, xdata_new, + extents[2] - xdiff, extents[3] + xdiff)) + + # resize W handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = 5 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert_allclose(tool.extents, (xdata_new, extents[1] - xdiff, + extents[2] + xdiff, extents[3] - xdiff)) + + # resize W handle negative diff + extents = tool.extents + xdata, ydata = extents[0], extents[2] + (extents[3] - extents[2]) / 2 + xdiff = -25 + xdata_new, ydata_new = xdata + xdiff, ydata + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert_allclose(tool.extents, (xdata_new, extents[1] - xdiff, + extents[2] + xdiff, extents[3] - xdiff)) + + # resize SW handle + extents = tool.extents + xdata, ydata = extents[0], extents[2] + xdiff, ydiff = 20, 25 + xdata_new, ydata_new = xdata + xdiff, ydata + ydiff + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert_allclose(tool.extents, (extents[0] + ydiff, extents[1] - ydiff, + ydata_new, extents[3] - ydiff)) + + +@pytest.mark.parametrize('selector_class', + [widgets.RectangleSelector, widgets.EllipseSelector]) +def test_rectangle_rotate(ax, selector_class): + tool = selector_class(ax, onselect=noop, interactive=True) + # Draw rectangle + click_and_drag(tool, start=(100, 100), end=(130, 140)) + assert tool.extents == (100, 130, 100, 140) + assert len(tool._state) == 0 + + # Rotate anticlockwise using top-right corner + do_event(tool, 'on_key_press', key='r') + assert tool._state == set(['rotate']) + assert len(tool._state) == 1 + click_and_drag(tool, start=(130, 140), end=(120, 145)) + do_event(tool, 'on_key_press', key='r') + assert len(tool._state) == 0 + # Extents shouldn't change (as shape of rectangle hasn't changed) + assert tool.extents == (100, 130, 100, 140) + assert_allclose(tool.rotation, 25.56, atol=0.01) + tool.rotation = 45 + assert tool.rotation == 45 + # Corners should move + assert_allclose(tool.corners, + np.array([[118.53, 139.75, 111.46, 90.25], + [95.25, 116.46, 144.75, 123.54]]), atol=0.01) + + # Scale using top-right corner + click_and_drag(tool, start=(110, 145), end=(110, 160)) + assert_allclose(tool.extents, (100, 139.75, 100, 151.82), atol=0.01) + + if selector_class == widgets.RectangleSelector: + with pytest.raises(ValueError): + tool._selection_artist.rotation_point = 'unvalid_value' + + +def test_rectangle_add_remove_set(ax): + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + # Draw rectangle + click_and_drag(tool, start=(100, 100), end=(130, 140)) + assert tool.extents == (100, 130, 100, 140) + assert len(tool._state) == 0 + for state in ['rotate', 'square', 'center']: + tool.add_state(state) + assert len(tool._state) == 1 + tool.remove_state(state) + assert len(tool._state) == 0 + + +@pytest.mark.parametrize('use_data_coordinates', [False, True]) +def test_rectangle_resize_square_center_aspect(ax, use_data_coordinates): + ax.set_aspect(0.8) + + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True, + use_data_coordinates=use_data_coordinates) + # Create rectangle + click_and_drag(tool, start=(70, 65), end=(120, 115)) + assert tool.extents == (70.0, 120.0, 65.0, 115.0) + tool.add_state('square') + tool.add_state('center') + + if use_data_coordinates: + # resize E handle + extents = tool.extents + xdata, ydata, width = extents[1], extents[3], extents[1] - extents[0] + xdiff, ycenter = 10, extents[2] + (extents[3] - extents[2]) / 2 + xdata_new, ydata_new = xdata + xdiff, ydata + ychange = width / 2 + xdiff + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new, + ycenter - ychange, ycenter + ychange]) + else: + # resize E handle + extents = tool.extents + xdata, ydata = extents[1], extents[3] + xdiff = 10 + xdata_new, ydata_new = xdata + xdiff, ydata + ychange = xdiff * 1 / tool._aspect_ratio_correction + click_and_drag(tool, start=(xdata, ydata), end=(xdata_new, ydata_new)) + assert_allclose(tool.extents, [extents[0] - xdiff, xdata_new, + 46.25, 133.75]) + + +def test_ellipse(ax): """For ellipse, test out the key modifiers""" - ax = get_ax() - - def onselect(epress, erelease): - pass - - tool = widgets.EllipseSelector(ax, onselect=onselect, - maxdist=10, interactive=True) + tool = widgets.EllipseSelector(ax, onselect=noop, + grab_range=10, interactive=True) tool.extents = (100, 150, 100, 150) # drag the rectangle - do_event(tool, 'press', xdata=10, ydata=10, button=1, - key=' ') - - do_event(tool, 'onmove', xdata=30, ydata=30, button=1) - do_event(tool, 'release', xdata=30, ydata=30, button=1) + click_and_drag(tool, start=(125, 125), end=(145, 145)) assert tool.extents == (120, 170, 120, 170) # create from center - do_event(tool, 'on_key_press', xdata=100, ydata=100, button=1, - key='control') - do_event(tool, 'press', xdata=100, ydata=100, button=1) - do_event(tool, 'onmove', xdata=125, ydata=125, button=1) - do_event(tool, 'release', xdata=125, ydata=125, button=1) - do_event(tool, 'on_key_release', xdata=100, ydata=100, button=1, - key='control') + click_and_drag(tool, start=(100, 100), end=(125, 125), key='control') assert tool.extents == (75, 125, 75, 125) # create a square - do_event(tool, 'on_key_press', xdata=10, ydata=10, button=1, - key='shift') - do_event(tool, 'press', xdata=10, ydata=10, button=1) - do_event(tool, 'onmove', xdata=35, ydata=30, button=1) - do_event(tool, 'release', xdata=35, ydata=30, button=1) - do_event(tool, 'on_key_release', xdata=10, ydata=10, button=1, - key='shift') + click_and_drag(tool, start=(10, 10), end=(35, 30), key='shift') extents = [int(e) for e in tool.extents] - assert extents == [10, 35, 10, 34] + assert extents == [10, 35, 10, 35] # create a square from center - do_event(tool, 'on_key_press', xdata=100, ydata=100, button=1, - key='ctrl+shift') - do_event(tool, 'press', xdata=100, ydata=100, button=1) - do_event(tool, 'onmove', xdata=125, ydata=130, button=1) - do_event(tool, 'release', xdata=125, ydata=130, button=1) - do_event(tool, 'on_key_release', xdata=100, ydata=100, button=1, - key='ctrl+shift') + click_and_drag(tool, start=(100, 100), end=(125, 130), key='ctrl+shift') extents = [int(e) for e in tool.extents] - assert extents == [70, 129, 70, 130] + assert extents == [70, 130, 70, 130] assert tool.geometry.shape == (2, 73) assert_allclose(tool.geometry[:, 0], [70., 100]) -def test_rectangle_handles(): - ax = get_ax() - - def onselect(epress, erelease): - pass - - tool = widgets.RectangleSelector(ax, onselect=onselect, - maxdist=10, interactive=True, - marker_props={'markerfacecolor': 'r', +def test_rectangle_handles(ax): + tool = widgets.RectangleSelector(ax, onselect=noop, + grab_range=10, + interactive=True, + handle_props={'markerfacecolor': 'r', 'markeredgecolor': 'b'}) tool.extents = (100, 150, 100, 150) - assert tool.corners == ( - (100, 150, 150, 100), (100, 100, 150, 150)) + assert_allclose(tool.corners, ((100, 150, 150, 100), (100, 100, 150, 150))) assert tool.extents == (100, 150, 100, 150) - assert tool.edge_centers == ( - (100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150)) + assert_allclose(tool.edge_centers, + ((100, 125.0, 150, 125.0), (125.0, 100, 125.0, 150))) assert tool.extents == (100, 150, 100, 150) # grab a corner and move it - do_event(tool, 'press', xdata=100, ydata=100) - do_event(tool, 'onmove', xdata=120, ydata=120) - do_event(tool, 'release', xdata=120, ydata=120) + click_and_drag(tool, start=(100, 100), end=(120, 120)) assert tool.extents == (120, 150, 120, 150) # grab the center and move it - do_event(tool, 'press', xdata=132, ydata=132) - do_event(tool, 'onmove', xdata=120, ydata=120) - do_event(tool, 'release', xdata=120, ydata=120) + click_and_drag(tool, start=(132, 132), end=(120, 120)) assert tool.extents == (108, 138, 108, 138) # create a new rectangle - do_event(tool, 'press', xdata=10, ydata=10) - do_event(tool, 'onmove', xdata=100, ydata=100) - do_event(tool, 'release', xdata=100, ydata=100) + click_and_drag(tool, start=(10, 10), end=(100, 100)) assert tool.extents == (10, 100, 10, 100) # Check that marker_props worked. assert mcolors.same_color( - tool._corner_handles.artist.get_markerfacecolor(), 'r') + tool._corner_handles.artists[0].get_markerfacecolor(), 'r') assert mcolors.same_color( - tool._corner_handles.artist.get_markeredgecolor(), 'b') + tool._corner_handles.artists[0].get_markeredgecolor(), 'b') -def check_span(*args, **kwargs): - ax = get_ax() +@pytest.mark.parametrize('interactive', [True, False]) +def test_rectangle_selector_onselect(ax, interactive): + # check when press and release events take place at the same position + onselect = mock.Mock(spec=noop, return_value=None) + + tool = widgets.RectangleSelector(ax, onselect, interactive=interactive) + # move outside of axis + click_and_drag(tool, start=(100, 110), end=(150, 120)) + + onselect.assert_called_once() + assert tool.extents == (100.0, 150.0, 110.0, 120.0) + + onselect.reset_mock() + click_and_drag(tool, start=(10, 100), end=(10, 100)) + onselect.assert_called_once() - def onselect(vmin, vmax): - ax._got_onselect = True - assert vmin == 100 - assert vmax == 150 - def onmove(vmin, vmax): - assert vmin == 100 - assert vmax == 125 - ax._got_on_move = True +@pytest.mark.parametrize('ignore_event_outside', [True, False]) +def test_rectangle_selector_ignore_outside(ax, ignore_event_outside): + onselect = mock.Mock(spec=noop, return_value=None) - if 'onmove_callback' in kwargs: + tool = widgets.RectangleSelector(ax, onselect, + ignore_event_outside=ignore_event_outside) + click_and_drag(tool, start=(100, 110), end=(150, 120)) + onselect.assert_called_once() + assert tool.extents == (100.0, 150.0, 110.0, 120.0) + + onselect.reset_mock() + # Trigger event outside of span + click_and_drag(tool, start=(150, 150), end=(160, 160)) + if ignore_event_outside: + # event have been ignored and span haven't changed. + onselect.assert_not_called() + assert tool.extents == (100.0, 150.0, 110.0, 120.0) + else: + # A new shape is created + onselect.assert_called_once() + assert tool.extents == (150.0, 160.0, 150.0, 160.0) + + +@pytest.mark.parametrize('orientation, onmove_callback, kwargs', [ + ('horizontal', False, dict(minspan=10, useblit=True)), + ('vertical', True, dict(button=1)), + ('horizontal', False, dict(props=dict(fill=True))), + ('horizontal', False, dict(interactive=True)), +]) +def test_span_selector(ax, orientation, onmove_callback, kwargs): + onselect = mock.Mock(spec=noop, return_value=None) + onmove = mock.Mock(spec=noop, return_value=None) + if onmove_callback: kwargs['onmove_callback'] = onmove - tool = widgets.SpanSelector(ax, onselect, *args, **kwargs) + tool = widgets.SpanSelector(ax, onselect, orientation, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) - do_event(tool, 'onmove', xdata=125, ydata=125, button=1) - do_event(tool, 'release', xdata=150, ydata=150, button=1) + # move outside of axis + do_event(tool, 'onmove', xdata=199, ydata=199, button=1) + do_event(tool, 'release', xdata=250, ydata=250, button=1) + + onselect.assert_called_once_with(100, 199) + if onmove_callback: + onmove.assert_called_once_with(100, 199) + + +@pytest.mark.parametrize('interactive', [True, False]) +def test_span_selector_onselect(ax, interactive): + onselect = mock.Mock(spec=noop, return_value=None) + + tool = widgets.SpanSelector(ax, onselect, 'horizontal', + interactive=interactive) + # move outside of axis + click_and_drag(tool, start=(100, 100), end=(150, 100)) + onselect.assert_called_once() + assert tool.extents == (100, 150) + + onselect.reset_mock() + click_and_drag(tool, start=(10, 100), end=(10, 100)) + onselect.assert_called_once() + + +@pytest.mark.parametrize('ignore_event_outside', [True, False]) +def test_span_selector_ignore_outside(ax, ignore_event_outside): + onselect = mock.Mock(spec=noop, return_value=None) + onmove = mock.Mock(spec=noop, return_value=None) + + tool = widgets.SpanSelector(ax, onselect, 'horizontal', + onmove_callback=onmove, + ignore_event_outside=ignore_event_outside) + click_and_drag(tool, start=(100, 100), end=(125, 125)) + onselect.assert_called_once() + onmove.assert_called_once() + assert tool.extents == (100, 125) + + onselect.reset_mock() + onmove.reset_mock() + # Trigger event outside of span + click_and_drag(tool, start=(150, 150), end=(160, 160)) + if ignore_event_outside: + # event have been ignored and span haven't changed. + onselect.assert_not_called() + onmove.assert_not_called() + assert tool.extents == (100, 125) + else: + # A new shape is created + onselect.assert_called_once() + onmove.assert_called_once() + assert tool.extents == (150, 160) + + +@pytest.mark.parametrize('drag_from_anywhere', [True, False]) +def test_span_selector_drag(ax, drag_from_anywhere): + # Create span + tool = widgets.SpanSelector(ax, onselect=noop, direction='horizontal', + interactive=True, + drag_from_anywhere=drag_from_anywhere) + click_and_drag(tool, start=(10, 10), end=(100, 120)) + assert tool.extents == (10, 100) + # Drag inside span + # + # If drag_from_anywhere == True, this will move the span by 10, + # giving new value extents = 20, 110 + # + # If drag_from_anywhere == False, this will create a new span with + # value extents = 25, 35 + click_and_drag(tool, start=(25, 15), end=(35, 25)) + if drag_from_anywhere: + assert tool.extents == (20, 110) + else: + assert tool.extents == (25, 35) - assert ax._got_onselect + # Check that in both cases, dragging outside the span draws a new span + click_and_drag(tool, start=(175, 185), end=(185, 195)) + assert tool.extents == (175, 185) - if 'onmove_callback' in kwargs: - assert ax._got_on_move +def test_span_selector_direction(ax): + tool = widgets.SpanSelector(ax, onselect=noop, direction='horizontal', + interactive=True) + assert tool.direction == 'horizontal' + assert tool._edge_handles.direction == 'horizontal' -def test_span_selector(): - check_span('horizontal', minspan=10, useblit=True) - check_span('vertical', onmove_callback=True, button=1) - check_span('horizontal', rectprops=dict(fill=True)) + with pytest.raises(ValueError): + tool = widgets.SpanSelector(ax, onselect=noop, + direction='invalid_direction') + tool.direction = 'vertical' + assert tool.direction == 'vertical' + assert tool._edge_handles.direction == 'vertical' -def check_lasso_selector(**kwargs): - ax = get_ax() + with pytest.raises(ValueError): + tool.direction = 'invalid_string' + + +def test_span_selector_set_props_handle_props(ax): + tool = widgets.SpanSelector(ax, onselect=noop, direction='horizontal', + interactive=True, + props=dict(facecolor='b', alpha=0.2), + handle_props=dict(alpha=0.5)) + # Create rectangle + click_and_drag(tool, start=(0, 10), end=(100, 120)) + + artist = tool._selection_artist + assert artist.get_facecolor() == mcolors.to_rgba('b', alpha=0.2) + tool.set_props(facecolor='r', alpha=0.3) + assert artist.get_facecolor() == mcolors.to_rgba('r', alpha=0.3) + + for artist in tool._handles_artists: + assert artist.get_color() == 'b' + assert artist.get_alpha() == 0.5 + tool.set_handle_props(color='r', alpha=0.3) + for artist in tool._handles_artists: + assert artist.get_color() == 'r' + assert artist.get_alpha() == 0.3 + + +@pytest.mark.parametrize('selector', ['span', 'rectangle']) +def test_selector_clear(ax, selector): + kwargs = dict(ax=ax, onselect=noop, interactive=True) + if selector == 'span': + Selector = widgets.SpanSelector + kwargs['direction'] = 'horizontal' + else: + Selector = widgets.RectangleSelector + + tool = Selector(**kwargs) + click_and_drag(tool, start=(10, 10), end=(100, 120)) + + # press-release event outside the selector to clear the selector + click_and_drag(tool, start=(130, 130), end=(130, 130)) + assert not tool._selection_completed + + kwargs['ignore_event_outside'] = True + tool = Selector(**kwargs) + assert tool.ignore_event_outside + click_and_drag(tool, start=(10, 10), end=(100, 120)) + + # press-release event outside the selector ignored + click_and_drag(tool, start=(130, 130), end=(130, 130)) + assert tool._selection_completed + + do_event(tool, 'on_key_press', key='escape') + assert not tool._selection_completed + + +@pytest.mark.parametrize('selector', ['span', 'rectangle']) +def test_selector_clear_method(ax, selector): + if selector == 'span': + tool = widgets.SpanSelector(ax, onselect=noop, direction='horizontal', + interactive=True, + ignore_event_outside=True) + else: + tool = widgets.RectangleSelector(ax, onselect=noop, interactive=True) + click_and_drag(tool, start=(10, 10), end=(100, 120)) + assert tool._selection_completed + assert tool.get_visible() + if selector == 'span': + assert tool.extents == (10, 100) + + tool.clear() + assert not tool._selection_completed + assert not tool.get_visible() + + # Do another cycle of events to make sure we can + click_and_drag(tool, start=(10, 10), end=(50, 120)) + assert tool._selection_completed + assert tool.get_visible() + if selector == 'span': + assert tool.extents == (10, 50) + + +def test_span_selector_add_state(ax): + tool = widgets.SpanSelector(ax, noop, 'horizontal', + interactive=True) + + with pytest.raises(ValueError): + tool.add_state('unsupported_state') + with pytest.raises(ValueError): + tool.add_state('center') + with pytest.raises(ValueError): + tool.add_state('square') + + tool.add_state('move') + + +def test_tool_line_handle(ax): + positions = [20, 30, 50] + tool_line_handle = widgets.ToolLineHandles(ax, positions, 'horizontal', + useblit=False) + + for artist in tool_line_handle.artists: + assert not artist.get_animated() + assert not artist.get_visible() + + tool_line_handle.set_visible(True) + tool_line_handle.set_animated(True) + + for artist in tool_line_handle.artists: + assert artist.get_animated() + assert artist.get_visible() + + assert tool_line_handle.positions == positions - def onselect(verts): + +@pytest.mark.parametrize('direction', ("horizontal", "vertical")) +def test_span_selector_bound(direction): + fig, ax = plt.subplots(1, 1) + ax.plot([10, 20], [10, 30]) + ax.figure.canvas.draw() + x_bound = ax.get_xbound() + y_bound = ax.get_ybound() + + tool = widgets.SpanSelector(ax, print, direction, interactive=True) + assert ax.get_xbound() == x_bound + assert ax.get_ybound() == y_bound + + bound = x_bound if direction == 'horizontal' else y_bound + assert tool._edge_handles.positions == list(bound) + + press_data = [10.5, 11.5] + move_data = [11, 13] # Updating selector is done in onmove + release_data = move_data + click_and_drag(tool, start=press_data, end=move_data) + + assert ax.get_xbound() == x_bound + assert ax.get_ybound() == y_bound + + index = 0 if direction == 'horizontal' else 1 + handle_positions = [press_data[index], release_data[index]] + assert tool._edge_handles.positions == handle_positions + + +@pytest.mark.backend('QtAgg', skip_on_importerror=True) +def test_span_selector_animated_artists_callback(): + """Check that the animated artists changed in callbacks are updated.""" + x = np.linspace(0, 2 * np.pi, 100) + values = np.sin(x) + + fig, ax = plt.subplots() + ln, = ax.plot(x, values, animated=True) + ln2, = ax.plot([], animated=True) + + # spin the event loop to let the backend process any pending operations + # before drawing artists + # See blitting tutorial + plt.pause(0.1) + ax.draw_artist(ln) + fig.canvas.blit(fig.bbox) + + def mean(vmin, vmax): + # Return mean of values in x between *vmin* and *vmax* + indmin, indmax = np.searchsorted(x, (vmin, vmax)) + v = values[indmin:indmax].mean() + ln2.set_data(x, np.full_like(x, v)) + + span = widgets.SpanSelector(ax, mean, direction='horizontal', + onmove_callback=mean, + interactive=True, + drag_from_anywhere=True, + useblit=True) + + # Add span selector and check that the line is draw after it was updated + # by the callback + press_data = [1, 2] + move_data = [2, 2] + do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1) + do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1) + assert span._get_animated_artists() == (ln, ln2) + assert ln.stale is False + assert ln2.stale + assert_allclose(ln2.get_ydata(), 0.9547335049088455) + span.update() + assert ln2.stale is False + + # Change span selector and check that the line is drawn/updated after its + # value was updated by the callback + press_data = [4, 2] + move_data = [5, 2] + release_data = [5, 2] + do_event(span, 'press', xdata=press_data[0], ydata=press_data[1], button=1) + do_event(span, 'onmove', xdata=move_data[0], ydata=move_data[1], button=1) + assert ln.stale is False + assert ln2.stale + assert_allclose(ln2.get_ydata(), -0.9424150707548072) + do_event(span, 'release', xdata=release_data[0], + ydata=release_data[1], button=1) + assert ln2.stale is False + + +def test_snapping_values_span_selector(ax): + def onselect(*args): + pass + + tool = widgets.SpanSelector(ax, onselect, direction='horizontal',) + snap_function = tool._snap + + snap_values = np.linspace(0, 5, 11) + values = np.array([-0.1, 0.1, 0.2, 0.5, 0.6, 0.7, 0.9, 4.76, 5.0, 5.5]) + expect = np.array([00.0, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 5.00, 5.0, 5.0]) + values = snap_function(values, snap_values) + assert_allclose(values, expect) + + +def test_span_selector_snap(ax): + def onselect(vmin, vmax): ax._got_onselect = True - assert verts == [(100, 100), (125, 125), (150, 150)] + + snap_values = np.arange(50) * 4 + + tool = widgets.SpanSelector(ax, onselect, direction='horizontal', + snap_values=snap_values) + tool.extents = (17, 35) + assert tool.extents == (16, 36) + + tool.snap_values = None + assert tool.snap_values is None + tool.extents = (17, 35) + assert tool.extents == (17, 35) + + +@pytest.mark.parametrize('kwargs', [ + dict(), + dict(useblit=False, props=dict(color='red')), + dict(useblit=True, button=1), +]) +def test_lasso_selector(ax, kwargs): + onselect = mock.Mock(spec=noop, return_value=None) tool = widgets.LassoSelector(ax, onselect, **kwargs) do_event(tool, 'press', xdata=100, ydata=100, button=1) do_event(tool, 'onmove', xdata=125, ydata=125, button=1) do_event(tool, 'release', xdata=150, ydata=150, button=1) - assert ax._got_onselect - + onselect.assert_called_once_with([(100, 100), (125, 125), (150, 150)]) -def test_lasso_selector(): - check_lasso_selector() - check_lasso_selector(useblit=False, lineprops=dict(color='red')) - check_lasso_selector(useblit=True, button=1) - -def test_CheckButtons(): - ax = get_ax() +def test_CheckButtons(ax): check = widgets.CheckButtons(ax, ('a', 'b', 'c'), (True, False, True)) assert check.get_status() == [True, False, True] check.set_active(0) @@ -208,27 +997,204 @@ def test_CheckButtons(): check.disconnect(cid) -@image_comparison(['check_radio_buttons.png'], style='mpl20', remove_text=True) -def test_check_radio_buttons_image(): - # Remove this line when this test image is regenerated. - plt.rcParams['text.kerning_factor'] = 6 +@pytest.mark.parametrize("toolbar", ["none", "toolbar2", "toolmanager"]) +def test_TextBox(ax, toolbar): + # Avoid "toolmanager is provisional" warning. + plt.rcParams._set("toolbar", toolbar) + + submit_event = mock.Mock(spec=noop, return_value=None) + text_change_event = mock.Mock(spec=noop, return_value=None) + tool = widgets.TextBox(ax, '') + tool.on_submit(submit_event) + tool.on_text_change(text_change_event) + + assert tool.text == '' - get_ax() - plt.subplots_adjust(left=0.3) - rax1 = plt.axes([0.05, 0.7, 0.15, 0.15]) - rax2 = plt.axes([0.05, 0.2, 0.15, 0.15]) - widgets.RadioButtons(rax1, ('Radio 1', 'Radio 2', 'Radio 3')) - widgets.CheckButtons(rax2, ('Check 1', 'Check 2', 'Check 3'), - (False, True, True)) + do_event(tool, '_click') + tool.set_val('x**2') -@image_comparison(['check_bunch_of_radio_buttons.png'], - style='mpl20', remove_text=True) -def test_check_bunch_of_radio_buttons(): - rax = plt.axes([0.05, 0.1, 0.15, 0.7]) - widgets.RadioButtons(rax, ('B1', 'B2', 'B3', 'B4', 'B5', 'B6', - 'B7', 'B8', 'B9', 'B10', 'B11', 'B12', - 'B13', 'B14', 'B15')) + assert tool.text == 'x**2' + assert text_change_event.call_count == 1 + + tool.begin_typing() + tool.stop_typing() + + assert submit_event.call_count == 2 + + do_event(tool, '_click') + do_event(tool, '_keypress', key='+') + do_event(tool, '_keypress', key='5') + + assert text_change_event.call_count == 3 + + +@image_comparison(['check_radio_buttons.png'], style='mpl20', remove_text=True) +def test_check_radio_buttons_image(): + ax = get_ax() + fig = ax.figure + fig.subplots_adjust(left=0.3) + + rax1 = fig.add_axes([0.05, 0.7, 0.2, 0.15]) + rb1 = widgets.RadioButtons(rax1, ('Radio 1', 'Radio 2', 'Radio 3')) + with pytest.warns(DeprecationWarning, + match='The circles attribute was deprecated'): + rb1.circles # Trigger the old-style elliptic radiobuttons. + + rax2 = fig.add_axes([0.05, 0.5, 0.2, 0.15]) + cb1 = widgets.CheckButtons(rax2, ('Check 1', 'Check 2', 'Check 3'), + (False, True, True)) + with pytest.warns(DeprecationWarning, + match='The rectangles attribute was deprecated'): + cb1.rectangles # Trigger old-style Rectangle check boxes + + rax3 = fig.add_axes([0.05, 0.3, 0.2, 0.15]) + rb3 = widgets.RadioButtons( + rax3, ('Radio 1', 'Radio 2', 'Radio 3'), + label_props={'fontsize': [8, 12, 16], + 'color': ['red', 'green', 'blue']}, + radio_props={'edgecolor': ['red', 'green', 'blue'], + 'facecolor': ['mistyrose', 'palegreen', 'lightblue']}) + + rax4 = fig.add_axes([0.05, 0.1, 0.2, 0.15]) + cb4 = widgets.CheckButtons( + rax4, ('Check 1', 'Check 2', 'Check 3'), (False, True, True), + label_props={'fontsize': [8, 12, 16], + 'color': ['red', 'green', 'blue']}, + frame_props={'edgecolor': ['red', 'green', 'blue'], + 'facecolor': ['mistyrose', 'palegreen', 'lightblue']}, + check_props={'color': ['red', 'green', 'blue']}) + + +@check_figures_equal(extensions=["png"]) +def test_radio_buttons(fig_test, fig_ref): + widgets.RadioButtons(fig_test.subplots(), ["tea", "coffee"]) + ax = fig_ref.add_subplot(xticks=[], yticks=[]) + ax.scatter([.15, .15], [2/3, 1/3], transform=ax.transAxes, + s=(plt.rcParams["font.size"] / 2) ** 2, c=["C0", "none"]) + ax.text(.25, 2/3, "tea", transform=ax.transAxes, va="center") + ax.text(.25, 1/3, "coffee", transform=ax.transAxes, va="center") + + +@check_figures_equal(extensions=['png']) +def test_radio_buttons_props(fig_test, fig_ref): + label_props = {'color': ['red'], 'fontsize': [24]} + radio_props = {'facecolor': 'green', 'edgecolor': 'blue', 'linewidth': 2} + + widgets.RadioButtons(fig_ref.subplots(), ['tea', 'coffee'], + label_props=label_props, radio_props=radio_props) + + cb = widgets.RadioButtons(fig_test.subplots(), ['tea', 'coffee']) + cb.set_label_props(label_props) + # Setting the label size automatically increases default marker size, so we + # need to do that here as well. + cb.set_radio_props({**radio_props, 's': (24 / 2)**2}) + + +def test_radio_button_active_conflict(ax): + with pytest.warns(UserWarning, + match=r'Both the \*activecolor\* parameter'): + rb = widgets.RadioButtons(ax, ['tea', 'coffee'], activecolor='red', + radio_props={'facecolor': 'green'}) + # *radio_props*' facecolor wins over *activecolor* + assert mcolors.same_color(rb._buttons.get_facecolor(), ['green', 'none']) + + +@check_figures_equal(extensions=['png']) +def test_radio_buttons_activecolor_change(fig_test, fig_ref): + widgets.RadioButtons(fig_ref.subplots(), ['tea', 'coffee'], + activecolor='green') + + # Test property setter. + cb = widgets.RadioButtons(fig_test.subplots(), ['tea', 'coffee'], + activecolor='red') + cb.activecolor = 'green' + + +@check_figures_equal(extensions=["png"]) +def test_check_buttons(fig_test, fig_ref): + widgets.CheckButtons(fig_test.subplots(), ["tea", "coffee"], [True, True]) + ax = fig_ref.add_subplot(xticks=[], yticks=[]) + ax.scatter([.15, .15], [2/3, 1/3], marker='s', transform=ax.transAxes, + s=(plt.rcParams["font.size"] / 2) ** 2, c=["none", "none"]) + ax.scatter([.15, .15], [2/3, 1/3], marker='x', transform=ax.transAxes, + s=(plt.rcParams["font.size"] / 2) ** 2, c=["k", "k"]) + ax.text(.25, 2/3, "tea", transform=ax.transAxes, va="center") + ax.text(.25, 1/3, "coffee", transform=ax.transAxes, va="center") + + +@check_figures_equal(extensions=['png']) +def test_check_button_props(fig_test, fig_ref): + label_props = {'color': ['red'], 'fontsize': [24]} + frame_props = {'facecolor': 'green', 'edgecolor': 'blue', 'linewidth': 2} + check_props = {'facecolor': 'red', 'linewidth': 2} + + widgets.CheckButtons(fig_ref.subplots(), ['tea', 'coffee'], [True, True], + label_props=label_props, frame_props=frame_props, + check_props=check_props) + + cb = widgets.CheckButtons(fig_test.subplots(), ['tea', 'coffee'], + [True, True]) + cb.set_label_props(label_props) + # Setting the label size automatically increases default marker size, so we + # need to do that here as well. + cb.set_frame_props({**frame_props, 's': (24 / 2)**2}) + # FIXME: Axes.scatter promotes facecolor to edgecolor on unfilled markers, + # but Collection.update doesn't do that (it forgot the marker already). + # This means we cannot pass facecolor to both setters directly. + check_props['edgecolor'] = check_props.pop('facecolor') + cb.set_check_props({**check_props, 's': (24 / 2)**2}) + + +@check_figures_equal(extensions=["png"]) +def test_check_buttons_rectangles(fig_test, fig_ref): + # Test should be removed once .rectangles is removed + cb = widgets.CheckButtons(fig_test.subplots(), ["", ""], + [False, False]) + with pytest.warns(DeprecationWarning, + match='The rectangles attribute was deprecated'): + cb.rectangles + ax = fig_ref.add_subplot(xticks=[], yticks=[]) + ys = [2/3, 1/3] + dy = 1/3 + w, h = dy / 2, dy / 2 + rectangles = [ + Rectangle(xy=(0.05, ys[i] - h / 2), width=w, height=h, + edgecolor="black", + facecolor="none", + transform=ax.transAxes + ) + for i, y in enumerate(ys) + ] + for rectangle in rectangles: + ax.add_patch(rectangle) + + +@check_figures_equal(extensions=["png"]) +def test_check_buttons_lines(fig_test, fig_ref): + # Test should be removed once .lines is removed + cb = widgets.CheckButtons(fig_test.subplots(), ["", ""], [True, True]) + with pytest.warns(DeprecationWarning, + match='The lines attribute was deprecated'): + cb.lines + for rectangle in cb._rectangles: + rectangle.set_visible(False) + ax = fig_ref.add_subplot(xticks=[], yticks=[]) + ys = [2/3, 1/3] + dy = 1/3 + w, h = dy / 2, dy / 2 + lineparams = {'color': 'k', 'linewidth': 1.25, + 'transform': ax.transAxes, + 'solid_capstyle': 'butt'} + for i, y in enumerate(ys): + x, y = 0.05, y - h / 2 + l1 = Line2D([x, x + w], [y + h, y], **lineparams) + l2 = Line2D([x, x + w], [y, y + h], **lineparams) + + l1.set_visible(True) + l2.set_visible(True) + ax.add_line(l1) + ax.add_line(l2) def test_slider_slidermin_slidermax_invalid(): @@ -286,7 +1252,7 @@ def test_slider_horizontal_vertical(): assert slider.val == 10 # check the dimension of the slider patch in axes units box = slider.poly.get_extents().transformed(ax.transAxes.inverted()) - assert_allclose(box.bounds, [0, 0, 10/24, 1]) + assert_allclose(box.bounds, [0, .25, 10/24, .5]) fig, ax = plt.subplots() slider = widgets.Slider(ax=ax, label='', valmin=0, valmax=24, @@ -295,7 +1261,15 @@ def test_slider_horizontal_vertical(): assert slider.val == 10 # check the dimension of the slider patch in axes units box = slider.poly.get_extents().transformed(ax.transAxes.inverted()) - assert_allclose(box.bounds, [0, 0, 1, 10/24]) + assert_allclose(box.bounds, [.25, 0, .5, 10/24]) + + +def test_slider_reset(): + fig, ax = plt.subplots() + slider = widgets.Slider(ax=ax, label='', valmin=0, valmax=1, valinit=.5) + slider.set_val(0.75) + slider.reset() + assert slider.val == 0.5 @pytest.mark.parametrize("orientation", ["horizontal", "vertical"]) @@ -312,24 +1286,56 @@ def test_range_slider(orientation): valinit=[0.1, 0.34] ) box = slider.poly.get_extents().transformed(ax.transAxes.inverted()) - assert_allclose(box.get_points().flatten()[idx], [0.1, 0, 0.34, 1]) + assert_allclose(box.get_points().flatten()[idx], [0.1, 0.25, 0.34, 0.75]) # Check initial value is set correctly assert_allclose(slider.val, (0.1, 0.34)) - slider.set_val((0.2, 0.6)) - assert_allclose(slider.val, (0.2, 0.6)) + def handle_positions(slider): + if orientation == "vertical": + return [h.get_ydata()[0] for h in slider._handles] + else: + return [h.get_xdata()[0] for h in slider._handles] + + slider.set_val((0.4, 0.6)) + assert_allclose(slider.val, (0.4, 0.6)) + assert_allclose(handle_positions(slider), (0.4, 0.6)) + box = slider.poly.get_extents().transformed(ax.transAxes.inverted()) - assert_allclose(box.get_points().flatten()[idx], [0.2, 0, 0.6, 1]) + assert_allclose(box.get_points().flatten()[idx], [0.4, .25, 0.6, .75]) slider.set_val((0.2, 0.1)) assert_allclose(slider.val, (0.1, 0.2)) + assert_allclose(handle_positions(slider), (0.1, 0.2)) slider.set_val((-1, 10)) assert_allclose(slider.val, (0, 1)) + assert_allclose(handle_positions(slider), (0, 1)) + + slider.reset() + assert_allclose(slider.val, (0.1, 0.34)) + assert_allclose(handle_positions(slider), (0.1, 0.34)) + + +@pytest.mark.parametrize("orientation", ["horizontal", "vertical"]) +def test_range_slider_same_init_values(orientation): + if orientation == "vertical": + idx = [1, 0, 3, 2] + else: + idx = [0, 1, 2, 3] + + fig, ax = plt.subplots() + + slider = widgets.RangeSlider( + ax=ax, label="", valmin=0.0, valmax=1.0, orientation=orientation, + valinit=[0, 0] + ) + box = slider.poly.get_extents().transformed(ax.transAxes.inverted()) + assert_allclose(box.get_points().flatten()[idx], [0, 0.25, 0, 0.75]) -def check_polygon_selector(event_sequence, expected_result, selections_count): +def check_polygon_selector(event_sequence, expected_result, selections_count, + **kwargs): """ Helper function to test Polygon Selector. @@ -346,22 +1352,20 @@ def check_polygon_selector(event_sequence, expected_result, selections_count): selections_count : int Wait for the tool to call its `onselect` function `selections_count` times, before comparing the result to the `expected_result` + **kwargs + Keyword arguments are passed to PolygonSelector. """ ax = get_ax() - ax._selections_count = 0 - - def onselect(vertices): - ax._selections_count += 1 - ax._current_result = vertices + onselect = mock.Mock(spec=noop, return_value=None) - tool = widgets.PolygonSelector(ax, onselect) + tool = widgets.PolygonSelector(ax, onselect, **kwargs) for (etype, event_args) in event_sequence: do_event(tool, etype, **event_args) - assert ax._selections_count == selections_count - assert ax._current_result == expected_result + assert onselect.call_count == selections_count + assert onselect.call_args == ((expected_result, ), {}) def polygon_place_vertex(xdata, ydata): @@ -370,133 +1374,352 @@ def polygon_place_vertex(xdata, ydata): ('release', dict(xdata=xdata, ydata=ydata))] -def test_polygon_selector(): +def polygon_remove_vertex(xdata, ydata): + return [('onmove', dict(xdata=xdata, ydata=ydata)), + ('press', dict(xdata=xdata, ydata=ydata, button=3)), + ('release', dict(xdata=xdata, ydata=ydata, button=3))] + + +@pytest.mark.parametrize('draw_bounding_box', [False, True]) +def test_polygon_selector(draw_bounding_box): + check_selector = functools.partial( + check_polygon_selector, draw_bounding_box=draw_bounding_box) + # Simple polygon expected_result = [(50, 50), (150, 50), (50, 150)] - event_sequence = (polygon_place_vertex(50, 50) - + polygon_place_vertex(150, 50) - + polygon_place_vertex(50, 150) - + polygon_place_vertex(50, 50)) - check_polygon_selector(event_sequence, expected_result, 1) + event_sequence = [ + *polygon_place_vertex(50, 50), + *polygon_place_vertex(150, 50), + *polygon_place_vertex(50, 150), + *polygon_place_vertex(50, 50), + ] + check_selector(event_sequence, expected_result, 1) # Move first vertex before completing the polygon. expected_result = [(75, 50), (150, 50), (50, 150)] - event_sequence = (polygon_place_vertex(50, 50) - + polygon_place_vertex(150, 50) - + [('on_key_press', dict(key='control')), - ('onmove', dict(xdata=50, ydata=50)), - ('press', dict(xdata=50, ydata=50)), - ('onmove', dict(xdata=75, ydata=50)), - ('release', dict(xdata=75, ydata=50)), - ('on_key_release', dict(key='control'))] - + polygon_place_vertex(50, 150) - + polygon_place_vertex(75, 50)) - check_polygon_selector(event_sequence, expected_result, 1) + event_sequence = [ + *polygon_place_vertex(50, 50), + *polygon_place_vertex(150, 50), + ('on_key_press', dict(key='control')), + ('onmove', dict(xdata=50, ydata=50)), + ('press', dict(xdata=50, ydata=50)), + ('onmove', dict(xdata=75, ydata=50)), + ('release', dict(xdata=75, ydata=50)), + ('on_key_release', dict(key='control')), + *polygon_place_vertex(50, 150), + *polygon_place_vertex(75, 50), + ] + check_selector(event_sequence, expected_result, 1) # Move first two vertices at once before completing the polygon. expected_result = [(50, 75), (150, 75), (50, 150)] - event_sequence = (polygon_place_vertex(50, 50) - + polygon_place_vertex(150, 50) - + [('on_key_press', dict(key='shift')), - ('onmove', dict(xdata=100, ydata=100)), - ('press', dict(xdata=100, ydata=100)), - ('onmove', dict(xdata=100, ydata=125)), - ('release', dict(xdata=100, ydata=125)), - ('on_key_release', dict(key='shift'))] - + polygon_place_vertex(50, 150) - + polygon_place_vertex(50, 75)) - check_polygon_selector(event_sequence, expected_result, 1) + event_sequence = [ + *polygon_place_vertex(50, 50), + *polygon_place_vertex(150, 50), + ('on_key_press', dict(key='shift')), + ('onmove', dict(xdata=100, ydata=100)), + ('press', dict(xdata=100, ydata=100)), + ('onmove', dict(xdata=100, ydata=125)), + ('release', dict(xdata=100, ydata=125)), + ('on_key_release', dict(key='shift')), + *polygon_place_vertex(50, 150), + *polygon_place_vertex(50, 75), + ] + check_selector(event_sequence, expected_result, 1) # Move first vertex after completing the polygon. expected_result = [(75, 50), (150, 50), (50, 150)] - event_sequence = (polygon_place_vertex(50, 50) - + polygon_place_vertex(150, 50) - + polygon_place_vertex(50, 150) - + polygon_place_vertex(50, 50) - + [('onmove', dict(xdata=50, ydata=50)), - ('press', dict(xdata=50, ydata=50)), - ('onmove', dict(xdata=75, ydata=50)), - ('release', dict(xdata=75, ydata=50))]) - check_polygon_selector(event_sequence, expected_result, 2) + event_sequence = [ + *polygon_place_vertex(50, 50), + *polygon_place_vertex(150, 50), + *polygon_place_vertex(50, 150), + *polygon_place_vertex(50, 50), + ('onmove', dict(xdata=50, ydata=50)), + ('press', dict(xdata=50, ydata=50)), + ('onmove', dict(xdata=75, ydata=50)), + ('release', dict(xdata=75, ydata=50)), + ] + check_selector(event_sequence, expected_result, 2) # Move all vertices after completing the polygon. expected_result = [(75, 75), (175, 75), (75, 175)] - event_sequence = (polygon_place_vertex(50, 50) - + polygon_place_vertex(150, 50) - + polygon_place_vertex(50, 150) - + polygon_place_vertex(50, 50) - + [('on_key_press', dict(key='shift')), - ('onmove', dict(xdata=100, ydata=100)), - ('press', dict(xdata=100, ydata=100)), - ('onmove', dict(xdata=125, ydata=125)), - ('release', dict(xdata=125, ydata=125)), - ('on_key_release', dict(key='shift'))]) - check_polygon_selector(event_sequence, expected_result, 2) + event_sequence = [ + *polygon_place_vertex(50, 50), + *polygon_place_vertex(150, 50), + *polygon_place_vertex(50, 150), + *polygon_place_vertex(50, 50), + ('on_key_press', dict(key='shift')), + ('onmove', dict(xdata=100, ydata=100)), + ('press', dict(xdata=100, ydata=100)), + ('onmove', dict(xdata=125, ydata=125)), + ('release', dict(xdata=125, ydata=125)), + ('on_key_release', dict(key='shift')), + ] + check_selector(event_sequence, expected_result, 2) # Try to move a vertex and move all before placing any vertices. expected_result = [(50, 50), (150, 50), (50, 150)] - event_sequence = ([('on_key_press', dict(key='control')), - ('onmove', dict(xdata=100, ydata=100)), - ('press', dict(xdata=100, ydata=100)), - ('onmove', dict(xdata=125, ydata=125)), - ('release', dict(xdata=125, ydata=125)), - ('on_key_release', dict(key='control')), - ('on_key_press', dict(key='shift')), - ('onmove', dict(xdata=100, ydata=100)), - ('press', dict(xdata=100, ydata=100)), - ('onmove', dict(xdata=125, ydata=125)), - ('release', dict(xdata=125, ydata=125)), - ('on_key_release', dict(key='shift'))] - + polygon_place_vertex(50, 50) - + polygon_place_vertex(150, 50) - + polygon_place_vertex(50, 150) - + polygon_place_vertex(50, 50)) - check_polygon_selector(event_sequence, expected_result, 1) + event_sequence = [ + ('on_key_press', dict(key='control')), + ('onmove', dict(xdata=100, ydata=100)), + ('press', dict(xdata=100, ydata=100)), + ('onmove', dict(xdata=125, ydata=125)), + ('release', dict(xdata=125, ydata=125)), + ('on_key_release', dict(key='control')), + ('on_key_press', dict(key='shift')), + ('onmove', dict(xdata=100, ydata=100)), + ('press', dict(xdata=100, ydata=100)), + ('onmove', dict(xdata=125, ydata=125)), + ('release', dict(xdata=125, ydata=125)), + ('on_key_release', dict(key='shift')), + *polygon_place_vertex(50, 50), + *polygon_place_vertex(150, 50), + *polygon_place_vertex(50, 150), + *polygon_place_vertex(50, 50), + ] + check_selector(event_sequence, expected_result, 1) # Try to place vertex out-of-bounds, then reset, and start a new polygon. expected_result = [(50, 50), (150, 50), (50, 150)] - event_sequence = (polygon_place_vertex(50, 50) - + polygon_place_vertex(250, 50) - + [('on_key_press', dict(key='escape')), - ('on_key_release', dict(key='escape'))] - + polygon_place_vertex(50, 50) - + polygon_place_vertex(150, 50) - + polygon_place_vertex(50, 150) - + polygon_place_vertex(50, 50)) - check_polygon_selector(event_sequence, expected_result, 1) - - -@pytest.mark.parametrize( - "horizOn, vertOn", - [(True, True), (True, False), (False, True)], -) + event_sequence = [ + *polygon_place_vertex(50, 50), + *polygon_place_vertex(250, 50), + ('on_key_press', dict(key='escape')), + ('on_key_release', dict(key='escape')), + *polygon_place_vertex(50, 50), + *polygon_place_vertex(150, 50), + *polygon_place_vertex(50, 150), + *polygon_place_vertex(50, 50), + ] + check_selector(event_sequence, expected_result, 1) + + +@pytest.mark.parametrize('draw_bounding_box', [False, True]) +def test_polygon_selector_set_props_handle_props(ax, draw_bounding_box): + tool = widgets.PolygonSelector(ax, onselect=noop, + props=dict(color='b', alpha=0.2), + handle_props=dict(alpha=0.5), + draw_bounding_box=draw_bounding_box) + + event_sequence = [ + *polygon_place_vertex(50, 50), + *polygon_place_vertex(150, 50), + *polygon_place_vertex(50, 150), + *polygon_place_vertex(50, 50), + ] + + for (etype, event_args) in event_sequence: + do_event(tool, etype, **event_args) + + artist = tool._selection_artist + assert artist.get_color() == 'b' + assert artist.get_alpha() == 0.2 + tool.set_props(color='r', alpha=0.3) + assert artist.get_color() == 'r' + assert artist.get_alpha() == 0.3 + + for artist in tool._handles_artists: + assert artist.get_color() == 'b' + assert artist.get_alpha() == 0.5 + tool.set_handle_props(color='r', alpha=0.3) + for artist in tool._handles_artists: + assert artist.get_color() == 'r' + assert artist.get_alpha() == 0.3 + + +@check_figures_equal() +def test_rect_visibility(fig_test, fig_ref): + # Check that requesting an invisible selector makes it invisible + ax_test = fig_test.subplots() + _ = fig_ref.subplots() + + tool = widgets.RectangleSelector(ax_test, onselect=noop, + props={'visible': False}) + tool.extents = (0.2, 0.8, 0.3, 0.7) + + +# Change the order that the extra point is inserted in +@pytest.mark.parametrize('idx', [1, 2, 3]) +@pytest.mark.parametrize('draw_bounding_box', [False, True]) +def test_polygon_selector_remove(idx, draw_bounding_box): + verts = [(50, 50), (150, 50), (50, 150)] + event_sequence = [polygon_place_vertex(*verts[0]), + polygon_place_vertex(*verts[1]), + polygon_place_vertex(*verts[2]), + # Finish the polygon + polygon_place_vertex(*verts[0])] + # Add an extra point + event_sequence.insert(idx, polygon_place_vertex(200, 200)) + # Remove the extra point + event_sequence.append(polygon_remove_vertex(200, 200)) + # Flatten list of lists + event_sequence = sum(event_sequence, []) + check_polygon_selector(event_sequence, verts, 2, + draw_bounding_box=draw_bounding_box) + + +@pytest.mark.parametrize('draw_bounding_box', [False, True]) +def test_polygon_selector_remove_first_point(draw_bounding_box): + verts = [(50, 50), (150, 50), (50, 150)] + event_sequence = [ + *polygon_place_vertex(*verts[0]), + *polygon_place_vertex(*verts[1]), + *polygon_place_vertex(*verts[2]), + *polygon_place_vertex(*verts[0]), + *polygon_remove_vertex(*verts[0]), + ] + check_polygon_selector(event_sequence, verts[1:], 2, + draw_bounding_box=draw_bounding_box) + + +@pytest.mark.parametrize('draw_bounding_box', [False, True]) +def test_polygon_selector_redraw(ax, draw_bounding_box): + verts = [(50, 50), (150, 50), (50, 150)] + event_sequence = [ + *polygon_place_vertex(*verts[0]), + *polygon_place_vertex(*verts[1]), + *polygon_place_vertex(*verts[2]), + *polygon_place_vertex(*verts[0]), + # Polygon completed, now remove first two verts. + *polygon_remove_vertex(*verts[1]), + *polygon_remove_vertex(*verts[2]), + # At this point the tool should be reset so we can add more vertices. + *polygon_place_vertex(*verts[1]), + ] + + tool = widgets.PolygonSelector(ax, onselect=noop, + draw_bounding_box=draw_bounding_box) + for (etype, event_args) in event_sequence: + do_event(tool, etype, **event_args) + # After removing two verts, only one remains, and the + # selector should be automatically resete + assert tool.verts == verts[0:2] + + +@pytest.mark.parametrize('draw_bounding_box', [False, True]) +@check_figures_equal(extensions=['png']) +def test_polygon_selector_verts_setter(fig_test, fig_ref, draw_bounding_box): + verts = [(0.1, 0.4), (0.5, 0.9), (0.3, 0.2)] + ax_test = fig_test.add_subplot() + + tool_test = widgets.PolygonSelector( + ax_test, onselect=noop, draw_bounding_box=draw_bounding_box) + tool_test.verts = verts + assert tool_test.verts == verts + + ax_ref = fig_ref.add_subplot() + tool_ref = widgets.PolygonSelector( + ax_ref, onselect=noop, draw_bounding_box=draw_bounding_box) + event_sequence = [ + *polygon_place_vertex(*verts[0]), + *polygon_place_vertex(*verts[1]), + *polygon_place_vertex(*verts[2]), + *polygon_place_vertex(*verts[0]), + ] + for (etype, event_args) in event_sequence: + do_event(tool_ref, etype, **event_args) + + +def test_polygon_selector_box(ax): + # Create a diamond shape + verts = [(20, 0), (0, 20), (20, 40), (40, 20)] + event_sequence = [ + *polygon_place_vertex(*verts[0]), + *polygon_place_vertex(*verts[1]), + *polygon_place_vertex(*verts[2]), + *polygon_place_vertex(*verts[3]), + *polygon_place_vertex(*verts[0]), + ] + + # Create selector + tool = widgets.PolygonSelector(ax, onselect=noop, draw_bounding_box=True) + for (etype, event_args) in event_sequence: + do_event(tool, etype, **event_args) + + # In order to trigger the correct callbacks, trigger events on the canvas + # instead of the individual tools + t = ax.transData + canvas = ax.figure.canvas + + # Scale to half size using the top right corner of the bounding box + MouseEvent( + "button_press_event", canvas, *t.transform((40, 40)), 1)._process() + MouseEvent( + "motion_notify_event", canvas, *t.transform((20, 20)))._process() + MouseEvent( + "button_release_event", canvas, *t.transform((20, 20)), 1)._process() + np.testing.assert_allclose( + tool.verts, [(10, 0), (0, 10), (10, 20), (20, 10)]) + + # Move using the center of the bounding box + MouseEvent( + "button_press_event", canvas, *t.transform((10, 10)), 1)._process() + MouseEvent( + "motion_notify_event", canvas, *t.transform((30, 30)))._process() + MouseEvent( + "button_release_event", canvas, *t.transform((30, 30)), 1)._process() + np.testing.assert_allclose( + tool.verts, [(30, 20), (20, 30), (30, 40), (40, 30)]) + + # Remove a point from the polygon and check that the box extents update + np.testing.assert_allclose( + tool._box.extents, (20.0, 40.0, 20.0, 40.0)) + + MouseEvent( + "button_press_event", canvas, *t.transform((30, 20)), 3)._process() + MouseEvent( + "button_release_event", canvas, *t.transform((30, 20)), 3)._process() + np.testing.assert_allclose( + tool.verts, [(20, 30), (30, 40), (40, 30)]) + np.testing.assert_allclose( + tool._box.extents, (20.0, 40.0, 30.0, 40.0)) + + +@pytest.mark.parametrize("horizOn", [False, True]) +@pytest.mark.parametrize("vertOn", [False, True]) def test_MultiCursor(horizOn, vertOn): - fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True) + (ax1, ax3) = plt.figure().subplots(2, sharex=True) + ax2 = plt.figure().subplots() # useblit=false to avoid having to draw the figure to cache the renderer multi = widgets.MultiCursor( - fig.canvas, (ax1, ax2), useblit=False, horizOn=horizOn, vertOn=vertOn + None, (ax1, ax2), useblit=False, horizOn=horizOn, vertOn=vertOn ) # Only two of the axes should have a line drawn on them. - if vertOn: - assert len(multi.vlines) == 2 - if horizOn: - assert len(multi.hlines) == 2 + assert len(multi.vlines) == 2 + assert len(multi.hlines) == 2 # mock a motion_notify_event # Can't use `do_event` as that helper requires the widget # to have a single .ax attribute. event = mock_event(ax1, xdata=.5, ydata=.25) multi.onmove(event) + # force a draw + draw event to exercise clear + ax1.figure.canvas.draw() # the lines in the first two ax should both move for l in multi.vlines: assert l.get_xdata() == (.5, .5) for l in multi.hlines: assert l.get_ydata() == (.25, .25) + # The relevant lines get turned on after move. + assert len([line for line in multi.vlines if line.get_visible()]) == ( + 2 if vertOn else 0) + assert len([line for line in multi.hlines if line.get_visible()]) == ( + 2 if horizOn else 0) + + # After toggling settings, the opposite lines should be visible after move. + multi.horizOn = not multi.horizOn + multi.vertOn = not multi.vertOn + event = mock_event(ax1, xdata=.5, ydata=.25) + multi.onmove(event) + assert len([line for line in multi.vlines if line.get_visible()]) == ( + 0 if vertOn else 2) + assert len([line for line in multi.hlines if line.get_visible()]) == ( + 0 if horizOn else 2) - # test a move event in an axes not part of the MultiCursor + # test a move event in an Axes not part of the MultiCursor # the lines in ax1 and ax2 should not have moved. event = mock_event(ax3, xdata=.75, ydata=.75) multi.onmove(event) diff --git a/lib/matplotlib/tests/tinypages/conf.py b/lib/matplotlib/tests/tinypages/conf.py index 970a3c5a4d45..08d59fa87ff9 100644 --- a/lib/matplotlib/tests/tinypages/conf.py +++ b/lib/matplotlib/tests/tinypages/conf.py @@ -1,262 +1,24 @@ -# tinypages documentation build configuration file, created by -# sphinx-quickstart on Tue Mar 18 11:58:34 2014. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. - -import sys -from os.path import join as pjoin, abspath import sphinx -from distutils.version import LooseVersion - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) -sys.path.insert(0, abspath(pjoin('..', '..'))) +from packaging.version import parse as parse_version # -- General configuration ------------------------------------------------ -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. extensions = ['matplotlib.sphinxext.plot_directive'] - -# Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] - -# The suffix of source filenames. source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. master_doc = 'index' - -# General information about the project. project = 'tinypages' copyright = '2014, Matplotlib developers' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. version = '0.1' -# The full version, including alpha/beta/rc tags. release = '0.1' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - # -- Options for HTML output ---------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -if LooseVersion(sphinx.__version__) >= LooseVersion('1.3'): +if parse_version(sphinx.__version__) >= parse_version('1.3'): html_theme = 'classic' else: html_theme = 'default' -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'tinypagesdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'tinypages.tex', 'tinypages Documentation', - 'Matplotlib developers', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'tinypages', 'tinypages Documentation', - ['Matplotlib developers'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'tinypages', 'tinypages Documentation', - 'Matplotlib developers', 'tinypages', 'One line description of project.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False diff --git a/lib/matplotlib/tests/tinypages/included_plot_21.rst b/lib/matplotlib/tests/tinypages/included_plot_21.rst new file mode 100644 index 000000000000..761beae6c02d --- /dev/null +++ b/lib/matplotlib/tests/tinypages/included_plot_21.rst @@ -0,0 +1,6 @@ +Plot 21 has length 6 + +.. plot:: + + plt.plot(range(6)) + diff --git a/lib/matplotlib/tests/tinypages/range6.py b/lib/matplotlib/tests/tinypages/range6.py index fa5d035e4ab2..b6655ae07e1f 100644 --- a/lib/matplotlib/tests/tinypages/range6.py +++ b/lib/matplotlib/tests/tinypages/range6.py @@ -11,3 +11,10 @@ def range6(): plt.figure() plt.plot(range(6)) plt.show() + + +def range10(): + """The function that should be executed.""" + plt.figure() + plt.plot(range(10)) + plt.show() diff --git a/lib/matplotlib/tests/tinypages/some_plots.rst b/lib/matplotlib/tests/tinypages/some_plots.rst index 514552decfee..dd1f79892b0e 100644 --- a/lib/matplotlib/tests/tinypages/some_plots.rst +++ b/lib/matplotlib/tests/tinypages/some_plots.rst @@ -15,11 +15,16 @@ Plot 2 doesn't use context either; has length 6: plt.plot(range(6)) -Plot 3 has length 4: +Plot 3 has length 4, and uses doctest syntax: .. plot:: + :format: doctest - plt.plot(range(4)) + This is a doctest... + + >>> plt.plot(range(4)) + + ... isn't it? Plot 4 shows that a new block with context does not see the variable defined in the no-context block: @@ -161,3 +166,11 @@ scenario: plt.figure() plt.plot(range(4)) + +Plot 21 is generated via an include directive: + +.. include:: included_plot_21.rst + +Plot 22 uses a different specific function in a file with plot commands: + +.. plot:: range6.py range10 diff --git a/lib/matplotlib/texmanager.py b/lib/matplotlib/texmanager.py index 495635eadd17..54d724b328ec 100644 --- a/lib/matplotlib/texmanager.py +++ b/lib/matplotlib/texmanager.py @@ -1,43 +1,58 @@ r""" -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. +Support for embedded TeX expressions in Matplotlib. Requirements: -* LaTeX -* \*Agg backends: dvipng>=1.6 -* PS backend: psfrag, dvips, and Ghostscript>=9.0 - -For raster output, you can get RGBA numpy arrays from TeX expressions -as follows:: - - texmanager = TexManager() - s = "\TeX\ is Number $\displaystyle\sum_{n=1}^\infty\frac{-e^{i\pi}}{2^n}$!" - Z = texmanager.get_rgba(s, fontsize=12, dpi=80, rgb=(1, 0, 0)) +* LaTeX. +* \*Agg backends: dvipng>=1.6. +* PS backend: PSfrag, dvips, and Ghostscript>=9.0. +* PDF and SVG backends: if LuaTeX is present, it will be used to speed up some + post-processing steps, but note that it is not used to parse the TeX string + itself (only LaTeX is supported). To enable TeX rendering of all text in your Matplotlib figure, set :rc:`text.usetex` to True. + +TeX and dvipng/dvips processing results are cached +in ~/.matplotlib/tex.cache for reuse between sessions. + +`TexManager.get_rgba` can also be used to directly obtain raster output as RGBA +NumPy arrays. """ import functools -import glob import hashlib import logging import os from pathlib import Path -import re import subprocess from tempfile import TemporaryDirectory import numpy as np import matplotlib as mpl -from matplotlib import _api, cbook, dviread, rcParams +from matplotlib import _api, cbook, dviread _log = logging.getLogger(__name__) +def _usepackage_if_not_loaded(package, *, option=None): + """ + Output LaTeX code that loads a package (possibly with an option) if it + hasn't been loaded yet. + + LaTeX cannot load twice a package with different options, so this helper + can be used to protect against users loading arbitrary packages/options in + their custom preamble. + """ + option = f"[{option}]" if option is not None else "" + return ( + r"\makeatletter" + r"\@ifpackageloaded{%(package)s}{}{\usepackage%(option)s{%(package)s}}" + r"\makeatother" + ) % {"package": package, "option": option} + + class TexManager: """ Convert strings to dvi files using TeX, caching the results to a directory. @@ -45,212 +60,200 @@ class TexManager: Repeated calls to this constructor always return the same instance. """ - # Caches. texcache = os.path.join(mpl.get_cachedir(), 'tex.cache') - grey_arrayd = {} - - font_family = 'serif' - font_families = ('serif', 'sans-serif', 'cursive', 'monospace') - - font_info = { - 'new century schoolbook': ('pnc', r'\renewcommand{\rmdefault}{pnc}'), - 'bookman': ('pbk', r'\renewcommand{\rmdefault}{pbk}'), - 'times': ('ptm', r'\usepackage{mathptmx}'), - 'palatino': ('ppl', r'\usepackage{mathpazo}'), - 'zapf chancery': ('pzc', r'\usepackage{chancery}'), - 'cursive': ('pzc', r'\usepackage{chancery}'), - 'charter': ('pch', r'\usepackage{charter}'), - 'serif': ('cmr', ''), - 'sans-serif': ('cmss', ''), - 'helvetica': ('phv', r'\usepackage{helvet}'), - 'avant garde': ('pag', r'\usepackage{avant}'), - 'courier': ('pcr', r'\usepackage{courier}'), + _grey_arrayd = {} + + _font_families = ('serif', 'sans-serif', 'cursive', 'monospace') + _font_preambles = { + 'new century schoolbook': r'\renewcommand{\rmdefault}{pnc}', + 'bookman': r'\renewcommand{\rmdefault}{pbk}', + 'times': r'\usepackage{mathptmx}', + 'palatino': r'\usepackage{mathpazo}', + 'zapf chancery': r'\usepackage{chancery}', + 'cursive': r'\usepackage{chancery}', + 'charter': r'\usepackage{charter}', + 'serif': '', + 'sans-serif': '', + 'helvetica': r'\usepackage{helvet}', + 'avant garde': r'\usepackage{avant}', + 'courier': r'\usepackage{courier}', # Loading the type1ec package ensures that cm-super is installed, which - # is necessary for unicode computer modern. (It also allows the use of + # is necessary for Unicode computer modern. (It also allows the use of # computer modern at arbitrary sizes, but that's just a side effect.) - 'monospace': ('cmtt', r'\usepackage{type1ec}'), - 'computer modern roman': ('cmr', r'\usepackage{type1ec}'), - 'computer modern sans serif': ('cmss', r'\usepackage{type1ec}'), - 'computer modern typewriter': ('cmtt', r'\usepackage{type1ec}')} - - cachedir = _api.deprecated( - "3.3", alternative="matplotlib.get_cachedir()")( - property(lambda self: mpl.get_cachedir())) - rgba_arrayd = _api.deprecated("3.3")(property(lambda self: {})) - _fonts = {} # Only for deprecation period. - serif = _api.deprecated("3.3")(property( - lambda self: self._fonts.get("serif", ('cmr', '')))) - sans_serif = _api.deprecated("3.3")(property( - lambda self: self._fonts.get("sans-serif", ('cmss', '')))) - cursive = _api.deprecated("3.3")(property( - lambda self: - self._fonts.get("cursive", ('pzc', r'\usepackage{chancery}')))) - monospace = _api.deprecated("3.3")(property( - lambda self: self._fonts.get("monospace", ('cmtt', '')))) + 'monospace': r'\usepackage{type1ec}', + 'computer modern roman': r'\usepackage{type1ec}', + 'computer modern sans serif': r'\usepackage{type1ec}', + 'computer modern typewriter': r'\usepackage{type1ec}', + } + _font_types = { + 'new century schoolbook': 'serif', + 'bookman': 'serif', + 'times': 'serif', + 'palatino': 'serif', + 'zapf chancery': 'cursive', + 'charter': 'serif', + 'helvetica': 'sans-serif', + 'avant garde': 'sans-serif', + 'courier': 'monospace', + 'computer modern roman': 'serif', + 'computer modern sans serif': 'sans-serif', + 'computer modern typewriter': 'monospace', + } @functools.lru_cache() # Always return the same instance. def __new__(cls): Path(cls.texcache).mkdir(parents=True, exist_ok=True) return object.__new__(cls) + @_api.deprecated("3.6") def get_font_config(self): - ff = rcParams['font.family'] - if len(ff) == 1 and ff[0].lower() in self.font_families: - self.font_family = ff[0].lower() + preamble, font_cmd = self._get_font_preamble_and_command() + # Add a hash of the latex preamble to fontconfig so that the + # correct png is selected for strings rendered with same font and dpi + # even if the latex preamble changes within the session + preambles = preamble + font_cmd + self.get_custom_preamble() + return hashlib.md5(preambles.encode('utf-8')).hexdigest() + + @classmethod + def _get_font_family_and_reduced(cls): + """Return the font family name and whether the font is reduced.""" + ff = mpl.rcParams['font.family'] + ff_val = ff[0].lower() if len(ff) == 1 else None + if len(ff) == 1 and ff_val in cls._font_families: + return ff_val, False + elif len(ff) == 1 and ff_val in cls._font_preambles: + return cls._font_types[ff_val], True else: _log.info('font.family must be one of (%s) when text.usetex is ' 'True. serif will be used by default.', - ', '.join(self.font_families)) - self.font_family = 'serif' - - fontconfig = [self.font_family] - for font_family in self.font_families: - for font in rcParams['font.' + font_family]: - if font.lower() in self.font_info: - self._fonts[font_family] = self.font_info[font.lower()] - _log.debug('family: %s, font: %s, info: %s', - font_family, font, self.font_info[font.lower()]) - break - else: - _log.debug('%s font is not compatible with usetex.', font) + ', '.join(cls._font_families)) + return 'serif', False + + @classmethod + def _get_font_preamble_and_command(cls): + requested_family, is_reduced_font = cls._get_font_family_and_reduced() + + preambles = {} + for font_family in cls._font_families: + if is_reduced_font and font_family == requested_family: + preambles[font_family] = cls._font_preambles[ + mpl.rcParams['font.family'][0].lower()] else: - _log.info('No LaTeX-compatible font found for the %s font ' - 'family in rcParams. Using default.', font_family) - self._fonts[font_family] = self.font_info[font_family] - fontconfig.append(self._fonts[font_family][0]) - # Add a hash of the latex preamble to fontconfig so that the - # correct png is selected for strings rendered with same font and dpi - # even if the latex preamble changes within the session - preamble_bytes = self.get_custom_preamble().encode('utf-8') - fontconfig.append(hashlib.md5(preamble_bytes).hexdigest()) + for font in mpl.rcParams['font.' + font_family]: + if font.lower() in cls._font_preambles: + preambles[font_family] = \ + cls._font_preambles[font.lower()] + _log.debug( + 'family: %s, font: %s, info: %s', + font_family, font, + cls._font_preambles[font.lower()]) + break + else: + _log.debug('%s font is not compatible with usetex.', + font) + else: + _log.info('No LaTeX-compatible font found for the %s font' + 'family in rcParams. Using default.', + font_family) + preambles[font_family] = cls._font_preambles[font_family] # The following packages and commands need to be included in the latex # file's preamble: - cmd = [self._fonts['serif'][1], - self._fonts['sans-serif'][1], - self._fonts['monospace'][1]] - if self.font_family == 'cursive': - cmd.append(self._fonts['cursive'][1]) - self._font_preamble = '\n'.join([r'\usepackage{type1cm}', *cmd]) - - return ''.join(fontconfig) - - def get_basefile(self, tex, fontsize, dpi=None): + cmd = {preambles[family] + for family in ['serif', 'sans-serif', 'monospace']} + if requested_family == 'cursive': + cmd.add(preambles['cursive']) + cmd.add(r'\usepackage{type1cm}') + preamble = '\n'.join(sorted(cmd)) + fontcmd = (r'\sffamily' if requested_family == 'sans-serif' else + r'\ttfamily' if requested_family == 'monospace' else + r'\rmfamily') + return preamble, fontcmd + + @classmethod + def get_basefile(cls, tex, fontsize, dpi=None): """ Return a filename based on a hash of the string, fontsize, and dpi. """ - s = ''.join([tex, self.get_font_config(), '%f' % fontsize, - self.get_custom_preamble(), str(dpi or '')]) - return os.path.join( - self.texcache, hashlib.md5(s.encode('utf-8')).hexdigest()) + src = cls._get_tex_source(tex, fontsize) + str(dpi) + filehash = hashlib.md5(src.encode('utf-8')).hexdigest() + filepath = Path(cls.texcache) - def get_font_preamble(self): + num_letters, num_levels = 2, 2 + for i in range(0, num_letters*num_levels, num_letters): + filepath = filepath / Path(filehash[i:i+2]) + + filepath.mkdir(parents=True, exist_ok=True) + return os.path.join(filepath, filehash) + + @classmethod + def get_font_preamble(cls): """ Return a string containing font configuration for the tex preamble. """ - return self._font_preamble + font_preamble, command = cls._get_font_preamble_and_command() + return font_preamble - def get_custom_preamble(self): + @classmethod + def get_custom_preamble(cls): """Return a string containing user additions to the tex preamble.""" - return rcParams['text.latex.preamble'] + return mpl.rcParams['text.latex.preamble'] - def _get_preamble(self): + @classmethod + def _get_tex_source(cls, tex, fontsize): + """Return the complete TeX source for processing a TeX string.""" + font_preamble, fontcmd = cls._get_font_preamble_and_command() + baselineskip = 1.25 * fontsize return "\n".join([ r"\documentclass{article}", - # Pass-through \mathdefault, which is used in non-usetex mode to - # use the default text font but was historically suppressed in - # usetex mode. + r"% Pass-through \mathdefault, which is used in non-usetex mode", + r"% to use the default text font but was historically suppressed", + r"% in usetex mode.", r"\newcommand{\mathdefault}[1]{#1}", - self._font_preamble, + font_preamble, r"\usepackage[utf8]{inputenc}", r"\DeclareUnicodeCharacter{2212}{\ensuremath{-}}", - # geometry is loaded before the custom preamble as convert_psfrags - # relies on a custom preamble to change the geometry. + r"% geometry is loaded before the custom preamble as ", + r"% convert_psfrags relies on a custom preamble to change the ", + r"% geometry.", r"\usepackage[papersize=72in, margin=1in]{geometry}", - self.get_custom_preamble(), - # textcomp is loaded last (if not already loaded by the custom - # preamble) in order not to clash with custom packages (e.g. - # newtxtext) which load it with different options. - r"\makeatletter" - r"\@ifpackageloaded{textcomp}{}{\usepackage{textcomp}}" - r"\makeatother", + cls.get_custom_preamble(), + r"% Use `underscore` package to take care of underscores in text.", + r"% The [strings] option allows to use underscores in file names.", + _usepackage_if_not_loaded("underscore", option="strings"), + r"% Custom packages (e.g. newtxtext) may already have loaded ", + r"% textcomp with different options.", + _usepackage_if_not_loaded("textcomp"), + r"\pagestyle{empty}", + r"\begin{document}", + r"% The empty hbox ensures that a page is printed even for empty", + r"% inputs, except when using psfrag which gets confused by it.", + r"% matplotlibbaselinemarker is used by dviread to detect the", + r"% last line's baseline.", + rf"\fontsize{{{fontsize}}}{{{baselineskip}}}%", + r"\ifdefined\psfrag\else\hbox{}\fi%", + rf"{{{fontcmd} {tex}}}%", + r"\end{document}", ]) - def make_tex(self, tex, fontsize): - """ - Generate a tex file to render the tex string at a specific font size. - - Return the file name. - """ - basefile = self.get_basefile(tex, fontsize) - texfile = '%s.tex' % basefile - fontcmd = {'sans-serif': r'{\sffamily %s}', - 'monospace': r'{\ttfamily %s}'}.get(self.font_family, - r'{\rmfamily %s}') - - Path(texfile).write_text( - r""" -%s -\pagestyle{empty} -\begin{document} -%% The empty hbox ensures that a page is printed even for empty inputs, except -%% when using psfrag which gets confused by it. -\fontsize{%f}{%f}%% -\ifdefined\psfrag\else\hbox{}\fi%% -%s -\end{document} -""" % (self._get_preamble(), fontsize, fontsize * 1.25, fontcmd % tex), - encoding='utf-8') - - return texfile - - _re_vbox = re.compile( - r"MatplotlibBox:\(([\d.]+)pt\+([\d.]+)pt\)x([\d.]+)pt") - - @_api.deprecated("3.3") - def make_tex_preview(self, tex, fontsize): + @classmethod + def make_tex(cls, tex, fontsize): """ Generate a tex file to render the tex string at a specific font size. - It uses the preview.sty to determine the dimension (width, height, - descent) of the output. - Return the file name. """ - basefile = self.get_basefile(tex, fontsize) - texfile = '%s.tex' % basefile - fontcmd = {'sans-serif': r'{\sffamily %s}', - 'monospace': r'{\ttfamily %s}'}.get(self.font_family, - r'{\rmfamily %s}') - - # newbox, setbox, immediate, etc. are used to find the box - # extent of the rendered text. - - Path(texfile).write_text( - r""" -%s -\usepackage[active,showbox,tightpage]{preview} - -%% we override the default showbox as it is treated as an error and makes -%% the exit status not zero -\def\showbox#1%% -{\immediate\write16{MatplotlibBox:(\the\ht#1+\the\dp#1)x\the\wd#1}} - -\begin{document} -\begin{preview} -{\fontsize{%f}{%f}%s} -\end{preview} -\end{document} -""" % (self._get_preamble(), fontsize, fontsize * 1.25, fontcmd % tex), - encoding='utf-8') - + texfile = cls.get_basefile(tex, fontsize) + ".tex" + Path(texfile).write_text(cls._get_tex_source(tex, fontsize), + encoding='utf-8') return texfile - def _run_checked_subprocess(self, command, tex, *, cwd=None): + @classmethod + def _run_checked_subprocess(cls, command, tex, *, cwd=None): _log.debug(cbook._pformat_subprocess(command)) try: report = subprocess.check_output( - command, cwd=cwd if cwd is not None else self.texcache, + command, cwd=cwd if cwd is not None else cls.texcache, stderr=subprocess.STDOUT) except FileNotFoundError as exc: raise RuntimeError( @@ -260,144 +263,111 @@ def _run_checked_subprocess(self, command, tex, *, cwd=None): raise RuntimeError( '{prog} was not able to process the following string:\n' '{tex!r}\n\n' - 'Here is the full report generated by {prog}:\n' + 'Here is the full command invocation and its output:\n\n' + '{format_command}\n\n' '{exc}\n\n'.format( prog=command[0], + format_command=cbook._pformat_subprocess(command), tex=tex.encode('unicode_escape'), - exc=exc.output.decode('utf-8'))) from exc + exc=exc.output.decode('utf-8', 'backslashreplace')) + ) from None _log.debug(report) return report - def make_dvi(self, tex, fontsize): + @classmethod + def make_dvi(cls, tex, fontsize): """ Generate a dvi file containing latex's layout of tex string. Return the file name. """ - - if dict.__getitem__(rcParams, 'text.latex.preview'): - return self.make_dvi_preview(tex, fontsize) - - basefile = self.get_basefile(tex, fontsize) + basefile = cls.get_basefile(tex, fontsize) dvifile = '%s.dvi' % basefile if not os.path.exists(dvifile): - texfile = self.make_tex(tex, fontsize) + texfile = Path(cls.make_tex(tex, fontsize)) # Generate the dvi in a temporary directory to avoid race # conditions e.g. if multiple processes try to process the same tex # string at the same time. Having tmpdir be a subdirectory of the # final output dir ensures that they are on the same filesystem, - # and thus replace() works atomically. - with TemporaryDirectory(dir=Path(dvifile).parent) as tmpdir: - self._run_checked_subprocess( + # and thus replace() works atomically. It also allows referring to + # the texfile with a relative path (for pathological MPLCONFIGDIRs, + # the absolute path may contain characters (e.g. ~) that TeX does + # not support; n.b. relative paths cannot traverse parents, or it + # will be blocked when `openin_any = p` in texmf.cnf). + cwd = Path(dvifile).parent + with TemporaryDirectory(dir=cwd) as tmpdir: + tmppath = Path(tmpdir) + cls._run_checked_subprocess( ["latex", "-interaction=nonstopmode", "--halt-on-error", - texfile], tex, cwd=tmpdir) - (Path(tmpdir) / Path(dvifile).name).replace(dvifile) + f"--output-directory={tmppath.name}", + f"{texfile.name}"], tex, cwd=cwd) + (tmppath / Path(dvifile).name).replace(dvifile) return dvifile - @_api.deprecated("3.3") - def make_dvi_preview(self, tex, fontsize): - """ - Generate a dvi file containing latex's layout of tex string. - - It calls make_tex_preview() method and store the size information - (width, height, descent) in a separate file. - - Return the file name. - """ - basefile = self.get_basefile(tex, fontsize) - dvifile = '%s.dvi' % basefile - baselinefile = '%s.baseline' % basefile - - if not os.path.exists(dvifile) or not os.path.exists(baselinefile): - texfile = self.make_tex_preview(tex, fontsize) - report = self._run_checked_subprocess( - ["latex", "-interaction=nonstopmode", "--halt-on-error", - texfile], tex) - - # find the box extent information in the latex output - # file and store them in ".baseline" file - m = TexManager._re_vbox.search(report.decode("utf-8")) - with open(basefile + '.baseline', "w") as fh: - fh.write(" ".join(m.groups())) - - for fname in glob.glob(basefile + '*'): - if not fname.endswith(('dvi', 'tex', 'baseline')): - try: - os.remove(fname) - except OSError: - pass - - return dvifile - - def make_png(self, tex, fontsize, dpi): + @classmethod + def make_png(cls, tex, fontsize, dpi): """ Generate a png file containing latex's rendering of tex string. Return the file name. """ - basefile = self.get_basefile(tex, fontsize, dpi) + basefile = cls.get_basefile(tex, fontsize, dpi) pngfile = '%s.png' % basefile # see get_rgba for a discussion of the background if not os.path.exists(pngfile): - dvifile = self.make_dvi(tex, fontsize) + dvifile = cls.make_dvi(tex, fontsize) 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"): + if (getattr(mpl, "_called_from_pytest", False) and + mpl._get_executable_info("dvipng").raw_version != "1.16"): cmd.insert(1, "--freetype0") - self._run_checked_subprocess(cmd, tex) + cls._run_checked_subprocess(cmd, tex) return pngfile - def get_grey(self, tex, fontsize=None, dpi=None): + @classmethod + def get_grey(cls, tex, fontsize=None, dpi=None): """Return the alpha channel.""" if not fontsize: - fontsize = rcParams['font.size'] + fontsize = mpl.rcParams['font.size'] if not dpi: - dpi = rcParams['savefig.dpi'] - key = tex, self.get_font_config(), fontsize, dpi - alpha = self.grey_arrayd.get(key) + dpi = mpl.rcParams['savefig.dpi'] + key = cls._get_tex_source(tex, fontsize), dpi + alpha = cls._grey_arrayd.get(key) if alpha is None: - pngfile = self.make_png(tex, fontsize, dpi) - rgba = mpl.image.imread(os.path.join(self.texcache, pngfile)) - self.grey_arrayd[key] = alpha = rgba[:, :, -1] + pngfile = cls.make_png(tex, fontsize, dpi) + rgba = mpl.image.imread(os.path.join(cls.texcache, pngfile)) + cls._grey_arrayd[key] = alpha = rgba[:, :, -1] return alpha - def get_rgba(self, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)): - """Return latex's rendering of the tex string as an rgba array.""" - alpha = self.get_grey(tex, fontsize, dpi) + @classmethod + def get_rgba(cls, tex, fontsize=None, dpi=None, rgb=(0, 0, 0)): + r""" + Return latex's rendering of the tex string as an rgba array. + + Examples + -------- + >>> texmanager = TexManager() + >>> s = r"\TeX\ is $\displaystyle\sum_n\frac{-e^{i\pi}}{2^n}$!" + >>> Z = texmanager.get_rgba(s, fontsize=12, dpi=80, rgb=(1, 0, 0)) + """ + alpha = cls.get_grey(tex, fontsize, dpi) rgba = np.empty((*alpha.shape, 4)) rgba[..., :3] = mpl.colors.to_rgb(rgb) rgba[..., -1] = alpha return rgba - def get_text_width_height_descent(self, tex, fontsize, renderer=None): + @classmethod + def get_text_width_height_descent(cls, tex, fontsize, renderer=None): """Return width, height and descent of the text.""" if tex.strip() == '': return 0, 0, 0 - + dvifile = cls.make_dvi(tex, fontsize) dpi_fraction = renderer.points_to_pixels(1.) if renderer else 1 - - if dict.__getitem__(rcParams, 'text.latex.preview'): - # use preview.sty - basefile = self.get_basefile(tex, fontsize) - baselinefile = '%s.baseline' % basefile - - if not os.path.exists(baselinefile): - dvifile = self.make_dvi_preview(tex, fontsize) - - with open(baselinefile) as fh: - l = fh.read().split() - height, depth, width = [float(l1) * dpi_fraction for l1 in l] - return width, height + depth, depth - - else: - # use dviread. - dvifile = self.make_dvi(tex, fontsize) - with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: - page, = dvi - # A total height (including the descent) needs to be returned. - return page.width, page.height + page.descent, page.descent + with dviread.Dvi(dvifile, 72 * dpi_fraction) as dvi: + page, = dvi + # A total height (including the descent) needs to be returned. + return page.width, page.height + page.descent, page.descent diff --git a/lib/matplotlib/text.py b/lib/matplotlib/text.py index bc865aa4fa03..9caa272a466b 100644 --- a/lib/matplotlib/text.py +++ b/lib/matplotlib/text.py @@ -2,19 +2,20 @@ Classes for including text in a figure. """ -import contextlib +import functools import logging import math +from numbers import Real import weakref import numpy as np import matplotlib as mpl -from . import _api, artist, cbook, docstring +from . import _api, artist, cbook, _docstring from .artist import Artist from .font_manager import FontProperties from .patches import FancyArrowPatch, FancyBboxPatch, Rectangle -from .textpath import TextPath # Unused, but imported by others. +from .textpath import TextPath, TextToPath # noqa # Logically located here from .transforms import ( Affine2D, Bbox, BboxBase, BboxTransformTo, IdentityTransform, Transform) @@ -22,21 +23,7 @@ _log = logging.getLogger(__name__) -@contextlib.contextmanager -def _wrap_text(textobj): - """Temporarily inserts newlines if the wrap option is enabled.""" - if textobj.get_wrap(): - old_text = textobj.get_text() - try: - textobj.set_text(textobj._get_wrapped_text()) - yield textobj - finally: - textobj.set_text(old_text) - else: - yield textobj - - -# Extracted from Text's method to serve as a function +@_api.deprecated("3.6") def get_rotation(rotation): """ Return *rotation* normalized to an angle between 0 and 360 degrees. @@ -103,7 +90,23 @@ def _get_textbox(text, renderer): return x_box, y_box, w_box, h_box -@cbook._define_aliases({ +def _get_text_metrics_with_cache(renderer, text, fontprop, ismath, dpi): + """Call ``renderer.get_text_width_height_descent``, caching the results.""" + # Cached based on a copy of fontprop so that later in-place mutations of + # the passed-in argument do not mess up the cache. + return _get_text_metrics_with_cache_impl( + weakref.ref(renderer), text, fontprop.copy(), ismath, dpi) + + +@functools.lru_cache(4096) +def _get_text_metrics_with_cache_impl( + renderer_ref, text, fontprop, ismath, dpi): + # dpi is unused, but participates in cache invalidation (via the renderer). + return renderer_ref().get_text_width_height_descent(text, fontprop, ismath) + + +@_docstring.interpd +@_api.define_aliases({ "color": ["c"], "fontfamily": ["family"], "fontproperties": ["font", "font_properties"], @@ -121,11 +124,12 @@ class Text(Artist): """Handle storing and drawing of text in window or data coordinates.""" zorder = 3 - _cached = cbook.maxdict(50) + _charsize_cache = dict() def __repr__(self): return "Text(%s, %s, %s)" % (self._x, self._y, repr(self._text)) + @_api.make_keyword_only("3.6", name="color") def __init__(self, x=0, y=0, text='', color=None, # defaults to rc params @@ -139,41 +143,84 @@ def __init__(self, usetex=None, # defaults to rcParams['text.usetex'] wrap=False, transform_rotates_text=False, + *, + parse_math=None, # defaults to rcParams['text.parse_math'] **kwargs ): """ Create a `.Text` instance at *x*, *y* with string *text*. + The text is aligned relative to the anchor point (*x*, *y*) according + to ``horizontalalignment`` (default: 'left') and ``verticalalignment`` + (default: 'bottom'). See also + :doc:`/gallery/text_labels_and_annotations/text_alignment`. + + While Text accepts the 'label' keyword argument, by default it is not + added to the handles of a legend. + Valid keyword arguments are: - %(Text_kwdoc)s + %(Text:kwdoc)s """ super().__init__() self._x, self._y = x, y self._text = '' + self._reset_visual_defaults( + text=text, + color=color, + fontproperties=fontproperties, + usetex=usetex, + parse_math=parse_math, + wrap=wrap, + verticalalignment=verticalalignment, + horizontalalignment=horizontalalignment, + multialignment=multialignment, + rotation=rotation, + transform_rotates_text=transform_rotates_text, + linespacing=linespacing, + rotation_mode=rotation_mode, + ) + self.update(kwargs) + + def _reset_visual_defaults( + self, + text='', + color=None, + fontproperties=None, + usetex=None, + parse_math=None, + wrap=False, + verticalalignment='baseline', + horizontalalignment='left', + multialignment=None, + rotation=None, + transform_rotates_text=False, + linespacing=None, + rotation_mode=None, + ): self.set_text(text) self.set_color( color if color is not None else mpl.rcParams["text.color"]) self.set_fontproperties(fontproperties) self.set_usetex(usetex) + self.set_parse_math(parse_math if parse_math is not None else + mpl.rcParams['text.parse_math']) self.set_wrap(wrap) self.set_verticalalignment(verticalalignment) self.set_horizontalalignment(horizontalalignment) self._multialignment = multialignment - self._rotation = rotation + self.set_rotation(rotation) self._transform_rotates_text = transform_rotates_text self._bbox_patch = None # a FancyBboxPatch instance self._renderer = None if linespacing is None: - linespacing = 1.2 # Maybe use rcParam later. - self._linespacing = linespacing + linespacing = 1.2 # Maybe use rcParam later. + self.set_linespacing(linespacing) self.set_rotation_mode(rotation_mode) - self.update(kwargs) def update(self, kwargs): # docstring inherited - # make a copy so we do not mutate user input! - kwargs = dict(kwargs) + kwargs = cbook.normalize_kwargs(kwargs, Text) sentinel = object() # bbox can be None, so use another sentinel. # Update fontproperties first, as it has lowest priority. fontproperties = kwargs.pop("fontproperties", sentinel) @@ -233,16 +280,45 @@ def _get_multialignment(self): else: return self._horizontalalignment + def _char_index_at(self, x): + """ + Calculate the index closest to the coordinate x in display space. + + The position of text[index] is assumed to be the sum of the widths + of all preceding characters text[:index]. + + This works only on single line texts. + """ + if not self._text: + return 0 + + text = self._text + + fontproperties = str(self._fontproperties) + if fontproperties not in Text._charsize_cache: + Text._charsize_cache[fontproperties] = dict() + + charsize_cache = Text._charsize_cache[fontproperties] + for char in set(text): + if char not in charsize_cache: + self.set_text(char) + bb = self.get_window_extent() + charsize_cache[char] = bb.x1 - bb.x0 + + self.set_text(text) + bb = self.get_window_extent() + + size_accum = np.cumsum([0] + [charsize_cache[x] for x in text]) + std_x = x - bb.x0 + return (np.abs(size_accum - std_x)).argmin() + def get_rotation(self): """Return the text angle in degrees between 0 and 360.""" if self.get_transform_rotates_text(): - angle = get_rotation(self._rotation) - x, y = self.get_unitless_position() - angles = [angle, ] - pts = [[x, y]] - return self.get_transform().transform_angles(angles, pts).item(0) + return self.get_transform().transform_angles( + [self._rotation], [self.get_unitless_position()]).item(0) else: - return get_rotation(self._rotation) # string_or_number -> number + return self._rotation def get_transform_rotates_text(self): """ @@ -290,12 +366,8 @@ def _get_layout(self, renderer): multiple-alignment information. Note that it returns an extent of a rotated text when necessary. """ - key = self.get_prop_tup(renderer=renderer) - if key in self._cached: - return self._cached[key] - thisx, thisy = 0.0, 0.0 - lines = self.get_text().split("\n") # Ensures lines is not empty. + lines = self._get_wrapped_text().split("\n") # Ensures lines is not empty. ws = [] hs = [] @@ -303,16 +375,17 @@ def _get_layout(self, renderer): ys = [] # Full vertical extent of font, including ascenders and descenders: - _, lp_h, lp_d = renderer.get_text_width_height_descent( - "lp", self._fontproperties, - ismath="TeX" if self.get_usetex() else False) + _, lp_h, lp_d = _get_text_metrics_with_cache( + renderer, "lp", self._fontproperties, + ismath="TeX" if self.get_usetex() else False, dpi=self.figure.dpi) min_dy = (lp_h - lp_d) * self._linespacing for i, line in enumerate(lines): clean_line, ismath = self._preprocess_math(line) if clean_line: - w, h, d = renderer.get_text_width_height_descent( - clean_line, self._fontproperties, ismath=ismath) + w, h, d = _get_text_metrics_with_cache( + renderer, clean_line, self._fontproperties, + ismath=ismath, dpi=self.figure.dpi) else: w = h = d = 0 @@ -349,7 +422,6 @@ def _get_layout(self, renderer): xmax = width ymax = 0 ymin = ys[-1] - descent # baseline of last line minus its descent - height = ymax - ymin # get the rotation matrix M = Affine2D().rotate_deg(self.get_rotation()) @@ -437,9 +509,7 @@ def _get_layout(self, renderer): # now rotate the positions around the first (x, y) position xys = M.transform(offset_layout) - (offsetx, offsety) - ret = bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent - self._cached[key] = ret - return ret + return bbox, list(zip(lines, zip(ws, hs), *xys.T)), descent def set_bbox(self, rectprops): """ @@ -616,9 +686,12 @@ def _get_rendered_text_width(self, text): def _get_wrapped_text(self): """ - Return a copy of the text with new lines added, so that - the text is wrapped relative to the parent figure. + Return a copy of the text string with new lines added so that the text + is wrapped relative to the parent figure (if `get_wrap` is True). """ + if not self.get_wrap(): + return self.get_text() + # Not fit to handle breaking up latex syntax correctly, so # ignore latex for now. if self.get_usetex(): @@ -675,14 +748,14 @@ def draw(self, renderer): renderer.open_group('text', self.get_gid()) - with _wrap_text(self) as textobj: - bbox, info, descent = textobj._get_layout(renderer) - trans = textobj.get_transform() + with self._cm_set(text=self._get_wrapped_text()): + bbox, info, descent = self._get_layout(renderer) + trans = self.get_transform() - # don't use textobj.get_position here, which refers to text + # don't use self.get_position here, which refers to text # position in Text: - posx = float(textobj.convert_xunits(textobj._x)) - posy = float(textobj.convert_yunits(textobj._y)) + posx = float(self.convert_xunits(self._x)) + posy = float(self.convert_yunits(self._y)) posx, posy = trans.transform((posx, posy)) if not np.isfinite(posx) or not np.isfinite(posy): _log.warning("posx and posy should be finite values") @@ -691,41 +764,41 @@ def draw(self, renderer): # Update the location and size of the bbox # (`.patches.FancyBboxPatch`), and draw it. - if textobj._bbox_patch: + if self._bbox_patch: self.update_bbox_position_size(renderer) self._bbox_patch.draw(renderer) gc = renderer.new_gc() - gc.set_foreground(textobj.get_color()) - gc.set_alpha(textobj.get_alpha()) - gc.set_url(textobj._url) - textobj._set_gc_clip(gc) + gc.set_foreground(self.get_color()) + gc.set_alpha(self.get_alpha()) + gc.set_url(self._url) + self._set_gc_clip(gc) - angle = textobj.get_rotation() + angle = self.get_rotation() for line, wh, x, y in info: - mtext = textobj if len(info) == 1 else None + mtext = self if len(info) == 1 else None x = x + posx y = y + posy if renderer.flipy(): y = canvash - y - clean_line, ismath = textobj._preprocess_math(line) + clean_line, ismath = self._preprocess_math(line) - if textobj.get_path_effects(): + if self.get_path_effects(): from matplotlib.patheffects import PathEffectRenderer textrenderer = PathEffectRenderer( - textobj.get_path_effects(), renderer) + self.get_path_effects(), renderer) else: textrenderer = renderer - if textobj.get_usetex(): + if self.get_usetex(): textrenderer.draw_tex(gc, x, y, clean_line, - textobj._fontproperties, angle, + self._fontproperties, angle, mtext=mtext) else: textrenderer.draw_text(gc, x, y, clean_line, - textobj._fontproperties, angle, + self._fontproperties, angle, ismath=ismath, mtext=mtext) gc.restore() @@ -831,25 +904,6 @@ def get_position(self): # specified with 'set_x' and 'set_y'. return self._x, self._y - def get_prop_tup(self, renderer=None): - """ - Return a hashable tuple of properties. - - Not intended to be human readable, but useful for backends who - want to cache derived information about text (e.g., layouts) and - need to know if the text has changed. - """ - x, y = self.get_unitless_position() - renderer = renderer or self._renderer - return (x, y, self.get_text(), self._color, - self._verticalalignment, self._horizontalalignment, - hash(self._fontproperties), - self._rotation, self._rotation_mode, - self._transform_rotates_text, - self.figure.dpi, weakref.ref(renderer), - self._linespacing - ) - def get_text(self): """Return the text string.""" return self._text @@ -874,15 +928,15 @@ def get_window_extent(self, renderer=None, dpi=None): A renderer is needed to compute the bounding box. If the artist has already been drawn, the renderer is cached; thus, it is only necessary to pass this argument when calling `get_window_extent` - before the first `draw`. In practice, it is usually easier to - trigger a draw first (e.g. by saving the figure). + before the first draw. In practice, it is usually easier to + trigger a draw first, e.g. by calling + `~.Figure.draw_without_rendering` or ``plt.show()``. dpi : float, optional The dpi value for computing the bbox, defaults to ``self.figure.dpi`` (*not* the renderer dpi); should be set e.g. if to match regions with a figure saved with a custom dpi value. """ - #return _unit_box if not self.get_visible(): return Bbox.unit() if dpi is None: @@ -895,9 +949,11 @@ def get_window_extent(self, renderer=None, dpi=None): if renderer is not None: self._renderer = renderer if self._renderer is None: - self._renderer = self.figure._cachedRenderer + self._renderer = self.figure._get_renderer() if self._renderer is None: - raise RuntimeError('Cannot get window extent w/o renderer') + raise RuntimeError( + "Cannot get window extent of text w/o renderer. You likely " + "want to call 'figure.draw_without_rendering()' first.") with cbook._setattr_cm(self.figure, dpi=dpi): bbox, info, descent = self._get_layout(self._renderer) @@ -938,21 +994,18 @@ def set_color(self, color): # out at draw time for simplicity. if not cbook._str_equal(color, "auto"): mpl.colors._check_color_like(color=color) - # Make sure it is hashable, or get_prop_tup will fail. - try: - hash(color) - except TypeError: - color = tuple(color) self._color = color self.stale = True def set_horizontalalignment(self, align): """ - Set the horizontal alignment to one of + Set the horizontal alignment relative to the anchor point. + + See also :doc:`/gallery/text_labels_and_annotations/text_alignment`. Parameters ---------- - align : {'center', 'right', 'left'} + align : {'left', 'center', 'right'} """ _api.check_in_list(['center', 'right', 'left'], align=align) self._horizontalalignment = align @@ -984,12 +1037,13 @@ def set_linespacing(self, spacing): ---------- spacing : float (multiple of font size) """ + _api.check_isinstance(Real, spacing=spacing) self._linespacing = spacing self.stale = True def set_fontfamily(self, fontname): """ - Set the font family. May be either a single string, or a list of + Set the font family. Can be either a single string, or a list of strings in decreasing priority. Each string may be either a real font name or a generic font class name. If the latter, the specific font names will be looked up in the corresponding rcParams. @@ -1049,7 +1103,7 @@ def set_fontsize(self, fontsize): ---------- fontsize : float or {'xx-small', 'x-small', 'small', 'medium', \ 'large', 'x-large', 'xx-large'} - If float, the fontsize in points. The string values denote sizes + If a float, the fontsize in points. The string values denote sizes relative to the default font size. See Also @@ -1170,7 +1224,15 @@ def set_rotation(self, s): The rotation angle in degrees in mathematically positive direction (counterclockwise). 'horizontal' equals 0, 'vertical' equals 90. """ - self._rotation = s + if isinstance(s, Real): + self._rotation = float(s) % 360 + elif cbook._str_equal(s, 'horizontal') or s is None: + self._rotation = 0. + elif cbook._str_equal(s, 'vertical'): + self._rotation = 90. + else: + raise ValueError("rotation must be 'vertical', 'horizontal' or " + f"a number, not {s}") self.stale = True def set_transform_rotates_text(self, t): @@ -1186,11 +1248,13 @@ def set_transform_rotates_text(self, t): def set_verticalalignment(self, align): """ - Set the vertical alignment. + Set the vertical alignment relative to the anchor point. + + See also :doc:`/gallery/text_labels_and_annotations/text_alignment`. Parameters ---------- - align : {'center', 'top', 'bottom', 'baseline', 'center_baseline'} + align : {'bottom', 'baseline', 'center', 'center_baseline', 'top'} """ _api.check_in_list( ['top', 'bottom', 'center', 'baseline', 'center_baseline'], @@ -1224,7 +1288,8 @@ def _preprocess_math(self, s): - If *self* is configured to use TeX, return *s* unchanged except that a single space gets escaped, and the flag "TeX". - Otherwise, if *s* is mathtext (has an even number of unescaped dollar - signs), return *s* and the flag True. + signs) and ``parse_math`` is not set to False, return *s* and the + flag True. - Otherwise, return *s* with dollar signs unescaped, and the flag False. """ @@ -1232,6 +1297,8 @@ def _preprocess_math(self, s): if s == " ": s = r"\ " return s, "TeX" + elif not self.get_parse_math(): + return s, False elif cbook.is_math_text(s): return s, True else: @@ -1269,6 +1336,22 @@ def get_usetex(self): """Return whether this `Text` object uses TeX for rendering.""" return self._usetex + def set_parse_math(self, parse_math): + """ + Override switch to disable any mathtext parsing for this `Text`. + + Parameters + ---------- + parse_math : bool + If False, this `Text` will never use mathtext. If True, mathtext + will be used if there is an even number of unescaped dollar signs. + """ + self._parse_math = bool(parse_math) + + def get_parse_math(self): + """Return whether mathtext parsing is considered for this `Text`.""" + return self._parse_math + def set_fontname(self, fontname): """ Alias for `set_family`. @@ -1288,10 +1371,6 @@ def set_fontname(self, fontname): return self.set_family(fontname) -docstring.interpd.update(Text_kwdoc=artist.kwdoc(Text)) -docstring.dedent_interpd(Text.__init__) - - class OffsetFrom: """Callable helper class for working with `Annotation`.""" @@ -1414,7 +1493,7 @@ def _get_xy_transform(self, renderer, s): elif isinstance(tr, Transform): return tr else: - raise RuntimeError("unknown return type ...") + raise RuntimeError("Unknown return type") elif isinstance(s, Artist): bbox = s.get_window_extent(renderer) return BboxTransformTo(bbox) @@ -1423,7 +1502,7 @@ def _get_xy_transform(self, renderer, s): elif isinstance(s, Transform): return s elif not isinstance(s, str): - raise RuntimeError("unknown coordinate type : %s" % s) + raise RuntimeError(f"Unknown coordinate type: {s!r}") if s == 'data': return self.axes.transData @@ -1435,7 +1514,7 @@ def _get_xy_transform(self, renderer, s): s_ = s.split() if len(s_) != 2: - raise ValueError("%s is not a recognized coordinate" % s) + raise ValueError(f"{s!r} is not a recognized coordinate") bbox0, xy0 = None, None @@ -1463,24 +1542,24 @@ def _get_xy_transform(self, renderer, s): ref_x, ref_y = xy0 if unit == "points": # dots per points - dpp = self.figure.get_dpi() / 72. + dpp = self.figure.dpi / 72 tr = Affine2D().scale(dpp) elif unit == "pixels": tr = Affine2D() elif unit == "fontsize": fontsize = self.get_size() - dpp = fontsize * self.figure.get_dpi() / 72. + dpp = fontsize * self.figure.dpi / 72 tr = Affine2D().scale(dpp) elif unit == "fraction": w, h = bbox0.size tr = Affine2D().scale(w, h) else: - raise ValueError("%s is not a recognized coordinate" % s) + raise ValueError(f"{unit!r} is not a recognized unit") return tr.translate(ref_x, ref_y) else: - raise ValueError("%s is not a recognized coordinate" % s) + raise ValueError(f"{s!r} is not a recognized coordinate") def _get_ref_xy(self, renderer): """ @@ -1506,12 +1585,11 @@ def set_annotation_clip(self, b): Parameters ---------- b : bool or None - - True: the annotation will only be drawn when ``self.xy`` is - inside the axes. - - False: the annotation will always be drawn regardless of its - position. - - None: the ``self.xy`` will be checked only if *xycoords* is - "data". + - True: The annotation will be clipped when ``self.xy`` is + outside the axes. + - False: The annotation will always be drawn. + - None: The annotation will be clipped when ``self.xy`` is + outside the axes and ``self.xycoords == "data"``. """ self._annotation_clip = b @@ -1528,8 +1606,10 @@ def _get_position_xy(self, renderer): x, y = self.xy return self._get_xy(renderer, x, y, self.xycoords) - def _check_xy(self, renderer): + def _check_xy(self, renderer=None): """Check whether the annotation at *xy_pixel* should be drawn.""" + if renderer is None: + renderer = self.figure._get_renderer() b = self.get_annotation_clip() if b or (b is None and self.xycoords == "data"): # check if self.xy is inside the axes. @@ -1546,6 +1626,9 @@ def draggable(self, state=None, use_blit=False): state : bool or None - True or False: set the draggability. - None: toggle the draggability. + use_blit : bool, default: False + Use blitting for faster image composition. For details see + :ref:`func-animation`. Returns ------- @@ -1618,8 +1701,8 @@ def __init__(self, text, xy, The position *(x, y)* to place the text at. The coordinate system is determined by *textcoords*. - xycoords : str or `.Artist` or `.Transform` or callable or \ -(float, float), default: 'data' + xycoords : single or two-tuple of str or `.Artist` or `.Transform` or \ +callable, default: 'data' The coordinate system that *xy* is given in. The following types of values are supported: @@ -1671,8 +1754,8 @@ def transform(renderer) -> Transform See :ref:`plotting-guide-annotation` for more details. - textcoords : str or `.Artist` or `.Transform` or callable or \ -(float, float), default: value of *xycoords* + textcoords : single or two-tuple of str or `.Artist` or `.Transform` \ +or callable, default: value of *xycoords* The coordinate system that *xytext* is given in. All *xycoords* values are valid as well as the following @@ -1687,9 +1770,13 @@ def transform(renderer) -> Transform arrowprops : dict, optional The properties used to draw a `.FancyArrowPatch` arrow between the - positions *xy* and *xytext*. Note that the edge of the arrow - pointing to *xytext* will be centered on the text itself and may - not point directly to the coordinates given in *xytext*. + positions *xy* and *xytext*. Defaults to None, i.e. no arrow is + drawn. + + For historical reasons there are two different ways to specify + arrows, "simple" and "fancy": + + **Simple arrow:** If *arrowprops* does not contain the key 'arrowstyle' the allowed keys are: @@ -1704,35 +1791,22 @@ def transform(renderer) -> Transform ? Any key to :class:`matplotlib.patches.FancyArrowPatch` ========== ====================================================== - If *arrowprops* contains the key 'arrowstyle' the - above keys are forbidden. The allowed values of - ``'arrowstyle'`` are: - - ============ ============================================= - Name Attrs - ============ ============================================= - ``'-'`` None - ``'->'`` head_length=0.4,head_width=0.2 - ``'-['`` widthB=1.0,lengthB=0.2,angleB=None - ``'|-|'`` widthA=1.0,widthB=1.0 - ``'-|>'`` head_length=0.4,head_width=0.2 - ``'<-'`` head_length=0.4,head_width=0.2 - ``'<->'`` head_length=0.4,head_width=0.2 - ``'<|-'`` head_length=0.4,head_width=0.2 - ``'<|-|>'`` head_length=0.4,head_width=0.2 - ``'fancy'`` head_length=0.4,head_width=0.4,tail_width=0.4 - ``'simple'`` head_length=0.5,head_width=0.5,tail_width=0.2 - ``'wedge'`` tail_width=0.3,shrink_factor=0.5 - ============ ============================================= - - Valid keys for `~matplotlib.patches.FancyArrowPatch` are: + The arrow is attached to the edge of the text box, the exact + position (corners or centers) depending on where it's pointing to. + + **Fancy arrow:** + + This is used if 'arrowstyle' is provided in the *arrowprops*. + + Valid keys are the following `~matplotlib.patches.FancyArrowPatch` + parameters: =============== ================================================== Key Description =============== ================================================== arrowstyle the arrow style connectionstyle the connection style - relpos default is (0.5, 0.5) + relpos see below; default is (0.5, 0.5) patchA default is bounding box of the text patchB default is None shrinkA default is 2 points @@ -1742,17 +1816,22 @@ def transform(renderer) -> Transform ? any key for :class:`matplotlib.patches.PathPatch` =============== ================================================== - Defaults to None, i.e. no arrow is drawn. + The exact starting point position of the arrow is defined by + *relpos*. It's a tuple of relative coordinates of the text box, + where (0, 0) is the lower left corner and (1, 1) is the upper + right corner. Values <0 and >1 are supported and specify points + outside the text box. By default (0.5, 0.5), so the starting point + is centered in the text box. annotation_clip : bool or None, default: None - Whether to draw the annotation when the annotation point *xy* is - outside the axes area. + Whether to clip (i.e. not draw) the annotation when the annotation + point *xy* is outside the axes area. - - If *True*, the annotation will only be drawn when *xy* is - within the axes. + - If *True*, the annotation will be clipped when *xy* is outside + the axes. - If *False*, the annotation will always be drawn. - - If *None*, the annotation will only be drawn when *xy* is - within the axes and *xycoords* is 'data'. + - If *None*, the annotation will be clipped when *xy* is outside + the axes and *xycoords* is 'data'. **kwargs Additional kwargs are passed to `~matplotlib.text.Text`. @@ -1871,32 +1950,30 @@ def update_positions(self, renderer): """ Update the pixel positions of the annotation text and the arrow patch. """ - x1, y1 = self._get_position_xy(renderer) # Annotated position. - # generate transformation, + # generate transformation self.set_transform(self._get_xy_transform(renderer, self.anncoords)) - if self.arrowprops is None: + arrowprops = self.arrowprops + if arrowprops is None: return bbox = Text.get_window_extent(self, renderer) - d = self.arrowprops.copy() - ms = d.pop("mutation_scale", self.get_size()) + arrow_end = x1, y1 = self._get_position_xy(renderer) # Annotated pos. + + ms = arrowprops.get("mutation_scale", self.get_size()) self.arrow_patch.set_mutation_scale(ms) - if "arrowstyle" not in d: + if "arrowstyle" not in arrowprops: # Approximately simulate the YAArrow. - # Pop its kwargs: - shrink = d.pop('shrink', 0.0) - width = d.pop('width', 4) - headwidth = d.pop('headwidth', 12) - # Ignore frac--it is useless. - frac = d.pop('frac', None) - if frac is not None: + shrink = arrowprops.get('shrink', 0.0) + width = arrowprops.get('width', 4) + headwidth = arrowprops.get('headwidth', 12) + if 'frac' in arrowprops: _api.warn_external( "'frac' option in 'arrowprops' is no longer supported;" " use 'headlength' to set the head length in points.") - headlength = d.pop('headlength', 12) + headlength = arrowprops.get('headlength', 12) # NB: ms is in pts stylekw = dict(head_length=headlength / ms, @@ -1918,29 +1995,25 @@ def update_positions(self, renderer): # adjust the starting point of the arrow relative to the textbox. # TODO : Rotation needs to be accounted. - relposx, relposy = self._arrow_relpos - x0 = bbox.x0 + bbox.width * relposx - y0 = bbox.y0 + bbox.height * relposy - - # The arrow will be drawn from (x0, y0) to (x1, y1). It will be first + arrow_begin = bbox.p0 + bbox.size * self._arrow_relpos + # The arrow is drawn from arrow_begin to arrow_end. It will be first # clipped by patchA and patchB. Then it will be shrunk by shrinkA and - # shrinkB (in points). If patch A is not set, self.bbox_patch is used. - self.arrow_patch.set_positions((x0, y0), (x1, y1)) - - if "patchA" in d: - self.arrow_patch.set_patchA(d.pop("patchA")) + # shrinkB (in points). If patchA is not set, self.bbox_patch is used. + self.arrow_patch.set_positions(arrow_begin, arrow_end) + + if "patchA" in arrowprops: + patchA = arrowprops["patchA"] + elif self._bbox_patch: + patchA = self._bbox_patch + elif self.get_text() == "": + patchA = None else: - if self._bbox_patch: - self.arrow_patch.set_patchA(self._bbox_patch) - else: - if self.get_text() == "": - self.arrow_patch.set_patchA(None) - return - pad = renderer.points_to_pixels(4) - r = Rectangle(xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2), - width=bbox.width + pad, height=bbox.height + pad, - transform=IdentityTransform(), clip_on=False) - self.arrow_patch.set_patchA(r) + pad = renderer.points_to_pixels(4) + patchA = Rectangle( + xy=(bbox.x0 - pad / 2, bbox.y0 - pad / 2), + width=bbox.width + pad, height=bbox.height + pad, + transform=IdentityTransform(), clip_on=False) + self.arrow_patch.set_patchA(patchA) @artist.allow_rasterization def draw(self, renderer): @@ -1953,7 +2026,7 @@ def draw(self, renderer): # FancyArrowPatch is correctly positioned. self.update_positions(renderer) self.update_bbox_position_size(renderer) - if self.arrow_patch is not None: # FancyArrowPatch + if self.arrow_patch is not None: # FancyArrowPatch if self.arrow_patch.figure is None and self.figure is not None: self.arrow_patch.figure = self.figure self.arrow_patch.draw(renderer) @@ -1962,18 +2035,7 @@ def draw(self, renderer): Text.draw(self, renderer) def get_window_extent(self, renderer=None): - """ - Return the `.Bbox` bounding the text and arrow, in display units. - - Parameters - ---------- - renderer : Renderer, optional - A renderer is needed to compute the bounding box. If the artist - has already been drawn, the renderer is cached; thus, it is only - necessary to pass this argument when calling `get_window_extent` - before the first `draw`. In practice, it is usually easier to - trigger a draw first (e.g. by saving the figure). - """ + # docstring inherited # This block is the same as in Text.get_window_extent, but we need to # set the renderer before calling update_positions(). if not self.get_visible() or not self._check_xy(renderer): @@ -1981,7 +2043,7 @@ def get_window_extent(self, renderer=None): if renderer is not None: self._renderer = renderer if self._renderer is None: - self._renderer = self.figure._cachedRenderer + self._renderer = self.figure._get_renderer() if self._renderer is None: raise RuntimeError('Cannot get window extent w/o renderer') @@ -1995,11 +2057,11 @@ def get_window_extent(self, renderer=None): return Bbox.union(bboxes) - def get_tightbbox(self, renderer): + def get_tightbbox(self, renderer=None): # docstring inherited if not self._check_xy(renderer): return Bbox.null() return super().get_tightbbox(renderer) -docstring.interpd.update(Annotation=Annotation.__init__.__doc__) +_docstring.interpd.update(Annotation=Annotation.__init__.__doc__) diff --git a/lib/matplotlib/textpath.py b/lib/matplotlib/textpath.py index 424d4df7da94..e4bb791f6f93 100644 --- a/lib/matplotlib/textpath.py +++ b/lib/matplotlib/textpath.py @@ -1,15 +1,17 @@ from collections import OrderedDict -import functools import logging import urllib.parse import numpy as np -from matplotlib import _text_layout, dviread, font_manager, rcParams -from matplotlib.font_manager import FontProperties, get_font +from matplotlib import _api, _text_helpers, dviread +from matplotlib.font_manager import ( + FontProperties, get_font, fontManager as _fontManager +) from matplotlib.ft2font import LOAD_NO_HINTING, LOAD_TARGET_LIGHT from matplotlib.mathtext import MathTextParser from matplotlib.path import Path +from matplotlib.texmanager import TexManager from matplotlib.transforms import Affine2D _log = logging.getLogger(__name__) @@ -29,8 +31,8 @@ def _get_font(self, prop): """ Find the `FT2Font` matching font properties *prop*, with its size set. """ - fname = font_manager.findfont(prop) - font = get_font(fname) + filenames = _fontManager._find_fonts_by_props(prop) + font = get_font(filenames) font.set_size(self.FONT_SCALE, self.DPI) return font @@ -44,14 +46,11 @@ def _get_char_id(self, font, ccode): return urllib.parse.quote(f"{font.postscript_name}-{ccode:x}") def get_text_width_height_descent(self, s, prop, ismath): + fontsize = prop.get_size_in_points() + if ismath == "TeX": - texmanager = self.get_texmanager() - fontsize = prop.get_size_in_points() - w, h, d = texmanager.get_text_width_height_descent(s, fontsize, - renderer=None) - return w, h, d + return TexManager().get_text_width_height_descent(s, fontsize) - fontsize = prop.get_size_in_points() scale = fontsize / self.FONT_SCALE if ismath: @@ -101,7 +100,7 @@ def get_text_path(self, prop, s, ismath=False): from those:: from matplotlib.path import Path - from matplotlib.textpath import TextToPath + from matplotlib.text import TextToPath from matplotlib.font_manager import FontProperties fp = FontProperties(family="Humor Sans", style="italic") @@ -119,18 +118,19 @@ def get_text_path(self, prop, s, ismath=False): glyph_info, glyph_map, rects = self.get_glyphs_mathtext(prop, s) verts, codes = [], [] - for glyph_id, xposition, yposition, scale in glyph_info: verts1, codes1 = glyph_map[glyph_id] - if len(verts1): - verts1 = np.array(verts1) * scale + [xposition, yposition] - verts.extend(verts1) - codes.extend(codes1) - + verts.extend(verts1 * scale + [xposition, yposition]) + codes.extend(codes1) for verts1, codes1 in rects: verts.extend(verts1) codes.extend(codes1) + # Make sure an empty string or one with nothing to print + # (e.g. only spaces & newlines) will be valid/empty path + if not verts: + verts = np.empty((0, 2)) + return verts, codes def get_glyphs_with_font(self, font, s, glyph_map=None, @@ -149,12 +149,12 @@ def get_glyphs_with_font(self, font, s, glyph_map=None, xpositions = [] glyph_ids = [] - for item in _text_layout.layout(s, font): - char_id = self._get_char_id(font, ord(item.char)) + for item in _text_helpers.layout(s, font): + char_id = self._get_char_id(item.ft_object, ord(item.char)) glyph_ids.append(char_id) xpositions.append(item.x) if char_id not in glyph_map: - glyph_map_new[char_id] = font.get_path() + glyph_map_new[char_id] = item.ft_object.get_path() ypositions = [0] * len(xpositions) sizes = [1.] * len(xpositions) @@ -215,10 +215,10 @@ def get_glyphs_mathtext(self, prop, s, glyph_map=None, return (list(zip(glyph_ids, xpositions, ypositions, sizes)), glyph_map_new, myrects) + @_api.deprecated("3.6", alternative="TexManager()") def get_texmanager(self): """Return the cached `~.texmanager.TexManager` instance.""" if self._texmanager is None: - from matplotlib.texmanager import TexManager self._texmanager = TexManager() return self._texmanager @@ -227,7 +227,7 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, """Convert the string *s* to vertices and codes using usetex mode.""" # Mostly borrowed from pdf backend. - dvifile = self.get_texmanager().make_dvi(s, self.FONT_SCALE) + dvifile = TexManager().make_dvi(s, self.FONT_SCALE) with dviread.Dvi(dvifile, self.DPI) as dvi: page, = dvi @@ -243,25 +243,29 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, # Gather font information and do some setup for combining # characters into strings. - for x1, y1, dvifont, glyph, width in page.text: - font, enc = self._get_ps_font_and_encoding(dvifont.texname) - char_id = self._get_char_id(font, glyph) - + for text in page.text: + font = get_font(text.font_path) + char_id = self._get_char_id(font, text.glyph) if char_id not in glyph_map: font.clear() font.set_size(self.FONT_SCALE, self.DPI) - # See comments in _get_ps_font_and_encoding. - if enc is not None: - index = font.get_name_index(enc[glyph]) + glyph_name_or_index = text.glyph_name_or_index + if isinstance(glyph_name_or_index, str): + index = font.get_name_index(glyph_name_or_index) font.load_glyph(index, flags=LOAD_TARGET_LIGHT) - else: - font.load_char(glyph, flags=LOAD_TARGET_LIGHT) + elif isinstance(glyph_name_or_index, int): + self._select_native_charmap(font) + font.load_char( + glyph_name_or_index, flags=LOAD_TARGET_LIGHT) + else: # Should not occur. + raise TypeError(f"Glyph spec of unexpected type: " + f"{glyph_name_or_index!r}") glyph_map_new[char_id] = font.get_path() glyph_ids.append(char_id) - xpositions.append(x1) - ypositions.append(y1) - sizes.append(dvifont.size / self.FONT_SCALE) + xpositions.append(text.x) + ypositions.append(text.y) + sizes.append(text.font_size / self.FONT_SCALE) myrects = [] @@ -277,48 +281,21 @@ def get_glyphs_tex(self, prop, s, glyph_map=None, glyph_map_new, myrects) @staticmethod - @functools.lru_cache(50) - def _get_ps_font_and_encoding(texname): - tex_font_map = dviread.PsfontsMap(dviread.find_tex_file('pdftex.map')) - psfont = tex_font_map[texname] - if psfont.filename is None: - raise ValueError( - f"No usable font file found for {psfont.psname} ({texname}). " - f"The font may lack a Type-1 version.") - - font = get_font(psfont.filename) - - if psfont.encoding: - # If psfonts.map specifies an encoding, use it: it gives us a - # mapping of glyph indices to Adobe glyph names; use it to convert - # dvi indices to glyph names and use the FreeType-synthesized - # unicode charmap to convert glyph names to glyph indices (with - # FT_Get_Name_Index/get_name_index), and load the glyph using - # FT_Load_Glyph/load_glyph. (That charmap has a coverage at least - # as good as, and possibly better than, the native charmaps.) - enc = dviread._parse_enc(psfont.encoding) - else: - # If psfonts.map specifies no encoding, the indices directly - # map to the font's "native" charmap; so don't use the - # FreeType-synthesized charmap but the native ones (we can't - # directly identify it but it's typically an Adobe charmap), and - # directly load the dvi glyph indices using FT_Load_Char/load_char. - for charmap_code in [ - 1094992451, # ADOBE_CUSTOM. - 1094995778, # ADOBE_STANDARD. - ]: - try: - font.select_charmap(charmap_code) - except (ValueError, RuntimeError): - pass - else: - break + def _select_native_charmap(font): + # Select the native charmap. (we can't directly identify it but it's + # typically an Adobe charmap). + for charmap_code in [ + 1094992451, # ADOBE_CUSTOM. + 1094995778, # ADOBE_STANDARD. + ]: + try: + font.select_charmap(charmap_code) + except (ValueError, RuntimeError): + pass else: - _log.warning("No supported encoding in font (%s).", - psfont.filename) - enc = None - - return font, enc + break + else: + _log.warning("No supported encoding in font (%s).", font.fname) text_to_path = TextToPath() @@ -333,7 +310,7 @@ def __init__(self, xy, s, size=None, prop=None, _interpolation_steps=1, usetex=False): r""" Create a path from the text. Note that it simply is a path, - not an artist. You need to use the `~.PathPatch` (or other artists) + not an artist. You need to use the `.PathPatch` (or other artists) to draw this path onto the canvas. Parameters @@ -351,7 +328,7 @@ def __init__(self, xy, s, size=None, prop=None, prop : `matplotlib.font_manager.FontProperties`, optional Font property. If not provided, will use a default ``FontProperties`` with parameters from the - :ref:`rcParams `. + :ref:`rcParams`. _interpolation_steps : int, optional (Currently ignored) @@ -364,7 +341,7 @@ def __init__(self, xy, s, size=None, prop=None, The following creates a path from the string "ABC" with Helvetica font face; and another path from the latex fraction 1/2:: - from matplotlib.textpath import TextPath + from matplotlib.text import TextPath from matplotlib.font_manager import FontProperties fp = FontProperties(family="Helvetica", style="italic") @@ -385,11 +362,11 @@ def __init__(self, xy, s, size=None, prop=None, self._cached_vertices = None s, ismath = Text(usetex=usetex)._preprocess_math(s) - self._vertices, self._codes = text_to_path.get_text_path( - prop, s, ismath=ismath) + super().__init__( + *text_to_path.get_text_path(prop, s, ismath=ismath), + _interpolation_steps=_interpolation_steps, + readonly=True) self._should_simplify = False - self._simplify_threshold = rcParams['path.simplify_threshold'] - self._interpolation_steps = _interpolation_steps def set_size(self, size): """Set the text size.""" @@ -420,11 +397,12 @@ def _revalidate_path(self): Update the path if necessary. The path for the text is initially create with the font size of - `~.FONT_SCALE`, and this path is rescaled to other size when necessary. + `.FONT_SCALE`, and this path is rescaled to other size when necessary. """ if self._invalid or self._cached_vertices is None: tr = (Affine2D() .scale(self._size / text_to_path.FONT_SCALE) .translate(*self._xy)) self._cached_vertices = tr.transform(self._vertices) + self._cached_vertices.flags.writeable = False self._invalid = False diff --git a/lib/matplotlib/ticker.py b/lib/matplotlib/ticker.py index 430d6112780b..db593838ea5f 100644 --- a/lib/matplotlib/ticker.py +++ b/lib/matplotlib/ticker.py @@ -18,50 +18,33 @@ `MultipleLocator`. It is initialized with a base, e.g., 10, and it picks axis limits and ticks that are multiples of that base. -The Locator subclasses defined here are - -:class:`AutoLocator` - `MaxNLocator` with simple defaults. This is the default tick locator for - most plotting. - -:class:`MaxNLocator` - Finds up to a max number of intervals with ticks at nice locations. - -:class:`LinearLocator` - Space ticks evenly from min to max. - -:class:`LogLocator` - Space ticks logarithmically from min to max. - -:class:`MultipleLocator` - Ticks and range are a multiple of base; either integer or float. - -:class:`FixedLocator` - Tick locations are fixed. - -:class:`IndexLocator` - Locator for index plots (e.g., where ``x = range(len(y))``). - -:class:`NullLocator` - No ticks. - -:class:`SymmetricalLogLocator` - Locator for use with with the symlog norm; works like `LogLocator` for the - part outside of the threshold and adds 0 if inside the limits. - -:class:`LogitLocator` - Locator for logit scaling. - -:class:`OldAutoLocator` - Choose a `MultipleLocator` and dynamically reassign it for intelligent - ticking during navigation. - -:class:`AutoMinorLocator` - Locator for minor ticks when the axis is linear and the - major ticks are uniformly spaced. Subdivides the major - tick interval into a specified number of minor intervals, - defaulting to 4 or 5 depending on the major interval. - +The Locator subclasses defined here are: + +======================= ======================================================= +`AutoLocator` `MaxNLocator` with simple defaults. This is the default + tick locator for most plotting. +`MaxNLocator` Finds up to a max number of intervals with ticks at + nice locations. +`LinearLocator` Space ticks evenly from min to max. +`LogLocator` Space ticks logarithmically from min to max. +`MultipleLocator` Ticks and range are a multiple of base; either integer + or float. +`FixedLocator` Tick locations are fixed. +`IndexLocator` Locator for index plots (e.g., where + ``x = range(len(y))``). +`NullLocator` No ticks. +`SymmetricalLogLocator` Locator for use with the symlog norm; works like + `LogLocator` for the part outside of the threshold and + adds 0 if inside the limits. +`AsinhLocator` Locator for use with the asinh norm, attempting to + space ticks approximately uniformly. +`LogitLocator` Locator for logit scaling. +`AutoMinorLocator` Locator for minor ticks when the axis is linear and the + major ticks are uniformly spaced. Subdivides the major + tick interval into a specified number of minor + intervals, defaulting to 4 or 5 depending on the major + interval. +======================= ======================================================= There are a number of locators specialized for date locations - see the :mod:`.dates` module. @@ -72,7 +55,7 @@ view limits from the data limits. If you want to override the default locator, use one of the above or a custom -locator and pass it to the x or y axis instance. The relevant methods are:: +locator and pass it to the x- or y-axis instance. The relevant methods are:: ax.xaxis.set_major_locator(xmajor_locator) ax.xaxis.set_minor_locator(xminor_locator) @@ -100,48 +83,24 @@ Tick formatting is controlled by classes derived from Formatter. The formatter operates on a single tick value and returns a string to the axis. -:class:`NullFormatter` - No labels on the ticks. - -:class:`IndexFormatter` - Set the strings from a list of labels. - -:class:`FixedFormatter` - Set the strings manually for the labels. - -:class:`FuncFormatter` - User defined function sets the labels. - -:class:`StrMethodFormatter` - Use string `format` method. - -:class:`FormatStrFormatter` - Use an old-style sprintf format string. - -:class:`ScalarFormatter` - Default formatter for scalars: autopick the format string. - -:class:`LogFormatter` - Formatter for log axes. - -:class:`LogFormatterExponent` - Format values for log axis using ``exponent = log_base(value)``. - -:class:`LogFormatterMathtext` - Format values for log axis using ``exponent = log_base(value)`` - using Math text. - -:class:`LogFormatterSciNotation` - Format values for log axis using scientific notation. - -:class:`LogitFormatter` - Probability formatter. - -:class:`EngFormatter` - Format labels in engineering notation. - -:class:`PercentFormatter` - Format labels as a percentage. +========================= ===================================================== +`NullFormatter` No labels on the ticks. +`FixedFormatter` Set the strings manually for the labels. +`FuncFormatter` User defined function sets the labels. +`StrMethodFormatter` Use string `format` method. +`FormatStrFormatter` Use an old-style sprintf format string. +`ScalarFormatter` Default formatter for scalars: autopick the format + string. +`LogFormatter` Formatter for log axes. +`LogFormatterExponent` Format values for log axis using + ``exponent = log_base(value)``. +`LogFormatterMathtext` Format values for log axis using + ``exponent = log_base(value)`` using Math text. +`LogFormatterSciNotation` Format values for log axis using scientific notation. +`LogitFormatter` Probability formatter. +`EngFormatter` Format labels in engineering notation. +`PercentFormatter` Format labels as a percentage. +========================= ===================================================== You can derive your own formatter from the Formatter base class by simply overriding the ``__call__`` method. The formatter class has @@ -161,9 +120,9 @@ the input ``str``. For function input, a `.FuncFormatter` with the input function will be generated and used. -See :doc:`/gallery/ticks_and_spines/major_minor_demo` for an -example of setting major and minor ticks. See the :mod:`matplotlib.dates` -module for more information and examples of using date locators and formatters. +See :doc:`/gallery/ticks/major_minor_demo` for an example of setting major +and minor ticks. See the :mod:`matplotlib.dates` module for more information +and examples of using date locators and formatters. """ import itertools @@ -184,37 +143,43 @@ 'NullFormatter', 'FuncFormatter', 'FormatStrFormatter', 'StrMethodFormatter', 'ScalarFormatter', 'LogFormatter', 'LogFormatterExponent', 'LogFormatterMathtext', - 'IndexFormatter', 'LogFormatterSciNotation', + 'LogFormatterSciNotation', 'LogitFormatter', 'EngFormatter', 'PercentFormatter', - 'OldScalarFormatter', 'Locator', 'IndexLocator', 'FixedLocator', 'NullLocator', 'LinearLocator', 'LogLocator', 'AutoLocator', 'MultipleLocator', 'MaxNLocator', 'AutoMinorLocator', - 'SymmetricalLogLocator', 'LogitLocator', 'OldAutoLocator') + 'SymmetricalLogLocator', 'AsinhLocator', 'LogitLocator') class _DummyAxis: __name__ = "dummy" + # Once the deprecation elapses, replace dataLim and viewLim by plain + # _view_interval and _data_interval private tuples. + dataLim = _api.deprecate_privatize_attribute( + "3.6", alternative="get_data_interval() and set_data_interval()") + viewLim = _api.deprecate_privatize_attribute( + "3.6", alternative="get_view_interval() and set_view_interval()") + def __init__(self, minpos=0): - self.dataLim = mtransforms.Bbox.unit() - self.viewLim = mtransforms.Bbox.unit() + self._dataLim = mtransforms.Bbox.unit() + self._viewLim = mtransforms.Bbox.unit() self._minpos = minpos def get_view_interval(self): - return self.viewLim.intervalx + return self._viewLim.intervalx def set_view_interval(self, vmin, vmax): - self.viewLim.intervalx = vmin, vmax + self._viewLim.intervalx = vmin, vmax def get_minpos(self): return self._minpos def get_data_interval(self): - return self.dataLim.intervalx + return self._dataLim.intervalx def set_data_interval(self, vmin, vmax): - self.dataLim.intervalx = vmin, vmax + self._dataLim.intervalx = vmin, vmax def get_tick_space(self): # Just use the long-standing default of nbins==9 @@ -231,16 +196,6 @@ def create_dummy_axis(self, **kwargs): if self.axis is None: self.axis = _DummyAxis(**kwargs) - def set_view_interval(self, vmin, vmax): - self.axis.set_view_interval(vmin, vmax) - - def set_data_interval(self, vmin, vmax): - self.axis.set_data_interval(vmin, vmax) - - def set_bounds(self, vmin, vmax): - self.set_view_interval(vmin, vmax) - self.set_data_interval(vmin, vmax) - class Formatter(TickHelper): """ @@ -293,7 +248,7 @@ def set_locs(self, locs): def fix_minus(s): """ Some classes may want to replace a hyphen for minus with the proper - unicode symbol (U+2212) for typographical correctness. This is a + Unicode symbol (U+2212) for typographical correctness. This is a helper method to perform such a replacement when it is enabled via :rc:`axes.unicode_minus`. """ @@ -306,35 +261,6 @@ def _set_locator(self, locator): pass -@_api.deprecated("3.3") -class IndexFormatter(Formatter): - """ - Format the position x to the nearest i-th label where ``i = int(x + 0.5)``. - Positions where ``i < 0`` or ``i > len(list)`` have no tick labels. - - Parameters - ---------- - labels : list - List of labels. - """ - def __init__(self, labels): - self.labels = labels - self.n = len(labels) - - def __call__(self, x, pos=None): - """ - Return the format for tick value *x* at position pos. - - The position is ignored and the value is rounded to the nearest - integer, which is used to look up the label. - """ - i = int(x + 0.5) - if i < 0 or i >= self.n: - return '' - else: - return self.labels[i] - - class NullFormatter(Formatter): """Always return the empty string.""" @@ -412,9 +338,9 @@ class FormatStrFormatter(Formatter): The format string should have a single variable format (%) in it. It will be applied to the value (not the position) of the tick. - Negative numeric values will use a dash not a unicode minus, - use mathtext to get a unicode minus by wrappping the format specifier - with $ (e.g. "$%g$"). + Negative numeric values will use a dash, not a Unicode minus; use mathtext + to get a Unicode minus by wrapping the format specifier with $ (e.g. + "$%g$"). """ def __init__(self, fmt): self.fmt = fmt @@ -448,40 +374,6 @@ def __call__(self, x, pos=None): return self.fmt.format(x=x, pos=pos) -@_api.deprecated("3.3") -class OldScalarFormatter(Formatter): - """ - Tick location is a plain old number. - """ - - def __call__(self, x, pos=None): - """ - Return the format for tick val *x* based on the width of the axis. - - The position *pos* is ignored. - """ - xmin, xmax = self.axis.get_view_interval() - # If the number is not too big and it's an int, format it as an int. - if abs(x) < 1e4 and x == int(x): - return '%d' % x - d = abs(xmax - xmin) - fmt = ('%1.3e' if d < 1e-2 else - '%1.3f' if d <= 1 else - '%1.2f' if d <= 10 else - '%1.1f' if d <= 1e5 else - '%1.1e') - s = fmt % x - tup = s.split('e') - if len(tup) == 2: - mantissa = tup[0].rstrip('0').rstrip('.') - sign = tup[1][0].replace('+', '') - exponent = tup[1][1:].lstrip('0') - s = '%se%s%s' % (mantissa, sign, exponent) - else: - s = s.rstrip('0').rstrip('.') - return s - - class ScalarFormatter(Formatter): """ Format tick values as a number. @@ -539,16 +431,12 @@ def __init__(self, useOffset=None, useMathText=None, useLocale=None): mpl.rcParams['axes.formatter.offset_threshold'] self.set_useOffset(useOffset) self._usetex = mpl.rcParams['text.usetex'] - if useMathText is None: - useMathText = mpl.rcParams['axes.formatter.use_mathtext'] self.set_useMathText(useMathText) self.orderOfMagnitude = 0 self.format = '' self._scientific = True self._powerlimits = mpl.rcParams['axes.formatter.limits'] - if useLocale is None: - useLocale = mpl.rcParams['axes.formatter.use_locale'] - self._useLocale = useLocale + self.set_useLocale(useLocale) def get_useOffset(self): """ @@ -627,7 +515,7 @@ def set_useLocale(self, val): def _format_maybe_minus_and_locale(self, fmt, arg): """ - Format *arg* with *fmt*, applying unicode minus and locale if desired. + Format *arg* with *fmt*, applying Unicode minus and locale if desired. """ return self.fix_minus(locale.format_string(fmt, (arg,), True) if self._useLocale else fmt % arg) @@ -655,6 +543,23 @@ def set_useMathText(self, val): """ if val is None: self._useMathText = mpl.rcParams['axes.formatter.use_mathtext'] + if self._useMathText is False: + try: + from matplotlib import font_manager + ufont = font_manager.findfont( + font_manager.FontProperties( + mpl.rcParams["font.family"] + ), + fallback_to_default=False, + ) + except ValueError: + ufont = None + + if ufont == str(cbook._get_data_path("fonts/ttf/cmr10.ttf")): + _api.warn_external( + "cmr10 font should ideally be used with " + "mathtext, set axes.formatter.use_mathtext to True" + ) else: self._useMathText = val @@ -691,7 +596,7 @@ def set_powerlimits(self, lims): lims : (int, int) A tuple *(min_exp, max_exp)* containing the powers of 10 that determine the switchover threshold. For a number representable as - :math:`a \times 10^\mathrm{exp}`` with :math:`1 <= |a| < 10`, + :math:`a \times 10^\mathrm{exp}` with :math:`1 <= |a| < 10`, scientific notation will be used if ``exp <= min_exp`` or ``exp >= max_exp``. @@ -718,7 +623,7 @@ def set_powerlimits(self, lims): def format_data_short(self, value): # docstring inherited - if isinstance(value, np.ma.MaskedArray) and value.mask: + if value is np.ma.masked: return "" if isinstance(value, Integral): fmt = "%d" @@ -739,17 +644,9 @@ def format_data_short(self, value): delta = abs(neighbor_values - value).max() else: # Rough approximation: no more than 1e4 divisions. - delta = np.diff(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" + a, b = self.axis.get_view_interval() + delta = (b - a) / 1e4 + fmt = "%-#.{}g".format(cbook._g_sig_digits(value, delta)) return self._format_maybe_minus_and_locale(fmt, value) def format_data(self, value): @@ -758,7 +655,7 @@ def format_data(self, value): s = round(value / 10**e, 10) exponent = self._format_maybe_minus_and_locale("%d", e) significand = self._format_maybe_minus_and_locale( - "%d" if s % 1 == 0 else "%1.10f", s) + "%d" if s % 1 == 0 else "%1.10g", s) if e == 0: return significand elif self._useMathText or self._usetex: @@ -846,7 +743,7 @@ def _compute_offset(self): def _set_order_of_magnitude(self): # if scientific notation is to be used, find the appropriate exponent - # if using an numerical offset, find the exponent after applying the + # if using a numerical offset, find the exponent after applying the # offset. When lower power limit = upper <> 0, use provided exponent. if not self._scientific: self.orderOfMagnitude = 0 @@ -866,10 +763,7 @@ def _set_order_of_magnitude(self): if self.offset: oom = math.floor(math.log10(vmax - vmin)) else: - if locs[0] > locs[-1]: - val = locs[0] - else: - val = locs[-1] + val = locs.max() if val == 0: oom = 0 else: @@ -910,7 +804,7 @@ def _set_format(self): else: break sigfigs += 1 - self.format = '%1.' + str(sigfigs) + 'f' + self.format = f'%1.{sigfigs}f' if self._usetex or self._useMathText: self.format = r'$\mathdefault{%s}$' % self.format @@ -977,8 +871,8 @@ def __init__(self, base=10.0, labelOnlyBase=False, minor_thresholds=None, linthresh=None): - self._base = float(base) - self.labelOnlyBase = labelOnlyBase + self.set_base(base) + self.set_label_minor(labelOnlyBase) if minor_thresholds is None: if mpl.rcParams['_internal.classic_mode']: minor_thresholds = (0, 0) @@ -988,6 +882,7 @@ def __init__(self, base=10.0, labelOnlyBase=False, self._sublabels = None self._linthresh = linthresh + @_api.deprecated("3.6", alternative='set_base()') def base(self, base): """ Change the *base* for labeling. @@ -995,12 +890,33 @@ def base(self, base): .. warning:: Should always match the base used for :class:`LogLocator` """ - self._base = base + self.set_base(base) + + def set_base(self, base): + """ + Change the *base* for labeling. + .. warning:: + Should always match the base used for :class:`LogLocator` + """ + self._base = float(base) + + @_api.deprecated("3.6", alternative='set_label_minor()') def label_minor(self, labelOnlyBase): """ Switch minor tick labeling on or off. + Parameters + ---------- + labelOnlyBase : bool + If True, label ticks only at integer powers of base. + """ + self.set_label_minor(labelOnlyBase) + + def set_label_minor(self, labelOnlyBase): + """ + Switch minor tick labeling on or off. + Parameters ---------- labelOnlyBase : bool @@ -1085,7 +1001,7 @@ def __call__(self, x, pos=None): b = self._base # only label the decades fx = math.log(x) / math.log(b) - is_x_decade = is_close_to_int(fx) + is_x_decade = _is_close_to_int(fx) exponent = round(fx) if is_x_decade else np.floor(fx) coeff = round(b ** (fx - exponent)) @@ -1097,7 +1013,7 @@ def __call__(self, x, pos=None): vmin, vmax = self.axis.get_view_interval() vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) s = self._num_to_string(x, vmin, vmax) - return s + return self.fix_minus(s) def format_data(self, value): with cbook._setattr_cm(self, labelOnlyBase=False): @@ -1169,7 +1085,7 @@ def __call__(self, x, pos=None): # only label the decades fx = math.log(x) / math.log(b) - is_x_decade = is_close_to_int(fx) + is_x_decade = _is_close_to_int(fx) exponent = round(fx) if is_x_decade else np.floor(fx) coeff = round(b ** (fx - exponent)) if is_x_decade: @@ -1204,7 +1120,7 @@ def _non_decade_format(self, sign_string, base, fx, usetex): b = float(base) exponent = math.floor(fx) coeff = b ** (fx - exponent) - if is_close_to_int(coeff): + if _is_close_to_int(coeff): coeff = round(coeff) return r'$\mathdefault{%s%g\times%s^{%d}}$' \ % (sign_string, coeff, base, exponent) @@ -1308,9 +1224,10 @@ def set_locs(self, locs): if not self._minor: return None if all( - is_decade(x, rtol=1e-7) - or is_decade(1 - x, rtol=1e-7) - or (is_close_to_int(2 * x) and int(np.round(2 * x)) == 1) + _is_decade(x, rtol=1e-7) + or _is_decade(1 - x, rtol=1e-7) + or (_is_close_to_int(2 * x) and + int(np.round(2 * x)) == 1) for x in locs ): # minor ticks are subsample from ideal, so no label @@ -1357,7 +1274,7 @@ def _format_value(self, x, locs, sci_notation=True): precision = -np.log10(diff) + exponent precision = ( int(np.round(precision)) - if is_close_to_int(precision) + if _is_close_to_int(precision) else math.ceil(precision) ) if precision < min_precision: @@ -1379,13 +1296,13 @@ def __call__(self, x, pos=None): return "" if x <= 0 or x >= 1: return "" - if is_close_to_int(2 * x) and round(2 * x) == 1: + if _is_close_to_int(2 * x) and round(2 * x) == 1: s = self._one_half - elif x < 0.5 and is_decade(x, rtol=1e-7): - exponent = round(np.log10(x)) + elif x < 0.5 and _is_decade(x, rtol=1e-7): + exponent = round(math.log10(x)) s = "10^{%d}" % exponent - elif x > 0.5 and is_decade(1 - x, rtol=1e-7): - exponent = round(np.log10(1 - x)) + elif x > 0.5 and _is_decade(1 - x, rtol=1e-7): + exponent = round(math.log10(1 - x)) s = self._one_minus("10^{%d}" % exponent) elif x < 0.1: s = self._format_value(x, self.locs) @@ -1509,13 +1426,13 @@ def format_eng(self, num): representing the power of 1000 of the original number. Some examples: - >>> format_eng(0) # for self.places = 0 + >>> format_eng(0) # for self.places = 0 '0' - >>> format_eng(1000000) # for self.places = 1 + >>> format_eng(1000000) # for self.places = 1 '1.0 M' - >>> format_eng("-1e-6") # for self.places = 2 + >>> format_eng(-1e-6) # for self.places = 2 '-1.00 \N{MICRO SIGN}' """ sign = 1 @@ -1603,17 +1520,14 @@ def format_pct(self, x, display_range): decimal point is set based on the *display_range* of the axis as follows: - +---------------+----------+------------------------+ - | display_range | decimals | sample | - +---------------+----------+------------------------+ - | >50 | 0 | ``x = 34.5`` => 35% | - +---------------+----------+------------------------+ - | >5 | 1 | ``x = 34.5`` => 34.5% | - +---------------+----------+------------------------+ - | >0.5 | 2 | ``x = 34.5`` => 34.50% | - +---------------+----------+------------------------+ - | ... | ... | ... | - +---------------+----------+------------------------+ + ============= ======== ======================= + display_range decimals sample + ============= ======== ======================= + >50 0 ``x = 34.5`` => 35% + >5 1 ``x = 34.5`` => 34.5% + >0.5 2 ``x = 34.5`` => 34.50% + ... ... ... + ============= ======== ======================= This method will not be very good for tiny axis ranges or extremely large ones. It assumes that the values on the chart @@ -1669,18 +1583,6 @@ 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 _api.suppress_matplotlib_deprecation_warning(): - locator.refresh() - - class Locator(TickHelper): """ Determine the tick locations; @@ -1702,7 +1604,7 @@ def tick_values(self, vmin, vmax): .. note:: To get tick locations with the vmin and vmax values defined - automatically for the associated :attr:`axis` simply call + automatically for the associated ``axis`` simply call the Locator instance:: >>> print(type(loc)) @@ -1770,38 +1672,6 @@ def view_limits(self, vmin, vmax): """ return mtransforms.nonsingular(vmin, vmax) - @_api.deprecated("3.3") - def pan(self, numsteps): - """Pan numticks (can be positive or negative)""" - ticks = self() - numticks = len(ticks) - - vmin, vmax = self.axis.get_view_interval() - vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) - if numticks > 2: - step = numsteps * abs(ticks[0] - ticks[1]) - else: - d = abs(vmax - vmin) - step = numsteps * d / 6. - - vmin += step - vmax += step - self.axis.set_view_interval(vmin, vmax, ignore=True) - - @_api.deprecated("3.3") - def zoom(self, direction): - """Zoom in/out on axis; if direction is >0 zoom in, else zoom out.""" - - vmin, vmax = self.axis.get_view_interval() - vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) - interval = abs(vmax - vmin) - step = 0.1 * interval * direction - self.axis.set_view_interval(vmin + step, vmax - step, ignore=True) - - @_api.deprecated("3.3") - def refresh(self): - """Refresh internal information based on current limits.""" - class IndexLocator(Locator): """ @@ -1834,10 +1704,10 @@ def tick_values(self, vmin, vmax): class FixedLocator(Locator): """ - Tick locations are fixed. If nbins is not None, - the array of possible positions will be subsampled to - keep the number of ticks <= nbins +1. - The subsampling will be done so as to include the smallest + Tick locations are fixed at *locs*. If *nbins* is not None, + the *locs* array of possible positions will be subsampled to + keep the number of ticks <= *nbins* +1. + The subsampling will be done to include the smallest absolute value; for example, if zero is included in the array of possibilities, then it is guaranteed to be one of the chosen ticks. @@ -1845,6 +1715,7 @@ class FixedLocator(Locator): def __init__(self, locs, nbins=None): self.locs = np.asarray(locs) + _api.check_shape((None,), locs=self.locs) self.nbins = max(nbins, 2) if nbins is not None else None def set_params(self, nbins=None): @@ -1901,14 +1772,21 @@ class LinearLocator(Locator): Determine the tick locations The first time this function is called it will try to set the - number of ticks to make a nice tick partitioning. Thereafter the + number of ticks to make a nice tick partitioning. Thereafter, the number of ticks will be fixed so that interactive navigation will be nice """ def __init__(self, numticks=None, presets=None): """ - Use presets to set locs based on lom. A dict mapping vmin, vmax->locs + Parameters + ---------- + numticks : int or None, default None + Number of ticks. If None, *numticks* = 11. + presets : dict or None, default: None + Dictionary mapping ``(vmin, vmax)`` to an array of locations. + Overrides *numticks* if there is an entry for the current + ``(vmin, vmax)``. """ self.numticks = numticks if presets is None: @@ -1974,7 +1852,8 @@ def view_limits(self, vmin, vmax): class MultipleLocator(Locator): """ - Set a tick on each integer multiple of a base within the view interval. + Set a tick on each integer multiple of the *base* within the view + interval. """ def __init__(self, base=1.0): @@ -2001,7 +1880,7 @@ def tick_values(self, vmin, vmax): def view_limits(self, dmin, dmax): """ - Set the view limits to the nearest multiples of base that + Set the view limits to the nearest multiples of *base* that contain the data. """ if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': @@ -2030,16 +1909,20 @@ def scale_range(vmin, vmax, n=1, threshold=100): class _Edge_integer: """ - Helper for MaxNLocator, MultipleLocator, etc. + Helper for `.MaxNLocator`, `.MultipleLocator`, etc. - Take floating point precision limitations into account when calculating + Take floating-point precision limitations into account when calculating tick locations as integer multiples of a step. """ def __init__(self, step, offset): """ - *step* is a positive floating-point interval between ticks. - *offset* is the offset subtracted from the data limits - prior to calculating tick locations. + Parameters + ---------- + step : float > 0 + Interval between ticks. + offset : float + Offset subtracted from the data limits prior to calculating tick + locations. """ if step <= 0: raise ValueError("'step' must be positive") @@ -2073,8 +1956,8 @@ def ge(self, x): class MaxNLocator(Locator): """ - Find nice tick locations with no more than N being within the view limits. - Locations beyond the limits are added to support autoscaling. + Find nice tick locations with no more than *nbins* + 1 being within the + view limits. Locations beyond the limits are added to support autoscaling. """ default_params = dict(nbins=10, steps=None, @@ -2083,7 +1966,7 @@ class MaxNLocator(Locator): prune=None, min_n_ticks=2) - def __init__(self, *args, **kwargs): + def __init__(self, nbins=None, **kwargs): """ Parameters ---------- @@ -2120,17 +2003,8 @@ def __init__(self, *args, **kwargs): Relax *nbins* and *integer* constraints if necessary to obtain this minimum number of ticks. """ - if args: - if 'nbins' in kwargs: - _api.deprecated("3.1", - message='Calling MaxNLocator with positional ' - 'and keyword parameter *nbins* is ' - 'considered an error and will fail ' - 'in future versions of matplotlib.') - kwargs['nbins'] = args[0] - if len(args) > 1: - raise ValueError( - "Keywords are required for all arguments except 'nbins'") + if nbins is not None: + kwargs['nbins'] = nbins self.set_params(**{**self.default_params, **kwargs}) @staticmethod @@ -2195,9 +2069,7 @@ def set_params(self, **kwargs): if 'integer' in kwargs: self._integer = kwargs.pop('integer') if kwargs: - key, _ = kwargs.popitem() - raise TypeError( - f"set_params() got an unexpected keyword argument '{key}'") + raise _api.kwarg_error("set_params", kwargs) def _raw_ticks(self, vmin, vmax): """ @@ -2294,6 +2166,7 @@ def view_limits(self, dmin, dmax): return dmin, dmax +@_api.deprecated("3.6") def is_decade(x, base=10, *, rtol=1e-10): if not np.isfinite(x): return False @@ -2303,6 +2176,19 @@ def is_decade(x, base=10, *, rtol=1e-10): return is_close_to_int(lx, atol=rtol) +def _is_decade(x, *, base=10, rtol=None): + """Return True if *x* is an integer power of *base*.""" + if not np.isfinite(x): + return False + if x == 0.0: + return True + lx = np.log(abs(x)) / np.log(base) + if rtol is None: + return np.isclose(lx, np.round(lx)) + else: + return np.isclose(lx, np.round(lx), rtol=rtol) + + def _decade_less_equal(x, base): """ Return the largest integer power of *base* that's less or equal to *x*. @@ -2353,69 +2239,82 @@ def _decade_greater(x, base): return greater +@_api.deprecated("3.6") def is_close_to_int(x, *, atol=1e-10): return abs(x - np.round(x)) < atol +def _is_close_to_int(x): + return math.isclose(x, round(x)) + + class LogLocator(Locator): """ - Determine the tick locations for log axes + + Determine the tick locations for log axes. + + Place ticks on the locations : ``subs[j] * base**i`` + + Parameters + ---------- + base : float, default: 10.0 + The base of the log used, so major ticks are placed at + ``base**n``, where ``n`` is an integer. + subs : None or {'auto', 'all'} or sequence of float, default: (1.0,) + Gives the multiples of integer powers of the base at which + to place ticks. The default of ``(1.0, )`` places ticks only at + integer powers of the base. + Permitted string values are ``'auto'`` and ``'all'``. + Both of these use an algorithm based on the axis view + limits to determine whether and how to put ticks between + integer powers of the base. With ``'auto'``, ticks are + placed only between integer powers; with ``'all'``, the + integer powers are included. A value of None is + equivalent to ``'auto'``. + numticks : None or int, default: None + The maximum number of ticks to allow on a given axis. The default + of ``None`` will try to choose intelligently as long as this + Locator has already been assigned to an axis using + `~.axis.Axis.get_tick_space`, but otherwise falls back to 9. + """ def __init__(self, base=10.0, subs=(1.0,), numdecs=4, numticks=None): - """ - Place ticks on the locations : subs[j] * base**i - - Parameters - ---------- - base : float, default: 10.0 - The base of the log used, so ticks are placed at ``base**n``. - subs : None or str or sequence of float, default: (1.0,) - Gives the multiples of integer powers of the base at which - to place ticks. The default places ticks only at - integer powers of the base. - The permitted string values are ``'auto'`` and ``'all'``, - both of which use an algorithm based on the axis view - limits to determine whether and how to put ticks between - integer powers of the base. With ``'auto'``, ticks are - placed only between integer powers; with ``'all'``, the - integer powers are included. A value of None is - equivalent to ``'auto'``. - numticks : None or int, default: None - The maximum number of ticks to allow on a given axis. The default - of ``None`` will try to choose intelligently as long as this - Locator has already been assigned to an axis using - `~.axis.Axis.get_tick_space`, but otherwise falls back to 9. - """ + """Place ticks on the locations : subs[j] * base**i.""" if numticks is None: if mpl.rcParams['_internal.classic_mode']: numticks = 15 else: numticks = 'auto' - self.base(base) - self.subs(subs) + self._base = float(base) + self._set_subs(subs) self.numdecs = numdecs self.numticks = numticks def set_params(self, base=None, subs=None, numdecs=None, numticks=None): """Set parameters within this locator.""" if base is not None: - self.base(base) + self._base = float(base) if subs is not None: - self.subs(subs) + self._set_subs(subs) if numdecs is not None: self.numdecs = numdecs if numticks is not None: self.numticks = numticks - # FIXME: these base and subs functions are contrary to our - # usual and desired API. - + @_api.deprecated("3.6", alternative='set_params(base=...)') def base(self, base): """Set the log base (major tick every ``base**i``, i integer).""" self._base = float(base) + @_api.deprecated("3.6", alternative='set_params(subs=...)') def subs(self, subs): + """ + Set the minor ticks for the log scaling every ``base**i*subs[j]``. + """ + self._set_subs(subs) + + def _set_subs(self, subs): """ Set the minor ticks for the log scaling every ``base**i*subs[j]``. """ @@ -2451,14 +2350,6 @@ def tick_values(self, vmin, vmax): numticks = self.numticks b = self._base - # dummy axis has no axes attribute - if hasattr(self.axis, 'axes') and self.axis.axes.name == 'polar': - vmax = math.ceil(math.log(vmax) / math.log(b)) - decades = np.arange(vmax - self.numdecs, vmax) - ticklocs = b ** decades - - return ticklocs - if vmin <= 0.0: if self.axis is not None: vmin = self.axis.get_minpos() @@ -2543,10 +2434,6 @@ def view_limits(self, vmin, vmax): vmin, vmax = self.nonsingular(vmin, vmax) - if self.axis.axes.name == 'polar': - vmax = math.ceil(math.log(vmax) / math.log(b)) - vmin = b ** (vmax - self.numdecs) - if mpl.rcParams['axes.autolimit_mode'] == 'round_numbers': vmin = _decade_less_equal(vmin, self._base) vmax = _decade_greater_equal(vmax, self._base) @@ -2733,6 +2620,118 @@ def view_limits(self, vmin, vmax): return result +class AsinhLocator(Locator): + """ + An axis tick locator specialized for the inverse-sinh scale + + This is very unlikely to have any use beyond + the `~.scale.AsinhScale` class. + + .. note:: + + This API is provisional and may be revised in the future + based on early user feedback. + """ + def __init__(self, linear_width, numticks=11, symthresh=0.2, + base=10, subs=None): + """ + Parameters + ---------- + linear_width : float + The scale parameter defining the extent + of the quasi-linear region. + numticks : int, default: 11 + The approximate number of major ticks that will fit + along the entire axis + symthresh : float, default: 0.2 + The fractional threshold beneath which data which covers + a range that is approximately symmetric about zero + will have ticks that are exactly symmetric. + base : int, default: 10 + The number base used for rounding tick locations + on a logarithmic scale. If this is less than one, + then rounding is to the nearest integer multiple + of powers of ten. + subs : tuple, default: None + Multiples of the number base, typically used + for the minor ticks, e.g. (2, 5) when base=10. + """ + super().__init__() + self.linear_width = linear_width + self.numticks = numticks + self.symthresh = symthresh + self.base = base + self.subs = subs + + def set_params(self, numticks=None, symthresh=None, + base=None, subs=None): + """Set parameters within this locator.""" + if numticks is not None: + self.numticks = numticks + if symthresh is not None: + self.symthresh = symthresh + if base is not None: + self.base = base + if subs is not None: + self.subs = subs if len(subs) > 0 else None + + def __call__(self): + vmin, vmax = self.axis.get_view_interval() + if (vmin * vmax) < 0 and abs(1 + vmax / vmin) < self.symthresh: + # Data-range appears to be almost symmetric, so round up: + bound = max(abs(vmin), abs(vmax)) + return self.tick_values(-bound, bound) + else: + return self.tick_values(vmin, vmax) + + def tick_values(self, vmin, vmax): + # Construct a set of "on-screen" locations + # that are uniformly spaced: + ymin, ymax = self.linear_width * np.arcsinh(np.array([vmin, vmax]) + / self.linear_width) + ys = np.linspace(ymin, ymax, self.numticks) + zero_dev = np.abs(ys / (ymax - ymin)) + if (ymin * ymax) < 0: + # Ensure that the zero tick-mark is included, + # if the axis straddles zero + ys = np.hstack([ys[(zero_dev > 0.5 / self.numticks)], 0.0]) + + # Transform the "on-screen" grid to the data space: + xs = self.linear_width * np.sinh(ys / self.linear_width) + zero_xs = (ys == 0) + + # Round the data-space values to be intuitive base-n numbers, + # keeping track of positive and negative values separately, + # but giving careful treatment to the zero value: + if self.base > 1: + log_base = math.log(self.base) + powers = ( + np.where(zero_xs, 0, np.sign(xs)) * + np.power(self.base, + np.where(zero_xs, 0.0, + np.floor(np.log(np.abs(xs) + zero_xs*1e-6) + / log_base))) + ) + if self.subs: + qs = np.outer(powers, self.subs).flatten() + else: + qs = powers + else: + powers = ( + np.where(xs >= 0, 1, -1) * + np.power(10, np.where(zero_xs, 0.0, + np.floor(np.log10(np.abs(xs) + + zero_xs*1e-6)))) + ) + qs = powers * np.round(xs / powers) + ticks = np.array(sorted(set(qs))) + + if len(ticks) >= 2: + return ticks + else: + return np.linspace(vmin, vmax, self.numticks) + + class LogitLocator(MaxNLocator): """ Determine the tick locations for logit axes @@ -2960,58 +2959,3 @@ def __call__(self): def tick_values(self, vmin, vmax): raise NotImplementedError('Cannot get tick locations for a ' '%s type.' % type(self)) - - -@_api.deprecated("3.3") -class OldAutoLocator(Locator): - """ - On autoscale this class picks the best MultipleLocator to set the - view limits and the tick locs. - """ - - def __call__(self): - # docstring inherited - vmin, vmax = self.axis.get_view_interval() - vmin, vmax = mtransforms.nonsingular(vmin, vmax, expander=0.05) - d = abs(vmax - vmin) - locator = self.get_locator(d) - return self.raise_if_exceeds(locator()) - - def tick_values(self, vmin, vmax): - raise NotImplementedError('Cannot get tick locations for a ' - '%s type.' % type(self)) - - def view_limits(self, vmin, vmax): - # docstring inherited - d = abs(vmax - vmin) - locator = self.get_locator(d) - return locator.view_limits(vmin, vmax) - - def get_locator(self, d): - """Pick the best locator based on a distance *d*.""" - d = abs(d) - if d <= 0: - locator = MultipleLocator(0.2) - else: - - try: - ld = math.log10(d) - except OverflowError as err: - raise RuntimeError('AutoLocator illegal data interval ' - 'range') from err - - fld = math.floor(ld) - base = 10 ** fld - - #if ld==fld: base = 10**(fld-1) - #else: base = 10**fld - - if d >= 5 * base: - ticksize = base - elif d >= 2 * base: - ticksize = base / 2.0 - else: - ticksize = base / 5.0 - locator = MultipleLocator(ticksize) - - return locator diff --git a/lib/matplotlib/tight_bbox.py b/lib/matplotlib/tight_bbox.py index 5904ebc1fa1c..88c4c1ec51af 100644 --- a/lib/matplotlib/tight_bbox.py +++ b/lib/matplotlib/tight_bbox.py @@ -1,88 +1,3 @@ -""" -Helper module for the *bbox_inches* parameter in `.Figure.savefig`. -""" - -from matplotlib.transforms import Bbox, TransformedBbox, Affine2D - - -def adjust_bbox(fig, bbox_inches, fixed_dpi=None): - """ - Temporarily adjust the figure so that only the specified area - (bbox_inches) is saved. - - It modifies fig.bbox, fig.bbox_inches, - fig.transFigure._boxout, and fig.patch. While the figure size - changes, the scale of the original figure is conserved. A - function which restores the original values are returned. - """ - origBbox = fig.bbox - origBboxInches = fig.bbox_inches - orig_tight_layout = fig.get_tight_layout() - _boxout = fig.transFigure._boxout - - fig.set_tight_layout(False) - - old_aspect = [] - locator_list = [] - sentinel = object() - for ax in fig.axes: - locator_list.append(ax.get_axes_locator()) - current_pos = ax.get_position(original=False).frozen() - ax.set_axes_locator(lambda a, r, _pos=current_pos: _pos) - # override the method that enforces the aspect ratio on the Axes - if 'apply_aspect' in ax.__dict__: - old_aspect.append(ax.apply_aspect) - else: - old_aspect.append(sentinel) - ax.apply_aspect = lambda pos=None: None - - def restore_bbox(): - for ax, loc, aspect in zip(fig.axes, locator_list, old_aspect): - ax.set_axes_locator(loc) - if aspect is sentinel: - # delete our no-op function which un-hides the original method - del ax.apply_aspect - else: - ax.apply_aspect = aspect - - fig.bbox = origBbox - fig.bbox_inches = origBboxInches - fig.set_tight_layout(orig_tight_layout) - fig.transFigure._boxout = _boxout - fig.transFigure.invalidate() - fig.patch.set_bounds(0, 0, 1, 1) - - if fixed_dpi is None: - fixed_dpi = fig.dpi - tr = Affine2D().scale(fixed_dpi) - dpi_scale = fixed_dpi / fig.dpi - - _bbox = TransformedBbox(bbox_inches, tr) - - fig.bbox_inches = Bbox.from_bounds(0, 0, - bbox_inches.width, bbox_inches.height) - x0, y0 = _bbox.x0, _bbox.y0 - w1, h1 = fig.bbox.width * dpi_scale, fig.bbox.height * dpi_scale - fig.transFigure._boxout = Bbox.from_bounds(-x0, -y0, w1, h1) - fig.transFigure.invalidate() - - fig.bbox = TransformedBbox(fig.bbox_inches, tr) - - fig.patch.set_bounds(x0 / w1, y0 / h1, - fig.bbox.width / w1, fig.bbox.height / h1) - - return restore_bbox - - -def process_figure_for_rasterizing(fig, bbox_inches_restore, fixed_dpi=None): - """ - 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. - """ - - bbox_inches, restore_bbox = bbox_inches_restore - restore_bbox() - r = adjust_bbox(fig, bbox_inches, fixed_dpi) - - return bbox_inches, r +from matplotlib._tight_bbox import * # noqa: F401, F403 +from matplotlib import _api +_api.warn_deprecated("3.6", name=__name__, obj_type="module") diff --git a/lib/matplotlib/tight_layout.py b/lib/matplotlib/tight_layout.py index 81eb1e6adc87..233e96c0d47a 100644 --- a/lib/matplotlib/tight_layout.py +++ b/lib/matplotlib/tight_layout.py @@ -1,347 +1,13 @@ -""" -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, 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). -""" - -import numpy as np - -from matplotlib import _api, rcParams -from matplotlib.font_manager import FontProperties -from matplotlib.transforms import TransformedBbox, Bbox - - -def auto_adjust_subplotpars( - fig, renderer, nrows_ncols, num1num2_list, subplot_list, - ax_bbox_list=None, pad=1.08, h_pad=None, w_pad=None, rect=None): - """ - Return a dict of subplot parameters to adjust spacing between subplots - or ``None`` if resulting axes would have zero height or width. - - Note that this function ignores geometry information of subplot - itself, but uses what is given by the *nrows_ncols* and *num1num2_list* - parameters. Also, the results could be incorrect if some subplots have - ``adjustable=datalim``. - - Parameters - ---------- - nrows_ncols : tuple[int, int] - Number of rows and number of columns of the grid. - num1num2_list : list[int] - List of numbers specifying the area occupied by the subplot - subplot_list : list of subplots - List of subplots that will be used to calculate optimal subplot_params. - pad : float - Padding between the figure edge and the edges of subplots, as a - fraction of the font size. - h_pad, w_pad : float - Padding (height/width) between edges of adjacent subplots, as a - fraction of the font size. Defaults to *pad*. - rect : tuple[float, float, float, float] - [left, bottom, right, top] in normalized (0, 1) figure coordinates. - """ - rows, cols = nrows_ncols - - font_size_inches = ( - FontProperties(size=rcParams["font.size"]).get_size_in_points() / 72) - pad_inches = pad * font_size_inches - vpad_inches = h_pad * font_size_inches if h_pad is not None else pad_inches - hpad_inches = w_pad * font_size_inches if w_pad is not None else pad_inches - - if len(num1num2_list) != len(subplot_list) or len(subplot_list) == 0: - raise ValueError - - if rect is None: - margin_left = margin_bottom = margin_right = margin_top = None - else: - margin_left, margin_bottom, _right, _top = rect - margin_right = 1 - _right if _right else None - margin_top = 1 - _top if _top else None - - vspaces = np.zeros((rows + 1, cols)) - hspaces = np.zeros((rows, cols + 1)) - - if ax_bbox_list is None: - ax_bbox_list = [ - Bbox.union([ax.get_position(original=True) for ax in subplots]) - for subplots in subplot_list] - - for subplots, ax_bbox, (num1, num2) in zip(subplot_list, - ax_bbox_list, - num1num2_list): - if all(not ax.get_visible() for ax in subplots): - continue - - bb = [] - for ax in subplots: - if ax.get_visible(): - try: - bb += [ax.get_tightbbox(renderer, for_layout_only=True)] - except TypeError: - bb += [ax.get_tightbbox(renderer)] - - tight_bbox_raw = Bbox.union(bb) - tight_bbox = TransformedBbox(tight_bbox_raw, - fig.transFigure.inverted()) - - row1, col1 = divmod(num1, cols) - if num2 is None: - num2 = num1 - row2, col2 = divmod(num2, cols) - - for row_i in range(row1, row2 + 1): - hspaces[row_i, col1] += ax_bbox.xmin - tight_bbox.xmin # left - hspaces[row_i, col2 + 1] += tight_bbox.xmax - ax_bbox.xmax # right - for col_i in range(col1, col2 + 1): - vspaces[row1, col_i] += tight_bbox.ymax - ax_bbox.ymax # top - vspaces[row2 + 1, col_i] += ax_bbox.ymin - tight_bbox.ymin # bot. - - fig_width_inch, fig_height_inch = fig.get_size_inches() - - # margins can be negative for axes with aspect applied, so use max(, 0) to - # make them nonnegative. - if not margin_left: - margin_left = (max(hspaces[:, 0].max(), 0) - + pad_inches / fig_width_inch) - suplabel = fig._supylabel - if suplabel and suplabel.get_in_layout(): - rel_width = fig.transFigure.inverted().transform_bbox( - suplabel.get_window_extent(renderer)).width - margin_left += rel_width + pad_inches / fig_width_inch - - if not margin_right: - margin_right = (max(hspaces[:, -1].max(), 0) - + pad_inches / fig_width_inch) - if not margin_top: - margin_top = (max(vspaces[0, :].max(), 0) - + pad_inches / fig_height_inch) - if fig._suptitle and fig._suptitle.get_in_layout(): - rel_height = fig.transFigure.inverted().transform_bbox( - fig._suptitle.get_window_extent(renderer)).height - margin_top += rel_height + pad_inches / fig_height_inch - if not margin_bottom: - margin_bottom = (max(vspaces[-1, :].max(), 0) - + pad_inches / fig_height_inch) - suplabel = fig._supxlabel - if suplabel and suplabel.get_in_layout(): - rel_height = fig.transFigure.inverted().transform_bbox( - suplabel.get_window_extent(renderer)).height - margin_bottom += rel_height + pad_inches / fig_height_inch - - if margin_left + margin_right >= 1: - _api.warn_external('Tight layout not applied. The left and right ' - 'margins cannot be made large enough to ' - 'accommodate all axes decorations. ') - return None - if margin_bottom + margin_top >= 1: - _api.warn_external('Tight layout not applied. The bottom and top ' - 'margins cannot be made large enough to ' - 'accommodate all axes decorations. ') - return None - - kwargs = dict(left=margin_left, - right=1 - margin_right, - bottom=margin_bottom, - top=1 - margin_top) - - if cols > 1: - hspace = hspaces[:, 1:-1].max() + hpad_inches / fig_width_inch - # axes widths: - h_axes = (1 - margin_right - margin_left - hspace * (cols - 1)) / cols - if h_axes < 0: - _api.warn_external('Tight layout not applied. tight_layout ' - 'cannot make axes width small enough to ' - 'accommodate all axes decorations') - return None - else: - kwargs["wspace"] = hspace / h_axes - if rows > 1: - vspace = vspaces[1:-1, :].max() + vpad_inches / fig_height_inch - v_axes = (1 - margin_top - margin_bottom - vspace * (rows - 1)) / rows - if v_axes < 0: - _api.warn_external('Tight layout not applied. tight_layout ' - 'cannot make axes height small enough to ' - 'accommodate all axes decorations') - return None - else: - kwargs["hspace"] = vspace / v_axes - - return kwargs +from matplotlib._tight_layout import * # noqa: F401, F403 +from matplotlib import _api +_api.warn_deprecated("3.6", name=__name__, obj_type="module") +@_api.deprecated("3.6", alternative="figure.canvas.get_renderer()") def get_renderer(fig): - if fig._cachedRenderer: - return fig._cachedRenderer + canvas = fig.canvas + if canvas and hasattr(canvas, "get_renderer"): + return canvas.get_renderer() else: - canvas = fig.canvas - if canvas and hasattr(canvas, "get_renderer"): - return canvas.get_renderer() - else: - from . import backend_bases - return backend_bases._get_renderer(fig) - - -def get_subplotspec_list(axes_list, grid_spec=None): - """ - 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. - - If grid_spec is given, None is inserted for those not from the given - grid_spec. - """ - subplotspec_list = [] - for ax in axes_list: - axes_or_locator = ax.get_axes_locator() - if axes_or_locator is None: - axes_or_locator = ax - - if hasattr(axes_or_locator, "get_subplotspec"): - subplotspec = axes_or_locator.get_subplotspec() - subplotspec = subplotspec.get_topmost_subplotspec() - gs = subplotspec.get_gridspec() - if grid_spec is not None: - if gs != grid_spec: - subplotspec = None - elif gs.locally_modified_subplot_params(): - subplotspec = None - else: - subplotspec = None - - subplotspec_list.append(subplotspec) - - return subplotspec_list - - -def get_tight_layout_figure(fig, axes_list, subplotspec_list, renderer, - pad=1.08, h_pad=None, w_pad=None, rect=None): - """ - Return subplot parameters for tight-layouted-figure with specified padding. - - Parameters - ---------- - fig : Figure - axes_list : list of Axes - subplotspec_list : list of `.SubplotSpec` - The subplotspecs of each axes. - renderer : renderer - pad : float - Padding between the figure edge and the edges of subplots, as a - fraction of the font size. - h_pad, w_pad : float - Padding (height/width) between edges of adjacent subplots. Defaults to - *pad*. - rect : tuple[float, float, float, float], optional - (left, bottom, right, top) rectangle in normalized figure coordinates - that the whole subplots area (including labels) will fit into. - Defaults to using the entire figure. - - Returns - ------- - subplotspec or None - subplotspec kwargs to be passed to `.Figure.subplots_adjust` or - None if tight_layout could not be accomplished. - """ - - subplot_list = [] - nrows_list = [] - ncols_list = [] - ax_bbox_list = [] - - # Multiple axes can share same subplot_interface (e.g., axes_grid1); thus - # we need to join them together. - subplot_dict = {} - - subplotspec_list2 = [] - - for ax, subplotspec in zip(axes_list, subplotspec_list): - if subplotspec is None: - continue - - subplots = subplot_dict.setdefault(subplotspec, []) - - if not subplots: - myrows, mycols, _, _ = subplotspec.get_geometry() - nrows_list.append(myrows) - ncols_list.append(mycols) - subplotspec_list2.append(subplotspec) - subplot_list.append(subplots) - ax_bbox_list.append(subplotspec.get_position(fig)) - - subplots.append(ax) - - if len(nrows_list) == 0 or len(ncols_list) == 0: - return {} - - max_nrows = max(nrows_list) - max_ncols = max(ncols_list) - - num1num2_list = [] - for subplotspec in subplotspec_list2: - rows, cols, num1, num2 = subplotspec.get_geometry() - div_row, mod_row = divmod(max_nrows, rows) - div_col, mod_col = divmod(max_ncols, cols) - if mod_row != 0: - _api.warn_external('tight_layout not applied: number of rows ' - 'in subplot specifications must be ' - 'multiples of one another.') - return {} - if mod_col != 0: - _api.warn_external('tight_layout not applied: number of ' - 'columns in subplot specifications must be ' - 'multiples of one another.') - return {} - - rowNum1, colNum1 = divmod(num1, cols) - if num2 is None: - rowNum2, colNum2 = rowNum1, colNum1 - else: - rowNum2, colNum2 = divmod(num2, cols) - - num1num2_list.append((rowNum1 * div_row * max_ncols + - colNum1 * div_col, - ((rowNum2 + 1) * div_row - 1) * max_ncols + - (colNum2 + 1) * div_col - 1)) - - kwargs = auto_adjust_subplotpars(fig, renderer, - nrows_ncols=(max_nrows, max_ncols), - num1num2_list=num1num2_list, - subplot_list=subplot_list, - ax_bbox_list=ax_bbox_list, - pad=pad, h_pad=h_pad, w_pad=w_pad) - - # kwargs can be none if tight_layout fails... - if rect is not None and kwargs is not None: - # if rect is given, the whole subplots area (including - # labels) will fit into the rect instead of the - # figure. Note that the rect argument of - # *auto_adjust_subplotpars* specify the area that will be - # covered by the total area of axes.bbox. Thus we call - # auto_adjust_subplotpars twice, where the second run - # with adjusted rect parameters. - - left, bottom, right, top = rect - if left is not None: - left += kwargs["left"] - if bottom is not None: - bottom += kwargs["bottom"] - if right is not None: - right -= (1 - kwargs["right"]) - if top is not None: - top -= (1 - kwargs["top"]) - - kwargs = auto_adjust_subplotpars(fig, renderer, - nrows_ncols=(max_nrows, max_ncols), - num1num2_list=num1num2_list, - subplot_list=subplot_list, - ax_bbox_list=ax_bbox_list, - pad=pad, h_pad=h_pad, w_pad=w_pad, - rect=(left, bottom, right, top)) - - return kwargs + from . import backend_bases + return backend_bases._get_renderer(fig) diff --git a/lib/matplotlib/transforms.py b/lib/matplotlib/transforms.py index 9a50e4bf0048..c5c051be570f 100644 --- a/lib/matplotlib/transforms.py +++ b/lib/matplotlib/transforms.py @@ -17,7 +17,7 @@ .. image:: ../_static/transforms.png The framework can be used for both affine and non-affine -transformations. However, for speed, we want use the backend +transformations. However, for speed, we want to use the backend renderers to perform affine transformations whenever possible. Therefore, it is possible to perform just the affine or non-affine part of a transformation on a set of data. The affine is always @@ -27,6 +27,9 @@ The backends are not expected to handle non-affine transformations themselves. + +See the tutorial :doc:`/tutorials/advanced/transforms_tutorial` for examples +of how to use transforms. """ # Note: There are a number of places in the code where we use `np.min` or @@ -151,22 +154,6 @@ def __copy__(self): other.set_children(val) # val == getattr(other, key) return other - def __deepcopy__(self, memo): - # We could deepcopy the entire transform tree, but nothing except - # `self` is accessible publicly, so we may as well just freeze `self`. - other = self.frozen() - if other is not self: - return other - # Some classes implement frozen() as returning self, which is not - # acceptable for deepcopying, so we need to handle them separately. - other = copy.deepcopy(super(), memo) - # If `c = a + b; a1 = copy(a)`, then modifications to `a1` do not - # propagate back to `c`, i.e. we need to clear the parents of `a1`. - other._parents = {} - # If `c = a + b; c1 = copy(c)`, this creates a separate tree - # (`c1 = a1 + b1`) so nothing needs to be done. - return other - def invalidate(self): """ Invalidate this `TransformNode` and triggers an invalidation of its @@ -493,14 +480,6 @@ def transformed(self, transform): [pts[0], [pts[0, 0], pts[1, 1]], [pts[1, 0], pts[0, 1]]])) return Bbox([ll, [lr[0], ul[1]]]) - @_api.deprecated("3.3", alternative="transformed(transform.inverted())") - def inverse_transformed(self, transform): - """ - Construct a `Bbox` by statically transforming this one by the inverse - of *transform*. - """ - return self.transformed(transform.inverted()) - coefs = {'C': (0.5, 0.5), 'SW': (0, 0), 'S': (0.5, 0), @@ -513,26 +492,21 @@ def inverse_transformed(self, transform): def anchored(self, c, container=None): """ - Return a copy of the `Bbox` shifted to position *c* within *container*. + Return a copy of the `Bbox` anchored to *c* within *container*. Parameters ---------- - c : (float, float) or str - May be either: - - * A sequence (*cx*, *cy*) where *cx* and *cy* range from 0 - to 1, where 0 is left or bottom and 1 is right or top - - * a string: - - 'C' for centered - - 'S' for bottom-center - - 'SE' for bottom-left - - 'E' for left - - etc. - + c : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', ...} + Either an (*x*, *y*) pair of relative coordinates (0 is left or + bottom, 1 is right or top), 'C' (center), or a cardinal direction + ('SW', southwest, is bottom left, etc.). container : `Bbox`, optional The box within which the `Bbox` is positioned; it defaults to the initial `Bbox`. + + See Also + -------- + .Axes.set_anchor """ if container is None: container = self @@ -781,7 +755,7 @@ def __init__(self, points, **kwargs): """ Parameters ---------- - points : ndarray + points : `~numpy.ndarray` A 2x2 numpy array of the form ``[[x0, y0], [x1, y1]]``. """ super().__init__(**kwargs) @@ -807,6 +781,12 @@ def invalidate(self): self._check(self._points) super().invalidate() + def frozen(self): + # docstring inherited + frozen_bbox = super().frozen() + frozen_bbox._minpos = self.minpos.copy() + return frozen_bbox + @staticmethod def unit(): """Create a new unit `Bbox` from (0, 0) to (1, 1).""" @@ -909,15 +889,55 @@ def update_from_path(self, path, ignore=None, updatex=True, updatey=True): self._points[:, 1] = points[:, 1] self._minpos[1] = minpos[1] + def update_from_data_x(self, x, ignore=None): + """ + Update the x-bounds of the `Bbox` based on the passed in data. After + updating, the bounds will have positive *width*, and *x0* will be the + minimal value. + + Parameters + ---------- + x : `~numpy.ndarray` + Array of x-values. + + ignore : bool, optional + - When ``True``, ignore the existing bounds of the `Bbox`. + - When ``False``, include the existing bounds of the `Bbox`. + - When ``None``, use the last value passed to :meth:`ignore`. + """ + x = np.ravel(x) + self.update_from_data_xy(np.column_stack([x, np.ones(x.size)]), + ignore=ignore, updatey=False) + + def update_from_data_y(self, y, ignore=None): + """ + Update the y-bounds of the `Bbox` based on the passed in data. After + updating, the bounds will have positive *height*, and *y0* will be the + minimal value. + + Parameters + ---------- + y : `~numpy.ndarray` + Array of y-values. + + ignore : bool, optional + - When ``True``, ignore the existing bounds of the `Bbox`. + - When ``False``, include the existing bounds of the `Bbox`. + - When ``None``, use the last value passed to :meth:`ignore`. + """ + y = np.ravel(y) + self.update_from_data_xy(np.column_stack([np.ones(y.size), y]), + ignore=ignore, updatex=False) + def update_from_data_xy(self, xy, ignore=None, updatex=True, updatey=True): """ - Update the bounds of the `Bbox` based on the passed in - data. After updating, the bounds will have positive *width* - and *height*; *x0* and *y0* will be the minimal values. + Update the bounds of the `Bbox` based on the passed in data. After + updating, the bounds will have positive *width* and *height*; + *x0* and *y0* will be the minimal values. Parameters ---------- - xy : ndarray + xy : `~numpy.ndarray` A numpy array of 2D points. ignore : bool, optional @@ -1378,7 +1398,7 @@ def contains_branch_seperately(self, other_transform): each separate dimension. A common use for this method is to identify if a transform is a blended - transform containing an axes' data transform. e.g.:: + transform containing an Axes' data transform. e.g.:: x_isdata, y_isdata = trans.contains_branch_seperately(ax.transData) @@ -1457,7 +1477,7 @@ def transform(self, values): Returns ------- array - The output values as NumPy array of length :attr:`input_dims` or + The output values as NumPy array of length :attr:`output_dims` or shape (N x :attr:`output_dims`), depending on the input. """ # Ensure that values is a 2d array (but remember whether @@ -1502,7 +1522,7 @@ def transform_affine(self, values): Returns ------- array - The output values as NumPy array of length :attr:`input_dims` or + The output values as NumPy array of length :attr:`output_dims` or shape (N x :attr:`output_dims`), depending on the input. """ return self.get_affine().transform(values) @@ -1527,7 +1547,7 @@ def transform_non_affine(self, values): Returns ------- array - The output values as NumPy array of length :attr:`input_dims` or + The output values as NumPy array of length :attr:`output_dims` or shape (N x :attr:`output_dims`), depending on the input. """ return values @@ -1623,11 +1643,10 @@ def transform_angles(self, angles, pts, radians=False, pushoff=1e-5): raise NotImplementedError('Only defined in 2D') angles = np.asarray(angles) pts = np.asarray(pts) - if angles.ndim != 1 or angles.shape[0] != pts.shape[0]: - raise ValueError("'angles' must be a column vector and have same " - "number of rows as 'pts'") - if pts.shape[1] != 2: - raise ValueError("'pts' must be array with 2 columns for x, y") + _api.check_shape((None, 2), pts=pts) + _api.check_shape((None,), angles=angles) + if len(angles) != len(pts): + raise ValueError("There must be as many 'angles' as 'pts'") # Convert to radians if desired if not radians: angles = np.deg2rad(angles) @@ -1680,15 +1699,8 @@ def __init__(self, child): be replaced with :meth:`set`. """ _api.check_isinstance(Transform, child=child) - self._init(child) - self.set_children(child) - - def _init(self, child): - Transform.__init__(self) - self.input_dims = child.input_dims - self.output_dims = child.output_dims - self._set(child) - self._invalid = 0 + super().__init__() + self.set(child) def __eq__(self, other): return self._child.__eq__(other) @@ -1699,8 +1711,25 @@ def frozen(self): # docstring inherited return self._child.frozen() - def _set(self, child): + def set(self, child): + """ + Replace the current child of this transform with another one. + + The new child must have the same number of input and output + dimensions as the current child. + """ + if hasattr(self, "_child"): # Absent during init. + self.invalidate() + new_dims = (child.input_dims, child.output_dims) + old_dims = (self._child.input_dims, self._child.output_dims) + if new_dims != old_dims: + raise ValueError( + f"The input and output dims of the new child {new_dims} " + f"do not match those of current child {old_dims}") + self._child._parents.pop(id(self), None) + self._child = child + self.set_children(child) self.transform = child.transform self.transform_affine = child.transform_affine @@ -1711,31 +1740,16 @@ def _set(self, child): self.get_affine = child.get_affine self.inverted = child.inverted self.get_matrix = child.get_matrix - # note we do not wrap other properties here since the transform's # child can be changed with WrappedTransform.set and so checking # is_affine and other such properties may be dangerous. - def set(self, child): - """ - Replace the current child of this transform with another one. - - The new child must have the same number of input and output - dimensions as the current child. - """ - if (child.input_dims != self.input_dims or - child.output_dims != self.output_dims): - raise ValueError( - "The new child must have the same number of input and output " - "dimensions as the current child") - - self.set_children(child) - self._set(child) - self._invalid = 0 self.invalidate() self._invalid = 0 + input_dims = property(lambda self: self._child.input_dims) + output_dims = property(lambda self: self._child.output_dims) is_affine = property(lambda self: self._child.is_affine) is_separable = property(lambda self: self._child.is_separable) has_inverse = property(lambda self: self._child.has_inverse) @@ -1841,7 +1855,7 @@ def transform_affine(self, points): # The major speed trap here is just converting to the # points to an array in the first place. If we can use # more arrays upstream, that should help here. - if not isinstance(points, (np.ma.MaskedArray, np.ndarray)): + if not isinstance(points, np.ndarray): _api.warn_external( f'A non-numpy array of type {type(points)} was passed in ' f'for transformation, which results in poor performance.') @@ -1877,11 +1891,18 @@ def __init__(self, matrix=None, **kwargs): super().__init__(**kwargs) if matrix is None: # A bit faster than np.identity(3). - matrix = IdentityTransform._mtx.copy() + matrix = IdentityTransform._mtx self._mtx = matrix.copy() self._invalid = 0 - __str__ = _make_str_method("_mtx") + _base_str = _make_str_method("_mtx") + + def __str__(self): + return (self._base_str() + if (self._mtx != np.diag(np.diag(self._mtx))).any() + else f"Affine2D().scale({self._mtx[0, 0]}, {self._mtx[1, 1]})" + if self._mtx[0, 0] != self._mtx[1, 1] + else f"Affine2D().scale({self._mtx[0, 0]})") @staticmethod def from_values(a, b, c, d, e, f): @@ -1935,6 +1956,7 @@ def set(self, other): self.invalidate() @staticmethod + @_api.deprecated("3.6", alternative="Affine2D()") def identity(): """ Return a new `Affine2D` object that is the identity transform. @@ -1963,9 +1985,16 @@ def rotate(self, theta): """ a = math.cos(theta) b = math.sin(theta) - rotate_mtx = np.array([[a, -b, 0.0], [b, a, 0.0], [0.0, 0.0, 1.0]], - float) - self._mtx = np.dot(rotate_mtx, self._mtx) + mtx = self._mtx + # Operating and assigning one scalar at a time is much faster. + (xx, xy, x0), (yx, yy, y0), _ = mtx.tolist() + # mtx = [[a -b 0], [b a 0], [0 0 1]] * mtx + mtx[0, 0] = a * xx - b * yx + mtx[0, 1] = a * xy - b * yy + mtx[0, 2] = a * x0 - b * y0 + mtx[1, 0] = b * xx + a * yx + mtx[1, 1] = b * xy + a * yy + mtx[1, 2] = b * x0 + a * y0 self.invalidate() return self @@ -2048,11 +2077,18 @@ def skew(self, xShear, yShear): calls to :meth:`rotate`, :meth:`rotate_deg`, :meth:`translate` and :meth:`scale`. """ - rotX = math.tan(xShear) - rotY = math.tan(yShear) - skew_mtx = np.array( - [[1.0, rotX, 0.0], [rotY, 1.0, 0.0], [0.0, 0.0, 1.0]], float) - self._mtx = np.dot(skew_mtx, self._mtx) + rx = math.tan(xShear) + ry = math.tan(yShear) + mtx = self._mtx + # Operating and assigning one scalar at a time is much faster. + (xx, xy, x0), (yx, yy, y0), _ = mtx.tolist() + # mtx = [[1 rx 0], [ry 1 0], [0 0 1]] * mtx + mtx[0, 0] += rx * yx + mtx[0, 1] += rx * yy + mtx[0, 2] += rx * y0 + mtx[1, 0] += ry * xx + mtx[1, 1] += ry * xy + mtx[1, 2] += ry * x0 self.invalidate() return self @@ -2385,8 +2421,7 @@ def transform_non_affine(self, points): elif not self._a.is_affine and self._b.is_affine: return self._a.transform_non_affine(points) else: - return self._b.transform_non_affine( - self._a.transform(points)) + return self._b.transform_non_affine(self._a.transform(points)) def transform_path_non_affine(self, path): # docstring inherited @@ -2763,37 +2798,25 @@ class TransformedPatchPath(TransformedPath): `~.patches.Patch`. This cached copy is automatically updated when the non-affine part of the transform or the patch changes. """ + def __init__(self, patch): """ Parameters ---------- patch : `~.patches.Patch` """ - TransformNode.__init__(self) - - transform = patch.get_transform() + # Defer to TransformedPath.__init__. + super().__init__(patch.get_path(), patch.get_transform()) self._patch = patch - self._transform = transform - self.set_children(transform) - self._path = patch.get_path() - self._transformed_path = None - self._transformed_points = None def _revalidate(self): patch_path = self._patch.get_path() - # Only recompute if the invalidation includes the non_affine part of - # the transform, or the Patch's Path has changed. - if (self._transformed_path is None or self._path != patch_path or - (self._invalid & self.INVALID_NON_AFFINE == - self.INVALID_NON_AFFINE)): + # Force invalidation if the patch path changed; otherwise, let base + # class check invalidation. + if patch_path != self._path: self._path = patch_path - self._transformed_path = \ - self._transform.transform_path_non_affine(patch_path) - self._transformed_points = \ - Path._fast_from_codes_and_verts( - self._transform.transform_non_affine(patch_path.vertices), - None, patch_path) - self._invalid = 0 + self._transformed_path = None + super()._revalidate() def nonsingular(vmin, vmax, expander=0.001, tiny=1e-15, increasing=True): diff --git a/lib/matplotlib/tri/__init__.py b/lib/matplotlib/tri/__init__.py index 7ff2b326920b..e000831d8a08 100644 --- a/lib/matplotlib/tri/__init__.py +++ b/lib/matplotlib/tri/__init__.py @@ -2,11 +2,22 @@ Unstructured triangular grid functions. """ -from .triangulation import * -from .tricontour import * -from .tritools import * -from .trifinder import * -from .triinterpolate import * -from .trirefine import * -from .tripcolor import * -from .triplot import * +from ._triangulation import Triangulation +from ._tricontour import TriContourSet, tricontour, tricontourf +from ._trifinder import TriFinder, TrapezoidMapTriFinder +from ._triinterpolate import (TriInterpolator, LinearTriInterpolator, + CubicTriInterpolator) +from ._tripcolor import tripcolor +from ._triplot import triplot +from ._trirefine import TriRefiner, UniformTriRefiner +from ._tritools import TriAnalyzer + + +__all__ = ["Triangulation", + "TriContourSet", "tricontour", "tricontourf", + "TriFinder", "TrapezoidMapTriFinder", + "TriInterpolator", "LinearTriInterpolator", "CubicTriInterpolator", + "tripcolor", + "triplot", + "TriRefiner", "UniformTriRefiner", + "TriAnalyzer"] diff --git a/lib/matplotlib/tri/_triangulation.py b/lib/matplotlib/tri/_triangulation.py new file mode 100644 index 000000000000..fa03a9c030f7 --- /dev/null +++ b/lib/matplotlib/tri/_triangulation.py @@ -0,0 +1,245 @@ +import numpy as np + +from matplotlib import _api + + +class Triangulation: + """ + An unstructured triangular grid consisting of npoints points and + ntri triangles. The triangles can either be specified by the user + or automatically generated using a Delaunay triangulation. + + Parameters + ---------- + x, y : (npoints,) array-like + Coordinates of grid points. + triangles : (ntri, 3) array-like of int, optional + For each triangle, the indices of the three points that make + up the triangle, ordered in an anticlockwise manner. If not + specified, the Delaunay triangulation is calculated. + mask : (ntri,) array-like of bool, optional + Which triangles are masked out. + + Attributes + ---------- + triangles : (ntri, 3) array of int + For each triangle, the indices of the three points that make + up the triangle, ordered in an anticlockwise manner. If you want to + take the *mask* into account, use `get_masked_triangles` instead. + mask : (ntri, 3) array of bool + Masked out triangles. + is_delaunay : bool + Whether the Triangulation is a calculated Delaunay + triangulation (where *triangles* was not specified) or not. + + Notes + ----- + For a Triangulation to be valid it must not have duplicate points, + triangles formed from colinear points, or overlapping triangles. + """ + def __init__(self, x, y, triangles=None, mask=None): + from matplotlib import _qhull + + self.x = np.asarray(x, dtype=np.float64) + self.y = np.asarray(y, dtype=np.float64) + if self.x.shape != self.y.shape or self.x.ndim != 1: + raise ValueError("x and y must be equal-length 1D arrays, but " + f"found shapes {self.x.shape!r} and " + f"{self.y.shape!r}") + + self.mask = None + self._edges = None + self._neighbors = None + self.is_delaunay = False + + if triangles is None: + # No triangulation specified, so use matplotlib._qhull to obtain + # Delaunay triangulation. + self.triangles, self._neighbors = _qhull.delaunay(x, y) + self.is_delaunay = True + else: + # Triangulation specified. Copy, since we may correct triangle + # orientation. + try: + self.triangles = np.array(triangles, dtype=np.int32, order='C') + except ValueError as e: + raise ValueError('triangles must be a (N, 3) int array, not ' + f'{triangles!r}') from e + if self.triangles.ndim != 2 or self.triangles.shape[1] != 3: + raise ValueError( + 'triangles must be a (N, 3) int array, but found shape ' + f'{self.triangles.shape!r}') + if self.triangles.max() >= len(self.x): + raise ValueError( + 'triangles are indices into the points and must be in the ' + f'range 0 <= i < {len(self.x)} but found value ' + f'{self.triangles.max()}') + if self.triangles.min() < 0: + raise ValueError( + 'triangles are indices into the points and must be in the ' + f'range 0 <= i < {len(self.x)} but found value ' + f'{self.triangles.min()}') + + # Underlying C++ object is not created until first needed. + self._cpp_triangulation = None + + # Default TriFinder not created until needed. + self._trifinder = None + + self.set_mask(mask) + + def calculate_plane_coefficients(self, z): + """ + Calculate plane equation coefficients for all unmasked triangles from + the point (x, y) coordinates and specified z-array of shape (npoints). + The returned array has shape (npoints, 3) and allows z-value at (x, y) + position in triangle tri to be calculated using + ``z = array[tri, 0] * x + array[tri, 1] * y + array[tri, 2]``. + """ + return self.get_cpp_triangulation().calculate_plane_coefficients(z) + + @property + def edges(self): + """ + Return integer array of shape (nedges, 2) containing all edges of + non-masked triangles. + + Each row defines an edge by its start point index and end point + index. Each edge appears only once, i.e. for an edge between points + *i* and *j*, there will only be either *(i, j)* or *(j, i)*. + """ + if self._edges is None: + self._edges = self.get_cpp_triangulation().get_edges() + return self._edges + + def get_cpp_triangulation(self): + """ + Return the underlying C++ Triangulation object, creating it + if necessary. + """ + from matplotlib import _tri + if self._cpp_triangulation is None: + self._cpp_triangulation = _tri.Triangulation( + # For unset arrays use empty tuple which has size of zero. + self.x, self.y, self.triangles, + self.mask if self.mask is not None else (), + self._edges if self._edges is not None else (), + self._neighbors if self._neighbors is not None else (), + not self.is_delaunay) + return self._cpp_triangulation + + def get_masked_triangles(self): + """ + Return an array of triangles taking the mask into account. + """ + if self.mask is not None: + return self.triangles[~self.mask] + else: + return self.triangles + + @staticmethod + def get_from_args_and_kwargs(*args, **kwargs): + """ + Return a Triangulation object from the args and kwargs, and + the remaining args and kwargs with the consumed values removed. + + There are two alternatives: either the first argument is a + Triangulation object, in which case it is returned, or the args + and kwargs are sufficient to create a new Triangulation to + return. In the latter case, see Triangulation.__init__ for + the possible args and kwargs. + """ + if isinstance(args[0], Triangulation): + triangulation, *args = args + if 'triangles' in kwargs: + _api.warn_external( + "Passing the keyword 'triangles' has no effect when also " + "passing a Triangulation") + if 'mask' in kwargs: + _api.warn_external( + "Passing the keyword 'mask' has no effect when also " + "passing a Triangulation") + else: + x, y, triangles, mask, args, kwargs = \ + Triangulation._extract_triangulation_params(args, kwargs) + triangulation = Triangulation(x, y, triangles, mask) + return triangulation, args, kwargs + + @staticmethod + def _extract_triangulation_params(args, kwargs): + x, y, *args = args + # Check triangles in kwargs then args. + triangles = kwargs.pop('triangles', None) + from_args = False + if triangles is None and args: + triangles = args[0] + from_args = True + if triangles is not None: + try: + triangles = np.asarray(triangles, dtype=np.int32) + except ValueError: + triangles = None + if triangles is not None and (triangles.ndim != 2 or + triangles.shape[1] != 3): + triangles = None + if triangles is not None and from_args: + args = args[1:] # Consumed first item in args. + # Check for mask in kwargs. + mask = kwargs.pop('mask', None) + return x, y, triangles, mask, args, kwargs + + def get_trifinder(self): + """ + Return the default `matplotlib.tri.TriFinder` of this + triangulation, creating it if necessary. This allows the same + TriFinder object to be easily shared. + """ + if self._trifinder is None: + # Default TriFinder class. + from matplotlib.tri._trifinder import TrapezoidMapTriFinder + self._trifinder = TrapezoidMapTriFinder(self) + return self._trifinder + + @property + def neighbors(self): + """ + Return integer array of shape (ntri, 3) containing neighbor triangles. + + For each triangle, the indices of the three triangles that + share the same edges, or -1 if there is no such neighboring + triangle. ``neighbors[i, j]`` is the triangle that is the neighbor + to the edge from point index ``triangles[i, j]`` to point index + ``triangles[i, (j+1)%3]``. + """ + if self._neighbors is None: + self._neighbors = self.get_cpp_triangulation().get_neighbors() + return self._neighbors + + def set_mask(self, mask): + """ + Set or clear the mask array. + + Parameters + ---------- + mask : None or bool array of length ntri + """ + if mask is None: + self.mask = None + else: + self.mask = np.asarray(mask, dtype=bool) + if self.mask.shape != (self.triangles.shape[0],): + raise ValueError('mask array must have same length as ' + 'triangles array') + + # Set mask in C++ Triangulation. + if self._cpp_triangulation is not None: + self._cpp_triangulation.set_mask( + self.mask if self.mask is not None else ()) + + # Clear derived fields so they are recalculated when needed. + self._edges = None + self._neighbors = None + + # Recalculate TriFinder if it exists. + if self._trifinder is not None: + self._trifinder._initialize() diff --git a/lib/matplotlib/tri/_tricontour.py b/lib/matplotlib/tri/_tricontour.py new file mode 100644 index 000000000000..5b6d745372f8 --- /dev/null +++ b/lib/matplotlib/tri/_tricontour.py @@ -0,0 +1,272 @@ +import numpy as np + +from matplotlib import _docstring +from matplotlib.contour import ContourSet +from matplotlib.tri._triangulation import Triangulation + + +@_docstring.dedent_interpd +class TriContourSet(ContourSet): + """ + Create and store a set of contour lines or filled regions for + a triangular grid. + + This class is typically not instantiated directly by the user but by + `~.Axes.tricontour` and `~.Axes.tricontourf`. + + %(contour_set_attributes)s + """ + def __init__(self, ax, *args, **kwargs): + """ + Draw triangular grid contour lines or filled regions, + depending on whether keyword arg *filled* is False + (default) or True. + + The first argument of the initializer must be an `~.axes.Axes` + object. The remaining arguments and keyword arguments + are described in the docstring of `~.Axes.tricontour`. + """ + super().__init__(ax, *args, **kwargs) + + def _process_args(self, *args, **kwargs): + """ + Process args and kwargs. + """ + if isinstance(args[0], TriContourSet): + C = args[0]._contour_generator + if self.levels is None: + self.levels = args[0].levels + self.zmin = args[0].zmin + self.zmax = args[0].zmax + self._mins = args[0]._mins + self._maxs = args[0]._maxs + else: + from matplotlib import _tri + tri, z = self._contour_args(args, kwargs) + C = _tri.TriContourGenerator(tri.get_cpp_triangulation(), z) + self._mins = [tri.x.min(), tri.y.min()] + self._maxs = [tri.x.max(), tri.y.max()] + + self._contour_generator = C + return kwargs + + def _contour_args(self, args, kwargs): + tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, + **kwargs) + z, *args = args + z = np.ma.asarray(z) + if z.shape != tri.x.shape: + raise ValueError('z array must have same length as triangulation x' + ' and y arrays') + + # z values must be finite, only need to check points that are included + # in the triangulation. + z_check = z[np.unique(tri.get_masked_triangles())] + if np.ma.is_masked(z_check): + raise ValueError('z must not contain masked points within the ' + 'triangulation') + if not np.isfinite(z_check).all(): + raise ValueError('z array must not contain non-finite values ' + 'within the triangulation') + + z = np.ma.masked_invalid(z, copy=False) + self.zmax = float(z_check.max()) + self.zmin = float(z_check.min()) + if self.logscale and self.zmin <= 0: + func = 'contourf' if self.filled else 'contour' + raise ValueError(f'Cannot {func} log of negative values.') + self._process_contour_level_args(args, z.dtype) + return (tri, z) + + +_docstring.interpd.update(_tricontour_doc=""" +Draw contour %%(type)s on an unstructured triangular grid. + +Call signatures:: + + %%(func)s(triangulation, z, [levels], ...) + %%(func)s(x, y, z, [levels], *, [triangles=triangles], [mask=mask], ...) + +The triangular grid can be specified either by passing a `.Triangulation` +object as the first parameter, or by passing the points *x*, *y* and +optionally the *triangles* and a *mask*. See `.Triangulation` for an +explanation of these parameters. If neither of *triangulation* or +*triangles* are given, the triangulation is calculated on the fly. + +It is possible to pass *triangles* positionally, i.e. +``%%(func)s(x, y, triangles, z, ...)``. However, this is discouraged. For more +clarity, pass *triangles* via keyword argument. + +Parameters +---------- +triangulation : `.Triangulation`, optional + An already created triangular grid. + +x, y, triangles, mask + Parameters defining the triangular grid. See `.Triangulation`. + This is mutually exclusive with specifying *triangulation*. + +z : array-like + The height values over which the contour is drawn. Color-mapping is + controlled by *cmap*, *norm*, *vmin*, and *vmax*. + + .. note:: + All values in *z* must be finite. Hence, nan and inf values must + either be removed or `~.Triangulation.set_mask` be used. + +levels : int or array-like, optional + Determines the number and positions of the contour lines / regions. + + If an int *n*, use `~matplotlib.ticker.MaxNLocator`, which tries to + automatically choose no more than *n+1* "nice" contour levels between + between minimum and maximum numeric values of *Z*. + + If array-like, draw contour lines at the specified levels. The values must + be in increasing order. + +Returns +------- +`~matplotlib.tri.TriContourSet` + +Other Parameters +---------------- +colors : color string or sequence of colors, optional + The colors of the levels, i.e., the contour %%(type)s. + + The sequence is cycled for the levels in ascending order. If the sequence + is shorter than the number of levels, it is repeated. + + As a shortcut, single color strings may be used in place of one-element + lists, i.e. ``'red'`` instead of ``['red']`` to color all levels with the + same color. This shortcut does only work for color strings, not for other + ways of specifying colors. + + By default (value *None*), the colormap specified by *cmap* will be used. + +alpha : float, default: 1 + The alpha blending value, between 0 (transparent) and 1 (opaque). + +%(cmap_doc)s + + This parameter is ignored if *colors* is set. + +%(norm_doc)s + + This parameter is ignored if *colors* is set. + +%(vmin_vmax_doc)s + + If *vmin* or *vmax* are not given, the default color scaling is based on + *levels*. + + This parameter is ignored if *colors* is set. + +origin : {*None*, 'upper', 'lower', 'image'}, default: None + Determines the orientation and exact position of *z* by specifying the + position of ``z[0, 0]``. This is only relevant, if *X*, *Y* are not given. + + - *None*: ``z[0, 0]`` is at X=0, Y=0 in the lower left corner. + - 'lower': ``z[0, 0]`` is at X=0.5, Y=0.5 in the lower left corner. + - 'upper': ``z[0, 0]`` is at X=N+0.5, Y=0.5 in the upper left corner. + - 'image': Use the value from :rc:`image.origin`. + +extent : (x0, x1, y0, y1), optional + If *origin* is not *None*, then *extent* is interpreted as in `.imshow`: it + gives the outer pixel boundaries. In this case, the position of z[0, 0] is + the center of the pixel, not a corner. If *origin* is *None*, then + (*x0*, *y0*) is the position of z[0, 0], and (*x1*, *y1*) is the position + of z[-1, -1]. + + This argument is ignored if *X* and *Y* are specified in the call to + contour. + +locator : ticker.Locator subclass, optional + The locator is used to determine the contour levels if they are not given + explicitly via *levels*. + Defaults to `~.ticker.MaxNLocator`. + +extend : {'neither', 'both', 'min', 'max'}, default: 'neither' + Determines the ``%%(func)s``-coloring of values that are outside the + *levels* range. + + If 'neither', values outside the *levels* range are not colored. If 'min', + 'max' or 'both', color the values below, above or below and above the + *levels* range. + + Values below ``min(levels)`` and above ``max(levels)`` are mapped to the + under/over values of the `.Colormap`. Note that most colormaps do not have + dedicated colors for these by default, so that the over and under values + are the edge values of the colormap. You may want to set these values + explicitly using `.Colormap.set_under` and `.Colormap.set_over`. + + .. note:: + + An existing `.TriContourSet` does not get notified if properties of its + colormap are changed. Therefore, an explicit call to + `.ContourSet.changed()` is needed after modifying the colormap. The + explicit call can be left out, if a colorbar is assigned to the + `.TriContourSet` because it internally calls `.ContourSet.changed()`. + +xunits, yunits : registered units, optional + Override axis units by specifying an instance of a + :class:`matplotlib.units.ConversionInterface`. + +antialiased : bool, optional + Enable antialiasing, overriding the defaults. For + filled contours, the default is *True*. For line contours, + it is taken from :rc:`lines.antialiased`.""" % _docstring.interpd.params) + + +@_docstring.Substitution(func='tricontour', type='lines') +@_docstring.dedent_interpd +def tricontour(ax, *args, **kwargs): + """ + %(_tricontour_doc)s + + linewidths : float or array-like, default: :rc:`contour.linewidth` + The line width of the contour lines. + + If a number, all levels will be plotted with this linewidth. + + If a sequence, the levels in ascending order will be plotted with + the linewidths in the order specified. + + If None, this falls back to :rc:`lines.linewidth`. + + linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, optional + If *linestyles* is *None*, the default is 'solid' unless the lines are + monochrome. In that case, negative contours will take their linestyle + from :rc:`contour.negative_linestyle` setting. + + *linestyles* can also be an iterable of the above strings specifying a + set of linestyles to be used. If this iterable is shorter than the + number of contour levels it will be repeated as necessary. + """ + kwargs['filled'] = False + return TriContourSet(ax, *args, **kwargs) + + +@_docstring.Substitution(func='tricontourf', type='regions') +@_docstring.dedent_interpd +def tricontourf(ax, *args, **kwargs): + """ + %(_tricontour_doc)s + + hatches : list[str], optional + A list of crosshatch patterns to use on the filled areas. + If None, no hatching will be added to the contour. + Hatching is supported in the PostScript, PDF, SVG and Agg + backends only. + + Notes + ----- + `.tricontourf` fills intervals that are closed at the top; that is, for + boundaries *z1* and *z2*, the filled region is:: + + z1 < Z <= z2 + + except for the lowest interval, which is closed on both sides (i.e. it + includes the lowest value). + """ + kwargs['filled'] = True + return TriContourSet(ax, *args, **kwargs) diff --git a/lib/matplotlib/tri/_trifinder.py b/lib/matplotlib/tri/_trifinder.py new file mode 100644 index 000000000000..e06b84c0d974 --- /dev/null +++ b/lib/matplotlib/tri/_trifinder.py @@ -0,0 +1,93 @@ +import numpy as np + +from matplotlib import _api +from matplotlib.tri import Triangulation + + +class TriFinder: + """ + Abstract base class for classes used to find the triangles of a + Triangulation in which (x, y) points lie. + + Rather than instantiate an object of a class derived from TriFinder, it is + usually better to use the function `.Triangulation.get_trifinder`. + + Derived classes implement __call__(x, y) where x and y are array-like point + coordinates of the same shape. + """ + + def __init__(self, triangulation): + _api.check_isinstance(Triangulation, triangulation=triangulation) + self._triangulation = triangulation + + +class TrapezoidMapTriFinder(TriFinder): + """ + `~matplotlib.tri.TriFinder` class implemented using the trapezoid + map algorithm from the book "Computational Geometry, Algorithms and + Applications", second edition, by M. de Berg, M. van Kreveld, M. Overmars + and O. Schwarzkopf. + + The triangulation must be valid, i.e. it must not have duplicate points, + triangles formed from colinear points, or overlapping triangles. The + algorithm has some tolerance to triangles formed from colinear points, but + this should not be relied upon. + """ + + def __init__(self, triangulation): + from matplotlib import _tri + super().__init__(triangulation) + self._cpp_trifinder = _tri.TrapezoidMapTriFinder( + triangulation.get_cpp_triangulation()) + self._initialize() + + def __call__(self, x, y): + """ + Return an array containing the indices of the triangles in which the + specified *x*, *y* points lie, or -1 for points that do not lie within + a triangle. + + *x*, *y* are array-like x and y coordinates of the same shape and any + number of dimensions. + + Returns integer array with the same shape and *x* and *y*. + """ + x = np.asarray(x, dtype=np.float64) + y = np.asarray(y, dtype=np.float64) + if x.shape != y.shape: + raise ValueError("x and y must be array-like with the same shape") + + # C++ does the heavy lifting, and expects 1D arrays. + indices = (self._cpp_trifinder.find_many(x.ravel(), y.ravel()) + .reshape(x.shape)) + return indices + + def _get_tree_stats(self): + """ + Return a python list containing the statistics about the node tree: + 0: number of nodes (tree size) + 1: number of unique nodes + 2: number of trapezoids (tree leaf nodes) + 3: number of unique trapezoids + 4: maximum parent count (max number of times a node is repeated in + tree) + 5: maximum depth of tree (one more than the maximum number of + comparisons needed to search through the tree) + 6: mean of all trapezoid depths (one more than the average number + of comparisons needed to search through the tree) + """ + return self._cpp_trifinder.get_tree_stats() + + def _initialize(self): + """ + Initialize the underlying C++ object. Can be called multiple times if, + for example, the triangulation is modified. + """ + self._cpp_trifinder.initialize() + + def _print_tree(self): + """ + Print a text representation of the node tree, which is useful for + debugging purposes. + """ + self._cpp_trifinder.print_tree() diff --git a/lib/matplotlib/tri/_triinterpolate.py b/lib/matplotlib/tri/_triinterpolate.py new file mode 100644 index 000000000000..df276d8c6447 --- /dev/null +++ b/lib/matplotlib/tri/_triinterpolate.py @@ -0,0 +1,1574 @@ +""" +Interpolation inside triangular grids. +""" + +import numpy as np + +from matplotlib import _api +from matplotlib.tri import Triangulation +from matplotlib.tri._trifinder import TriFinder +from matplotlib.tri._tritools import TriAnalyzer + +__all__ = ('TriInterpolator', 'LinearTriInterpolator', 'CubicTriInterpolator') + + +class TriInterpolator: + """ + 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). + """ + + def __init__(self, triangulation, z, trifinder=None): + _api.check_isinstance(Triangulation, triangulation=triangulation) + self._triangulation = triangulation + + self._z = np.asarray(z) + if self._z.shape != self._triangulation.x.shape: + raise ValueError("z array must have same length as triangulation x" + " and y arrays") + + _api.check_isinstance((TriFinder, None), trifinder=trifinder) + self._trifinder = trifinder or self._triangulation.get_trifinder() + + # Default scaling factors : 1.0 (= no scaling) + # Scaling may be used for interpolations for which the order of + # magnitude of x, y has an impact on the interpolant definition. + # Please refer to :meth:`_interpolate_multikeys` for details. + self._unit_x = 1.0 + self._unit_y = 1.0 + + # Default triangle renumbering: None (= no renumbering) + # Renumbering may be used to avoid unnecessary computations + # if complex calculations are done inside the Interpolator. + # Please refer to :meth:`_interpolate_multikeys` for details. + self._tri_renum = None + + # __call__ and gradient docstrings are shared by all subclasses + # (except, if needed, relevant additions). + # However these methods are only implemented in subclasses to avoid + # confusion in the documentation. + _docstring__call__ = """ + Returns a masked array containing interpolated values at the specified + (x, y) points. + + Parameters + ---------- + x, y : array-like + x and y coordinates of the same shape and any number of + dimensions. + + Returns + ------- + np.ma.array + Masked array of the same shape as *x* and *y*; values corresponding + to (*x*, *y*) points outside of the triangulation are masked out. + + """ + + _docstringgradient = r""" + Returns a list of 2 masked arrays containing interpolated derivatives + at the specified (x, y) points. + + Parameters + ---------- + x, y : array-like + x and y coordinates of the same shape and any number of + dimensions. + + Returns + ------- + dzdx, dzdy : np.ma.array + 2 masked arrays of the same shape as *x* and *y*; values + corresponding to (x, y) points outside of the triangulation + are masked out. + The first returned array contains the values of + :math:`\frac{\partial z}{\partial x}` and the second those of + :math:`\frac{\partial z}{\partial y}`. + + """ + + def _interpolate_multikeys(self, x, y, tri_index=None, + return_keys=('z',)): + """ + Versatile (private) method defined for all TriInterpolators. + + :meth:`_interpolate_multikeys` is a wrapper around method + :meth:`_interpolate_single_key` (to be defined in the child + subclasses). + :meth:`_interpolate_single_key actually performs the interpolation, + but only for 1-dimensional inputs and at valid locations (inside + unmasked triangles of the triangulation). + + The purpose of :meth:`_interpolate_multikeys` is to implement the + following common tasks needed in all subclasses implementations: + + - calculation of containing triangles + - dealing with more than one interpolation request at the same + location (e.g., if the 2 derivatives are requested, it is + unnecessary to compute the containing triangles twice) + - scaling according to self._unit_x, self._unit_y + - dealing with points outside of the grid (with fill value np.nan) + - dealing with multi-dimensional *x*, *y* arrays: flattening for + :meth:`_interpolate_params` call and final reshaping. + + (Note that np.vectorize could do most of those things very well for + you, but it does it by function evaluations over successive tuples of + the input arrays. Therefore, this tends to be more time-consuming than + using optimized numpy functions - e.g., np.dot - which can be used + easily on the flattened inputs, in the child-subclass methods + :meth:`_interpolate_single_key`.) + + It is guaranteed that the calls to :meth:`_interpolate_single_key` + will be done with flattened (1-d) array-like input parameters *x*, *y* + and with flattened, valid `tri_index` arrays (no -1 index allowed). + + Parameters + ---------- + x, y : array-like + x and y coordinates where interpolated values are requested. + tri_index : array-like of int, optional + Array of the containing triangle indices, same shape as + *x* and *y*. Defaults to None. If None, these indices + will be computed by a TriFinder instance. + (Note: For point outside the grid, tri_index[ipt] shall be -1). + return_keys : tuple of keys from {'z', 'dzdx', 'dzdy'} + Defines the interpolation arrays to return, and in which order. + + Returns + ------- + list of arrays + Each array-like contains the expected interpolated values in the + order defined by *return_keys* parameter. + """ + # Flattening and rescaling inputs arrays x, y + # (initial shape is stored for output) + x = np.asarray(x, dtype=np.float64) + y = np.asarray(y, dtype=np.float64) + sh_ret = x.shape + if x.shape != y.shape: + raise ValueError("x and y shall have same shapes." + " Given: {0} and {1}".format(x.shape, y.shape)) + x = np.ravel(x) + y = np.ravel(y) + x_scaled = x/self._unit_x + y_scaled = y/self._unit_y + size_ret = np.size(x_scaled) + + # Computes & ravels the element indexes, extract the valid ones. + if tri_index is None: + tri_index = self._trifinder(x, y) + else: + if tri_index.shape != sh_ret: + raise ValueError( + "tri_index array is provided and shall" + " have same shape as x and y. Given: " + "{0} and {1}".format(tri_index.shape, sh_ret)) + tri_index = np.ravel(tri_index) + + mask_in = (tri_index != -1) + if self._tri_renum is None: + valid_tri_index = tri_index[mask_in] + else: + valid_tri_index = self._tri_renum[tri_index[mask_in]] + valid_x = x_scaled[mask_in] + valid_y = y_scaled[mask_in] + + ret = [] + for return_key in return_keys: + # Find the return index associated with the key. + try: + return_index = {'z': 0, 'dzdx': 1, 'dzdy': 2}[return_key] + except KeyError as err: + raise ValueError("return_keys items shall take values in" + " {'z', 'dzdx', 'dzdy'}") from err + + # Sets the scale factor for f & df components + scale = [1., 1./self._unit_x, 1./self._unit_y][return_index] + + # Computes the interpolation + ret_loc = np.empty(size_ret, dtype=np.float64) + ret_loc[~mask_in] = np.nan + ret_loc[mask_in] = self._interpolate_single_key( + return_key, valid_tri_index, valid_x, valid_y) * scale + ret += [np.ma.masked_invalid(ret_loc.reshape(sh_ret), copy=False)] + + return ret + + def _interpolate_single_key(self, return_key, tri_index, x, y): + """ + Interpolate at points belonging to the triangulation + (inside an unmasked triangles). + + Parameters + ---------- + return_key : {'z', 'dzdx', 'dzdy'} + The requested values (z or its derivatives). + tri_index : 1D int array + Valid triangle index (cannot be -1). + x, y : 1D arrays, same shape as `tri_index` + Valid locations where interpolation is requested. + + Returns + ------- + 1-d array + Returned array of the same size as *tri_index* + """ + raise NotImplementedError("TriInterpolator subclasses" + + "should implement _interpolate_single_key!") + + +class LinearTriInterpolator(TriInterpolator): + """ + 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). + Interpolated values are therefore continuous across the triangulation, but + their first derivatives are discontinuous at edges between triangles. + + Parameters + ---------- + triangulation : `~matplotlib.tri.Triangulation` + The triangulation to interpolate over. + z : (npoints,) array-like + Array of values, defined at grid points, to interpolate between. + trifinder : `~matplotlib.tri.TriFinder`, optional + If this is not specified, the Triangulation's default TriFinder will + be used by calling `.Triangulation.get_trifinder`. + + Methods + ------- + `__call__` (x, y) : Returns interpolated values at (x, y) points. + `gradient` (x, y) : Returns interpolated derivatives at (x, y) points. + + """ + def __init__(self, triangulation, z, trifinder=None): + super().__init__(triangulation, z, trifinder) + + # Store plane coefficients for fast interpolation calculations. + self._plane_coefficients = \ + self._triangulation.calculate_plane_coefficients(self._z) + + def __call__(self, x, y): + return self._interpolate_multikeys(x, y, tri_index=None, + return_keys=('z',))[0] + __call__.__doc__ = TriInterpolator._docstring__call__ + + def gradient(self, x, y): + return self._interpolate_multikeys(x, y, tri_index=None, + return_keys=('dzdx', 'dzdy')) + gradient.__doc__ = TriInterpolator._docstringgradient + + def _interpolate_single_key(self, return_key, tri_index, x, y): + _api.check_in_list(['z', 'dzdx', 'dzdy'], return_key=return_key) + if return_key == 'z': + return (self._plane_coefficients[tri_index, 0]*x + + self._plane_coefficients[tri_index, 1]*y + + self._plane_coefficients[tri_index, 2]) + elif return_key == 'dzdx': + return self._plane_coefficients[tri_index, 0] + else: # 'dzdy' + return self._plane_coefficients[tri_index, 1] + + +class CubicTriInterpolator(TriInterpolator): + r""" + 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. + This is almost the same in 2D inside a triangle, except that the values + of the function and its 2 derivatives have to be defined at each triangle + node. + + The CubicTriInterpolator takes the value of the function at each node - + provided by the user - and internally computes the value of the + derivatives, resulting in a smooth interpolation. + (As a special feature, the user can also impose the value of the + derivatives at each node, but this is not supposed to be the common + usage.) + + Parameters + ---------- + triangulation : `~matplotlib.tri.Triangulation` + The triangulation to interpolate over. + z : (npoints,) array-like + Array of values, defined at grid points, to interpolate between. + kind : {'min_E', 'geom', 'user'}, optional + Choice of the smoothing algorithm, in order to compute + the interpolant derivatives (defaults to 'min_E'): + + - if 'min_E': (default) The derivatives at each node is computed + to minimize a bending energy. + - if 'geom': The derivatives at each node is computed as a + weighted average of relevant triangle normals. To be used for + speed optimization (large grids). + - if 'user': The user provides the argument *dz*, no computation + is hence needed. + + trifinder : `~matplotlib.tri.TriFinder`, optional + If not specified, the Triangulation's default TriFinder will + be used by calling `.Triangulation.get_trifinder`. + dz : tuple of array-likes (dzdx, dzdy), optional + Used only if *kind* ='user'. In this case *dz* must be provided as + (dzdx, dzdy) where dzdx, dzdy are arrays of the same shape as *z* and + are the interpolant first derivatives at the *triangulation* points. + + Methods + ------- + `__call__` (x, y) : Returns interpolated values at (x, y) points. + `gradient` (x, y) : Returns interpolated derivatives at (x, y) points. + + Notes + ----- + This note is a bit technical and details how the cubic interpolation is + computed. + + The interpolation is based on a Clough-Tocher subdivision scheme of + the *triangulation* mesh (to make it clearer, each triangle of the + grid will be divided in 3 child-triangles, and on each child triangle + the interpolated function is a cubic polynomial of the 2 coordinates). + This technique originates from FEM (Finite Element Method) analysis; + the element used is a reduced Hsieh-Clough-Tocher (HCT) + element. Its shape functions are described in [1]_. + The assembled function is guaranteed to be C1-smooth, i.e. it is + continuous and its first derivatives are also continuous (this + is easy to show inside the triangles but is also true when crossing the + edges). + + In the default case (*kind* ='min_E'), the interpolant minimizes a + curvature energy on the functional space generated by the HCT element + shape functions - with imposed values but arbitrary derivatives at each + node. The minimized functional is the integral of the so-called total + curvature (implementation based on an algorithm from [2]_ - PCG sparse + solver): + + .. math:: + + E(z) = \frac{1}{2} \int_{\Omega} \left( + \left( \frac{\partial^2{z}}{\partial{x}^2} \right)^2 + + \left( \frac{\partial^2{z}}{\partial{y}^2} \right)^2 + + 2\left( \frac{\partial^2{z}}{\partial{y}\partial{x}} \right)^2 + \right) dx\,dy + + If the case *kind* ='geom' is chosen by the user, a simple geometric + approximation is used (weighted average of the triangle normal + vectors), which could improve speed on very large grids. + + References + ---------- + .. [1] Michel Bernadou, Kamal Hassan, "Basis functions for general + Hsieh-Clough-Tocher triangles, complete or reduced.", + International Journal for Numerical Methods in Engineering, + 17(5):784 - 789. 2.01. + .. [2] C.T. Kelley, "Iterative Methods for Optimization". + + """ + def __init__(self, triangulation, z, kind='min_E', trifinder=None, + dz=None): + super().__init__(triangulation, z, trifinder) + + # Loads the underlying c++ _triangulation. + # (During loading, reordering of triangulation._triangles may occur so + # that all final triangles are now anti-clockwise) + self._triangulation.get_cpp_triangulation() + + # To build the stiffness matrix and avoid zero-energy spurious modes + # we will only store internally the valid (unmasked) triangles and + # the necessary (used) points coordinates. + # 2 renumbering tables need to be computed and stored: + # - a triangle renum table in order to translate the result from a + # TriFinder instance into the internal stored triangle number. + # - a node renum table to overwrite the self._z values into the new + # (used) node numbering. + tri_analyzer = TriAnalyzer(self._triangulation) + (compressed_triangles, compressed_x, compressed_y, tri_renum, + node_renum) = tri_analyzer._get_compressed_triangulation() + self._triangles = compressed_triangles + self._tri_renum = tri_renum + # Taking into account the node renumbering in self._z: + valid_node = (node_renum != -1) + self._z[node_renum[valid_node]] = self._z[valid_node] + + # Computing scale factors + self._unit_x = np.ptp(compressed_x) + self._unit_y = np.ptp(compressed_y) + self._pts = np.column_stack([compressed_x / self._unit_x, + compressed_y / self._unit_y]) + # Computing triangle points + self._tris_pts = self._pts[self._triangles] + # Computing eccentricities + self._eccs = self._compute_tri_eccentricities(self._tris_pts) + # Computing dof estimations for HCT triangle shape function + _api.check_in_list(['user', 'geom', 'min_E'], kind=kind) + self._dof = self._compute_dof(kind, dz=dz) + # Loading HCT element + self._ReferenceElement = _ReducedHCT_Element() + + def __call__(self, x, y): + return self._interpolate_multikeys(x, y, tri_index=None, + return_keys=('z',))[0] + __call__.__doc__ = TriInterpolator._docstring__call__ + + def gradient(self, x, y): + return self._interpolate_multikeys(x, y, tri_index=None, + return_keys=('dzdx', 'dzdy')) + gradient.__doc__ = TriInterpolator._docstringgradient + + def _interpolate_single_key(self, return_key, tri_index, x, y): + _api.check_in_list(['z', 'dzdx', 'dzdy'], return_key=return_key) + tris_pts = self._tris_pts[tri_index] + alpha = self._get_alpha_vec(x, y, tris_pts) + ecc = self._eccs[tri_index] + dof = np.expand_dims(self._dof[tri_index], axis=1) + if return_key == 'z': + return self._ReferenceElement.get_function_values( + alpha, ecc, dof) + else: # 'dzdx', 'dzdy' + J = self._get_jacobian(tris_pts) + dzdx = self._ReferenceElement.get_function_derivatives( + alpha, J, ecc, dof) + if return_key == 'dzdx': + return dzdx[:, 0, 0] + else: + return dzdx[:, 1, 0] + + def _compute_dof(self, kind, dz=None): + """ + Compute and return nodal dofs according to kind. + + Parameters + ---------- + kind : {'min_E', 'geom', 'user'} + 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`. + + Returns + ------- + array-like, shape (npts, 2) + Estimation of the gradient at triangulation nodes (stored as + degree of freedoms of reduced-HCT triangle elements). + """ + if kind == 'user': + if dz is None: + raise ValueError("For a CubicTriInterpolator with " + "*kind*='user', a valid *dz* " + "argument is expected.") + TE = _DOF_estimator_user(self, dz=dz) + elif kind == 'geom': + TE = _DOF_estimator_geom(self) + else: # 'min_E', checked in __init__ + TE = _DOF_estimator_min_E(self) + return TE.compute_dof_from_df() + + @staticmethod + def _get_alpha_vec(x, y, tris_pts): + """ + Fast (vectorized) function to compute barycentric coordinates alpha. + + Parameters + ---------- + x, y : array-like of dim 1 (shape (nx,)) + Coordinates of the points whose points barycentric coordinates are + requested. + tris_pts : array like of dim 3 (shape: (nx, 3, 2)) + Coordinates of the containing triangles apexes. + + Returns + ------- + array of dim 2 (shape (nx, 3)) + Barycentric coordinates of the points inside the containing + triangles. + """ + ndim = tris_pts.ndim-2 + + a = tris_pts[:, 1, :] - tris_pts[:, 0, :] + b = tris_pts[:, 2, :] - tris_pts[:, 0, :] + abT = np.stack([a, b], axis=-1) + ab = _transpose_vectorized(abT) + OM = np.stack([x, y], axis=1) - tris_pts[:, 0, :] + + metric = ab @ abT + # Here we try to deal with the colinear cases. + # metric_inv is in this case set to the Moore-Penrose pseudo-inverse + # meaning that we will still return a set of valid barycentric + # coordinates. + metric_inv = _pseudo_inv22sym_vectorized(metric) + Covar = ab @ _transpose_vectorized(np.expand_dims(OM, ndim)) + ksi = metric_inv @ Covar + alpha = _to_matrix_vectorized([ + [1-ksi[:, 0, 0]-ksi[:, 1, 0]], [ksi[:, 0, 0]], [ksi[:, 1, 0]]]) + return alpha + + @staticmethod + def _get_jacobian(tris_pts): + """ + Fast (vectorized) function to compute triangle jacobian matrix. + + Parameters + ---------- + tris_pts : array like of dim 3 (shape: (nx, 3, 2)) + Coordinates of the containing triangles apexes. + + Returns + ------- + array of dim 3 (shape (nx, 2, 2)) + Barycentric coordinates of the points inside the containing + triangles. + J[itri, :, :] is the jacobian matrix at apex 0 of the triangle + itri, so that the following (matrix) relationship holds: + [dz/dksi] = [J] x [dz/dx] + with x: global coordinates + ksi: element parametric coordinates in triangle first apex + local basis. + """ + a = np.array(tris_pts[:, 1, :] - tris_pts[:, 0, :]) + b = np.array(tris_pts[:, 2, :] - tris_pts[:, 0, :]) + J = _to_matrix_vectorized([[a[:, 0], a[:, 1]], + [b[:, 0], b[:, 1]]]) + return J + + @staticmethod + def _compute_tri_eccentricities(tris_pts): + """ + Compute triangle eccentricities. + + Parameters + ---------- + tris_pts : array like of dim 3 (shape: (nx, 3, 2)) + Coordinates of the triangles apexes. + + Returns + ------- + array like of dim 2 (shape: (nx, 3)) + The so-called eccentricity parameters [1] needed for HCT triangular + element. + """ + a = np.expand_dims(tris_pts[:, 2, :] - tris_pts[:, 1, :], axis=2) + b = np.expand_dims(tris_pts[:, 0, :] - tris_pts[:, 2, :], axis=2) + c = np.expand_dims(tris_pts[:, 1, :] - tris_pts[:, 0, :], axis=2) + # Do not use np.squeeze, this is dangerous if only one triangle + # in the triangulation... + dot_a = (_transpose_vectorized(a) @ a)[:, 0, 0] + dot_b = (_transpose_vectorized(b) @ b)[:, 0, 0] + dot_c = (_transpose_vectorized(c) @ c)[:, 0, 0] + # Note that this line will raise a warning for dot_a, dot_b or dot_c + # zeros, but we choose not to support triangles with duplicate points. + return _to_matrix_vectorized([[(dot_c-dot_b) / dot_a], + [(dot_a-dot_c) / dot_b], + [(dot_b-dot_a) / dot_c]]) + + +# FEM element used for interpolation and for solving minimisation +# problem (Reduced HCT element) +class _ReducedHCT_Element: + """ + Implementation of reduced HCT triangular element with explicit shape + functions. + + Computes z, dz, d2z and the element stiffness matrix for bending energy: + E(f) = integral( (d2z/dx2 + d2z/dy2)**2 dA) + + *** Reference for the shape functions: *** + [1] Basis functions for general Hsieh-Clough-Tocher _triangles, complete or + reduced. + Michel Bernadou, Kamal Hassan + International Journal for Numerical Methods in Engineering. + 17(5):784 - 789. 2.01 + + *** Element description: *** + 9 dofs: z and dz given at 3 apex + C1 (conform) + + """ + # 1) Loads matrices to generate shape functions as a function of + # triangle eccentricities - based on [1] p.11 ''' + M = np.array([ + [ 0.00, 0.00, 0.00, 4.50, 4.50, 0.00, 0.00, 0.00, 0.00, 0.00], + [-0.25, 0.00, 0.00, 0.50, 1.25, 0.00, 0.00, 0.00, 0.00, 0.00], + [-0.25, 0.00, 0.00, 1.25, 0.50, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.50, 1.00, 0.00, -1.50, 0.00, 3.00, 3.00, 0.00, 0.00, 3.00], + [ 0.00, 0.00, 0.00, -0.25, 0.25, 0.00, 1.00, 0.00, 0.00, 0.50], + [ 0.25, 0.00, 0.00, -0.50, -0.25, 1.00, 0.00, 0.00, 0.00, 1.00], + [ 0.50, 0.00, 1.00, 0.00, -1.50, 0.00, 0.00, 3.00, 3.00, 3.00], + [ 0.25, 0.00, 0.00, -0.25, -0.50, 0.00, 0.00, 0.00, 1.00, 1.00], + [ 0.00, 0.00, 0.00, 0.25, -0.25, 0.00, 0.00, 1.00, 0.00, 0.50]]) + M0 = np.array([ + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [-1.00, 0.00, 0.00, 1.50, 1.50, 0.00, 0.00, 0.00, 0.00, -3.00], + [-0.50, 0.00, 0.00, 0.75, 0.75, 0.00, 0.00, 0.00, 0.00, -1.50], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 1.00, 0.00, 0.00, -1.50, -1.50, 0.00, 0.00, 0.00, 0.00, 3.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.50, 0.00, 0.00, -0.75, -0.75, 0.00, 0.00, 0.00, 0.00, 1.50]]) + M1 = np.array([ + [-0.50, 0.00, 0.00, 1.50, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [-0.25, 0.00, 0.00, 0.75, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.50, 0.00, 0.00, -1.50, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.25, 0.00, 0.00, -0.75, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00]]) + M2 = np.array([ + [ 0.50, 0.00, 0.00, 0.00, -1.50, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.25, 0.00, 0.00, 0.00, -0.75, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [-0.50, 0.00, 0.00, 0.00, 1.50, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [-0.25, 0.00, 0.00, 0.00, 0.75, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], + [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00]]) + + # 2) Loads matrices to rotate components of gradient & Hessian + # vectors in the reference basis of triangle first apex (a0) + rotate_dV = np.array([[ 1., 0.], [ 0., 1.], + [ 0., 1.], [-1., -1.], + [-1., -1.], [ 1., 0.]]) + + rotate_d2V = np.array([[1., 0., 0.], [0., 1., 0.], [ 0., 0., 1.], + [0., 1., 0.], [1., 1., 1.], [ 0., -2., -1.], + [1., 1., 1.], [1., 0., 0.], [-2., 0., -1.]]) + + # 3) Loads Gauss points & weights on the 3 sub-_triangles for P2 + # exact integral - 3 points on each subtriangles. + # NOTE: as the 2nd derivative is discontinuous , we really need those 9 + # points! + n_gauss = 9 + gauss_pts = np.array([[13./18., 4./18., 1./18.], + [ 4./18., 13./18., 1./18.], + [ 7./18., 7./18., 4./18.], + [ 1./18., 13./18., 4./18.], + [ 1./18., 4./18., 13./18.], + [ 4./18., 7./18., 7./18.], + [ 4./18., 1./18., 13./18.], + [13./18., 1./18., 4./18.], + [ 7./18., 4./18., 7./18.]], dtype=np.float64) + gauss_w = np.ones([9], dtype=np.float64) / 9. + + # 4) Stiffness matrix for curvature energy + E = np.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 2.]]) + + # 5) Loads the matrix to compute DOF_rot from tri_J at apex 0 + J0_to_J1 = np.array([[-1., 1.], [-1., 0.]]) + J0_to_J2 = np.array([[ 0., -1.], [ 1., -1.]]) + + def get_function_values(self, alpha, ecc, dofs): + """ + Parameters + ---------- + alpha : is a (N x 3 x 1) array (array of column-matrices) of + barycentric coordinates, + ecc : is a (N x 3 x 1) array (array of column-matrices) of triangle + eccentricities, + dofs : is a (N x 1 x 9) arrays (arrays of row-matrices) of computed + degrees of freedom. + + Returns + ------- + Returns the N-array of interpolated function values. + """ + subtri = np.argmin(alpha, axis=1)[:, 0] + ksi = _roll_vectorized(alpha, -subtri, axis=0) + E = _roll_vectorized(ecc, -subtri, axis=0) + x = ksi[:, 0, 0] + y = ksi[:, 1, 0] + z = ksi[:, 2, 0] + x_sq = x*x + y_sq = y*y + z_sq = z*z + V = _to_matrix_vectorized([ + [x_sq*x], [y_sq*y], [z_sq*z], [x_sq*z], [x_sq*y], [y_sq*x], + [y_sq*z], [z_sq*y], [z_sq*x], [x*y*z]]) + prod = self.M @ V + prod += _scalar_vectorized(E[:, 0, 0], self.M0 @ V) + prod += _scalar_vectorized(E[:, 1, 0], self.M1 @ V) + prod += _scalar_vectorized(E[:, 2, 0], self.M2 @ V) + s = _roll_vectorized(prod, 3*subtri, axis=0) + return (dofs @ s)[:, 0, 0] + + def get_function_derivatives(self, alpha, J, ecc, dofs): + """ + Parameters + ---------- + *alpha* is a (N x 3 x 1) array (array of column-matrices of + barycentric coordinates) + *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at + triangle first apex) + *ecc* is a (N x 3 x 1) array (array of column-matrices of triangle + eccentricities) + *dofs* is a (N x 1 x 9) arrays (arrays of row-matrices) of computed + degrees of freedom. + + Returns + ------- + Returns the values of interpolated function derivatives [dz/dx, dz/dy] + in global coordinates at locations alpha, as a column-matrices of + shape (N x 2 x 1). + """ + subtri = np.argmin(alpha, axis=1)[:, 0] + ksi = _roll_vectorized(alpha, -subtri, axis=0) + E = _roll_vectorized(ecc, -subtri, axis=0) + x = ksi[:, 0, 0] + y = ksi[:, 1, 0] + z = ksi[:, 2, 0] + x_sq = x*x + y_sq = y*y + z_sq = z*z + dV = _to_matrix_vectorized([ + [ -3.*x_sq, -3.*x_sq], + [ 3.*y_sq, 0.], + [ 0., 3.*z_sq], + [ -2.*x*z, -2.*x*z+x_sq], + [-2.*x*y+x_sq, -2.*x*y], + [ 2.*x*y-y_sq, -y_sq], + [ 2.*y*z, y_sq], + [ z_sq, 2.*y*z], + [ -z_sq, 2.*x*z-z_sq], + [ x*z-y*z, x*y-y*z]]) + # Puts back dV in first apex basis + dV = dV @ _extract_submatrices( + self.rotate_dV, subtri, block_size=2, axis=0) + + prod = self.M @ dV + prod += _scalar_vectorized(E[:, 0, 0], self.M0 @ dV) + prod += _scalar_vectorized(E[:, 1, 0], self.M1 @ dV) + prod += _scalar_vectorized(E[:, 2, 0], self.M2 @ dV) + dsdksi = _roll_vectorized(prod, 3*subtri, axis=0) + dfdksi = dofs @ dsdksi + # In global coordinates: + # Here we try to deal with the simplest colinear cases, returning a + # null matrix. + J_inv = _safe_inv22_vectorized(J) + dfdx = J_inv @ _transpose_vectorized(dfdksi) + return dfdx + + def get_function_hessians(self, alpha, J, ecc, dofs): + """ + Parameters + ---------- + *alpha* is a (N x 3 x 1) array (array of column-matrices) of + barycentric coordinates + *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at + triangle first apex) + *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle + eccentricities + *dofs* is a (N x 1 x 9) arrays (arrays of row-matrices) of computed + degrees of freedom. + + Returns + ------- + Returns the values of interpolated function 2nd-derivatives + [d2z/dx2, d2z/dy2, d2z/dxdy] in global coordinates at locations alpha, + as a column-matrices of shape (N x 3 x 1). + """ + d2sdksi2 = self.get_d2Sidksij2(alpha, ecc) + d2fdksi2 = dofs @ d2sdksi2 + H_rot = self.get_Hrot_from_J(J) + d2fdx2 = d2fdksi2 @ H_rot + return _transpose_vectorized(d2fdx2) + + def get_d2Sidksij2(self, alpha, ecc): + """ + Parameters + ---------- + *alpha* is a (N x 3 x 1) array (array of column-matrices) of + barycentric coordinates + *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle + eccentricities + + Returns + ------- + Returns the arrays d2sdksi2 (N x 3 x 1) Hessian of shape functions + expressed in covariant coordinates in first apex basis. + """ + subtri = np.argmin(alpha, axis=1)[:, 0] + ksi = _roll_vectorized(alpha, -subtri, axis=0) + E = _roll_vectorized(ecc, -subtri, axis=0) + x = ksi[:, 0, 0] + y = ksi[:, 1, 0] + z = ksi[:, 2, 0] + d2V = _to_matrix_vectorized([ + [ 6.*x, 6.*x, 6.*x], + [ 6.*y, 0., 0.], + [ 0., 6.*z, 0.], + [ 2.*z, 2.*z-4.*x, 2.*z-2.*x], + [2.*y-4.*x, 2.*y, 2.*y-2.*x], + [2.*x-4.*y, 0., -2.*y], + [ 2.*z, 0., 2.*y], + [ 0., 2.*y, 2.*z], + [ 0., 2.*x-4.*z, -2.*z], + [ -2.*z, -2.*y, x-y-z]]) + # Puts back d2V in first apex basis + d2V = d2V @ _extract_submatrices( + self.rotate_d2V, subtri, block_size=3, axis=0) + prod = self.M @ d2V + prod += _scalar_vectorized(E[:, 0, 0], self.M0 @ d2V) + prod += _scalar_vectorized(E[:, 1, 0], self.M1 @ d2V) + prod += _scalar_vectorized(E[:, 2, 0], self.M2 @ d2V) + d2sdksi2 = _roll_vectorized(prod, 3*subtri, axis=0) + return d2sdksi2 + + def get_bending_matrices(self, J, ecc): + """ + Parameters + ---------- + *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at + triangle first apex) + *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle + eccentricities + + Returns + ------- + Returns the element K matrices for bending energy expressed in + GLOBAL nodal coordinates. + K_ij = integral [ (d2zi/dx2 + d2zi/dy2) * (d2zj/dx2 + d2zj/dy2) dA] + tri_J is needed to rotate dofs from local basis to global basis + """ + n = np.size(ecc, 0) + + # 1) matrix to rotate dofs in global coordinates + J1 = self.J0_to_J1 @ J + J2 = self.J0_to_J2 @ J + DOF_rot = np.zeros([n, 9, 9], dtype=np.float64) + DOF_rot[:, 0, 0] = 1 + DOF_rot[:, 3, 3] = 1 + DOF_rot[:, 6, 6] = 1 + DOF_rot[:, 1:3, 1:3] = J + DOF_rot[:, 4:6, 4:6] = J1 + DOF_rot[:, 7:9, 7:9] = J2 + + # 2) matrix to rotate Hessian in global coordinates. + H_rot, area = self.get_Hrot_from_J(J, return_area=True) + + # 3) Computes stiffness matrix + # Gauss quadrature. + K = np.zeros([n, 9, 9], dtype=np.float64) + weights = self.gauss_w + pts = self.gauss_pts + for igauss in range(self.n_gauss): + alpha = np.tile(pts[igauss, :], n).reshape(n, 3) + alpha = np.expand_dims(alpha, 2) + weight = weights[igauss] + d2Skdksi2 = self.get_d2Sidksij2(alpha, ecc) + d2Skdx2 = d2Skdksi2 @ H_rot + K += weight * (d2Skdx2 @ self.E @ _transpose_vectorized(d2Skdx2)) + + # 4) With nodal (not elem) dofs + K = _transpose_vectorized(DOF_rot) @ K @ DOF_rot + + # 5) Need the area to compute total element energy + return _scalar_vectorized(area, K) + + def get_Hrot_from_J(self, J, return_area=False): + """ + Parameters + ---------- + *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at + triangle first apex) + + Returns + ------- + Returns H_rot used to rotate Hessian from local basis of first apex, + to global coordinates. + if *return_area* is True, returns also the triangle area (0.5*det(J)) + """ + # Here we try to deal with the simplest colinear cases; a null + # energy and area is imposed. + J_inv = _safe_inv22_vectorized(J) + Ji00 = J_inv[:, 0, 0] + Ji11 = J_inv[:, 1, 1] + Ji10 = J_inv[:, 1, 0] + Ji01 = J_inv[:, 0, 1] + H_rot = _to_matrix_vectorized([ + [Ji00*Ji00, Ji10*Ji10, Ji00*Ji10], + [Ji01*Ji01, Ji11*Ji11, Ji01*Ji11], + [2*Ji00*Ji01, 2*Ji11*Ji10, Ji00*Ji11+Ji10*Ji01]]) + if not return_area: + return H_rot + else: + area = 0.5 * (J[:, 0, 0]*J[:, 1, 1] - J[:, 0, 1]*J[:, 1, 0]) + return H_rot, area + + def get_Kff_and_Ff(self, J, ecc, triangles, Uc): + """ + Build K and F for the following elliptic formulation: + minimization of curvature energy with value of function at node + imposed and derivatives 'free'. + + Build the global Kff matrix in cco format. + Build the full Ff vec Ff = - Kfc x Uc. + + Parameters + ---------- + *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at + triangle first apex) + *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle + eccentricities + *triangles* is a (N x 3) array of nodes indexes. + *Uc* is (N x 3) array of imposed displacements at nodes + + Returns + ------- + (Kff_rows, Kff_cols, Kff_vals) Kff matrix in coo format - Duplicate + (row, col) entries must be summed. + Ff: force vector - dim npts * 3 + """ + ntri = np.size(ecc, 0) + vec_range = np.arange(ntri, dtype=np.int32) + c_indices = np.full(ntri, -1, dtype=np.int32) # for unused dofs, -1 + f_dof = [1, 2, 4, 5, 7, 8] + c_dof = [0, 3, 6] + + # vals, rows and cols indices in global dof numbering + f_dof_indices = _to_matrix_vectorized([[ + c_indices, triangles[:, 0]*2, triangles[:, 0]*2+1, + c_indices, triangles[:, 1]*2, triangles[:, 1]*2+1, + c_indices, triangles[:, 2]*2, triangles[:, 2]*2+1]]) + + expand_indices = np.ones([ntri, 9, 1], dtype=np.int32) + f_row_indices = _transpose_vectorized(expand_indices @ f_dof_indices) + f_col_indices = expand_indices @ f_dof_indices + K_elem = self.get_bending_matrices(J, ecc) + + # Extracting sub-matrices + # Explanation & notations: + # * Subscript f denotes 'free' degrees of freedom (i.e. dz/dx, dz/dx) + # * Subscript c denotes 'condensated' (imposed) degrees of freedom + # (i.e. z at all nodes) + # * F = [Ff, Fc] is the force vector + # * U = [Uf, Uc] is the imposed dof vector + # [ Kff Kfc ] + # * K = [ ] is the laplacian stiffness matrix + # [ Kcf Kff ] + # * As F = K x U one gets straightforwardly: Ff = - Kfc x Uc + + # Computing Kff stiffness matrix in sparse coo format + Kff_vals = np.ravel(K_elem[np.ix_(vec_range, f_dof, f_dof)]) + Kff_rows = np.ravel(f_row_indices[np.ix_(vec_range, f_dof, f_dof)]) + Kff_cols = np.ravel(f_col_indices[np.ix_(vec_range, f_dof, f_dof)]) + + # Computing Ff force vector in sparse coo format + Kfc_elem = K_elem[np.ix_(vec_range, f_dof, c_dof)] + Uc_elem = np.expand_dims(Uc, axis=2) + Ff_elem = -(Kfc_elem @ Uc_elem)[:, :, 0] + Ff_indices = f_dof_indices[np.ix_(vec_range, [0], f_dof)][:, 0, :] + + # Extracting Ff force vector in dense format + # We have to sum duplicate indices - using bincount + Ff = np.bincount(np.ravel(Ff_indices), weights=np.ravel(Ff_elem)) + return Kff_rows, Kff_cols, Kff_vals, Ff + + +# :class:_DOF_estimator, _DOF_estimator_user, _DOF_estimator_geom, +# _DOF_estimator_min_E +# Private classes used to compute the degree of freedom of each triangular +# element for the TriCubicInterpolator. +class _DOF_estimator: + """ + 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. + """ + def __init__(self, interpolator, **kwargs): + _api.check_isinstance(CubicTriInterpolator, interpolator=interpolator) + self._pts = interpolator._pts + self._tris_pts = interpolator._tris_pts + self.z = interpolator._z + self._triangles = interpolator._triangles + (self._unit_x, self._unit_y) = (interpolator._unit_x, + interpolator._unit_y) + self.dz = self.compute_dz(**kwargs) + self.compute_dof_from_df() + + def compute_dz(self, **kwargs): + raise NotImplementedError + + def compute_dof_from_df(self): + """ + Compute reduced-HCT elements degrees of freedom, from the gradient. + """ + J = CubicTriInterpolator._get_jacobian(self._tris_pts) + tri_z = self.z[self._triangles] + tri_dz = self.dz[self._triangles] + tri_dof = self.get_dof_vec(tri_z, tri_dz, J) + return tri_dof + + @staticmethod + def get_dof_vec(tri_z, tri_dz, J): + """ + Compute the dof vector of a triangle, from the value of f, df and + of the local Jacobian at each node. + + 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``:: + + 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) + J1 = _ReducedHCT_Element.J0_to_J1 @ J + J2 = _ReducedHCT_Element.J0_to_J2 @ J + + col0 = J @ np.expand_dims(tri_dz[:, 0, :], axis=2) + col1 = J1 @ np.expand_dims(tri_dz[:, 1, :], axis=2) + col2 = J2 @ np.expand_dims(tri_dz[:, 2, :], axis=2) + + dfdksi = _to_matrix_vectorized([ + [col0[:, 0, 0], col1[:, 0, 0], col2[:, 0, 0]], + [col0[:, 1, 0], col1[:, 1, 0], col2[:, 1, 0]]]) + dof[:, 0:7:3] = tri_z + dof[:, 1:8:3] = dfdksi[:, 0] + dof[:, 2:9:3] = dfdksi[:, 1] + return dof + + +class _DOF_estimator_user(_DOF_estimator): + """dz is imposed by user; accounts for scaling if any.""" + + def compute_dz(self, dz): + (dzdx, dzdy) = dz + dzdx = dzdx * self._unit_x + dzdy = dzdy * self._unit_y + return np.vstack([dzdx, dzdy]).T + + +class _DOF_estimator_geom(_DOF_estimator): + """Fast 'geometric' approximation, recommended for large arrays.""" + + def compute_dz(self): + """ + self.df is computed as weighted average of _triangles sharing a common + node. On each triangle itri f is first assumed linear (= ~f), which + allows to compute d~f[itri] + Then the following approximation of df nodal values is then proposed: + f[ipt] = SUM ( w[itri] x d~f[itri] , for itri sharing apex ipt) + The weighted coeff. w[itri] are proportional to the angle of the + triangle itri at apex ipt + """ + el_geom_w = self.compute_geom_weights() + el_geom_grad = self.compute_geom_grads() + + # Sum of weights coeffs + w_node_sum = np.bincount(np.ravel(self._triangles), + weights=np.ravel(el_geom_w)) + + # Sum of weighted df = (dfx, dfy) + dfx_el_w = np.empty_like(el_geom_w) + dfy_el_w = np.empty_like(el_geom_w) + for iapex in range(3): + dfx_el_w[:, iapex] = el_geom_w[:, iapex]*el_geom_grad[:, 0] + dfy_el_w[:, iapex] = el_geom_w[:, iapex]*el_geom_grad[:, 1] + dfx_node_sum = np.bincount(np.ravel(self._triangles), + weights=np.ravel(dfx_el_w)) + dfy_node_sum = np.bincount(np.ravel(self._triangles), + weights=np.ravel(dfy_el_w)) + + # Estimation of df + dfx_estim = dfx_node_sum/w_node_sum + dfy_estim = dfy_node_sum/w_node_sum + return np.vstack([dfx_estim, dfy_estim]).T + + def compute_geom_weights(self): + """ + 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]) + tris_pts = self._tris_pts + for ipt in range(3): + p0 = tris_pts[:, ipt % 3, :] + p1 = tris_pts[:, (ipt+1) % 3, :] + p2 = tris_pts[:, (ipt-1) % 3, :] + alpha1 = np.arctan2(p1[:, 1]-p0[:, 1], p1[:, 0]-p0[:, 0]) + alpha2 = np.arctan2(p2[:, 1]-p0[:, 1], p2[:, 0]-p0[:, 0]) + # In the below formula we could take modulo 2. but + # modulo 1. is safer regarding round-off errors (flat triangles). + angle = np.abs(((alpha2-alpha1) / np.pi) % 1) + # Weight proportional to angle up np.pi/2; null weight for + # degenerated cases 0 and np.pi (note that *angle* is normalized + # by np.pi). + weights[:, ipt] = 0.5 - np.abs(angle-0.5) + return weights + + def compute_geom_grads(self): + """ + Compute the (global) gradient component of f assumed linear (~f). + returns array df of shape (nelems, 2) + df[ielem].dM[ielem] = dz[ielem] i.e. df = dz x dM = dM.T^-1 x dz + """ + tris_pts = self._tris_pts + tris_f = self.z[self._triangles] + + dM1 = tris_pts[:, 1, :] - tris_pts[:, 0, :] + dM2 = tris_pts[:, 2, :] - tris_pts[:, 0, :] + dM = np.dstack([dM1, dM2]) + # Here we try to deal with the simplest colinear cases: a null + # gradient is assumed in this case. + dM_inv = _safe_inv22_vectorized(dM) + + dZ1 = tris_f[:, 1] - tris_f[:, 0] + dZ2 = tris_f[:, 2] - tris_f[:, 0] + dZ = np.vstack([dZ1, dZ2]).T + df = np.empty_like(dZ) + + # With np.einsum: could be ej,eji -> ej + df[:, 0] = dZ[:, 0]*dM_inv[:, 0, 0] + dZ[:, 1]*dM_inv[:, 1, 0] + df[:, 1] = dZ[:, 0]*dM_inv[:, 0, 1] + dZ[:, 1]*dM_inv[:, 1, 1] + return df + + +class _DOF_estimator_min_E(_DOF_estimator_geom): + """ + The 'smoothest' approximation, df is computed through global minimization + of the bending energy: + E(f) = integral[(d2z/dx2 + d2z/dy2 + 2 d2z/dxdy)**2 dA] + """ + def __init__(self, Interpolator): + self._eccs = Interpolator._eccs + super().__init__(Interpolator) + + def compute_dz(self): + """ + Elliptic solver for bending energy minimization. + Uses a dedicated 'toy' sparse Jacobi PCG solver. + """ + # Initial guess for iterative PCG solver. + dz_init = super().compute_dz() + Uf0 = np.ravel(dz_init) + + reference_element = _ReducedHCT_Element() + J = CubicTriInterpolator._get_jacobian(self._tris_pts) + eccs = self._eccs + triangles = self._triangles + Uc = self.z[self._triangles] + + # Building stiffness matrix and force vector in coo format + Kff_rows, Kff_cols, Kff_vals, Ff = reference_element.get_Kff_and_Ff( + J, eccs, triangles, Uc) + + # Building sparse matrix and solving minimization problem + # We could use scipy.sparse direct solver; however to avoid this + # external dependency an implementation of a simple PCG solver with + # a simple diagonal Jacobi preconditioner is implemented. + tol = 1.e-10 + n_dof = Ff.shape[0] + Kff_coo = _Sparse_Matrix_coo(Kff_vals, Kff_rows, Kff_cols, + shape=(n_dof, n_dof)) + Kff_coo.compress_csc() + Uf, err = _cg(A=Kff_coo, b=Ff, x0=Uf0, tol=tol) + # If the PCG did not converge, we return the best guess between Uf0 + # and Uf. + err0 = np.linalg.norm(Kff_coo.dot(Uf0) - Ff) + if err0 < err: + # Maybe a good occasion to raise a warning here ? + _api.warn_external("In TriCubicInterpolator initialization, " + "PCG sparse solver did not converge after " + "1000 iterations. `geom` approximation is " + "used instead of `min_E`") + Uf = Uf0 + + # Building dz from Uf + dz = np.empty([self._pts.shape[0], 2], dtype=np.float64) + dz[:, 0] = Uf[::2] + dz[:, 1] = Uf[1::2] + return dz + + +# The following private :class:_Sparse_Matrix_coo and :func:_cg provide +# a PCG sparse solver for (symmetric) elliptic problems. +class _Sparse_Matrix_coo: + def __init__(self, vals, rows, cols, shape): + """ + 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) + self.rows = np.asarray(rows, dtype=np.int32) + self.cols = np.asarray(cols, dtype=np.int32) + + def dot(self, V): + """ + Dot product of self by a vector *V* in sparse-dense to dense format + *V* dense vector of shape (self.m,). + """ + assert V.shape == (self.m,) + return np.bincount(self.rows, + weights=self.vals*V[self.cols], + minlength=self.m) + + def compress_csc(self): + """ + Compress rows, cols, vals / summing duplicates. Sort for csc format. + """ + _, unique, indices = np.unique( + self.rows + self.n*self.cols, + return_index=True, return_inverse=True) + self.rows = self.rows[unique] + self.cols = self.cols[unique] + self.vals = np.bincount(indices, weights=self.vals) + + def compress_csr(self): + """ + Compress rows, cols, vals / summing duplicates. Sort for csr format. + """ + _, unique, indices = np.unique( + self.m*self.rows + self.cols, + return_index=True, return_inverse=True) + self.rows = self.rows[unique] + self.cols = self.cols[unique] + self.vals = np.bincount(indices, weights=self.vals) + + def to_dense(self): + """ + Return a dense matrix representing self, mainly for debugging purposes. + """ + ret = np.zeros([self.n, self.m], dtype=np.float64) + nvals = self.vals.size + for i in range(nvals): + ret[self.rows[i], self.cols[i]] += self.vals[i] + return ret + + def __str__(self): + return self.to_dense().__str__() + + @property + def diag(self): + """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] + return diag + + +def _cg(A, b, x0=None, tol=1.e-10, maxiter=1000): + """ + Use Preconditioned Conjugate Gradient iteration to solve A x = b + A simple Jacobi (diagonal) preconditioner is used. + + Parameters + ---------- + 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 + ------- + x : array + The converged solution. + err : float + The absolute error np.linalg.norm(A.dot(x) - b) + """ + n = b.size + assert A.n == n + assert A.m == n + b_norm = np.linalg.norm(b) + + # Jacobi pre-conditioner + kvec = A.diag + # For diag elem < 1e-6 we keep 1e-6. + kvec = np.maximum(kvec, 1e-6) + + # Initial guess + if x0 is None: + x = np.zeros(n) + else: + x = x0 + + r = b - A.dot(x) + w = r/kvec + + p = np.zeros(n) + beta = 0.0 + rho = np.dot(r, w) + k = 0 + + # Following C. T. Kelley + while (np.sqrt(abs(rho)) > tol*b_norm) and (k < maxiter): + p = w + beta*p + z = A.dot(p) + alpha = rho/np.dot(p, z) + r = r - alpha*z + w = r/kvec + rhoold = rho + rho = np.dot(r, w) + x = x + alpha*p + beta = rho/rhoold + # err = np.linalg.norm(A.dot(x) - b) # absolute accuracy - not used + k += 1 + err = np.linalg.norm(A.dot(x) - b) + return x, err + + +# The following private functions: +# :func:`_safe_inv22_vectorized` +# :func:`_pseudo_inv22sym_vectorized` +# :func:`_scalar_vectorized` +# :func:`_transpose_vectorized` +# :func:`_roll_vectorized` +# :func:`_to_matrix_vectorized` +# :func:`_extract_submatrices` +# provide fast numpy implementation of some standard operations on arrays of +# matrices - stored as (:, n_rows, n_cols)-shaped np.arrays. + +# Development note: Dealing with pathologic 'flat' triangles in the +# CubicTriInterpolator code and impact on (2, 2)-matrix inversion functions +# :func:`_safe_inv22_vectorized` and :func:`_pseudo_inv22sym_vectorized`. +# +# Goals: +# 1) The CubicTriInterpolator should be able to handle flat or almost flat +# triangles without raising an error, +# 2) These degenerated triangles should have no impact on the automatic dof +# calculation (associated with null weight for the _DOF_estimator_geom and +# with null energy for the _DOF_estimator_min_E), +# 3) Linear patch test should be passed exactly on degenerated meshes, +# 4) Interpolation (with :meth:`_interpolate_single_key` or +# :meth:`_interpolate_multi_key`) shall be correctly handled even *inside* +# the pathologic triangles, to interact correctly with a TriRefiner class. +# +# Difficulties: +# Flat triangles have rank-deficient *J* (so-called jacobian matrix) and +# *metric* (the metric tensor = J x J.T). Computation of the local +# tangent plane is also problematic. +# +# Implementation: +# Most of the time, when computing the inverse of a rank-deficient matrix it +# is safe to simply return the null matrix (which is the implementation in +# :func:`_safe_inv22_vectorized`). This is because of point 2), itself +# enforced by: +# - null area hence null energy in :class:`_DOF_estimator_min_E` +# - angles close or equal to 0 or np.pi hence null weight in +# :class:`_DOF_estimator_geom`. +# Note that the function angle -> weight is continuous and maximum for an +# angle np.pi/2 (refer to :meth:`compute_geom_weights`) +# The exception is the computation of barycentric coordinates, which is done +# by inversion of the *metric* matrix. In this case, we need to compute a set +# of valid coordinates (1 among numerous possibilities), to ensure point 4). +# We benefit here from the symmetry of metric = J x J.T, which makes it easier +# to compute a pseudo-inverse in :func:`_pseudo_inv22sym_vectorized` +def _safe_inv22_vectorized(M): + """ + Inversion of arrays of (2, 2) matrices, returns 0 for rank-deficient + matrices. + + *M* : array of (2, 2) matrices to inverse, shape (n, 2, 2) + """ + _api.check_shape((None, 2, 2), M=M) + M_inv = np.empty_like(M) + prod1 = M[:, 0, 0]*M[:, 1, 1] + delta = prod1 - M[:, 0, 1]*M[:, 1, 0] + + # We set delta_inv to 0. in case of a rank deficient matrix; a + # rank-deficient input matrix *M* will lead to a null matrix in output + rank2 = (np.abs(delta) > 1e-8*np.abs(prod1)) + if np.all(rank2): + # Normal 'optimized' flow. + delta_inv = 1./delta + else: + # 'Pathologic' flow. + delta_inv = np.zeros(M.shape[0]) + delta_inv[rank2] = 1./delta[rank2] + + M_inv[:, 0, 0] = M[:, 1, 1]*delta_inv + M_inv[:, 0, 1] = -M[:, 0, 1]*delta_inv + M_inv[:, 1, 0] = -M[:, 1, 0]*delta_inv + M_inv[:, 1, 1] = M[:, 0, 0]*delta_inv + return M_inv + + +def _pseudo_inv22sym_vectorized(M): + """ + Inversion of arrays of (2, 2) SYMMETRIC matrices; returns the + (Moore-Penrose) pseudo-inverse for rank-deficient matrices. + + In case M is of rank 1, we have M = trace(M) x P where P is the orthogonal + projection on Im(M), and we return trace(M)^-1 x P == M / trace(M)**2 + In case M is of rank 0, we return the null matrix. + + *M* : array of (2, 2) matrices to inverse, shape (n, 2, 2) + """ + _api.check_shape((None, 2, 2), M=M) + M_inv = np.empty_like(M) + prod1 = M[:, 0, 0]*M[:, 1, 1] + delta = prod1 - M[:, 0, 1]*M[:, 1, 0] + rank2 = (np.abs(delta) > 1e-8*np.abs(prod1)) + + if np.all(rank2): + # Normal 'optimized' flow. + M_inv[:, 0, 0] = M[:, 1, 1] / delta + M_inv[:, 0, 1] = -M[:, 0, 1] / delta + M_inv[:, 1, 0] = -M[:, 1, 0] / delta + M_inv[:, 1, 1] = M[:, 0, 0] / delta + else: + # 'Pathologic' flow. + # Here we have to deal with 2 sub-cases + # 1) First sub-case: matrices of rank 2: + delta = delta[rank2] + M_inv[rank2, 0, 0] = M[rank2, 1, 1] / delta + M_inv[rank2, 0, 1] = -M[rank2, 0, 1] / delta + M_inv[rank2, 1, 0] = -M[rank2, 1, 0] / delta + M_inv[rank2, 1, 1] = M[rank2, 0, 0] / delta + # 2) Second sub-case: rank-deficient matrices of rank 0 and 1: + rank01 = ~rank2 + tr = M[rank01, 0, 0] + M[rank01, 1, 1] + tr_zeros = (np.abs(tr) < 1.e-8) + sq_tr_inv = (1.-tr_zeros) / (tr**2+tr_zeros) + # sq_tr_inv = 1. / tr**2 + M_inv[rank01, 0, 0] = M[rank01, 0, 0] * sq_tr_inv + M_inv[rank01, 0, 1] = M[rank01, 0, 1] * sq_tr_inv + M_inv[rank01, 1, 0] = M[rank01, 1, 0] * sq_tr_inv + M_inv[rank01, 1, 1] = M[rank01, 1, 1] * sq_tr_inv + + return M_inv + + +def _scalar_vectorized(scalar, M): + """ + Scalar product between scalars and matrices. + """ + return scalar[:, np.newaxis, np.newaxis]*M + + +def _transpose_vectorized(M): + """ + Transposition of an array of matrices *M*. + """ + return np.transpose(M, [0, 2, 1]) + + +def _roll_vectorized(M, roll_indices, axis): + """ + 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 + assert ndim == 3 + ndim_roll = roll_indices.ndim + assert ndim_roll == 1 + sh = M.shape + r, c = sh[-2:] + assert sh[0] == roll_indices.shape[0] + vec_indices = np.arange(sh[0], dtype=np.int32) + + # Builds the rolled matrix + M_roll = np.empty_like(M) + if axis == 0: + for ir in range(r): + for ic in range(c): + M_roll[:, ir, ic] = M[vec_indices, (-roll_indices+ir) % r, ic] + else: # 1 + for ir in range(r): + for ic in range(c): + M_roll[:, ir, ic] = M[vec_indices, ir, (-roll_indices+ic) % c] + return M_roll + + +def _to_matrix_vectorized(M): + """ + Build an array of matrices from individuals np.arrays of identical shapes. + + 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) + c_vec = np.asarray([len(item) for item in M]) + assert np.all(c_vec-c_vec[0] == 0) + r = len(M) + c = c_vec[0] + M00 = np.asarray(M[0][0]) + dt = M00.dtype + sh = [M00.shape[0], r, c] + M_ret = np.empty(sh, dtype=dt) + for irow in range(r): + for icol in range(c): + M_ret[:, irow, icol] = np.asarray(M[irow][icol]) + return M_ret + + +def _extract_submatrices(M, block_indices, block_size, axis): + """ + Extract selected blocks of a matrices *M* depending on parameters + *block_indices* and *block_size*. + + Returns the array of extracted matrices *Mres* so that :: + + M_res[..., ir, :] = M[(block_indices*block_size+ir), :] + """ + assert block_indices.ndim == 1 + assert axis in [0, 1] + + r, c = M.shape + if axis == 0: + sh = [block_indices.shape[0], block_size, c] + else: # 1 + sh = [block_indices.shape[0], r, block_size] + + dt = M.dtype + M_res = np.empty(sh, dtype=dt) + if axis == 0: + for ir in range(block_size): + M_res[:, ir, :] = M[(block_indices*block_size+ir), :] + else: # 1 + for ic in range(block_size): + M_res[:, :, ic] = M[:, (block_indices*block_size+ic)] + + return M_res diff --git a/lib/matplotlib/tri/_tripcolor.py b/lib/matplotlib/tri/_tripcolor.py new file mode 100644 index 000000000000..3c252cdbc31b --- /dev/null +++ b/lib/matplotlib/tri/_tripcolor.py @@ -0,0 +1,154 @@ +import numpy as np + +from matplotlib import _api +from matplotlib.collections import PolyCollection, TriMesh +from matplotlib.colors import Normalize +from matplotlib.tri._triangulation import Triangulation + + +def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None, + vmax=None, shading='flat', facecolors=None, **kwargs): + """ + Create a pseudocolor plot of an unstructured triangular grid. + + Call signatures:: + + tripcolor(triangulation, c, *, ...) + tripcolor(x, y, c, *, [triangles=triangles], [mask=mask], ...) + + The triangular grid can be specified either by passing a `.Triangulation` + object as the first parameter, or by passing the points *x*, *y* and + optionally the *triangles* and a *mask*. See `.Triangulation` for an + explanation of these parameters. + + It is possible to pass the triangles positionally, i.e. + ``tripcolor(x, y, triangles, c, ...)``. However, this is discouraged. + For more clarity, pass *triangles* via keyword argument. + + If neither of *triangulation* or *triangles* are given, the triangulation + is calculated on the fly. In this case, it does not make sense to provide + colors at the triangle faces via *c* or *facecolors* because there are + multiple possible triangulations for a group of points and you don't know + which triangles will be constructed. + + Parameters + ---------- + triangulation : `.Triangulation` + An already created triangular grid. + x, y, triangles, mask + Parameters defining the triangular grid. See `.Triangulation`. + This is mutually exclusive with specifying *triangulation*. + c : array-like + The color values, either for the points or for the triangles. Which one + is automatically inferred from the length of *c*, i.e. does it match + the number of points or the number of triangles. If there are the same + number of points and triangles in the triangulation it is assumed that + color values are defined at points; to force the use of color values at + triangles use the keyword argument ``facecolors=c`` instead of just + ``c``. + This parameter is position-only. + facecolors : array-like, optional + Can be used alternatively to *c* to specify colors at the triangle + faces. This parameter takes precedence over *c*. + shading : {'flat', 'gouraud'}, default: 'flat' + If 'flat' and the color values *c* are defined at points, the color + values used for each triangle are from the mean c of the triangle's + three points. If *shading* is 'gouraud' then color values must be + defined at points. + other_parameters + All other parameters are the same as for `~.Axes.pcolor`. + """ + _api.check_in_list(['flat', 'gouraud'], shading=shading) + + tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs) + + # Parse the color to be in one of (the other variable will be None): + # - facecolors: if specified at the triangle faces + # - point_colors: if specified at the points + if facecolors is not None: + if args: + _api.warn_external( + "Positional parameter c has no effect when the keyword " + "facecolors is given") + point_colors = None + if len(facecolors) != len(tri.triangles): + raise ValueError("The length of facecolors must match the number " + "of triangles") + else: + # Color from positional parameter c + if not args: + raise TypeError( + "tripcolor() missing 1 required positional argument: 'c'; or " + "1 required keyword-only argument: 'facecolors'") + elif len(args) > 1: + _api.warn_deprecated( + "3.6", message=f"Additional positional parameters " + f"{args[1:]!r} are ignored; support for them is deprecated " + f"since %(since)s and will be removed %(removal)s") + c = np.asarray(args[0]) + if len(c) == len(tri.x): + # having this before the len(tri.triangles) comparison gives + # precedence to nodes if there are as many nodes as triangles + point_colors = c + facecolors = None + elif len(c) == len(tri.triangles): + point_colors = None + facecolors = c + else: + raise ValueError('The length of c must match either the number ' + 'of points or the number of triangles') + + # Handling of linewidths, shading, edgecolors and antialiased as + # in Axes.pcolor + linewidths = (0.25,) + if 'linewidth' in kwargs: + kwargs['linewidths'] = kwargs.pop('linewidth') + kwargs.setdefault('linewidths', linewidths) + + edgecolors = 'none' + if 'edgecolor' in kwargs: + kwargs['edgecolors'] = kwargs.pop('edgecolor') + ec = kwargs.setdefault('edgecolors', edgecolors) + + if 'antialiased' in kwargs: + kwargs['antialiaseds'] = kwargs.pop('antialiased') + if 'antialiaseds' not in kwargs and ec.lower() == "none": + kwargs['antialiaseds'] = False + + _api.check_isinstance((Normalize, None), norm=norm) + if shading == 'gouraud': + if facecolors is not None: + raise ValueError( + "shading='gouraud' can only be used when the colors " + "are specified at the points, not at the faces.") + collection = TriMesh(tri, alpha=alpha, array=point_colors, + cmap=cmap, norm=norm, **kwargs) + else: # 'flat' + # Vertices of triangles. + maskedTris = tri.get_masked_triangles() + verts = np.stack((tri.x[maskedTris], tri.y[maskedTris]), axis=-1) + + # Color values. + if facecolors is None: + # One color per triangle, the mean of the 3 vertex color values. + colors = point_colors[maskedTris].mean(axis=1) + elif tri.mask is not None: + # Remove color values of masked triangles. + colors = facecolors[~tri.mask] + else: + colors = facecolors + collection = PolyCollection(verts, alpha=alpha, array=colors, + cmap=cmap, norm=norm, **kwargs) + + collection._scale_norm(norm, vmin, vmax) + ax.grid(False) + + minx = tri.x.min() + maxx = tri.x.max() + miny = tri.y.min() + maxy = tri.y.max() + corners = (minx, miny), (maxx, maxy) + ax.update_datalim(corners) + ax.autoscale_view() + ax.add_collection(collection) + return collection diff --git a/lib/matplotlib/tri/_triplot.py b/lib/matplotlib/tri/_triplot.py new file mode 100644 index 000000000000..6168946b1531 --- /dev/null +++ b/lib/matplotlib/tri/_triplot.py @@ -0,0 +1,86 @@ +import numpy as np +from matplotlib.tri._triangulation import Triangulation +import matplotlib.cbook as cbook +import matplotlib.lines as mlines + + +def triplot(ax, *args, **kwargs): + """ + Draw an unstructured triangular grid as lines and/or markers. + + Call signatures:: + + triplot(triangulation, ...) + triplot(x, y, [triangles], *, [mask=mask], ...) + + The triangular grid can be specified either by passing a `.Triangulation` + object as the first parameter, or by passing the points *x*, *y* and + optionally the *triangles* and a *mask*. If neither of *triangulation* or + *triangles* are given, the triangulation is calculated on the fly. + + Parameters + ---------- + triangulation : `.Triangulation` + An already created triangular grid. + x, y, triangles, mask + Parameters defining the triangular grid. See `.Triangulation`. + This is mutually exclusive with specifying *triangulation*. + other_parameters + All other args and kwargs are forwarded to `~.Axes.plot`. + + Returns + ------- + lines : `~matplotlib.lines.Line2D` + The drawn triangles edges. + markers : `~matplotlib.lines.Line2D` + The drawn marker nodes. + """ + import matplotlib.axes + + tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs) + x, y, edges = (tri.x, tri.y, tri.edges) + + # Decode plot format string, e.g., 'ro-' + fmt = args[0] if args else "" + linestyle, marker, color = matplotlib.axes._base._process_plot_format(fmt) + + # Insert plot format string into a copy of kwargs (kwargs values prevail). + kw = cbook.normalize_kwargs(kwargs, mlines.Line2D) + for key, val in zip(('linestyle', 'marker', 'color'), + (linestyle, marker, color)): + if val is not None: + kw.setdefault(key, val) + + # Draw lines without markers. + # Note 1: If we drew markers here, most markers would be drawn more than + # once as they belong to several edges. + # Note 2: We insert nan values in the flattened edges arrays rather than + # plotting directly (triang.x[edges].T, triang.y[edges].T) + # as it considerably speeds-up code execution. + linestyle = kw['linestyle'] + kw_lines = { + **kw, + 'marker': 'None', # No marker to draw. + 'zorder': kw.get('zorder', 1), # Path default zorder is used. + } + if linestyle not in [None, 'None', '', ' ']: + tri_lines_x = np.insert(x[edges], 2, np.nan, axis=1) + tri_lines_y = np.insert(y[edges], 2, np.nan, axis=1) + tri_lines = ax.plot(tri_lines_x.ravel(), tri_lines_y.ravel(), + **kw_lines) + else: + tri_lines = ax.plot([], [], **kw_lines) + + # Draw markers separately. + marker = kw['marker'] + kw_markers = { + **kw, + 'linestyle': 'None', # No line to draw. + } + kw_markers.pop('label', None) + if marker not in [None, 'None', '', ' ']: + tri_markers = ax.plot(x, y, **kw_markers) + else: + tri_markers = ax.plot([], [], **kw_markers) + + return tri_lines + tri_markers diff --git a/lib/matplotlib/tri/_trirefine.py b/lib/matplotlib/tri/_trirefine.py new file mode 100644 index 000000000000..a0a57935fb99 --- /dev/null +++ b/lib/matplotlib/tri/_trirefine.py @@ -0,0 +1,307 @@ +""" +Mesh refinement for triangular grids. +""" + +import numpy as np + +from matplotlib import _api +from matplotlib.tri._triangulation import Triangulation +import matplotlib.tri._triinterpolate + + +class TriRefiner: + """ + Abstract base class for classes implementing mesh refinement. + + A TriRefiner encapsulates a Triangulation object and provides tools for + mesh refinement and interpolation. + + Derived classes must implement: + + - ``refine_triangulation(return_tri_index=False, **kwargs)`` , where + the optional keyword arguments *kwargs* are defined in each + TriRefiner concrete implementation, and which returns: + + - a refined triangulation, + - optionally (depending on *return_tri_index*), for each + point of the refined triangulation: the index of + the initial triangulation triangle to which it belongs. + + - ``refine_field(z, triinterpolator=None, **kwargs)``, where: + + - *z* array of field values (to refine) defined at the base + triangulation nodes, + - *triinterpolator* is an optional `~matplotlib.tri.TriInterpolator`, + - the other optional keyword arguments *kwargs* are defined in + each TriRefiner concrete implementation; + + and which returns (as a tuple) a refined triangular mesh and the + interpolated values of the field at the refined triangulation nodes. + """ + + def __init__(self, triangulation): + _api.check_isinstance(Triangulation, triangulation=triangulation) + self._triangulation = triangulation + + +class UniformTriRefiner(TriRefiner): + """ + Uniform mesh refinement by recursive subdivisions. + + Parameters + ---------- + triangulation : `~matplotlib.tri.Triangulation` + The encapsulated triangulation (to be refined) + """ +# See Also +# -------- +# :class:`~matplotlib.tri.CubicTriInterpolator` and +# :class:`~matplotlib.tri.TriAnalyzer`. +# """ + def __init__(self, triangulation): + super().__init__(triangulation) + + def refine_triangulation(self, return_tri_index=False, subdiv=3): + """ + Compute a uniformly refined triangulation *refi_triangulation* of + the encapsulated :attr:`triangulation`. + + This function refines the encapsulated triangulation by splitting each + father triangle into 4 child sub-triangles built on the edges midside + nodes, recursing *subdiv* times. In the end, each triangle is hence + divided into ``4**subdiv`` child triangles. + + Parameters + ---------- + return_tri_index : bool, default: False + Whether an index table indicating the father triangle index of each + point is returned. + subdiv : int, default: 3 + Recursion level for the subdivision. + Each triangle is divided into ``4**subdiv`` child triangles; + hence, the default results in 64 refined subtriangles for each + triangle of the initial triangulation. + + Returns + ------- + refi_triangulation : `~matplotlib.tri.Triangulation` + The refined triangulation. + found_index : int array + Index of the initial triangulation containing triangle, for each + point of *refi_triangulation*. + Returned only if *return_tri_index* is set to True. + """ + refi_triangulation = self._triangulation + ntri = refi_triangulation.triangles.shape[0] + + # Computes the triangulation ancestors numbers in the reference + # triangulation. + ancestors = np.arange(ntri, dtype=np.int32) + for _ in range(subdiv): + refi_triangulation, ancestors = self._refine_triangulation_once( + refi_triangulation, ancestors) + refi_npts = refi_triangulation.x.shape[0] + refi_triangles = refi_triangulation.triangles + + # Now we compute found_index table if needed + if return_tri_index: + # We have to initialize found_index with -1 because some nodes + # may very well belong to no triangle at all, e.g., in case of + # Delaunay Triangulation with DuplicatePointWarning. + found_index = np.full(refi_npts, -1, dtype=np.int32) + tri_mask = self._triangulation.mask + if tri_mask is None: + found_index[refi_triangles] = np.repeat(ancestors, + 3).reshape(-1, 3) + else: + # There is a subtlety here: we want to avoid whenever possible + # that refined points container is a masked triangle (which + # would result in artifacts in plots). + # So we impose the numbering from masked ancestors first, + # then overwrite it with unmasked ancestor numbers. + ancestor_mask = tri_mask[ancestors] + found_index[refi_triangles[ancestor_mask, :] + ] = np.repeat(ancestors[ancestor_mask], + 3).reshape(-1, 3) + found_index[refi_triangles[~ancestor_mask, :] + ] = np.repeat(ancestors[~ancestor_mask], + 3).reshape(-1, 3) + return refi_triangulation, found_index + else: + return refi_triangulation + + def refine_field(self, z, triinterpolator=None, subdiv=3): + """ + Refine a field defined on the encapsulated triangulation. + + Parameters + ---------- + z : (npoints,) array-like + Values of the field to refine, defined at the nodes of the + encapsulated triangulation. (``n_points`` is the number of points + in the initial triangulation) + triinterpolator : `~matplotlib.tri.TriInterpolator`, optional + Interpolator used for field interpolation. If not specified, + a `~matplotlib.tri.CubicTriInterpolator` will be used. + subdiv : int, default: 3 + Recursion level for the subdivision. + Each triangle is divided into ``4**subdiv`` child triangles. + + Returns + ------- + refi_tri : `~matplotlib.tri.Triangulation` + The returned refined triangulation. + refi_z : 1D array of length: *refi_tri* node count. + The returned interpolated field (at *refi_tri* nodes). + """ + if triinterpolator is None: + interp = matplotlib.tri.CubicTriInterpolator( + self._triangulation, z) + else: + _api.check_isinstance(matplotlib.tri.TriInterpolator, + triinterpolator=triinterpolator) + interp = triinterpolator + + refi_tri, found_index = self.refine_triangulation( + subdiv=subdiv, return_tri_index=True) + refi_z = interp._interpolate_multikeys( + refi_tri.x, refi_tri.y, tri_index=found_index)[0] + return refi_tri, refi_z + + @staticmethod + def _refine_triangulation_once(triangulation, ancestors=None): + """ + 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: + child_triangulation. + + If the array-like key table *ancestor* is given, it shall be of shape + (ntri,) where ntri is the number of *triangulation* masked_triangles. + In this case, the function returns + (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 + + # According to tri.triangulation doc: + # neighbors[i, j] is the triangle that is the neighbor + # to the edge from point index masked_triangles[i, j] to point + # index masked_triangles[i, (j+1)%3]. + neighbors = triangulation.neighbors + triangles = triangulation.triangles + npts = np.shape(x)[0] + ntri = np.shape(triangles)[0] + if ancestors is not None: + ancestors = np.asarray(ancestors) + if np.shape(ancestors) != (ntri,): + raise ValueError( + "Incompatible shapes provide for triangulation" + ".masked_triangles and ancestors: {0} and {1}".format( + np.shape(triangles), np.shape(ancestors))) + + # Initiating tables refi_x and refi_y of the refined triangulation + # points + # hint: each apex is shared by 2 masked_triangles except the borders. + borders = np.sum(neighbors == -1) + added_pts = (3*ntri + borders) // 2 + refi_npts = npts + added_pts + refi_x = np.zeros(refi_npts) + refi_y = np.zeros(refi_npts) + + # First part of refi_x, refi_y is just the initial points + refi_x[:npts] = x + refi_y[:npts] = y + + # Second part contains the edge midside nodes. + # Each edge belongs to 1 triangle (if border edge) or is shared by 2 + # masked_triangles (interior edge). + # We first build 2 * ntri arrays of edge starting nodes (edge_elems, + # edge_apexes); we then extract only the masters to avoid overlaps. + # The so-called 'master' is the triangle with biggest index + # The 'slave' is the triangle with lower index + # (can be -1 if border edge) + # For slave and master we will identify the apex pointing to the edge + # start + edge_elems = np.tile(np.arange(ntri, dtype=np.int32), 3) + edge_apexes = np.repeat(np.arange(3, dtype=np.int32), ntri) + edge_neighbors = neighbors[edge_elems, edge_apexes] + mask_masters = (edge_elems > edge_neighbors) + + # Identifying the "masters" and adding to refi_x, refi_y vec + masters = edge_elems[mask_masters] + apex_masters = edge_apexes[mask_masters] + x_add = (x[triangles[masters, apex_masters]] + + x[triangles[masters, (apex_masters+1) % 3]]) * 0.5 + y_add = (y[triangles[masters, apex_masters]] + + y[triangles[masters, (apex_masters+1) % 3]]) * 0.5 + refi_x[npts:] = x_add + refi_y[npts:] = y_add + + # Building the new masked_triangles; each old masked_triangles hosts + # 4 new masked_triangles + # there are 6 pts to identify per 'old' triangle, 3 new_pt_corner and + # 3 new_pt_midside + new_pt_corner = triangles + + # What is the index in refi_x, refi_y of point at middle of apex iapex + # of elem ielem ? + # If ielem is the apex master: simple count, given the way refi_x was + # built. + # If ielem is the apex slave: yet we do not know; but we will soon + # using the neighbors table. + new_pt_midside = np.empty([ntri, 3], dtype=np.int32) + cum_sum = npts + for imid in range(3): + mask_st_loc = (imid == apex_masters) + n_masters_loc = np.sum(mask_st_loc) + elem_masters_loc = masters[mask_st_loc] + new_pt_midside[:, imid][elem_masters_loc] = np.arange( + n_masters_loc, dtype=np.int32) + cum_sum + cum_sum += n_masters_loc + + # Now dealing with slave elems. + # for each slave element we identify the master and then the inode + # once slave_masters is identified, slave_masters_apex is such that: + # neighbors[slaves_masters, slave_masters_apex] == slaves + mask_slaves = np.logical_not(mask_masters) + slaves = edge_elems[mask_slaves] + slaves_masters = edge_neighbors[mask_slaves] + diff_table = np.abs(neighbors[slaves_masters, :] - + np.outer(slaves, np.ones(3, dtype=np.int32))) + slave_masters_apex = np.argmin(diff_table, axis=1) + slaves_apex = edge_apexes[mask_slaves] + new_pt_midside[slaves, slaves_apex] = new_pt_midside[ + slaves_masters, slave_masters_apex] + + # Builds the 4 child masked_triangles + child_triangles = np.empty([ntri*4, 3], dtype=np.int32) + child_triangles[0::4, :] = np.vstack([ + new_pt_corner[:, 0], new_pt_midside[:, 0], + new_pt_midside[:, 2]]).T + child_triangles[1::4, :] = np.vstack([ + new_pt_corner[:, 1], new_pt_midside[:, 1], + new_pt_midside[:, 0]]).T + child_triangles[2::4, :] = np.vstack([ + new_pt_corner[:, 2], new_pt_midside[:, 2], + new_pt_midside[:, 1]]).T + child_triangles[3::4, :] = np.vstack([ + new_pt_midside[:, 0], new_pt_midside[:, 1], + new_pt_midside[:, 2]]).T + child_triangulation = Triangulation(refi_x, refi_y, child_triangles) + + # Builds the child mask + if triangulation.mask is not None: + child_triangulation.set_mask(np.repeat(triangulation.mask, 4)) + + if ancestors is None: + return child_triangulation + else: + return child_triangulation, np.repeat(ancestors, 4) diff --git a/lib/matplotlib/tri/_tritools.py b/lib/matplotlib/tri/_tritools.py new file mode 100644 index 000000000000..9837309f7e81 --- /dev/null +++ b/lib/matplotlib/tri/_tritools.py @@ -0,0 +1,263 @@ +""" +Tools for triangular grids. +""" + +import numpy as np + +from matplotlib import _api +from matplotlib.tri import Triangulation + + +class TriAnalyzer: + """ + Define basic tools for triangular mesh analysis and improvement. + + A TriAnalyzer encapsulates a `.Triangulation` object and provides basic + tools for mesh analysis and mesh improvement. + + Attributes + ---------- + scale_factors + + Parameters + ---------- + triangulation : `~matplotlib.tri.Triangulation` + The encapsulated triangulation to analyze. + """ + + def __init__(self, triangulation): + _api.check_isinstance(Triangulation, triangulation=triangulation) + self._triangulation = triangulation + + @property + def scale_factors(self): + """ + Factors to rescale the triangulation into a unit square. + + Returns + ------- + (float, float) + Scaling factors (kx, ky) so that the triangulation + ``[triangulation.x * kx, triangulation.y * ky]`` + fits exactly inside a unit square. + """ + compressed_triangles = self._triangulation.get_masked_triangles() + node_used = (np.bincount(np.ravel(compressed_triangles), + minlength=self._triangulation.x.size) != 0) + return (1 / np.ptp(self._triangulation.x[node_used]), + 1 / np.ptp(self._triangulation.y[node_used])) + + def circle_ratios(self, rescale=True): + """ + Return a measure of the triangulation triangles flatness. + + The ratio of the incircle radius over the circumcircle radius is a + widely used indicator of a triangle flatness. + It is always ``<= 0.5`` and ``== 0.5`` only for equilateral + triangles. Circle ratios below 0.01 denote very flat triangles. + + To avoid unduly low values due to a difference of scale between the 2 + axis, the triangular mesh can first be rescaled to fit inside a unit + square with `scale_factors` (Only if *rescale* is True, which is + its default value). + + Parameters + ---------- + rescale : bool, default: True + If True, internally rescale (based on `scale_factors`), so that the + (unmasked) triangles fit exactly inside a unit square mesh. + + Returns + ------- + masked array + Ratio of the incircle radius over the circumcircle radius, for + each 'rescaled' triangle of the encapsulated triangulation. + Values corresponding to masked triangles are masked out. + + """ + # Coords rescaling + if rescale: + (kx, ky) = self.scale_factors + else: + (kx, ky) = (1.0, 1.0) + pts = np.vstack([self._triangulation.x*kx, + self._triangulation.y*ky]).T + tri_pts = pts[self._triangulation.triangles] + # Computes the 3 side lengths + a = tri_pts[:, 1, :] - tri_pts[:, 0, :] + b = tri_pts[:, 2, :] - tri_pts[:, 1, :] + c = tri_pts[:, 0, :] - tri_pts[:, 2, :] + a = np.hypot(a[:, 0], a[:, 1]) + b = np.hypot(b[:, 0], b[:, 1]) + c = np.hypot(c[:, 0], c[:, 1]) + # circumcircle and incircle radii + s = (a+b+c)*0.5 + prod = s*(a+b-s)*(a+c-s)*(b+c-s) + # We have to deal with flat triangles with infinite circum_radius + bool_flat = (prod == 0.) + if np.any(bool_flat): + # Pathologic flow + ntri = tri_pts.shape[0] + circum_radius = np.empty(ntri, dtype=np.float64) + circum_radius[bool_flat] = np.inf + abc = a*b*c + circum_radius[~bool_flat] = abc[~bool_flat] / ( + 4.0*np.sqrt(prod[~bool_flat])) + else: + # Normal optimized flow + circum_radius = (a*b*c) / (4.0*np.sqrt(prod)) + in_radius = (a*b*c) / (4.0*circum_radius*s) + circle_ratio = in_radius/circum_radius + mask = self._triangulation.mask + if mask is None: + return circle_ratio + else: + return np.ma.array(circle_ratio, mask=mask) + + def get_flat_tri_mask(self, min_circle_ratio=0.01, rescale=True): + """ + Eliminate excessively flat border triangles from the triangulation. + + Returns a mask *new_mask* which allows to clean the encapsulated + triangulation from its border-located flat triangles + (according to their :meth:`circle_ratios`). + This mask is meant to be subsequently applied to the triangulation + using `.Triangulation.set_mask`. + *new_mask* is an extension of the initial triangulation mask + in the sense that an initially masked triangle will remain masked. + + The *new_mask* array is computed recursively; at each step flat + triangles are removed only if they share a side with the current mesh + border. Thus, no new holes in the triangulated domain will be created. + + Parameters + ---------- + min_circle_ratio : float, default: 0.01 + Border triangles with incircle/circumcircle radii ratio r/R will + be removed if r/R < *min_circle_ratio*. + rescale : bool, default: True + 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 + ------- + array of bool + Mask to apply to encapsulated triangulation. + All the initially masked triangles remain masked in the + *new_mask*. + + Notes + ----- + The rationale behind this function is that a Delaunay + triangulation - of an unstructured set of points - sometimes contains + almost flat triangles at its border, leading to artifacts in plots + (especially for high-resolution contouring). + Masked with computed *new_mask*, the encapsulated + triangulation would contain no more unmasked border triangles + with a circle ratio below *min_circle_ratio*, thus improving the + mesh quality for subsequent plots or interpolation. + """ + # Recursively computes the mask_current_borders, true if a triangle is + # at the border of the mesh OR touching the border through a chain of + # invalid aspect ratio masked_triangles. + ntri = self._triangulation.triangles.shape[0] + mask_bad_ratio = self.circle_ratios(rescale) < min_circle_ratio + + current_mask = self._triangulation.mask + if current_mask is None: + current_mask = np.zeros(ntri, dtype=bool) + valid_neighbors = np.copy(self._triangulation.neighbors) + renum_neighbors = np.arange(ntri, dtype=np.int32) + nadd = -1 + while nadd != 0: + # The active wavefront is the triangles from the border (unmasked + # but with a least 1 neighbor equal to -1 + wavefront = (np.min(valid_neighbors, axis=1) == -1) & ~current_mask + # The element from the active wavefront will be masked if their + # circle ratio is bad. + added_mask = wavefront & mask_bad_ratio + current_mask = added_mask | current_mask + nadd = np.sum(added_mask) + + # now we have to update the tables valid_neighbors + valid_neighbors[added_mask, :] = -1 + renum_neighbors[added_mask] = -1 + valid_neighbors = np.where(valid_neighbors == -1, -1, + renum_neighbors[valid_neighbors]) + + return np.ma.filled(current_mask, True) + + def _get_compressed_triangulation(self): + """ + Compress (if masked) the encapsulated triangulation. + + Returns minimal-length triangles array (*compressed_triangles*) and + coordinates arrays (*compressed_x*, *compressed_y*) that can still + describe the unmasked triangles of the encapsulated triangulation. + + Returns + ------- + compressed_triangles : array-like + the returned compressed triangulation triangles + compressed_x : array-like + the returned compressed triangulation 1st coordinate + compressed_y : array-like + the returned compressed triangulation 2nd coordinate + tri_renum : int array + renumbering table to translate the triangle numbers from the + encapsulated triangulation into the new (compressed) renumbering. + -1 for masked triangles (deleted from *compressed_triangles*). + node_renum : int array + renumbering table to translate the point numbers from the + encapsulated triangulation into the new (compressed) renumbering. + -1 for unused points (i.e. those deleted from *compressed_x* and + *compressed_y*). + + """ + # Valid triangles and renumbering + tri_mask = self._triangulation.mask + compressed_triangles = self._triangulation.get_masked_triangles() + ntri = self._triangulation.triangles.shape[0] + if tri_mask is not None: + tri_renum = self._total_to_compress_renum(~tri_mask) + else: + tri_renum = np.arange(ntri, dtype=np.int32) + + # Valid nodes and renumbering + valid_node = (np.bincount(np.ravel(compressed_triangles), + minlength=self._triangulation.x.size) != 0) + compressed_x = self._triangulation.x[valid_node] + compressed_y = self._triangulation.y[valid_node] + node_renum = self._total_to_compress_renum(valid_node) + + # Now renumbering the valid triangles nodes + compressed_triangles = node_renum[compressed_triangles] + + return (compressed_triangles, compressed_x, compressed_y, tri_renum, + node_renum) + + @staticmethod + def _total_to_compress_renum(valid): + """ + Parameters + ---------- + valid : 1D bool array + Validity mask. + + Returns + ------- + int array + Array so that (`valid_array` being a compressed array + based on a `masked_array` with mask ~*valid*): + + - For all i with valid[i] = True: + valid_array[renum[i]] = masked_array[i] + - For all i with valid[i] = False: + renum[i] = -1 (invalid value) + """ + renum = np.full(np.size(valid), -1, dtype=np.int32) + n_valid = np.sum(valid) + renum[valid] = np.arange(n_valid, dtype=np.int32) + return renum diff --git a/lib/matplotlib/tri/triangulation.py b/lib/matplotlib/tri/triangulation.py index 24e99634a466..c48b09b280ff 100644 --- a/lib/matplotlib/tri/triangulation.py +++ b/lib/matplotlib/tri/triangulation.py @@ -1,220 +1,9 @@ -import numpy as np +from ._triangulation import * # noqa: F401, F403 +from matplotlib import _api -class Triangulation: - """ - An unstructured triangular grid consisting of npoints points and - ntri triangles. The triangles can either be specified by the user - or automatically generated using a Delaunay triangulation. - - Parameters - ---------- - x, y : (npoints,) array-like - Coordinates of grid points. - triangles : (ntri, 3) array-like of int, optional - For each triangle, the indices of the three points that make - up the triangle, ordered in an anticlockwise manner. If not - specified, the Delaunay triangulation is calculated. - mask : (ntri,) array-like of bool, optional - Which triangles are masked out. - - Attributes - ---------- - triangles : (ntri, 3) array of int - For each triangle, the indices of the three points that make - up the triangle, ordered in an anticlockwise manner. If you want to - take the *mask* into account, use `get_masked_triangles` instead. - mask : (ntri, 3) array of bool - Masked out triangles. - is_delaunay : bool - Whether the Triangulation is a calculated Delaunay - triangulation (where *triangles* was not specified) or not. - - Notes - ----- - For a Triangulation to be valid it must not have duplicate points, - triangles formed from colinear points, or overlapping triangles. - """ - def __init__(self, x, y, triangles=None, mask=None): - from matplotlib import _qhull - - self.x = np.asarray(x, dtype=np.float64) - self.y = np.asarray(y, dtype=np.float64) - if self.x.shape != self.y.shape or self.x.ndim != 1: - raise ValueError("x and y must be equal-length 1D arrays") - - self.mask = None - self._edges = None - self._neighbors = None - self.is_delaunay = False - - if triangles is None: - # No triangulation specified, so use matplotlib._qhull to obtain - # Delaunay triangulation. - self.triangles, self._neighbors = _qhull.delaunay(x, y) - self.is_delaunay = True - else: - # Triangulation specified. Copy, since we may correct triangle - # orientation. - self.triangles = np.array(triangles, dtype=np.int32, order='C') - if self.triangles.ndim != 2 or self.triangles.shape[1] != 3: - raise ValueError('triangles must be a (?, 3) array') - if self.triangles.max() >= len(self.x): - raise ValueError('triangles max element is out of bounds') - if self.triangles.min() < 0: - raise ValueError('triangles min element is out of bounds') - - if mask is not None: - self.mask = np.asarray(mask, dtype=bool) - if self.mask.shape != (self.triangles.shape[0],): - raise ValueError('mask array must have same length as ' - 'triangles array') - - # Underlying C++ object is not created until first needed. - self._cpp_triangulation = None - - # Default TriFinder not created until needed. - self._trifinder = None - - def calculate_plane_coefficients(self, z): - """ - Calculate plane equation coefficients for all unmasked triangles from - the point (x, y) coordinates and specified z-array of shape (npoints). - The returned array has shape (npoints, 3) and allows z-value at (x, y) - position in triangle tri to be calculated using - ``z = array[tri, 0] * x + array[tri, 1] * y + array[tri, 2]``. - """ - return self.get_cpp_triangulation().calculate_plane_coefficients(z) - - @property - def edges(self): - """ - Return integer array of shape (nedges, 2) containing all edges of - non-masked triangles. - - Each row defines an edge by it's start point index and end point - index. Each edge appears only once, i.e. for an edge between points - *i* and *j*, there will only be either *(i, j)* or *(j, i)*. - """ - if self._edges is None: - self._edges = self.get_cpp_triangulation().get_edges() - return self._edges - - def get_cpp_triangulation(self): - """ - Return the underlying C++ Triangulation object, creating it - if necessary. - """ - from matplotlib import _tri - if self._cpp_triangulation is None: - self._cpp_triangulation = _tri.Triangulation( - self.x, self.y, self.triangles, self.mask, self._edges, - self._neighbors, not self.is_delaunay) - return self._cpp_triangulation - - def get_masked_triangles(self): - """ - Return an array of triangles that are not masked. - """ - if self.mask is not None: - return self.triangles[~self.mask] - else: - return self.triangles - - @staticmethod - def get_from_args_and_kwargs(*args, **kwargs): - """ - Return a Triangulation object from the args and kwargs, and - the remaining args and kwargs with the consumed values removed. - - There are two alternatives: either the first argument is a - Triangulation object, in which case it is returned, or the args - and kwargs are sufficient to create a new Triangulation to - return. In the latter case, see Triangulation.__init__ for - the possible args and kwargs. - """ - if isinstance(args[0], Triangulation): - triangulation, *args = args - else: - x, y, *args = args - - # Check triangles in kwargs then args. - triangles = kwargs.pop('triangles', None) - from_args = False - if triangles is None and args: - triangles = args[0] - from_args = True - - if triangles is not None: - try: - triangles = np.asarray(triangles, dtype=np.int32) - except ValueError: - triangles = None - - if triangles is not None and (triangles.ndim != 2 or - triangles.shape[1] != 3): - triangles = None - - if triangles is not None and from_args: - args = args[1:] # Consumed first item in args. - - # Check for mask in kwargs. - mask = kwargs.pop('mask', None) - - triangulation = Triangulation(x, y, triangles, mask) - return triangulation, args, kwargs - - def get_trifinder(self): - """ - Return the default `matplotlib.tri.TriFinder` of this - triangulation, creating it if necessary. This allows the same - TriFinder object to be easily shared. - """ - if self._trifinder is None: - # Default TriFinder class. - from matplotlib.tri.trifinder import TrapezoidMapTriFinder - self._trifinder = TrapezoidMapTriFinder(self) - return self._trifinder - - @property - def neighbors(self): - """ - Return integer array of shape (ntri, 3) containing neighbor triangles. - - For each triangle, the indices of the three triangles that - share the same edges, or -1 if there is no such neighboring - triangle. ``neighbors[i, j]`` is the triangle that is the neighbor - to the edge from point index ``triangles[i, j]`` to point index - ``triangles[i, (j+1)%3]``. - """ - if self._neighbors is None: - self._neighbors = self.get_cpp_triangulation().get_neighbors() - return self._neighbors - - def set_mask(self, mask): - """ - Set or clear the mask array. - - Parameters - ---------- - mask : None or bool array of length ntri - """ - if mask is None: - self.mask = None - else: - self.mask = np.asarray(mask, dtype=bool) - if self.mask.shape != (self.triangles.shape[0],): - raise ValueError('mask array must have same length as ' - 'triangles array') - - # Set mask in C++ Triangulation. - if self._cpp_triangulation is not None: - self._cpp_triangulation.set_mask(self.mask) - - # Clear derived fields so they are recalculated when needed. - self._edges = None - self._neighbors = None - - # Recalculate TriFinder if it exists. - if self._trifinder is not None: - self._trifinder._initialize() +_api.warn_deprecated( + "3.7", + message=f"Importing {__name__} was deprecated in Matplotlib 3.7 and will " + f"be removed two minor releases later. All functionality is " + f"available via the top-level module matplotlib.tri") diff --git a/lib/matplotlib/tri/tricontour.py b/lib/matplotlib/tri/tricontour.py index a9f1339ef5b2..37406451d376 100644 --- a/lib/matplotlib/tri/tricontour.py +++ b/lib/matplotlib/tri/tricontour.py @@ -1,323 +1,9 @@ -import numpy as np +from ._tricontour import * # noqa: F401, F403 +from matplotlib import _api -from matplotlib import docstring -from matplotlib.contour import ContourSet -from matplotlib.tri.triangulation import Triangulation - -@docstring.dedent_interpd -class TriContourSet(ContourSet): - """ - Create and store a set of contour lines or filled regions for - a triangular grid. - - This class is typically not instantiated directly by the user but by - `~.Axes.tricontour` and `~.Axes.tricontourf`. - - %(contour_set_attributes)s - """ - def __init__(self, ax, *args, **kwargs): - """ - Draw triangular grid contour lines or filled regions, - depending on whether keyword arg 'filled' is False - (default) or True. - - The first argument of the initializer must be an axes - object. The remaining arguments and keyword arguments - are described in the docstring of `~.Axes.tricontour`. - """ - super().__init__(ax, *args, **kwargs) - - def _process_args(self, *args, **kwargs): - """ - Process args and kwargs. - """ - if isinstance(args[0], TriContourSet): - C = args[0].cppContourGenerator - if self.levels is None: - self.levels = args[0].levels - else: - from matplotlib import _tri - tri, z = self._contour_args(args, kwargs) - C = _tri.TriContourGenerator(tri.get_cpp_triangulation(), z) - self._mins = [tri.x.min(), tri.y.min()] - self._maxs = [tri.x.max(), tri.y.max()] - - self.cppContourGenerator = C - return kwargs - - def _get_allsegs_and_allkinds(self): - """ - Create and return allsegs and allkinds by calling underlying C code. - """ - allsegs = [] - if self.filled: - lowers, uppers = self._get_lowers_and_uppers() - allkinds = [] - for lower, upper in zip(lowers, uppers): - segs, kinds = self.cppContourGenerator.create_filled_contour( - lower, upper) - allsegs.append([segs]) - allkinds.append([kinds]) - else: - allkinds = None - for level in self.levels: - segs = self.cppContourGenerator.create_contour(level) - allsegs.append(segs) - return allsegs, allkinds - - def _contour_args(self, args, kwargs): - if self.filled: - fn = 'contourf' - else: - fn = 'contour' - tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, - **kwargs) - z = np.ma.asarray(args[0]) - if z.shape != tri.x.shape: - raise ValueError('z array must have same length as triangulation x' - ' and y arrays') - - # z values must be finite, only need to check points that are included - # in the triangulation. - z_check = z[np.unique(tri.get_masked_triangles())] - if np.ma.is_masked(z_check): - raise ValueError('z must not contain masked points within the ' - 'triangulation') - if not np.isfinite(z_check).all(): - raise ValueError('z array must not contain non-finite values ' - 'within the triangulation') - - z = np.ma.masked_invalid(z, copy=False) - self.zmax = float(z_check.max()) - self.zmin = float(z_check.min()) - if self.logscale and self.zmin <= 0: - raise ValueError('Cannot %s log of negative values.' % fn) - self._process_contour_level_args(args[1:]) - return (tri, z) - - -docstring.interpd.update(_tricontour_doc=""" -Draw contour %(type)s on an unstructured triangular grid. - -The triangulation can be specified in one of two ways; either :: - - %(func)s(triangulation, ...) - -where *triangulation* is a `.Triangulation` object, or :: - - %(func)s(x, y, ...) - %(func)s(x, y, triangles, ...) - %(func)s(x, y, triangles=triangles, ...) - %(func)s(x, y, mask=mask, ...) - %(func)s(x, y, triangles, mask=mask, ...) - -in which case a `.Triangulation` object will be created. See that class' -docstring for an explanation of these cases. - -The remaining arguments may be:: - - %(func)s(..., Z) - -where *Z* is the array of values to contour, one per point in the -triangulation. The level values are chosen automatically. - -:: - - %(func)s(..., Z, levels) - -contour up to *levels+1* automatically chosen contour levels (*levels* -intervals). - -:: - - %(func)s(..., Z, levels) - -draw contour %(type)s at the values specified in sequence *levels*, which must -be in increasing order. - -:: - - %(func)s(Z, **kwargs) - -Use keyword arguments to control colors, linewidth, origin, cmap ... see below -for more details. - -Parameters ----------- -triangulation : `.Triangulation`, optional - The unstructured triangular grid. - - If specified, then *x*, *y*, *triangles*, and *mask* are not accepted. - -x, y : array-like, optional - The coordinates of the values in *Z*. - -triangles : (ntri, 3) array-like of int, optional - For each triangle, the indices of the three points that make up the - triangle, ordered in an anticlockwise manner. If not specified, the - Delaunay triangulation is calculated. - -mask : (ntri,) array-like of bool, optional - Which triangles are masked out. - -Z : 2D array-like - The height values over which the contour is drawn. - -levels : int or array-like, optional - Determines the number and positions of the contour lines / regions. - - If an int *n*, use `~matplotlib.ticker.MaxNLocator`, which tries to - automatically choose no more than *n+1* "nice" contour levels between - *vmin* and *vmax*. - - If array-like, draw contour lines at the specified levels. The values must - be in increasing order. - -Returns -------- -`~matplotlib.tri.TriContourSet` - -Other Parameters ----------------- -colors : color string or sequence of colors, optional - The colors of the levels, i.e., the contour %(type)s. - - The sequence is cycled for the levels in ascending order. If the sequence - is shorter than the number of levels, it's repeated. - - As a shortcut, single color strings may be used in place of one-element - lists, i.e. ``'red'`` instead of ``['red']`` to color all levels with the - same color. This shortcut does only work for color strings, not for other - ways of specifying colors. - - By default (value *None*), the colormap specified by *cmap* will be used. - -alpha : float, default: 1 - The alpha blending value, between 0 (transparent) and 1 (opaque). - -cmap : str or `.Colormap`, default: :rc:`image.cmap` - A `.Colormap` instance or registered colormap name. The colormap maps the - level values to colors. - - If both *colors* and *cmap* are given, an error is raised. - -norm : `~matplotlib.colors.Normalize`, optional - If a colormap is used, the `.Normalize` instance scales the level values to - the canonical colormap range [0, 1] for mapping to colors. If not given, - the default linear scaling is used. - -vmin, vmax : float, optional - If not *None*, either or both of these values will be supplied to - the `.Normalize` instance, overriding the default color scaling - based on *levels*. - -origin : {*None*, 'upper', 'lower', 'image'}, default: None - Determines the orientation and exact position of *Z* by specifying the - position of ``Z[0, 0]``. This is only relevant, if *X*, *Y* are not given. - - - *None*: ``Z[0, 0]`` is at X=0, Y=0 in the lower left corner. - - 'lower': ``Z[0, 0]`` is at X=0.5, Y=0.5 in the lower left corner. - - 'upper': ``Z[0, 0]`` is at X=N+0.5, Y=0.5 in the upper left corner. - - 'image': Use the value from :rc:`image.origin`. - -extent : (x0, x1, y0, y1), optional - If *origin* is not *None*, then *extent* is interpreted as in `.imshow`: it - gives the outer pixel boundaries. In this case, the position of Z[0, 0] is - the center of the pixel, not a corner. If *origin* is *None*, then - (*x0*, *y0*) is the position of Z[0, 0], and (*x1*, *y1*) is the position - of Z[-1, -1]. - - This argument is ignored if *X* and *Y* are specified in the call to - contour. - -locator : ticker.Locator subclass, optional - The locator is used to determine the contour levels if they are not given - explicitly via *levels*. - Defaults to `~.ticker.MaxNLocator`. - -extend : {'neither', 'both', 'min', 'max'}, default: 'neither' - Determines the ``%(func)s``-coloring of values that are outside the - *levels* range. - - If 'neither', values outside the *levels* range are not colored. If 'min', - 'max' or 'both', color the values below, above or below and above the - *levels* range. - - Values below ``min(levels)`` and above ``max(levels)`` are mapped to the - under/over values of the `.Colormap`. Note that most colormaps do not have - dedicated colors for these by default, so that the over and under values - are the edge values of the colormap. You may want to set these values - explicitly using `.Colormap.set_under` and `.Colormap.set_over`. - - .. note:: - - An existing `.TriContourSet` does not get notified if properties of its - colormap are changed. Therefore, an explicit call to - `.ContourSet.changed()` is needed after modifying the colormap. The - explicit call can be left out, if a colorbar is assigned to the - `.TriContourSet` because it internally calls `.ContourSet.changed()`. - -xunits, yunits : registered units, optional - Override axis units by specifying an instance of a - :class:`matplotlib.units.ConversionInterface`. - -antialiased : bool, optional - Enable antialiasing, overriding the defaults. For - filled contours, the default is *True*. For line contours, - it is taken from :rc:`lines.antialiased`.""") - - -@docstring.Substitution(func='tricontour', type='lines') -@docstring.dedent_interpd -def tricontour(ax, *args, **kwargs): - """ - %(_tricontour_doc)s - - linewidths : float or array-like, default: :rc:`contour.linewidth` - The line width of the contour lines. - - If a number, all levels will be plotted with this linewidth. - - If a sequence, the levels in ascending order will be plotted with - the linewidths in the order specified. - - If None, this falls back to :rc:`lines.linewidth`. - - linestyles : {*None*, 'solid', 'dashed', 'dashdot', 'dotted'}, optional - If *linestyles* is *None*, the default is 'solid' unless the lines are - monochrome. In that case, negative contours will take their linestyle - from :rc:`contour.negative_linestyle` setting. - - *linestyles* can also be an iterable of the above strings specifying a - set of linestyles to be used. If this iterable is shorter than the - number of contour levels it will be repeated as necessary. - """ - kwargs['filled'] = False - return TriContourSet(ax, *args, **kwargs) - - -@docstring.Substitution(func='tricontourf', type='regions') -@docstring.dedent_interpd -def tricontourf(ax, *args, **kwargs): - """ - %(_tricontour_doc)s - - hatches : list[str], optional - A list of cross hatch patterns to use on the filled areas. - If None, no hatching will be added to the contour. - Hatching is supported in the PostScript, PDF, SVG and Agg - backends only. - - Notes - ----- - `.tricontourf` fills intervals that are closed at the top; that is, for - boundaries *z1* and *z2*, the filled region is:: - - z1 < Z <= z2 - - except for the lowest interval, which is closed on both sides (i.e. it - includes the lowest value). - """ - kwargs['filled'] = True - return TriContourSet(ax, *args, **kwargs) +_api.warn_deprecated( + "3.7", + message=f"Importing {__name__} was deprecated in Matplotlib 3.7 and will " + f"be removed two minor releases later. All functionality is " + f"available via the top-level module matplotlib.tri") diff --git a/lib/matplotlib/tri/trifinder.py b/lib/matplotlib/tri/trifinder.py index e06b84c0d974..1aff5c9d3280 100644 --- a/lib/matplotlib/tri/trifinder.py +++ b/lib/matplotlib/tri/trifinder.py @@ -1,93 +1,9 @@ -import numpy as np - +from ._trifinder import * # noqa: F401, F403 from matplotlib import _api -from matplotlib.tri import Triangulation - - -class TriFinder: - """ - Abstract base class for classes used to find the triangles of a - Triangulation in which (x, y) points lie. - - Rather than instantiate an object of a class derived from TriFinder, it is - usually better to use the function `.Triangulation.get_trifinder`. - - Derived classes implement __call__(x, y) where x and y are array-like point - coordinates of the same shape. - """ - - def __init__(self, triangulation): - _api.check_isinstance(Triangulation, triangulation=triangulation) - self._triangulation = triangulation - - -class TrapezoidMapTriFinder(TriFinder): - """ - `~matplotlib.tri.TriFinder` class implemented using the trapezoid - map algorithm from the book "Computational Geometry, Algorithms and - Applications", second edition, by M. de Berg, M. van Kreveld, M. Overmars - and O. Schwarzkopf. - - The triangulation must be valid, i.e. it must not have duplicate points, - triangles formed from colinear points, or overlapping triangles. The - algorithm has some tolerance to triangles formed from colinear points, but - this should not be relied upon. - """ - - def __init__(self, triangulation): - from matplotlib import _tri - super().__init__(triangulation) - self._cpp_trifinder = _tri.TrapezoidMapTriFinder( - triangulation.get_cpp_triangulation()) - self._initialize() - - def __call__(self, x, y): - """ - Return an array containing the indices of the triangles in which the - specified *x*, *y* points lie, or -1 for points that do not lie within - a triangle. - - *x*, *y* are array-like x and y coordinates of the same shape and any - number of dimensions. - - Returns integer array with the same shape and *x* and *y*. - """ - x = np.asarray(x, dtype=np.float64) - y = np.asarray(y, dtype=np.float64) - if x.shape != y.shape: - raise ValueError("x and y must be array-like with the same shape") - - # C++ does the heavy lifting, and expects 1D arrays. - indices = (self._cpp_trifinder.find_many(x.ravel(), y.ravel()) - .reshape(x.shape)) - return indices - - def _get_tree_stats(self): - """ - Return a python list containing the statistics about the node tree: - 0: number of nodes (tree size) - 1: number of unique nodes - 2: number of trapezoids (tree leaf nodes) - 3: number of unique trapezoids - 4: maximum parent count (max number of times a node is repeated in - tree) - 5: maximum depth of tree (one more than the maximum number of - comparisons needed to search through the tree) - 6: mean of all trapezoid depths (one more than the average number - of comparisons needed to search through the tree) - """ - return self._cpp_trifinder.get_tree_stats() - def _initialize(self): - """ - Initialize the underlying C++ object. Can be called multiple times if, - for example, the triangulation is modified. - """ - self._cpp_trifinder.initialize() - def _print_tree(self): - """ - Print a text representation of the node tree, which is useful for - debugging purposes. - """ - self._cpp_trifinder.print_tree() +_api.warn_deprecated( + "3.7", + message=f"Importing {__name__} was deprecated in Matplotlib 3.7 and will " + f"be removed two minor releases later. All functionality is " + f"available via the top-level module matplotlib.tri") diff --git a/lib/matplotlib/tri/triinterpolate.py b/lib/matplotlib/tri/triinterpolate.py index 9c8311892b2a..3112bd38e6c6 100644 --- a/lib/matplotlib/tri/triinterpolate.py +++ b/lib/matplotlib/tri/triinterpolate.py @@ -1,1579 +1,9 @@ -""" -Interpolation inside triangular grids. -""" - -import numpy as np - +from ._triinterpolate import * # noqa: F401, F403 from matplotlib import _api -from matplotlib.tri import Triangulation -from matplotlib.tri.trifinder import TriFinder -from matplotlib.tri.tritools import TriAnalyzer - -__all__ = ('TriInterpolator', 'LinearTriInterpolator', 'CubicTriInterpolator') - - -class TriInterpolator: - """ - 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). - """ - - def __init__(self, triangulation, z, trifinder=None): - _api.check_isinstance(Triangulation, triangulation=triangulation) - self._triangulation = triangulation - - self._z = np.asarray(z) - if self._z.shape != self._triangulation.x.shape: - raise ValueError("z array must have same length as triangulation x" - " and y arrays") - - _api.check_isinstance((TriFinder, None), trifinder=trifinder) - self._trifinder = trifinder or self._triangulation.get_trifinder() - - # Default scaling factors : 1.0 (= no scaling) - # Scaling may be used for interpolations for which the order of - # magnitude of x, y has an impact on the interpolant definition. - # Please refer to :meth:`_interpolate_multikeys` for details. - self._unit_x = 1.0 - self._unit_y = 1.0 - - # Default triangle renumbering: None (= no renumbering) - # Renumbering may be used to avoid unnecessary computations - # if complex calculations are done inside the Interpolator. - # Please refer to :meth:`_interpolate_multikeys` for details. - self._tri_renum = None - - # __call__ and gradient docstrings are shared by all subclasses - # (except, if needed, relevant additions). - # However these methods are only implemented in subclasses to avoid - # confusion in the documentation. - _docstring__call__ = """ - Returns a masked array containing interpolated values at the specified - (x, y) points. - - Parameters - ---------- - x, y : array-like - x and y coordinates of the same shape and any number of - dimensions. - - Returns - ------- - np.ma.array - Masked array of the same shape as *x* and *y*; values corresponding - to (*x*, *y*) points outside of the triangulation are masked out. - - """ - - _docstringgradient = r""" - Returns a list of 2 masked arrays containing interpolated derivatives - at the specified (x, y) points. - - Parameters - ---------- - x, y : array-like - x and y coordinates of the same shape and any number of - dimensions. - - Returns - ------- - dzdx, dzdy : np.ma.array - 2 masked arrays of the same shape as *x* and *y*; values - corresponding to (x, y) points outside of the triangulation - are masked out. - The first returned array contains the values of - :math:`\frac{\partial z}{\partial x}` and the second those of - :math:`\frac{\partial z}{\partial y}`. - - """ - - def _interpolate_multikeys(self, x, y, tri_index=None, - return_keys=('z',)): - """ - Versatile (private) method defined for all TriInterpolators. - - :meth:`_interpolate_multikeys` is a wrapper around method - :meth:`_interpolate_single_key` (to be defined in the child - subclasses). - :meth:`_interpolate_single_key actually performs the interpolation, - but only for 1-dimensional inputs and at valid locations (inside - unmasked triangles of the triangulation). - - The purpose of :meth:`_interpolate_multikeys` is to implement the - following common tasks needed in all subclasses implementations: - - - calculation of containing triangles - - dealing with more than one interpolation request at the same - location (e.g., if the 2 derivatives are requested, it is - unnecessary to compute the containing triangles twice) - - scaling according to self._unit_x, self._unit_y - - dealing with points outside of the grid (with fill value np.nan) - - dealing with multi-dimensional *x*, *y* arrays: flattening for - :meth:`_interpolate_params` call and final reshaping. - - (Note that np.vectorize could do most of those things very well for - you, but it does it by function evaluations over successive tuples of - the input arrays. Therefore, this tends to be more time consuming than - using optimized numpy functions - e.g., np.dot - which can be used - easily on the flattened inputs, in the child-subclass methods - :meth:`_interpolate_single_key`.) - - It is guaranteed that the calls to :meth:`_interpolate_single_key` - will be done with flattened (1-d) array-like input parameters *x*, *y* - and with flattened, valid `tri_index` arrays (no -1 index allowed). - - Parameters - ---------- - x, y : array-like - x and y coordinates where interpolated values are requested. - tri_index : array-like of int, optional - Array of the containing triangle indices, same shape as - *x* and *y*. Defaults to None. If None, these indices - will be computed by a TriFinder instance. - (Note: For point outside the grid, tri_index[ipt] shall be -1). - return_keys : tuple of keys from {'z', 'dzdx', 'dzdy'} - Defines the interpolation arrays to return, and in which order. - - Returns - ------- - list of arrays - Each array-like contains the expected interpolated values in the - order defined by *return_keys* parameter. - """ - # Flattening and rescaling inputs arrays x, y - # (initial shape is stored for output) - x = np.asarray(x, dtype=np.float64) - y = np.asarray(y, dtype=np.float64) - sh_ret = x.shape - if x.shape != y.shape: - raise ValueError("x and y shall have same shapes." - " Given: {0} and {1}".format(x.shape, y.shape)) - x = np.ravel(x) - y = np.ravel(y) - x_scaled = x/self._unit_x - y_scaled = y/self._unit_y - size_ret = np.size(x_scaled) - - # Computes & ravels the element indexes, extract the valid ones. - if tri_index is None: - tri_index = self._trifinder(x, y) - else: - if tri_index.shape != sh_ret: - raise ValueError( - "tri_index array is provided and shall" - " have same shape as x and y. Given: " - "{0} and {1}".format(tri_index.shape, sh_ret)) - tri_index = np.ravel(tri_index) - - mask_in = (tri_index != -1) - if self._tri_renum is None: - valid_tri_index = tri_index[mask_in] - else: - valid_tri_index = self._tri_renum[tri_index[mask_in]] - valid_x = x_scaled[mask_in] - valid_y = y_scaled[mask_in] - - ret = [] - for return_key in return_keys: - # Find the return index associated with the key. - try: - return_index = {'z': 0, 'dzdx': 1, 'dzdy': 2}[return_key] - except KeyError as err: - raise ValueError("return_keys items shall take values in" - " {'z', 'dzdx', 'dzdy'}") from err - - # Sets the scale factor for f & df components - scale = [1., 1./self._unit_x, 1./self._unit_y][return_index] - - # Computes the interpolation - ret_loc = np.empty(size_ret, dtype=np.float64) - ret_loc[~mask_in] = np.nan - ret_loc[mask_in] = self._interpolate_single_key( - return_key, valid_tri_index, valid_x, valid_y) * scale - ret += [np.ma.masked_invalid(ret_loc.reshape(sh_ret), copy=False)] - - return ret - - def _interpolate_single_key(self, return_key, tri_index, x, y): - """ - Interpolate at points belonging to the triangulation - (inside an unmasked triangles). - - Parameters - ---------- - return_key : {'z', 'dzdx', 'dzdy'} - The requested values (z or its derivatives). - tri_index : 1D int array - Valid triangle index (cannot be -1). - x, y : 1D arrays, same shape as `tri_index` - Valid locations where interpolation is requested. - - Returns - ------- - 1-d array - Returned array of the same size as *tri_index* - """ - raise NotImplementedError("TriInterpolator subclasses" + - "should implement _interpolate_single_key!") - - -class LinearTriInterpolator(TriInterpolator): - """ - 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). - Interpolated values are therefore continuous across the triangulation, but - their first derivatives are discontinuous at edges between triangles. - - Parameters - ---------- - triangulation : `~matplotlib.tri.Triangulation` - The triangulation to interpolate over. - z : (npoints,) array-like - Array of values, defined at grid points, to interpolate between. - trifinder : `~matplotlib.tri.TriFinder`, optional - If this is not specified, the Triangulation's default TriFinder will - be used by calling `.Triangulation.get_trifinder`. - - Methods - ------- - `__call__` (x, y) : Returns interpolated values at (x, y) points. - `gradient` (x, y) : Returns interpolated derivatives at (x, y) points. - - """ - def __init__(self, triangulation, z, trifinder=None): - super().__init__(triangulation, z, trifinder) - - # Store plane coefficients for fast interpolation calculations. - self._plane_coefficients = \ - self._triangulation.calculate_plane_coefficients(self._z) - - def __call__(self, x, y): - return self._interpolate_multikeys(x, y, tri_index=None, - return_keys=('z',))[0] - __call__.__doc__ = TriInterpolator._docstring__call__ - - def gradient(self, x, y): - return self._interpolate_multikeys(x, y, tri_index=None, - return_keys=('dzdx', 'dzdy')) - gradient.__doc__ = TriInterpolator._docstringgradient - - def _interpolate_single_key(self, return_key, tri_index, x, y): - if return_key == 'z': - return (self._plane_coefficients[tri_index, 0]*x + - self._plane_coefficients[tri_index, 1]*y + - self._plane_coefficients[tri_index, 2]) - elif return_key == 'dzdx': - return self._plane_coefficients[tri_index, 0] - elif return_key == 'dzdy': - return self._plane_coefficients[tri_index, 1] - else: - raise ValueError("Invalid return_key: " + return_key) - - -class CubicTriInterpolator(TriInterpolator): - r""" - 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. - This is almost the same in 2D inside a triangle, except that the values - of the function and its 2 derivatives have to be defined at each triangle - node. - - The CubicTriInterpolator takes the value of the function at each node - - provided by the user - and internally computes the value of the - derivatives, resulting in a smooth interpolation. - (As a special feature, the user can also impose the value of the - derivatives at each node, but this is not supposed to be the common - usage.) - - Parameters - ---------- - triangulation : `~matplotlib.tri.Triangulation` - The triangulation to interpolate over. - z : (npoints,) array-like - Array of values, defined at grid points, to interpolate between. - kind : {'min_E', 'geom', 'user'}, optional - Choice of the smoothing algorithm, in order to compute - the interpolant derivatives (defaults to 'min_E'): - - - if 'min_E': (default) The derivatives at each node is computed - to minimize a bending energy. - - if 'geom': The derivatives at each node is computed as a - weighted average of relevant triangle normals. To be used for - speed optimization (large grids). - - if 'user': The user provides the argument *dz*, no computation - is hence needed. - - trifinder : `~matplotlib.tri.TriFinder`, optional - If not specified, the Triangulation's default TriFinder will - be used by calling `.Triangulation.get_trifinder`. - dz : tuple of array-likes (dzdx, dzdy), optional - Used only if *kind* ='user'. In this case *dz* must be provided as - (dzdx, dzdy) where dzdx, dzdy are arrays of the same shape as *z* and - are the interpolant first derivatives at the *triangulation* points. - - Methods - ------- - `__call__` (x, y) : Returns interpolated values at (x, y) points. - `gradient` (x, y) : Returns interpolated derivatives at (x, y) points. - - Notes - ----- - This note is a bit technical and details how the cubic interpolation is - computed. - - The interpolation is based on a Clough-Tocher subdivision scheme of - the *triangulation* mesh (to make it clearer, each triangle of the - grid will be divided in 3 child-triangles, and on each child triangle - the interpolated function is a cubic polynomial of the 2 coordinates). - This technique originates from FEM (Finite Element Method) analysis; - the element used is a reduced Hsieh-Clough-Tocher (HCT) - element. Its shape functions are described in [1]_. - The assembled function is guaranteed to be C1-smooth, i.e. it is - continuous and its first derivatives are also continuous (this - is easy to show inside the triangles but is also true when crossing the - edges). - - In the default case (*kind* ='min_E'), the interpolant minimizes a - curvature energy on the functional space generated by the HCT element - shape functions - with imposed values but arbitrary derivatives at each - node. The minimized functional is the integral of the so-called total - curvature (implementation based on an algorithm from [2]_ - PCG sparse - solver): - - .. math:: - - E(z) = \frac{1}{2} \int_{\Omega} \left( - \left( \frac{\partial^2{z}}{\partial{x}^2} \right)^2 + - \left( \frac{\partial^2{z}}{\partial{y}^2} \right)^2 + - 2\left( \frac{\partial^2{z}}{\partial{y}\partial{x}} \right)^2 - \right) dx\,dy - - If the case *kind* ='geom' is chosen by the user, a simple geometric - approximation is used (weighted average of the triangle normal - vectors), which could improve speed on very large grids. - - References - ---------- - .. [1] Michel Bernadou, Kamal Hassan, "Basis functions for general - Hsieh-Clough-Tocher triangles, complete or reduced.", - International Journal for Numerical Methods in Engineering, - 17(5):784 - 789. 2.01. - .. [2] C.T. Kelley, "Iterative Methods for Optimization". - - """ - def __init__(self, triangulation, z, kind='min_E', trifinder=None, - dz=None): - super().__init__(triangulation, z, trifinder) - - # Loads the underlying c++ _triangulation. - # (During loading, reordering of triangulation._triangles may occur so - # that all final triangles are now anti-clockwise) - self._triangulation.get_cpp_triangulation() - - # To build the stiffness matrix and avoid zero-energy spurious modes - # we will only store internally the valid (unmasked) triangles and - # the necessary (used) points coordinates. - # 2 renumbering tables need to be computed and stored: - # - a triangle renum table in order to translate the result from a - # TriFinder instance into the internal stored triangle number. - # - a node renum table to overwrite the self._z values into the new - # (used) node numbering. - tri_analyzer = TriAnalyzer(self._triangulation) - (compressed_triangles, compressed_x, compressed_y, tri_renum, - node_renum) = tri_analyzer._get_compressed_triangulation() - self._triangles = compressed_triangles - self._tri_renum = tri_renum - # Taking into account the node renumbering in self._z: - valid_node = (node_renum != -1) - self._z[node_renum[valid_node]] = self._z[valid_node] - - # Computing scale factors - self._unit_x = np.ptp(compressed_x) - self._unit_y = np.ptp(compressed_y) - self._pts = np.column_stack([compressed_x / self._unit_x, - compressed_y / self._unit_y]) - # Computing triangle points - self._tris_pts = self._pts[self._triangles] - # Computing eccentricities - self._eccs = self._compute_tri_eccentricities(self._tris_pts) - # Computing dof estimations for HCT triangle shape function - self._dof = self._compute_dof(kind, dz=dz) - # Loading HCT element - self._ReferenceElement = _ReducedHCT_Element() - - def __call__(self, x, y): - return self._interpolate_multikeys(x, y, tri_index=None, - return_keys=('z',))[0] - __call__.__doc__ = TriInterpolator._docstring__call__ - - def gradient(self, x, y): - return self._interpolate_multikeys(x, y, tri_index=None, - return_keys=('dzdx', 'dzdy')) - gradient.__doc__ = TriInterpolator._docstringgradient - - def _interpolate_single_key(self, return_key, tri_index, x, y): - tris_pts = self._tris_pts[tri_index] - alpha = self._get_alpha_vec(x, y, tris_pts) - ecc = self._eccs[tri_index] - dof = np.expand_dims(self._dof[tri_index], axis=1) - if return_key == 'z': - return self._ReferenceElement.get_function_values( - alpha, ecc, dof) - elif return_key in ['dzdx', 'dzdy']: - J = self._get_jacobian(tris_pts) - dzdx = self._ReferenceElement.get_function_derivatives( - alpha, J, ecc, dof) - if return_key == 'dzdx': - return dzdx[:, 0, 0] - else: - return dzdx[:, 1, 0] - else: - raise ValueError("Invalid return_key: " + return_key) - - def _compute_dof(self, kind, dz=None): - """ - Compute and return nodal dofs according to kind. - - Parameters - ---------- - kind : {'min_E', 'geom', 'user'} - 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`. - - Returns - ------- - array-like, shape (npts, 2) - Estimation of the gradient at triangulation nodes (stored as - degree of freedoms of reduced-HCT triangle elements). - """ - if kind == 'user': - if dz is None: - raise ValueError("For a CubicTriInterpolator with " - "*kind*='user', a valid *dz* " - "argument is expected.") - TE = _DOF_estimator_user(self, dz=dz) - elif kind == 'geom': - TE = _DOF_estimator_geom(self) - elif kind == 'min_E': - TE = _DOF_estimator_min_E(self) - else: - _api.check_in_list(['user', 'geom', 'min_E'], kind=kind) - return TE.compute_dof_from_df() - - @staticmethod - def _get_alpha_vec(x, y, tris_pts): - """ - Fast (vectorized) function to compute barycentric coordinates alpha. - - Parameters - ---------- - x, y : array-like of dim 1 (shape (nx,)) - Coordinates of the points whose points barycentric coordinates are - requested. - tris_pts : array like of dim 3 (shape: (nx, 3, 2)) - Coordinates of the containing triangles apexes. - - Returns - ------- - array of dim 2 (shape (nx, 3)) - Barycentric coordinates of the points inside the containing - triangles. - """ - ndim = tris_pts.ndim-2 - - a = tris_pts[:, 1, :] - tris_pts[:, 0, :] - b = tris_pts[:, 2, :] - tris_pts[:, 0, :] - abT = np.stack([a, b], axis=-1) - ab = _transpose_vectorized(abT) - OM = np.stack([x, y], axis=1) - tris_pts[:, 0, :] - - metric = ab @ abT - # Here we try to deal with the colinear cases. - # metric_inv is in this case set to the Moore-Penrose pseudo-inverse - # meaning that we will still return a set of valid barycentric - # coordinates. - metric_inv = _pseudo_inv22sym_vectorized(metric) - Covar = ab @ _transpose_vectorized(np.expand_dims(OM, ndim)) - ksi = metric_inv @ Covar - alpha = _to_matrix_vectorized([ - [1-ksi[:, 0, 0]-ksi[:, 1, 0]], [ksi[:, 0, 0]], [ksi[:, 1, 0]]]) - return alpha - - @staticmethod - def _get_jacobian(tris_pts): - """ - Fast (vectorized) function to compute triangle jacobian matrix. - - Parameters - ---------- - tris_pts : array like of dim 3 (shape: (nx, 3, 2)) - Coordinates of the containing triangles apexes. - - Returns - ------- - array of dim 3 (shape (nx, 2, 2)) - Barycentric coordinates of the points inside the containing - triangles. - J[itri, :, :] is the jacobian matrix at apex 0 of the triangle - itri, so that the following (matrix) relationship holds: - [dz/dksi] = [J] x [dz/dx] - with x: global coordinates - ksi: element parametric coordinates in triangle first apex - local basis. - """ - a = np.array(tris_pts[:, 1, :] - tris_pts[:, 0, :]) - b = np.array(tris_pts[:, 2, :] - tris_pts[:, 0, :]) - J = _to_matrix_vectorized([[a[:, 0], a[:, 1]], - [b[:, 0], b[:, 1]]]) - return J - - @staticmethod - def _compute_tri_eccentricities(tris_pts): - """ - Compute triangle eccentricities. - - Parameters - ---------- - tris_pts : array like of dim 3 (shape: (nx, 3, 2)) - Coordinates of the triangles apexes. - - Returns - ------- - array like of dim 2 (shape: (nx, 3)) - The so-called eccentricity parameters [1] needed for HCT triangular - element. - """ - a = np.expand_dims(tris_pts[:, 2, :] - tris_pts[:, 1, :], axis=2) - b = np.expand_dims(tris_pts[:, 0, :] - tris_pts[:, 2, :], axis=2) - c = np.expand_dims(tris_pts[:, 1, :] - tris_pts[:, 0, :], axis=2) - # Do not use np.squeeze, this is dangerous if only one triangle - # in the triangulation... - dot_a = (_transpose_vectorized(a) @ a)[:, 0, 0] - dot_b = (_transpose_vectorized(b) @ b)[:, 0, 0] - dot_c = (_transpose_vectorized(c) @ c)[:, 0, 0] - # Note that this line will raise a warning for dot_a, dot_b or dot_c - # zeros, but we choose not to support triangles with duplicate points. - return _to_matrix_vectorized([[(dot_c-dot_b) / dot_a], - [(dot_a-dot_c) / dot_b], - [(dot_b-dot_a) / dot_c]]) - - -# FEM element used for interpolation and for solving minimisation -# problem (Reduced HCT element) -class _ReducedHCT_Element: - """ - Implementation of reduced HCT triangular element with explicit shape - functions. - - Computes z, dz, d2z and the element stiffness matrix for bending energy: - E(f) = integral( (d2z/dx2 + d2z/dy2)**2 dA) - - *** Reference for the shape functions: *** - [1] Basis functions for general Hsieh-Clough-Tocher _triangles, complete or - reduced. - Michel Bernadou, Kamal Hassan - International Journal for Numerical Methods in Engineering. - 17(5):784 - 789. 2.01 - - *** Element description: *** - 9 dofs: z and dz given at 3 apex - C1 (conform) - - """ - # 1) Loads matrices to generate shape functions as a function of - # triangle eccentricities - based on [1] p.11 ''' - M = np.array([ - [ 0.00, 0.00, 0.00, 4.50, 4.50, 0.00, 0.00, 0.00, 0.00, 0.00], - [-0.25, 0.00, 0.00, 0.50, 1.25, 0.00, 0.00, 0.00, 0.00, 0.00], - [-0.25, 0.00, 0.00, 1.25, 0.50, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.50, 1.00, 0.00, -1.50, 0.00, 3.00, 3.00, 0.00, 0.00, 3.00], - [ 0.00, 0.00, 0.00, -0.25, 0.25, 0.00, 1.00, 0.00, 0.00, 0.50], - [ 0.25, 0.00, 0.00, -0.50, -0.25, 1.00, 0.00, 0.00, 0.00, 1.00], - [ 0.50, 0.00, 1.00, 0.00, -1.50, 0.00, 0.00, 3.00, 3.00, 3.00], - [ 0.25, 0.00, 0.00, -0.25, -0.50, 0.00, 0.00, 0.00, 1.00, 1.00], - [ 0.00, 0.00, 0.00, 0.25, -0.25, 0.00, 0.00, 1.00, 0.00, 0.50]]) - M0 = np.array([ - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [-1.00, 0.00, 0.00, 1.50, 1.50, 0.00, 0.00, 0.00, 0.00, -3.00], - [-0.50, 0.00, 0.00, 0.75, 0.75, 0.00, 0.00, 0.00, 0.00, -1.50], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 1.00, 0.00, 0.00, -1.50, -1.50, 0.00, 0.00, 0.00, 0.00, 3.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.50, 0.00, 0.00, -0.75, -0.75, 0.00, 0.00, 0.00, 0.00, 1.50]]) - M1 = np.array([ - [-0.50, 0.00, 0.00, 1.50, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [-0.25, 0.00, 0.00, 0.75, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.50, 0.00, 0.00, -1.50, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.25, 0.00, 0.00, -0.75, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00]]) - M2 = np.array([ - [ 0.50, 0.00, 0.00, 0.00, -1.50, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.25, 0.00, 0.00, 0.00, -0.75, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [-0.50, 0.00, 0.00, 0.00, 1.50, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [-0.25, 0.00, 0.00, 0.00, 0.75, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00], - [ 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00]]) - - # 2) Loads matrices to rotate components of gradient & Hessian - # vectors in the reference basis of triangle first apex (a0) - rotate_dV = np.array([[ 1., 0.], [ 0., 1.], - [ 0., 1.], [-1., -1.], - [-1., -1.], [ 1., 0.]]) - - rotate_d2V = np.array([[1., 0., 0.], [0., 1., 0.], [ 0., 0., 1.], - [0., 1., 0.], [1., 1., 1.], [ 0., -2., -1.], - [1., 1., 1.], [1., 0., 0.], [-2., 0., -1.]]) - - # 3) Loads Gauss points & weights on the 3 sub-_triangles for P2 - # exact integral - 3 points on each subtriangles. - # NOTE: as the 2nd derivative is discontinuous , we really need those 9 - # points! - n_gauss = 9 - gauss_pts = np.array([[13./18., 4./18., 1./18.], - [ 4./18., 13./18., 1./18.], - [ 7./18., 7./18., 4./18.], - [ 1./18., 13./18., 4./18.], - [ 1./18., 4./18., 13./18.], - [ 4./18., 7./18., 7./18.], - [ 4./18., 1./18., 13./18.], - [13./18., 1./18., 4./18.], - [ 7./18., 4./18., 7./18.]], dtype=np.float64) - gauss_w = np.ones([9], dtype=np.float64) / 9. - - # 4) Stiffness matrix for curvature energy - E = np.array([[1., 0., 0.], [0., 1., 0.], [0., 0., 2.]]) - - # 5) Loads the matrix to compute DOF_rot from tri_J at apex 0 - J0_to_J1 = np.array([[-1., 1.], [-1., 0.]]) - J0_to_J2 = np.array([[ 0., -1.], [ 1., -1.]]) - - def get_function_values(self, alpha, ecc, dofs): - """ - Parameters - ---------- - alpha : is a (N x 3 x 1) array (array of column-matrices) of - barycentric coordinates, - ecc : is a (N x 3 x 1) array (array of column-matrices) of triangle - eccentricities, - dofs : is a (N x 1 x 9) arrays (arrays of row-matrices) of computed - degrees of freedom. - - Returns - ------- - Returns the N-array of interpolated function values. - """ - subtri = np.argmin(alpha, axis=1)[:, 0] - ksi = _roll_vectorized(alpha, -subtri, axis=0) - E = _roll_vectorized(ecc, -subtri, axis=0) - x = ksi[:, 0, 0] - y = ksi[:, 1, 0] - z = ksi[:, 2, 0] - x_sq = x*x - y_sq = y*y - z_sq = z*z - V = _to_matrix_vectorized([ - [x_sq*x], [y_sq*y], [z_sq*z], [x_sq*z], [x_sq*y], [y_sq*x], - [y_sq*z], [z_sq*y], [z_sq*x], [x*y*z]]) - prod = self.M @ V - prod += _scalar_vectorized(E[:, 0, 0], self.M0 @ V) - prod += _scalar_vectorized(E[:, 1, 0], self.M1 @ V) - prod += _scalar_vectorized(E[:, 2, 0], self.M2 @ V) - s = _roll_vectorized(prod, 3*subtri, axis=0) - return (dofs @ s)[:, 0, 0] - - def get_function_derivatives(self, alpha, J, ecc, dofs): - """ - Parameters - ---------- - *alpha* is a (N x 3 x 1) array (array of column-matrices of - barycentric coordinates) - *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at - triangle first apex) - *ecc* is a (N x 3 x 1) array (array of column-matrices of triangle - eccentricities) - *dofs* is a (N x 1 x 9) arrays (arrays of row-matrices) of computed - degrees of freedom. - - Returns - ------- - Returns the values of interpolated function derivatives [dz/dx, dz/dy] - in global coordinates at locations alpha, as a column-matrices of - shape (N x 2 x 1). - """ - subtri = np.argmin(alpha, axis=1)[:, 0] - ksi = _roll_vectorized(alpha, -subtri, axis=0) - E = _roll_vectorized(ecc, -subtri, axis=0) - x = ksi[:, 0, 0] - y = ksi[:, 1, 0] - z = ksi[:, 2, 0] - x_sq = x*x - y_sq = y*y - z_sq = z*z - dV = _to_matrix_vectorized([ - [ -3.*x_sq, -3.*x_sq], - [ 3.*y_sq, 0.], - [ 0., 3.*z_sq], - [ -2.*x*z, -2.*x*z+x_sq], - [-2.*x*y+x_sq, -2.*x*y], - [ 2.*x*y-y_sq, -y_sq], - [ 2.*y*z, y_sq], - [ z_sq, 2.*y*z], - [ -z_sq, 2.*x*z-z_sq], - [ x*z-y*z, x*y-y*z]]) - # Puts back dV in first apex basis - dV = dV @ _extract_submatrices( - self.rotate_dV, subtri, block_size=2, axis=0) - - prod = self.M @ dV - prod += _scalar_vectorized(E[:, 0, 0], self.M0 @ dV) - prod += _scalar_vectorized(E[:, 1, 0], self.M1 @ dV) - prod += _scalar_vectorized(E[:, 2, 0], self.M2 @ dV) - dsdksi = _roll_vectorized(prod, 3*subtri, axis=0) - dfdksi = dofs @ dsdksi - # In global coordinates: - # Here we try to deal with the simplest colinear cases, returning a - # null matrix. - J_inv = _safe_inv22_vectorized(J) - dfdx = J_inv @ _transpose_vectorized(dfdksi) - return dfdx - - def get_function_hessians(self, alpha, J, ecc, dofs): - """ - Parameters - ---------- - *alpha* is a (N x 3 x 1) array (array of column-matrices) of - barycentric coordinates - *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at - triangle first apex) - *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle - eccentricities - *dofs* is a (N x 1 x 9) arrays (arrays of row-matrices) of computed - degrees of freedom. - - Returns - ------- - Returns the values of interpolated function 2nd-derivatives - [d2z/dx2, d2z/dy2, d2z/dxdy] in global coordinates at locations alpha, - as a column-matrices of shape (N x 3 x 1). - """ - d2sdksi2 = self.get_d2Sidksij2(alpha, ecc) - d2fdksi2 = dofs @ d2sdksi2 - H_rot = self.get_Hrot_from_J(J) - d2fdx2 = d2fdksi2 @ H_rot - return _transpose_vectorized(d2fdx2) - - def get_d2Sidksij2(self, alpha, ecc): - """ - Parameters - ---------- - *alpha* is a (N x 3 x 1) array (array of column-matrices) of - barycentric coordinates - *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle - eccentricities - - Returns - ------- - Returns the arrays d2sdksi2 (N x 3 x 1) Hessian of shape functions - expressed in covariant coordinates in first apex basis. - """ - subtri = np.argmin(alpha, axis=1)[:, 0] - ksi = _roll_vectorized(alpha, -subtri, axis=0) - E = _roll_vectorized(ecc, -subtri, axis=0) - x = ksi[:, 0, 0] - y = ksi[:, 1, 0] - z = ksi[:, 2, 0] - d2V = _to_matrix_vectorized([ - [ 6.*x, 6.*x, 6.*x], - [ 6.*y, 0., 0.], - [ 0., 6.*z, 0.], - [ 2.*z, 2.*z-4.*x, 2.*z-2.*x], - [2.*y-4.*x, 2.*y, 2.*y-2.*x], - [2.*x-4.*y, 0., -2.*y], - [ 2.*z, 0., 2.*y], - [ 0., 2.*y, 2.*z], - [ 0., 2.*x-4.*z, -2.*z], - [ -2.*z, -2.*y, x-y-z]]) - # Puts back d2V in first apex basis - d2V = d2V @ _extract_submatrices( - self.rotate_d2V, subtri, block_size=3, axis=0) - prod = self.M @ d2V - prod += _scalar_vectorized(E[:, 0, 0], self.M0 @ d2V) - prod += _scalar_vectorized(E[:, 1, 0], self.M1 @ d2V) - prod += _scalar_vectorized(E[:, 2, 0], self.M2 @ d2V) - d2sdksi2 = _roll_vectorized(prod, 3*subtri, axis=0) - return d2sdksi2 - - def get_bending_matrices(self, J, ecc): - """ - Parameters - ---------- - *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at - triangle first apex) - *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle - eccentricities - - Returns - ------- - Returns the element K matrices for bending energy expressed in - GLOBAL nodal coordinates. - K_ij = integral [ (d2zi/dx2 + d2zi/dy2) * (d2zj/dx2 + d2zj/dy2) dA] - tri_J is needed to rotate dofs from local basis to global basis - """ - n = np.size(ecc, 0) - - # 1) matrix to rotate dofs in global coordinates - J1 = self.J0_to_J1 @ J - J2 = self.J0_to_J2 @ J - DOF_rot = np.zeros([n, 9, 9], dtype=np.float64) - DOF_rot[:, 0, 0] = 1 - DOF_rot[:, 3, 3] = 1 - DOF_rot[:, 6, 6] = 1 - DOF_rot[:, 1:3, 1:3] = J - DOF_rot[:, 4:6, 4:6] = J1 - DOF_rot[:, 7:9, 7:9] = J2 - - # 2) matrix to rotate Hessian in global coordinates. - H_rot, area = self.get_Hrot_from_J(J, return_area=True) - - # 3) Computes stiffness matrix - # Gauss quadrature. - K = np.zeros([n, 9, 9], dtype=np.float64) - weights = self.gauss_w - pts = self.gauss_pts - for igauss in range(self.n_gauss): - alpha = np.tile(pts[igauss, :], n).reshape(n, 3) - alpha = np.expand_dims(alpha, 2) - weight = weights[igauss] - d2Skdksi2 = self.get_d2Sidksij2(alpha, ecc) - d2Skdx2 = d2Skdksi2 @ H_rot - K += weight * (d2Skdx2 @ self.E @ _transpose_vectorized(d2Skdx2)) - - # 4) With nodal (not elem) dofs - K = _transpose_vectorized(DOF_rot) @ K @ DOF_rot - - # 5) Need the area to compute total element energy - return _scalar_vectorized(area, K) - - def get_Hrot_from_J(self, J, return_area=False): - """ - Parameters - ---------- - *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at - triangle first apex) - - Returns - ------- - Returns H_rot used to rotate Hessian from local basis of first apex, - to global coordinates. - if *return_area* is True, returns also the triangle area (0.5*det(J)) - """ - # Here we try to deal with the simplest colinear cases; a null - # energy and area is imposed. - J_inv = _safe_inv22_vectorized(J) - Ji00 = J_inv[:, 0, 0] - Ji11 = J_inv[:, 1, 1] - Ji10 = J_inv[:, 1, 0] - Ji01 = J_inv[:, 0, 1] - H_rot = _to_matrix_vectorized([ - [Ji00*Ji00, Ji10*Ji10, Ji00*Ji10], - [Ji01*Ji01, Ji11*Ji11, Ji01*Ji11], - [2*Ji00*Ji01, 2*Ji11*Ji10, Ji00*Ji11+Ji10*Ji01]]) - if not return_area: - return H_rot - else: - area = 0.5 * (J[:, 0, 0]*J[:, 1, 1] - J[:, 0, 1]*J[:, 1, 0]) - return H_rot, area - - def get_Kff_and_Ff(self, J, ecc, triangles, Uc): - """ - Build K and F for the following elliptic formulation: - minimization of curvature energy with value of function at node - imposed and derivatives 'free'. - - Build the global Kff matrix in cco format. - Build the full Ff vec Ff = - Kfc x Uc. - - Parameters - ---------- - *J* is a (N x 2 x 2) array of jacobian matrices (jacobian matrix at - triangle first apex) - *ecc* is a (N x 3 x 1) array (array of column-matrices) of triangle - eccentricities - *triangles* is a (N x 3) array of nodes indexes. - *Uc* is (N x 3) array of imposed displacements at nodes - - Returns - ------- - (Kff_rows, Kff_cols, Kff_vals) Kff matrix in coo format - Duplicate - (row, col) entries must be summed. - Ff: force vector - dim npts * 3 - """ - ntri = np.size(ecc, 0) - vec_range = np.arange(ntri, dtype=np.int32) - c_indices = np.full(ntri, -1, dtype=np.int32) # for unused dofs, -1 - f_dof = [1, 2, 4, 5, 7, 8] - c_dof = [0, 3, 6] - - # vals, rows and cols indices in global dof numbering - f_dof_indices = _to_matrix_vectorized([[ - c_indices, triangles[:, 0]*2, triangles[:, 0]*2+1, - c_indices, triangles[:, 1]*2, triangles[:, 1]*2+1, - c_indices, triangles[:, 2]*2, triangles[:, 2]*2+1]]) - - expand_indices = np.ones([ntri, 9, 1], dtype=np.int32) - f_row_indices = _transpose_vectorized(expand_indices @ f_dof_indices) - f_col_indices = expand_indices @ f_dof_indices - K_elem = self.get_bending_matrices(J, ecc) - - # Extracting sub-matrices - # Explanation & notations: - # * Subscript f denotes 'free' degrees of freedom (i.e. dz/dx, dz/dx) - # * Subscript c denotes 'condensated' (imposed) degrees of freedom - # (i.e. z at all nodes) - # * F = [Ff, Fc] is the force vector - # * U = [Uf, Uc] is the imposed dof vector - # [ Kff Kfc ] - # * K = [ ] is the laplacian stiffness matrix - # [ Kcf Kff ] - # * As F = K x U one gets straightforwardly: Ff = - Kfc x Uc - - # Computing Kff stiffness matrix in sparse coo format - Kff_vals = np.ravel(K_elem[np.ix_(vec_range, f_dof, f_dof)]) - Kff_rows = np.ravel(f_row_indices[np.ix_(vec_range, f_dof, f_dof)]) - Kff_cols = np.ravel(f_col_indices[np.ix_(vec_range, f_dof, f_dof)]) - - # Computing Ff force vector in sparse coo format - Kfc_elem = K_elem[np.ix_(vec_range, f_dof, c_dof)] - Uc_elem = np.expand_dims(Uc, axis=2) - Ff_elem = -(Kfc_elem @ Uc_elem)[:, :, 0] - Ff_indices = f_dof_indices[np.ix_(vec_range, [0], f_dof)][:, 0, :] - - # Extracting Ff force vector in dense format - # We have to sum duplicate indices - using bincount - Ff = np.bincount(np.ravel(Ff_indices), weights=np.ravel(Ff_elem)) - return Kff_rows, Kff_cols, Kff_vals, Ff - - -# :class:_DOF_estimator, _DOF_estimator_user, _DOF_estimator_geom, -# _DOF_estimator_min_E -# Private classes used to compute the degree of freedom of each triangular -# element for the TriCubicInterpolator. -class _DOF_estimator: - """ - 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. - """ - def __init__(self, interpolator, **kwargs): - _api.check_isinstance(CubicTriInterpolator, interpolator=interpolator) - self._pts = interpolator._pts - self._tris_pts = interpolator._tris_pts - self.z = interpolator._z - self._triangles = interpolator._triangles - (self._unit_x, self._unit_y) = (interpolator._unit_x, - interpolator._unit_y) - self.dz = self.compute_dz(**kwargs) - self.compute_dof_from_df() - - def compute_dz(self, **kwargs): - raise NotImplementedError - - def compute_dof_from_df(self): - """ - Compute reduced-HCT elements degrees of freedom, from the gradient. - """ - J = CubicTriInterpolator._get_jacobian(self._tris_pts) - tri_z = self.z[self._triangles] - tri_dz = self.dz[self._triangles] - tri_dof = self.get_dof_vec(tri_z, tri_dz, J) - return tri_dof - - @staticmethod - def get_dof_vec(tri_z, tri_dz, J): - """ - Compute the dof vector of a triangle, from the value of f, df and - of the local Jacobian at each node. - - 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``:: - - 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) - J1 = _ReducedHCT_Element.J0_to_J1 @ J - J2 = _ReducedHCT_Element.J0_to_J2 @ J - - col0 = J @ np.expand_dims(tri_dz[:, 0, :], axis=2) - col1 = J1 @ np.expand_dims(tri_dz[:, 1, :], axis=2) - col2 = J2 @ np.expand_dims(tri_dz[:, 2, :], axis=2) - - dfdksi = _to_matrix_vectorized([ - [col0[:, 0, 0], col1[:, 0, 0], col2[:, 0, 0]], - [col0[:, 1, 0], col1[:, 1, 0], col2[:, 1, 0]]]) - dof[:, 0:7:3] = tri_z - dof[:, 1:8:3] = dfdksi[:, 0] - dof[:, 2:9:3] = dfdksi[:, 1] - return dof - - -class _DOF_estimator_user(_DOF_estimator): - """dz is imposed by user; accounts for scaling if any.""" - - def compute_dz(self, dz): - (dzdx, dzdy) = dz - dzdx = dzdx * self._unit_x - dzdy = dzdy * self._unit_y - return np.vstack([dzdx, dzdy]).T - - -class _DOF_estimator_geom(_DOF_estimator): - """Fast 'geometric' approximation, recommended for large arrays.""" - - def compute_dz(self): - """ - self.df is computed as weighted average of _triangles sharing a common - node. On each triangle itri f is first assumed linear (= ~f), which - allows to compute d~f[itri] - Then the following approximation of df nodal values is then proposed: - f[ipt] = SUM ( w[itri] x d~f[itri] , for itri sharing apex ipt) - The weighted coeff. w[itri] are proportional to the angle of the - triangle itri at apex ipt - """ - el_geom_w = self.compute_geom_weights() - el_geom_grad = self.compute_geom_grads() - - # Sum of weights coeffs - w_node_sum = np.bincount(np.ravel(self._triangles), - weights=np.ravel(el_geom_w)) - - # Sum of weighted df = (dfx, dfy) - dfx_el_w = np.empty_like(el_geom_w) - dfy_el_w = np.empty_like(el_geom_w) - for iapex in range(3): - dfx_el_w[:, iapex] = el_geom_w[:, iapex]*el_geom_grad[:, 0] - dfy_el_w[:, iapex] = el_geom_w[:, iapex]*el_geom_grad[:, 1] - dfx_node_sum = np.bincount(np.ravel(self._triangles), - weights=np.ravel(dfx_el_w)) - dfy_node_sum = np.bincount(np.ravel(self._triangles), - weights=np.ravel(dfy_el_w)) - - # Estimation of df - dfx_estim = dfx_node_sum/w_node_sum - dfy_estim = dfy_node_sum/w_node_sum - return np.vstack([dfx_estim, dfy_estim]).T - - def compute_geom_weights(self): - """ - 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]) - tris_pts = self._tris_pts - for ipt in range(3): - p0 = tris_pts[:, ipt % 3, :] - p1 = tris_pts[:, (ipt+1) % 3, :] - p2 = tris_pts[:, (ipt-1) % 3, :] - alpha1 = np.arctan2(p1[:, 1]-p0[:, 1], p1[:, 0]-p0[:, 0]) - alpha2 = np.arctan2(p2[:, 1]-p0[:, 1], p2[:, 0]-p0[:, 0]) - # In the below formula we could take modulo 2. but - # modulo 1. is safer regarding round-off errors (flat triangles). - angle = np.abs(((alpha2-alpha1) / np.pi) % 1) - # Weight proportional to angle up np.pi/2; null weight for - # degenerated cases 0 and np.pi (note that *angle* is normalized - # by np.pi). - weights[:, ipt] = 0.5 - np.abs(angle-0.5) - return weights - - def compute_geom_grads(self): - """ - Compute the (global) gradient component of f assumed linear (~f). - returns array df of shape (nelems, 2) - df[ielem].dM[ielem] = dz[ielem] i.e. df = dz x dM = dM.T^-1 x dz - """ - tris_pts = self._tris_pts - tris_f = self.z[self._triangles] - - dM1 = tris_pts[:, 1, :] - tris_pts[:, 0, :] - dM2 = tris_pts[:, 2, :] - tris_pts[:, 0, :] - dM = np.dstack([dM1, dM2]) - # Here we try to deal with the simplest colinear cases: a null - # gradient is assumed in this case. - dM_inv = _safe_inv22_vectorized(dM) - - dZ1 = tris_f[:, 1] - tris_f[:, 0] - dZ2 = tris_f[:, 2] - tris_f[:, 0] - dZ = np.vstack([dZ1, dZ2]).T - df = np.empty_like(dZ) - - # With np.einsum: could be ej,eji -> ej - df[:, 0] = dZ[:, 0]*dM_inv[:, 0, 0] + dZ[:, 1]*dM_inv[:, 1, 0] - df[:, 1] = dZ[:, 0]*dM_inv[:, 0, 1] + dZ[:, 1]*dM_inv[:, 1, 1] - return df - - -class _DOF_estimator_min_E(_DOF_estimator_geom): - """ - The 'smoothest' approximation, df is computed through global minimization - of the bending energy: - E(f) = integral[(d2z/dx2 + d2z/dy2 + 2 d2z/dxdy)**2 dA] - """ - def __init__(self, Interpolator): - self._eccs = Interpolator._eccs - super().__init__(Interpolator) - - def compute_dz(self): - """ - Elliptic solver for bending energy minimization. - Uses a dedicated 'toy' sparse Jacobi PCG solver. - """ - # Initial guess for iterative PCG solver. - dz_init = super().compute_dz() - Uf0 = np.ravel(dz_init) - - reference_element = _ReducedHCT_Element() - J = CubicTriInterpolator._get_jacobian(self._tris_pts) - eccs = self._eccs - triangles = self._triangles - Uc = self.z[self._triangles] - - # Building stiffness matrix and force vector in coo format - Kff_rows, Kff_cols, Kff_vals, Ff = reference_element.get_Kff_and_Ff( - J, eccs, triangles, Uc) - - # Building sparse matrix and solving minimization problem - # We could use scipy.sparse direct solver; however to avoid this - # external dependency an implementation of a simple PCG solver with - # a simple diagonal Jacobi preconditioner is implemented. - tol = 1.e-10 - n_dof = Ff.shape[0] - Kff_coo = _Sparse_Matrix_coo(Kff_vals, Kff_rows, Kff_cols, - shape=(n_dof, n_dof)) - Kff_coo.compress_csc() - Uf, err = _cg(A=Kff_coo, b=Ff, x0=Uf0, tol=tol) - # If the PCG did not converge, we return the best guess between Uf0 - # and Uf. - err0 = np.linalg.norm(Kff_coo.dot(Uf0) - Ff) - if err0 < err: - # Maybe a good occasion to raise a warning here ? - _api.warn_external("In TriCubicInterpolator initialization, " - "PCG sparse solver did not converge after " - "1000 iterations. `geom` approximation is " - "used instead of `min_E`") - Uf = Uf0 - - # Building dz from Uf - dz = np.empty([self._pts.shape[0], 2], dtype=np.float64) - dz[:, 0] = Uf[::2] - dz[:, 1] = Uf[1::2] - return dz - - -# The following private :class:_Sparse_Matrix_coo and :func:_cg provide -# a PCG sparse solver for (symmetric) elliptic problems. -class _Sparse_Matrix_coo: - def __init__(self, vals, rows, cols, shape): - """ - 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) - self.rows = np.asarray(rows, dtype=np.int32) - self.cols = np.asarray(cols, dtype=np.int32) - - def dot(self, V): - """ - Dot product of self by a vector *V* in sparse-dense to dense format - *V* dense vector of shape (self.m,). - """ - assert V.shape == (self.m,) - return np.bincount(self.rows, - weights=self.vals*V[self.cols], - minlength=self.m) - - def compress_csc(self): - """ - Compress rows, cols, vals / summing duplicates. Sort for csc format. - """ - _, unique, indices = np.unique( - self.rows + self.n*self.cols, - return_index=True, return_inverse=True) - self.rows = self.rows[unique] - self.cols = self.cols[unique] - self.vals = np.bincount(indices, weights=self.vals) - - def compress_csr(self): - """ - Compress rows, cols, vals / summing duplicates. Sort for csr format. - """ - _, unique, indices = np.unique( - self.m*self.rows + self.cols, - return_index=True, return_inverse=True) - self.rows = self.rows[unique] - self.cols = self.cols[unique] - self.vals = np.bincount(indices, weights=self.vals) - - def to_dense(self): - """ - Return a dense matrix representing self, mainly for debugging purposes. - """ - ret = np.zeros([self.n, self.m], dtype=np.float64) - nvals = self.vals.size - for i in range(nvals): - ret[self.rows[i], self.cols[i]] += self.vals[i] - return ret - - def __str__(self): - return self.to_dense().__str__() - - @property - def diag(self): - """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] - return diag - - -def _cg(A, b, x0=None, tol=1.e-10, maxiter=1000): - """ - Use Preconditioned Conjugate Gradient iteration to solve A x = b - A simple Jacobi (diagonal) preconditionner is used. - - Parameters - ---------- - 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 - ------- - x : array - The converged solution. - err : float - The absolute error np.linalg.norm(A.dot(x) - b) - """ - n = b.size - assert A.n == n - assert A.m == n - b_norm = np.linalg.norm(b) - - # Jacobi pre-conditioner - kvec = A.diag - # For diag elem < 1e-6 we keep 1e-6. - kvec = np.maximum(kvec, 1e-6) - - # Initial guess - if x0 is None: - x = np.zeros(n) - else: - x = x0 - - r = b - A.dot(x) - w = r/kvec - - p = np.zeros(n) - beta = 0.0 - rho = np.dot(r, w) - k = 0 - - # Following C. T. Kelley - while (np.sqrt(abs(rho)) > tol*b_norm) and (k < maxiter): - p = w + beta*p - z = A.dot(p) - alpha = rho/np.dot(p, z) - r = r - alpha*z - w = r/kvec - rhoold = rho - rho = np.dot(r, w) - x = x + alpha*p - beta = rho/rhoold - #err = np.linalg.norm(A.dot(x) - b) # absolute accuracy - not used - k += 1 - err = np.linalg.norm(A.dot(x) - b) - return x, err - - -# The following private functions: -# :func:`_safe_inv22_vectorized` -# :func:`_pseudo_inv22sym_vectorized` -# :func:`_scalar_vectorized` -# :func:`_transpose_vectorized` -# :func:`_roll_vectorized` -# :func:`_to_matrix_vectorized` -# :func:`_extract_submatrices` -# provide fast numpy implementation of some standard operations on arrays of -# matrices - stored as (:, n_rows, n_cols)-shaped np.arrays. - -# Development note: Dealing with pathologic 'flat' triangles in the -# CubicTriInterpolator code and impact on (2, 2)-matrix inversion functions -# :func:`_safe_inv22_vectorized` and :func:`_pseudo_inv22sym_vectorized`. -# -# Goals: -# 1) The CubicTriInterpolator should be able to handle flat or almost flat -# triangles without raising an error, -# 2) These degenerated triangles should have no impact on the automatic dof -# calculation (associated with null weight for the _DOF_estimator_geom and -# with null energy for the _DOF_estimator_min_E), -# 3) Linear patch test should be passed exactly on degenerated meshes, -# 4) Interpolation (with :meth:`_interpolate_single_key` or -# :meth:`_interpolate_multi_key`) shall be correctly handled even *inside* -# the pathologic triangles, to interact correctly with a TriRefiner class. -# -# Difficulties: -# Flat triangles have rank-deficient *J* (so-called jacobian matrix) and -# *metric* (the metric tensor = J x J.T). Computation of the local -# tangent plane is also problematic. -# -# Implementation: -# Most of the time, when computing the inverse of a rank-deficient matrix it -# is safe to simply return the null matrix (which is the implementation in -# :func:`_safe_inv22_vectorized`). This is because of point 2), itself -# enforced by: -# - null area hence null energy in :class:`_DOF_estimator_min_E` -# - angles close or equal to 0 or np.pi hence null weight in -# :class:`_DOF_estimator_geom`. -# Note that the function angle -> weight is continuous and maximum for an -# angle np.pi/2 (refer to :meth:`compute_geom_weights`) -# The exception is the computation of barycentric coordinates, which is done -# by inversion of the *metric* matrix. In this case, we need to compute a set -# of valid coordinates (1 among numerous possibilities), to ensure point 4). -# We benefit here from the symmetry of metric = J x J.T, which makes it easier -# to compute a pseudo-inverse in :func:`_pseudo_inv22sym_vectorized` -def _safe_inv22_vectorized(M): - """ - Inversion of arrays of (2, 2) matrices, returns 0 for rank-deficient - matrices. - - *M* : array of (2, 2) matrices to inverse, shape (n, 2, 2) - """ - assert M.ndim == 3 - assert M.shape[-2:] == (2, 2) - M_inv = np.empty_like(M) - prod1 = M[:, 0, 0]*M[:, 1, 1] - delta = prod1 - M[:, 0, 1]*M[:, 1, 0] - - # We set delta_inv to 0. in case of a rank deficient matrix; a - # rank-deficient input matrix *M* will lead to a null matrix in output - rank2 = (np.abs(delta) > 1e-8*np.abs(prod1)) - if np.all(rank2): - # Normal 'optimized' flow. - delta_inv = 1./delta - else: - # 'Pathologic' flow. - delta_inv = np.zeros(M.shape[0]) - delta_inv[rank2] = 1./delta[rank2] - - M_inv[:, 0, 0] = M[:, 1, 1]*delta_inv - M_inv[:, 0, 1] = -M[:, 0, 1]*delta_inv - M_inv[:, 1, 0] = -M[:, 1, 0]*delta_inv - M_inv[:, 1, 1] = M[:, 0, 0]*delta_inv - return M_inv - - -def _pseudo_inv22sym_vectorized(M): - """ - Inversion of arrays of (2, 2) SYMMETRIC matrices; returns the - (Moore-Penrose) pseudo-inverse for rank-deficient matrices. - - In case M is of rank 1, we have M = trace(M) x P where P is the orthogonal - projection on Im(M), and we return trace(M)^-1 x P == M / trace(M)**2 - In case M is of rank 0, we return the null matrix. - - *M* : array of (2, 2) matrices to inverse, shape (n, 2, 2) - """ - assert M.ndim == 3 - assert M.shape[-2:] == (2, 2) - M_inv = np.empty_like(M) - prod1 = M[:, 0, 0]*M[:, 1, 1] - delta = prod1 - M[:, 0, 1]*M[:, 1, 0] - rank2 = (np.abs(delta) > 1e-8*np.abs(prod1)) - - if np.all(rank2): - # Normal 'optimized' flow. - M_inv[:, 0, 0] = M[:, 1, 1] / delta - M_inv[:, 0, 1] = -M[:, 0, 1] / delta - M_inv[:, 1, 0] = -M[:, 1, 0] / delta - M_inv[:, 1, 1] = M[:, 0, 0] / delta - else: - # 'Pathologic' flow. - # Here we have to deal with 2 sub-cases - # 1) First sub-case: matrices of rank 2: - delta = delta[rank2] - M_inv[rank2, 0, 0] = M[rank2, 1, 1] / delta - M_inv[rank2, 0, 1] = -M[rank2, 0, 1] / delta - M_inv[rank2, 1, 0] = -M[rank2, 1, 0] / delta - M_inv[rank2, 1, 1] = M[rank2, 0, 0] / delta - # 2) Second sub-case: rank-deficient matrices of rank 0 and 1: - rank01 = ~rank2 - tr = M[rank01, 0, 0] + M[rank01, 1, 1] - tr_zeros = (np.abs(tr) < 1.e-8) - sq_tr_inv = (1.-tr_zeros) / (tr**2+tr_zeros) - #sq_tr_inv = 1. / tr**2 - M_inv[rank01, 0, 0] = M[rank01, 0, 0] * sq_tr_inv - M_inv[rank01, 0, 1] = M[rank01, 0, 1] * sq_tr_inv - M_inv[rank01, 1, 0] = M[rank01, 1, 0] * sq_tr_inv - M_inv[rank01, 1, 1] = M[rank01, 1, 1] * sq_tr_inv - - return M_inv - - -def _scalar_vectorized(scalar, M): - """ - Scalar product between scalars and matrices. - """ - return scalar[:, np.newaxis, np.newaxis]*M - - -def _transpose_vectorized(M): - """ - Transposition of an array of matrices *M*. - """ - return np.transpose(M, [0, 2, 1]) - - -def _roll_vectorized(M, roll_indices, axis): - """ - 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 - assert ndim == 3 - ndim_roll = roll_indices.ndim - assert ndim_roll == 1 - sh = M.shape - r, c = sh[-2:] - assert sh[0] == roll_indices.shape[0] - vec_indices = np.arange(sh[0], dtype=np.int32) - - # Builds the rolled matrix - M_roll = np.empty_like(M) - if axis == 0: - for ir in range(r): - for ic in range(c): - M_roll[:, ir, ic] = M[vec_indices, (-roll_indices+ir) % r, ic] - elif axis == 1: - for ir in range(r): - for ic in range(c): - M_roll[:, ir, ic] = M[vec_indices, ir, (-roll_indices+ic) % c] - return M_roll - - -def _to_matrix_vectorized(M): - """ - Build an array of matrices from individuals np.arrays of identical shapes. - - 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) - c_vec = np.asarray([len(item) for item in M]) - assert np.all(c_vec-c_vec[0] == 0) - r = len(M) - c = c_vec[0] - M00 = np.asarray(M[0][0]) - dt = M00.dtype - sh = [M00.shape[0], r, c] - M_ret = np.empty(sh, dtype=dt) - for irow in range(r): - for icol in range(c): - M_ret[:, irow, icol] = np.asarray(M[irow][icol]) - return M_ret - - -def _extract_submatrices(M, block_indices, block_size, axis): - """ - Extract selected blocks of a matrices *M* depending on parameters - *block_indices* and *block_size*. - - Returns the array of extracted matrices *Mres* so that :: - - M_res[..., ir, :] = M[(block_indices*block_size+ir), :] - """ - assert block_indices.ndim == 1 - assert axis in [0, 1] - - r, c = M.shape - if axis == 0: - sh = [block_indices.shape[0], block_size, c] - elif axis == 1: - sh = [block_indices.shape[0], r, block_size] - dt = M.dtype - M_res = np.empty(sh, dtype=dt) - if axis == 0: - for ir in range(block_size): - M_res[:, ir, :] = M[(block_indices*block_size+ir), :] - elif axis == 1: - for ic in range(block_size): - M_res[:, :, ic] = M[:, (block_indices*block_size+ic)] - return M_res +_api.warn_deprecated( + "3.7", + message=f"Importing {__name__} was deprecated in Matplotlib 3.7 and will " + f"be removed two minor releases later. All functionality is " + f"available via the top-level module matplotlib.tri") diff --git a/lib/matplotlib/tri/tripcolor.py b/lib/matplotlib/tri/tripcolor.py index f1f7de2285dc..0da87891810d 100644 --- a/lib/matplotlib/tri/tripcolor.py +++ b/lib/matplotlib/tri/tripcolor.py @@ -1,131 +1,9 @@ -import numpy as np - +from ._tripcolor import * # noqa: F401, F403 from matplotlib import _api -from matplotlib.collections import PolyCollection, TriMesh -from matplotlib.colors import Normalize -from matplotlib.tri.triangulation import Triangulation - - -def tripcolor(ax, *args, alpha=1.0, norm=None, cmap=None, vmin=None, - vmax=None, shading='flat', facecolors=None, **kwargs): - """ - Create a pseudocolor plot of an unstructured triangular grid. - - The triangulation can be specified in one of two ways; either:: - - tripcolor(triangulation, ...) - - where triangulation is a `.Triangulation` object, or - - :: - - tripcolor(x, y, ...) - tripcolor(x, y, triangles, ...) - tripcolor(x, y, triangles=triangles, ...) - tripcolor(x, y, mask=mask, ...) - tripcolor(x, y, triangles, mask=mask, ...) - - in which case a Triangulation object will be created. See `.Triangulation` - for a explanation of these possibilities. - - The next argument must be *C*, the array of color values, either - one per point in the triangulation if color values are defined at - points, or one per triangle in the triangulation if color values - are defined at triangles. If there are the same number of points - and triangles in the triangulation it is assumed that color - values are defined at points; to force the use of color values at - triangles use the kwarg ``facecolors=C`` instead of just ``C``. - - *shading* may be 'flat' (the default) or 'gouraud'. If *shading* - is 'flat' and C values are defined at points, the color values - used for each triangle are from the mean C of the triangle's - three points. If *shading* is 'gouraud' then color values must be - defined at points. - - The remaining kwargs are the same as for `~.Axes.pcolor`. - """ - _api.check_in_list(['flat', 'gouraud'], shading=shading) - - tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs) - - # C is the colors array defined at either points or faces (i.e. triangles). - # If facecolors is None, C are defined at points. - # If facecolors is not None, C are defined at faces. - if facecolors is not None: - C = facecolors - else: - C = np.asarray(args[0]) - - # If there are a different number of points and triangles in the - # triangulation, can omit facecolors kwarg as it is obvious from - # length of C whether it refers to points or faces. - # Do not do this for gouraud shading. - if (facecolors is None and len(C) == len(tri.triangles) and - len(C) != len(tri.x) and shading != 'gouraud'): - facecolors = C - - # Check length of C is OK. - if ((facecolors is None and len(C) != len(tri.x)) or - (facecolors is not None and len(C) != len(tri.triangles))): - raise ValueError('Length of color values array must be the same ' - 'as either the number of triangulation points ' - 'or triangles') - - # Handling of linewidths, shading, edgecolors and antialiased as - # in Axes.pcolor - linewidths = (0.25,) - if 'linewidth' in kwargs: - kwargs['linewidths'] = kwargs.pop('linewidth') - kwargs.setdefault('linewidths', linewidths) - - edgecolors = 'none' - if 'edgecolor' in kwargs: - kwargs['edgecolors'] = kwargs.pop('edgecolor') - ec = kwargs.setdefault('edgecolors', edgecolors) - - if 'antialiased' in kwargs: - kwargs['antialiaseds'] = kwargs.pop('antialiased') - if 'antialiaseds' not in kwargs and ec.lower() == "none": - kwargs['antialiaseds'] = False - - if shading == 'gouraud': - if facecolors is not None: - raise ValueError('Gouraud shading does not support the use ' - 'of facecolors kwarg') - if len(C) != len(tri.x): - raise ValueError('For gouraud shading, the length of color ' - 'values array must be the same as the ' - 'number of triangulation points') - collection = TriMesh(tri, **kwargs) - else: - # Vertices of triangles. - maskedTris = tri.get_masked_triangles() - verts = np.stack((tri.x[maskedTris], tri.y[maskedTris]), axis=-1) - - # Color values. - if facecolors is None: - # One color per triangle, the mean of the 3 vertex color values. - C = C[maskedTris].mean(axis=1) - elif tri.mask is not None: - # Remove color values of masked triangles. - C = C[~tri.mask] - - collection = PolyCollection(verts, **kwargs) - collection.set_alpha(alpha) - collection.set_array(C) - _api.check_isinstance((Normalize, None), norm=norm) - collection.set_cmap(cmap) - collection.set_norm(norm) - collection._scale_norm(norm, vmin, vmax) - ax.grid(False) - minx = tri.x.min() - maxx = tri.x.max() - miny = tri.y.min() - maxy = tri.y.max() - corners = (minx, miny), (maxx, maxy) - ax.update_datalim(corners) - ax.autoscale_view() - ax.add_collection(collection) - return collection +_api.warn_deprecated( + "3.7", + message=f"Importing {__name__} was deprecated in Matplotlib 3.7 and will " + f"be removed two minor releases later. All functionality is " + f"available via the top-level module matplotlib.tri") diff --git a/lib/matplotlib/tri/triplot.py b/lib/matplotlib/tri/triplot.py index 97e3725464e7..7c012b1a59e7 100644 --- a/lib/matplotlib/tri/triplot.py +++ b/lib/matplotlib/tri/triplot.py @@ -1,82 +1,9 @@ -import numpy as np -from matplotlib.tri.triangulation import Triangulation +from ._triplot import * # noqa: F401, F403 +from matplotlib import _api -def triplot(ax, *args, **kwargs): - """ - Draw a unstructured triangular grid as lines and/or markers. - - The triangulation to plot can be specified in one of two ways; either:: - - triplot(triangulation, ...) - - where triangulation is a `.Triangulation` object, or - - :: - - triplot(x, y, ...) - triplot(x, y, triangles, ...) - triplot(x, y, triangles=triangles, ...) - triplot(x, y, mask=mask, ...) - triplot(x, y, triangles, mask=mask, ...) - - in which case a Triangulation object will be created. See `.Triangulation` - for a explanation of these possibilities. - - The remaining args and kwargs are the same as for `~.Axes.plot`. - - Returns - ------- - lines : `~matplotlib.lines.Line2D` - The drawn triangles edges. - markers : `~matplotlib.lines.Line2D` - The drawn marker nodes. - """ - import matplotlib.axes - - tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs) - x, y, edges = (tri.x, tri.y, tri.edges) - - # Decode plot format string, e.g., 'ro-' - fmt = args[0] if args else "" - linestyle, marker, color = matplotlib.axes._base._process_plot_format(fmt) - - # Insert plot format string into a copy of kwargs (kwargs values prevail). - kw = kwargs.copy() - for key, val in zip(('linestyle', 'marker', 'color'), - (linestyle, marker, color)): - if val is not None: - kw[key] = kwargs.get(key, val) - - # Draw lines without markers. - # Note 1: If we drew markers here, most markers would be drawn more than - # once as they belong to several edges. - # Note 2: We insert nan values in the flattened edges arrays rather than - # plotting directly (triang.x[edges].T, triang.y[edges].T) - # as it considerably speeds-up code execution. - linestyle = kw['linestyle'] - kw_lines = { - **kw, - 'marker': 'None', # No marker to draw. - 'zorder': kw.get('zorder', 1), # Path default zorder is used. - } - if linestyle not in [None, 'None', '', ' ']: - tri_lines_x = np.insert(x[edges], 2, np.nan, axis=1) - tri_lines_y = np.insert(y[edges], 2, np.nan, axis=1) - tri_lines = ax.plot(tri_lines_x.ravel(), tri_lines_y.ravel(), - **kw_lines) - else: - tri_lines = ax.plot([], [], **kw_lines) - - # Draw markers separately. - marker = kw['marker'] - kw_markers = { - **kw, - 'linestyle': 'None', # No line to draw. - } - if marker not in [None, 'None', '', ' ']: - tri_markers = ax.plot(x, y, **kw_markers) - else: - tri_markers = ax.plot([], [], **kw_markers) - - return tri_lines + tri_markers +_api.warn_deprecated( + "3.7", + message=f"Importing {__name__} was deprecated in Matplotlib 3.7 and will " + f"be removed two minor releases later. All functionality is " + f"available via the top-level module matplotlib.tri") diff --git a/lib/matplotlib/tri/trirefine.py b/lib/matplotlib/tri/trirefine.py index 674ee211cf46..6f22f9e8d203 100644 --- a/lib/matplotlib/tri/trirefine.py +++ b/lib/matplotlib/tri/trirefine.py @@ -1,307 +1,9 @@ -""" -Mesh refinement for triangular grids. -""" - -import numpy as np - +from ._trirefine import * # noqa: F401, F403 from matplotlib import _api -from matplotlib.tri.triangulation import Triangulation -import matplotlib.tri.triinterpolate - - -class TriRefiner: - """ - Abstract base class for classes implementing mesh refinement. - - A TriRefiner encapsulates a Triangulation object and provides tools for - mesh refinement and interpolation. - - Derived classes must implement: - - - ``refine_triangulation(return_tri_index=False, **kwargs)`` , where - the optional keyword arguments *kwargs* are defined in each - TriRefiner concrete implementation, and which returns: - - - a refined triangulation, - - optionally (depending on *return_tri_index*), for each - point of the refined triangulation: the index of - the initial triangulation triangle to which it belongs. - - - ``refine_field(z, triinterpolator=None, **kwargs)``, where: - - - *z* array of field values (to refine) defined at the base - triangulation nodes, - - *triinterpolator* is an optional `~matplotlib.tri.TriInterpolator`, - - the other optional keyword arguments *kwargs* are defined in - each TriRefiner concrete implementation; - - and which returns (as a tuple) a refined triangular mesh and the - interpolated values of the field at the refined triangulation nodes. - """ - - def __init__(self, triangulation): - _api.check_isinstance(Triangulation, triangulation=triangulation) - self._triangulation = triangulation - - -class UniformTriRefiner(TriRefiner): - """ - Uniform mesh refinement by recursive subdivisions. - - Parameters - ---------- - triangulation : `~matplotlib.tri.Triangulation` - The encapsulated triangulation (to be refined) - """ -# See Also -# -------- -# :class:`~matplotlib.tri.CubicTriInterpolator` and -# :class:`~matplotlib.tri.TriAnalyzer`. -# """ - def __init__(self, triangulation): - super().__init__(triangulation) - - def refine_triangulation(self, return_tri_index=False, subdiv=3): - """ - Compute an uniformly refined triangulation *refi_triangulation* of - the encapsulated :attr:`triangulation`. - - This function refines the encapsulated triangulation by splitting each - father triangle into 4 child sub-triangles built on the edges midside - nodes, recursing *subdiv* times. In the end, each triangle is hence - divided into ``4**subdiv`` child triangles. - - Parameters - ---------- - return_tri_index : bool, default: False - Whether an index table indicating the father triangle index of each - point is returned. - subdiv : int, default: 3 - Recursion level for the subdivision. - Each triangle is divided into ``4**subdiv`` child triangles; - hence, the default results in 64 refined subtriangles for each - triangle of the initial triangulation. - - Returns - ------- - refi_triangulation : `~matplotlib.tri.Triangulation` - The refined triangulation. - found_index : int array - Index of the initial triangulation containing triangle, for each - point of *refi_triangulation*. - Returned only if *return_tri_index* is set to True. - """ - refi_triangulation = self._triangulation - ntri = refi_triangulation.triangles.shape[0] - - # Computes the triangulation ancestors numbers in the reference - # triangulation. - ancestors = np.arange(ntri, dtype=np.int32) - for _ in range(subdiv): - refi_triangulation, ancestors = self._refine_triangulation_once( - refi_triangulation, ancestors) - refi_npts = refi_triangulation.x.shape[0] - refi_triangles = refi_triangulation.triangles - - # Now we compute found_index table if needed - if return_tri_index: - # We have to initialize found_index with -1 because some nodes - # may very well belong to no triangle at all, e.g., in case of - # Delaunay Triangulation with DuplicatePointWarning. - found_index = np.full(refi_npts, -1, dtype=np.int32) - tri_mask = self._triangulation.mask - if tri_mask is None: - found_index[refi_triangles] = np.repeat(ancestors, - 3).reshape(-1, 3) - else: - # There is a subtlety here: we want to avoid whenever possible - # that refined points container is a masked triangle (which - # would result in artifacts in plots). - # So we impose the numbering from masked ancestors first, - # then overwrite it with unmasked ancestor numbers. - ancestor_mask = tri_mask[ancestors] - found_index[refi_triangles[ancestor_mask, :] - ] = np.repeat(ancestors[ancestor_mask], - 3).reshape(-1, 3) - found_index[refi_triangles[~ancestor_mask, :] - ] = np.repeat(ancestors[~ancestor_mask], - 3).reshape(-1, 3) - return refi_triangulation, found_index - else: - return refi_triangulation - - def refine_field(self, z, triinterpolator=None, subdiv=3): - """ - Refine a field defined on the encapsulated triangulation. - - Parameters - ---------- - z : (npoints,) array-like - Values of the field to refine, defined at the nodes of the - encapsulated triangulation. (``n_points`` is the number of points - in the initial triangulation) - triinterpolator : `~matplotlib.tri.TriInterpolator`, optional - Interpolator used for field interpolation. If not specified, - a `~matplotlib.tri.CubicTriInterpolator` will be used. - subdiv : int, default: 3 - Recursion level for the subdivision. - Each triangle is divided into ``4**subdiv`` child triangles. - - Returns - ------- - refi_tri : `~matplotlib.tri.Triangulation` - The returned refined triangulation. - refi_z : 1D array of length: *refi_tri* node count. - The returned interpolated field (at *refi_tri* nodes). - """ - if triinterpolator is None: - interp = matplotlib.tri.CubicTriInterpolator( - self._triangulation, z) - else: - _api.check_isinstance(matplotlib.tri.TriInterpolator, - triinterpolator=triinterpolator) - interp = triinterpolator - - refi_tri, found_index = self.refine_triangulation( - subdiv=subdiv, return_tri_index=True) - refi_z = interp._interpolate_multikeys( - refi_tri.x, refi_tri.y, tri_index=found_index)[0] - return refi_tri, refi_z - - @staticmethod - def _refine_triangulation_once(triangulation, ancestors=None): - """ - 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: - child_triangulation. - - If the array-like key table *ancestor* is given, it shall be of shape - (ntri,) where ntri is the number of *triangulation* masked_triangles. - In this case, the function returns - (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 - - # According to tri.triangulation doc: - # neighbors[i, j] is the triangle that is the neighbor - # to the edge from point index masked_triangles[i, j] to point - # index masked_triangles[i, (j+1)%3]. - neighbors = triangulation.neighbors - triangles = triangulation.triangles - npts = np.shape(x)[0] - ntri = np.shape(triangles)[0] - if ancestors is not None: - ancestors = np.asarray(ancestors) - if np.shape(ancestors) != (ntri,): - raise ValueError( - "Incompatible shapes provide for triangulation" - ".masked_triangles and ancestors: {0} and {1}".format( - np.shape(triangles), np.shape(ancestors))) - - # Initiating tables refi_x and refi_y of the refined triangulation - # points - # hint: each apex is shared by 2 masked_triangles except the borders. - borders = np.sum(neighbors == -1) - added_pts = (3*ntri + borders) // 2 - refi_npts = npts + added_pts - refi_x = np.zeros(refi_npts) - refi_y = np.zeros(refi_npts) - - # First part of refi_x, refi_y is just the initial points - refi_x[:npts] = x - refi_y[:npts] = y - - # Second part contains the edge midside nodes. - # Each edge belongs to 1 triangle (if border edge) or is shared by 2 - # masked_triangles (interior edge). - # We first build 2 * ntri arrays of edge starting nodes (edge_elems, - # edge_apexes); we then extract only the masters to avoid overlaps. - # The so-called 'master' is the triangle with biggest index - # The 'slave' is the triangle with lower index - # (can be -1 if border edge) - # For slave and master we will identify the apex pointing to the edge - # start - edge_elems = np.tile(np.arange(ntri, dtype=np.int32), 3) - edge_apexes = np.repeat(np.arange(3, dtype=np.int32), ntri) - edge_neighbors = neighbors[edge_elems, edge_apexes] - mask_masters = (edge_elems > edge_neighbors) - - # Identifying the "masters" and adding to refi_x, refi_y vec - masters = edge_elems[mask_masters] - apex_masters = edge_apexes[mask_masters] - x_add = (x[triangles[masters, apex_masters]] + - x[triangles[masters, (apex_masters+1) % 3]]) * 0.5 - y_add = (y[triangles[masters, apex_masters]] + - y[triangles[masters, (apex_masters+1) % 3]]) * 0.5 - refi_x[npts:] = x_add - refi_y[npts:] = y_add - - # Building the new masked_triangles; each old masked_triangles hosts - # 4 new masked_triangles - # there are 6 pts to identify per 'old' triangle, 3 new_pt_corner and - # 3 new_pt_midside - new_pt_corner = triangles - - # What is the index in refi_x, refi_y of point at middle of apex iapex - # of elem ielem ? - # If ielem is the apex master: simple count, given the way refi_x was - # built. - # If ielem is the apex slave: yet we do not know; but we will soon - # using the neighbors table. - new_pt_midside = np.empty([ntri, 3], dtype=np.int32) - cum_sum = npts - for imid in range(3): - mask_st_loc = (imid == apex_masters) - n_masters_loc = np.sum(mask_st_loc) - elem_masters_loc = masters[mask_st_loc] - new_pt_midside[:, imid][elem_masters_loc] = np.arange( - n_masters_loc, dtype=np.int32) + cum_sum - cum_sum += n_masters_loc - - # Now dealing with slave elems. - # for each slave element we identify the master and then the inode - # once slave_masters is identified, slave_masters_apex is such that: - # neighbors[slaves_masters, slave_masters_apex] == slaves - mask_slaves = np.logical_not(mask_masters) - slaves = edge_elems[mask_slaves] - slaves_masters = edge_neighbors[mask_slaves] - diff_table = np.abs(neighbors[slaves_masters, :] - - np.outer(slaves, np.ones(3, dtype=np.int32))) - slave_masters_apex = np.argmin(diff_table, axis=1) - slaves_apex = edge_apexes[mask_slaves] - new_pt_midside[slaves, slaves_apex] = new_pt_midside[ - slaves_masters, slave_masters_apex] - - # Builds the 4 child masked_triangles - child_triangles = np.empty([ntri*4, 3], dtype=np.int32) - child_triangles[0::4, :] = np.vstack([ - new_pt_corner[:, 0], new_pt_midside[:, 0], - new_pt_midside[:, 2]]).T - child_triangles[1::4, :] = np.vstack([ - new_pt_corner[:, 1], new_pt_midside[:, 1], - new_pt_midside[:, 0]]).T - child_triangles[2::4, :] = np.vstack([ - new_pt_corner[:, 2], new_pt_midside[:, 2], - new_pt_midside[:, 1]]).T - child_triangles[3::4, :] = np.vstack([ - new_pt_midside[:, 0], new_pt_midside[:, 1], - new_pt_midside[:, 2]]).T - child_triangulation = Triangulation(refi_x, refi_y, child_triangles) - # Builds the child mask - if triangulation.mask is not None: - child_triangulation.set_mask(np.repeat(triangulation.mask, 4)) - if ancestors is None: - return child_triangulation - else: - return child_triangulation, np.repeat(ancestors, 4) +_api.warn_deprecated( + "3.7", + message=f"Importing {__name__} was deprecated in Matplotlib 3.7 and will " + f"be removed two minor releases later. All functionality is " + f"available via the top-level module matplotlib.tri") diff --git a/lib/matplotlib/tri/tritools.py b/lib/matplotlib/tri/tritools.py index 11b500fcdd8f..9c6839ca2049 100644 --- a/lib/matplotlib/tri/tritools.py +++ b/lib/matplotlib/tri/tritools.py @@ -1,263 +1,9 @@ -""" -Tools for triangular grids. -""" - -import numpy as np - +from ._tritools import * # noqa: F401, F403 from matplotlib import _api -from matplotlib.tri import Triangulation - - -class TriAnalyzer: - """ - Define basic tools for triangular mesh analysis and improvement. - - A TriAnalyzer encapsulates a `.Triangulation` object and provides basic - tools for mesh analysis and mesh improvement. - - Attributes - ---------- - scale_factors - - Parameters - ---------- - triangulation : `~matplotlib.tri.Triangulation` - The encapsulated triangulation to analyze. - """ - - def __init__(self, triangulation): - _api.check_isinstance(Triangulation, triangulation=triangulation) - self._triangulation = triangulation - - @property - def scale_factors(self): - """ - Factors to rescale the triangulation into a unit square. - - Returns - ------- - (float, float) - Scaling factors (kx, ky) so that the triangulation - ``[triangulation.x * kx, triangulation.y * ky]`` - fits exactly inside a unit square. - """ - compressed_triangles = self._triangulation.get_masked_triangles() - node_used = (np.bincount(np.ravel(compressed_triangles), - minlength=self._triangulation.x.size) != 0) - return (1 / np.ptp(self._triangulation.x[node_used]), - 1 / np.ptp(self._triangulation.y[node_used])) - - def circle_ratios(self, rescale=True): - """ - Return a measure of the triangulation triangles flatness. - - The ratio of the incircle radius over the circumcircle radius is a - widely used indicator of a triangle flatness. - It is always ``<= 0.5`` and ``== 0.5`` only for equilateral - triangles. Circle ratios below 0.01 denote very flat triangles. - - To avoid unduly low values due to a difference of scale between the 2 - axis, the triangular mesh can first be rescaled to fit inside a unit - square with `scale_factors` (Only if *rescale* is True, which is - its default value). - - Parameters - ---------- - rescale : bool, default: True - If True, internally rescale (based on `scale_factors`), so that the - (unmasked) triangles fit exactly inside a unit square mesh. - - Returns - ------- - masked array - Ratio of the incircle radius over the circumcircle radius, for - each 'rescaled' triangle of the encapsulated triangulation. - Values corresponding to masked triangles are masked out. - - """ - # Coords rescaling - if rescale: - (kx, ky) = self.scale_factors - else: - (kx, ky) = (1.0, 1.0) - pts = np.vstack([self._triangulation.x*kx, - self._triangulation.y*ky]).T - tri_pts = pts[self._triangulation.triangles] - # Computes the 3 side lengths - a = tri_pts[:, 1, :] - tri_pts[:, 0, :] - b = tri_pts[:, 2, :] - tri_pts[:, 1, :] - c = tri_pts[:, 0, :] - tri_pts[:, 2, :] - a = np.hypot(a[:, 0], a[:, 1]) - b = np.hypot(b[:, 0], b[:, 1]) - c = np.hypot(c[:, 0], c[:, 1]) - # circumcircle and incircle radii - s = (a+b+c)*0.5 - prod = s*(a+b-s)*(a+c-s)*(b+c-s) - # We have to deal with flat triangles with infinite circum_radius - bool_flat = (prod == 0.) - if np.any(bool_flat): - # Pathologic flow - ntri = tri_pts.shape[0] - circum_radius = np.empty(ntri, dtype=np.float64) - circum_radius[bool_flat] = np.inf - abc = a*b*c - circum_radius[~bool_flat] = abc[~bool_flat] / ( - 4.0*np.sqrt(prod[~bool_flat])) - else: - # Normal optimized flow - circum_radius = (a*b*c) / (4.0*np.sqrt(prod)) - in_radius = (a*b*c) / (4.0*circum_radius*s) - circle_ratio = in_radius/circum_radius - mask = self._triangulation.mask - if mask is None: - return circle_ratio - else: - return np.ma.array(circle_ratio, mask=mask) - - def get_flat_tri_mask(self, min_circle_ratio=0.01, rescale=True): - """ - Eliminate excessively flat border triangles from the triangulation. - - Returns a mask *new_mask* which allows to clean the encapsulated - triangulation from its border-located flat triangles - (according to their :meth:`circle_ratios`). - This mask is meant to be subsequently applied to the triangulation - using `.Triangulation.set_mask`. - *new_mask* is an extension of the initial triangulation mask - in the sense that an initially masked triangle will remain masked. - - The *new_mask* array is computed recursively; at each step flat - triangles are removed only if they share a side with the current mesh - border. Thus no new holes in the triangulated domain will be created. - - Parameters - ---------- - min_circle_ratio : float, default: 0.01 - Border triangles with incircle/circumcircle radii ratio r/R will - be removed if r/R < *min_circle_ratio*. - rescale : bool, default: True - 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 - ------- - array of bool - Mask to apply to encapsulated triangulation. - All the initially masked triangles remain masked in the - *new_mask*. - - Notes - ----- - The rationale behind this function is that a Delaunay - triangulation - of an unstructured set of points - sometimes contains - almost flat triangles at its border, leading to artifacts in plots - (especially for high-resolution contouring). - Masked with computed *new_mask*, the encapsulated - triangulation would contain no more unmasked border triangles - with a circle ratio below *min_circle_ratio*, thus improving the - mesh quality for subsequent plots or interpolation. - """ - # Recursively computes the mask_current_borders, true if a triangle is - # at the border of the mesh OR touching the border through a chain of - # invalid aspect ratio masked_triangles. - ntri = self._triangulation.triangles.shape[0] - mask_bad_ratio = self.circle_ratios(rescale) < min_circle_ratio - - current_mask = self._triangulation.mask - if current_mask is None: - current_mask = np.zeros(ntri, dtype=bool) - valid_neighbors = np.copy(self._triangulation.neighbors) - renum_neighbors = np.arange(ntri, dtype=np.int32) - nadd = -1 - while nadd != 0: - # The active wavefront is the triangles from the border (unmasked - # but with a least 1 neighbor equal to -1 - wavefront = (np.min(valid_neighbors, axis=1) == -1) & ~current_mask - # The element from the active wavefront will be masked if their - # circle ratio is bad. - added_mask = wavefront & mask_bad_ratio - current_mask = added_mask | current_mask - nadd = np.sum(added_mask) - - # now we have to update the tables valid_neighbors - valid_neighbors[added_mask, :] = -1 - renum_neighbors[added_mask] = -1 - valid_neighbors = np.where(valid_neighbors == -1, -1, - renum_neighbors[valid_neighbors]) - - return np.ma.filled(current_mask, True) - - def _get_compressed_triangulation(self): - """ - Compress (if masked) the encapsulated triangulation. - - Returns minimal-length triangles array (*compressed_triangles*) and - coordinates arrays (*compressed_x*, *compressed_y*) that can still - describe the unmasked triangles of the encapsulated triangulation. - - Returns - ------- - compressed_triangles : array-like - the returned compressed triangulation triangles - compressed_x : array-like - the returned compressed triangulation 1st coordinate - compressed_y : array-like - the returned compressed triangulation 2nd coordinate - tri_renum : int array - renumbering table to translate the triangle numbers from the - encapsulated triangulation into the new (compressed) renumbering. - -1 for masked triangles (deleted from *compressed_triangles*). - node_renum : int array - renumbering table to translate the point numbers from the - encapsulated triangulation into the new (compressed) renumbering. - -1 for unused points (i.e. those deleted from *compressed_x* and - *compressed_y*). - - """ - # Valid triangles and renumbering - tri_mask = self._triangulation.mask - compressed_triangles = self._triangulation.get_masked_triangles() - ntri = self._triangulation.triangles.shape[0] - if tri_mask is not None: - tri_renum = self._total_to_compress_renum(~tri_mask) - else: - tri_renum = np.arange(ntri, dtype=np.int32) - - # Valid nodes and renumbering - valid_node = (np.bincount(np.ravel(compressed_triangles), - minlength=self._triangulation.x.size) != 0) - compressed_x = self._triangulation.x[valid_node] - compressed_y = self._triangulation.y[valid_node] - node_renum = self._total_to_compress_renum(valid_node) - - # Now renumbering the valid triangles nodes - compressed_triangles = node_renum[compressed_triangles] - - return (compressed_triangles, compressed_x, compressed_y, tri_renum, - node_renum) - - @staticmethod - def _total_to_compress_renum(valid): - """ - Parameters - ---------- - valid : 1D bool array - Validity mask. - Returns - ------- - int array - Array so that (`valid_array` being a compressed array - based on a `masked_array` with mask ~*valid*): - - For all i with valid[i] = True: - valid_array[renum[i]] = masked_array[i] - - For all i with valid[i] = False: - renum[i] = -1 (invalid value) - """ - renum = np.full(np.size(valid), -1, dtype=np.int32) - n_valid = np.sum(valid) - renum[valid] = np.arange(n_valid, dtype=np.int32) - return renum +_api.warn_deprecated( + "3.7", + message=f"Importing {__name__} was deprecated in Matplotlib 3.7 and will " + f"be removed two minor releases later. All functionality is " + f"available via the top-level module matplotlib.tri") diff --git a/lib/matplotlib/ttconv.py b/lib/matplotlib/ttconv.py deleted file mode 100644 index bfb30d5fe58d..000000000000 --- a/lib/matplotlib/ttconv.py +++ /dev/null @@ -1,9 +0,0 @@ -""" -Converting and subsetting TrueType fonts to PS types 3 and 42, and PDF type 3. -""" - -from . import _api -from ._ttconv import convert_ttf_to_ps, get_pdf_charprocs # noqa - - -_api.warn_deprecated('3.3', name=__name__, obj_type='module') diff --git a/lib/matplotlib/type1font.py b/lib/matplotlib/type1font.py index 3fcee64613ce..85d93c714dad 100644 --- a/lib/matplotlib/type1font.py +++ b/lib/matplotlib/type1font.py @@ -1,335 +1,3 @@ -""" -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. - -Usage:: - - >>> font = Type1Font(filename) - >>> clear_part, encrypted_part, finale = font.parts - >>> slanted_font = font.transform({'slant': 0.167}) - >>> extended_font = font.transform({'extend': 1.2}) - -Sources: - -* Adobe Technical Note #5040, Supporting Downloadable PostScript - Language Fonts. - -* Adobe Type 1 Font Format, Adobe Systems Incorporated, third printing, - v1.1, 1993. ISBN 0-201-57044-0. -""" - -import binascii -import enum -import itertools -import re -import struct - -import numpy as np - -from matplotlib.cbook import _format_approx - - -# token types -_TokenType = enum.Enum('_TokenType', - 'whitespace name string delimiter number') - - -class Type1Font: - """ - A class representing a Type-1 font, for use by backends. - - Attributes - ---------- - parts : tuple - A 3-tuple of the cleartext part, the encrypted part, and the finale of - zeros. - prop : dict[str, Any] - A dictionary of font properties. - """ - __slots__ = ('parts', 'prop') - - def __init__(self, input): - """ - Initialize a Type-1 font. - - Parameters - ---------- - input : str or 3-tuple - Either a pfb file name, or a 3-tuple of already-decoded Type-1 - font `~.Type1Font.parts`. - """ - if isinstance(input, tuple) and len(input) == 3: - self.parts = input - else: - with open(input, 'rb') as file: - data = self._read(file) - self.parts = self._split(data) - - self._parse() - - def _read(self, file): - """Read the font from a file, decoding into usable parts.""" - rawdata = file.read() - if not rawdata.startswith(b'\x80'): - return rawdata - - data = b'' - while rawdata: - if not rawdata.startswith(b'\x80'): - raise RuntimeError('Broken pfb file (expected byte 128, ' - 'got %d)' % rawdata[0]) - type = rawdata[1] - if type in (1, 2): - length, = struct.unpack('{}/%[]+') - _comment_re = re.compile(br'%[^\r\n\v]*') - _instring_re = re.compile(br'[()\\]') - - @classmethod - def _tokens(cls, text): - """ - A PostScript tokenizer. Yield (token, value) pairs such as - (_TokenType.whitespace, ' ') or (_TokenType.name, '/Foobar'). - """ - pos = 0 - while pos < len(text): - match = (cls._comment_re.match(text[pos:]) or - cls._whitespace_re.match(text[pos:])) - if match: - yield (_TokenType.whitespace, match.group()) - pos += match.end() - elif text[pos] == b'(': - start = pos - pos += 1 - depth = 1 - while depth: - match = cls._instring_re.search(text[pos:]) - if match is None: - return - pos += match.end() - if match.group() == b'(': - depth += 1 - elif match.group() == b')': - depth -= 1 - else: # a backslash - skip the next character - pos += 1 - yield (_TokenType.string, text[start:pos]) - elif text[pos:pos + 2] in (b'<<', b'>>'): - yield (_TokenType.delimiter, text[pos:pos + 2]) - pos += 2 - elif text[pos] == b'<': - start = pos - pos += text[pos:].index(b'>') - yield (_TokenType.string, text[start:pos]) - else: - match = cls._token_re.match(text[pos:]) - if match: - try: - float(match.group()) - yield (_TokenType.number, match.group()) - except ValueError: - yield (_TokenType.name, match.group()) - pos += match.end() - else: - yield (_TokenType.delimiter, text[pos:pos + 1]) - pos += 1 - - def _parse(self): - """ - Find the values of various font properties. This limited kind - of parsing is described in Chapter 10 "Adobe Type Manager - Compatibility" of the Type-1 spec. - """ - # Start with reasonable defaults - prop = {'weight': 'Regular', 'ItalicAngle': 0.0, 'isFixedPitch': False, - 'UnderlinePosition': -100, 'UnderlineThickness': 50} - filtered = ((token, value) - for token, value in self._tokens(self.parts[0]) - if token is not _TokenType.whitespace) - # The spec calls this an ASCII format; in Python 2.x we could - # just treat the strings and names as opaque bytes but let's - # turn them into proper Unicode, and be lenient in case of high bytes. - def convert(x): return x.decode('ascii', 'replace') - for token, value in filtered: - if token is _TokenType.name and value.startswith(b'/'): - key = convert(value[1:]) - token, value = next(filtered) - if token is _TokenType.name: - if value in (b'true', b'false'): - value = value == b'true' - else: - value = convert(value.lstrip(b'/')) - elif token is _TokenType.string: - value = convert(value.lstrip(b'(').rstrip(b')')) - elif token is _TokenType.number: - if b'.' in value: - value = float(value) - else: - value = int(value) - else: # more complicated value such as an array - value = None - if key != 'FontInfo' and value is not None: - prop[key] = value - - # Fill in the various *Name properties - if 'FontName' not in prop: - prop['FontName'] = (prop.get('FullName') or - prop.get('FamilyName') or - 'Unknown') - if 'FullName' not in prop: - prop['FullName'] = prop['FontName'] - if 'FamilyName' not in prop: - extras = ('(?i)([ -](regular|plain|italic|oblique|(semi)?bold|' - '(ultra)?light|extra|condensed))+$') - prop['FamilyName'] = re.sub(extras, '', prop['FullName']) - - self.prop = prop - - @classmethod - def _transformer(cls, tokens, slant, extend): - def fontname(name): - result = name - if slant: - result += b'_Slant_%d' % int(1000 * slant) - if extend != 1.0: - result += b'_Extend_%d' % int(1000 * extend) - return result - - def italicangle(angle): - return b'%a' % round( - float(angle) - np.arctan(slant) / np.pi * 180, - 5 - ) - - def fontmatrix(array): - array = array.lstrip(b'[').rstrip(b']').split() - array = [float(x) for x in array] - oldmatrix = np.eye(3, 3) - oldmatrix[0:3, 0] = array[::2] - oldmatrix[0:3, 1] = array[1::2] - modifier = np.array([[extend, 0, 0], - [slant, 1, 0], - [0, 0, 1]]) - newmatrix = np.dot(modifier, oldmatrix) - array[::2] = newmatrix[0:3, 0] - array[1::2] = newmatrix[0:3, 1] - return ( - '[%s]' % ' '.join(_format_approx(x, 6) for x in array) - ).encode('ascii') - - def replace(fun): - def replacer(tokens): - token, value = next(tokens) # name, e.g., /FontMatrix - yield value - token, value = next(tokens) # possible whitespace - while token is _TokenType.whitespace: - yield value - token, value = next(tokens) - if value != b'[': # name/number/etc. - yield fun(value) - else: # array, e.g., [1 2 3] - result = b'' - while value != b']': - result += value - token, value = next(tokens) - result += value - yield fun(result) - return replacer - - def suppress(tokens): - for _ in itertools.takewhile(lambda x: x[1] != b'def', tokens): - pass - yield b'' - - table = {b'/FontName': replace(fontname), - b'/ItalicAngle': replace(italicangle), - b'/FontMatrix': replace(fontmatrix), - b'/UniqueID': suppress} - - for token, value in tokens: - if token is _TokenType.name and value in table: - yield from table[value]( - itertools.chain([(token, value)], tokens)) - else: - yield value - - def transform(self, effects): - """ - Return a new font that is slanted and/or extended. - - Parameters - ---------- - effects : dict - A dict with optional entries: - - - 'slant' : float, default: 0 - Tangent of the angle that the font is to be slanted to the - right. Negative values slant to the left. - - 'extend' : float, default: 1 - Scaling factor for the font width. Values less than 1 condense - the glyphs. - - Returns - ------- - `Type1Font` - """ - tokenizer = self._tokens(self.parts[0]) - transformed = self._transformer(tokenizer, - slant=effects.get('slant', 0.0), - extend=effects.get('extend', 1.0)) - return Type1Font((b"".join(transformed), self.parts[1], self.parts[2])) +from matplotlib._type1font import * # noqa: F401, F403 +from matplotlib import _api +_api.warn_deprecated("3.6", name=__name__, obj_type="module") diff --git a/lib/matplotlib/units.py b/lib/matplotlib/units.py index 311764c26be9..e3480f228bb4 100644 --- a/lib/matplotlib/units.py +++ b/lib/matplotlib/units.py @@ -5,8 +5,8 @@ units and units conversion. Use cases include converters for custom objects, e.g., a list of datetime objects, as well as for objects that are unit aware. We don't assume any particular units implementation; -rather a units implementation must provide the register with the Registry -converter dictionary and a `ConversionInterface`. For example, +rather a units implementation must register with the Registry converter +dictionary and provide a `ConversionInterface`. For example, here is a complete implementation which supports plotting with native datetime objects:: @@ -19,27 +19,25 @@ class DateConverter(units.ConversionInterface): @staticmethod def convert(value, unit, axis): - 'Convert a datetime value to a scalar or array' + "Convert a datetime value to a scalar or array." return dates.date2num(value) @staticmethod def axisinfo(unit, axis): - 'Return major and minor tick locators and formatters' - if unit!='date': return None + "Return major and minor tick locators and formatters." + if unit != 'date': + return None majloc = dates.AutoDateLocator() majfmt = dates.AutoDateFormatter(majloc) - return AxisInfo(majloc=majloc, - majfmt=majfmt, - label='date') + return units.AxisInfo(majloc=majloc, majfmt=majfmt, label='date') @staticmethod def default_units(x, axis): - 'Return the default unit for x or None' + "Return the default unit for x or None." return 'date' # Finally we register our object type with the Matplotlib units registry. units.registry[datetime.date] = DateConverter() - """ from decimal import Decimal @@ -133,22 +131,6 @@ def convert(obj, unit, axis): """ return obj - @staticmethod - def is_numlike(x): - """ - The Matplotlib datalim, autoscaling, locators etc work with scalars - which are the units converted to floats given the current unit. The - converter may be passed these floats, or arrays of them, even when - units are set. - """ - if np.iterable(x): - for thisx in x: - if thisx is ma.masked: - continue - return isinstance(thisx, Number) - else: - return isinstance(x, Number) - class DecimalConverter(ConversionInterface): """Converter for decimal.Decimal data to float.""" @@ -165,25 +147,15 @@ def convert(value, unit, axis): value : decimal.Decimal or iterable Decimal or list of Decimal need to be converted """ - # If value is a Decimal if isinstance(value, Decimal): return float(value) + # value is Iterable[Decimal] + elif isinstance(value, ma.MaskedArray): + return ma.asarray(value, dtype=float) else: - # assume x is a list of Decimal - converter = np.asarray - if isinstance(value, ma.MaskedArray): - converter = ma.asarray - return converter(value, dtype=float) + return np.asarray(value, dtype=float) - @staticmethod - def axisinfo(unit, axis): - # Since Decimal is a kind of Number, don't need specific axisinfo. - return AxisInfo() - - @staticmethod - def default_units(x, axis): - # Return None since Decimal is a kind of Number. - return None + # axisinfo and default_units can be inherited as Decimals are Numbers. class Registry(dict): @@ -191,8 +163,9 @@ class Registry(dict): def get_converter(self, x): """Get the converter interface instance for *x*, or None.""" - if hasattr(x, "values"): - x = x.values # Unpack pandas Series and DataFrames. + # Unpack in case of e.g. Pandas or xarray object + x = cbook._unpack_to_numpy(x) + if isinstance(x, np.ndarray): # In case x in a masked array, access the underlying data (only its # type matters). If x is a regular ndarray, getdata() just returns @@ -207,7 +180,7 @@ def get_converter(self, x): except KeyError: pass try: # If cache lookup fails, look up based on first element... - first = cbook.safe_first_element(x) + first = cbook._safe_first_finite(x) except (TypeError, StopIteration): pass else: diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 7527973b82b1..c25644dbe6b8 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -11,14 +11,18 @@ from contextlib import ExitStack import copy +import itertools from numbers import Integral, Number +from cycler import cycler import numpy as np import matplotlib as mpl -from . import _api, cbook, colors, ticker +from . import (_api, _docstring, backend_tools, cbook, collections, colors, + text as mtext, ticker, transforms) from .lines import Line2D -from .patches import Circle, Rectangle, Ellipse +from .patches import Circle, Rectangle, Ellipse, Polygon +from .transforms import TransformedPatchPath, Affine2D class LockDraw: @@ -86,6 +90,22 @@ def ignore(self, event): """ return not self.active + def _changed_canvas(self): + """ + Someone has switched the canvas on us! + + This happens if `savefig` needs to save to a format the previous + backend did not support (e.g. saving a figure using an Agg based + backend saved to a vector format). + + Returns + ------- + bool + True if the canvas has been changed. + + """ + return self.canvas is not self.ax.figure.canvas + class AxesWidget(Widget): """ @@ -102,15 +122,13 @@ class AxesWidget(Widget): Attributes ---------- ax : `~matplotlib.axes.Axes` - The parent axes for the widget. + The parent Axes for the widget. canvas : `~matplotlib.backend_bases.FigureCanvasBase` The parent figure canvas for the widget. active : bool If False, the widget does not respond to events. """ - cids = _api.deprecated("3.4")(property(lambda self: self._cids)) - def __init__(self, ax): self.ax = ax self.canvas = ax.figure.canvas @@ -151,13 +169,8 @@ class Button(AxesWidget): The color of the button when hovering. """ - cnt = _api.deprecated("3.4")(property( # Not real, but close enough. - lambda self: len(self._observers.callbacks['clicked']))) - observers = _api.deprecated("3.4")(property( - lambda self: self._observers.callbacks['clicked'])) - def __init__(self, ax, label, image=None, - color='0.85', hovercolor='0.95'): + color='0.85', hovercolor='0.95', *, useblit=True): """ Parameters ---------- @@ -172,6 +185,9 @@ def __init__(self, ax, label, image=None, The color of the button when not activated. hovercolor : color The color of the button when the mouse is over it. + useblit : bool, default: True + Use blitting for faster drawing if supported by the backend. + See the tutorial :doc:`/tutorials/advanced/blitting` for details. """ super().__init__(ax) @@ -182,7 +198,9 @@ def __init__(self, ax, label, image=None, horizontalalignment='center', transform=ax.transAxes) - self._observers = cbook.CallbackRegistry() + self._useblit = useblit and self.canvas.supports_blit + + self._observers = cbook.CallbackRegistry(signals=["clicked"]) self.connect_event('button_press_event', self._click) self.connect_event('button_release_event', self._release) @@ -214,7 +232,11 @@ def _motion(self, event): if not colors.same_color(c, self.ax.get_facecolor()): self.ax.set_facecolor(c) if self.drawon: - self.ax.figure.canvas.draw() + if self._useblit: + self.ax.draw_artist(self.ax) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw() def on_clicked(self, func): """ @@ -267,14 +289,14 @@ def __init__(self, ax, orientation, closedmin, closedmax, 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_axis_off() ax.set_navigate(False) + self.connect_event("button_press_event", self._update) self.connect_event("button_release_event", self._update) if dragging: self.connect_event("motion_notify_event", self._update) - self._observers = cbook.CallbackRegistry() + self._observers = cbook.CallbackRegistry(signals=["changed"]) def _stepped_value(self, val): """Return *val* coerced to closest number in the ``valstep`` grid.""" @@ -303,7 +325,7 @@ def disconnect(self, cid): def reset(self): """Reset the slider to the initial value.""" - if self.val != self.valinit: + if np.any(self.val != self.valinit): self.set_val(self.valinit) @@ -311,7 +333,7 @@ class Slider(SliderBase): """ A slider representing a floating point range. - Create a slider from *valmin* to *valmax* in axes *ax*. For the slider to + Create a slider from *valmin* to *valmax* in Axes *ax*. For the slider to remain responsive you must maintain a reference to it. Call :meth:`on_changed` to connect to the slider event. @@ -321,15 +343,12 @@ class Slider(SliderBase): Slider value. """ - cnt = _api.deprecated("3.4")(property( # Not real, but close enough. - lambda self: len(self._observers.callbacks['changed']))) - observers = _api.deprecated("3.4")(property( - lambda self: self._observers.callbacks['changed'])) - + @_api.make_keyword_only("3.7", name="valinit") 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', *, initcolor='r', **kwargs): + orientation='horizontal', *, initcolor='r', + track_color='lightgrey', handle_style=None, **kwargs): """ Parameters ---------- @@ -380,11 +399,30 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None, The color of the line at the *valinit* position. Set to ``'none'`` for no line. + track_color : color, default: 'lightgrey' + The color of the background track. The track is accessible for + further styling via the *track* attribute. + + handle_style : dict + Properties of the slider handle. Default values are + + ========= ===== ======= ======================================== + Key Value Default Description + ========= ===== ======= ======================================== + facecolor color 'white' The facecolor of the slider handle. + edgecolor color '.75' The edgecolor of the slider handle. + size int 10 The size of the slider handle in points. + ========= ===== ======= ======================================== + + Other values will be transformed as marker{foo} and passed to the + `~.Line2D` constructor. e.g. ``handle_style = {'style'='x'}`` will + result in ``markerstyle = 'x'``. + Notes ----- Additional kwargs are passed on to ``self.poly`` which is the - `~matplotlib.patches.Rectangle` that draws the slider knob. See the - `.Rectangle` documentation for valid property names (``facecolor``, + `~matplotlib.patches.Polygon` that draws the slider knob. See the + `.Polygon` documentation for valid property names (``facecolor``, ``edgecolor``, ``alpha``, etc.). """ super().__init__(ax, orientation, closedmin, closedmax, @@ -403,12 +441,43 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None, valinit = valmin self.val = valinit self.valinit = valinit + + defaults = {'facecolor': 'white', 'edgecolor': '.75', 'size': 10} + handle_style = {} if handle_style is None else handle_style + marker_props = { + f'marker{k}': v for k, v in {**defaults, **handle_style}.items() + } + if orientation == 'vertical': - self.poly = ax.axhspan(valmin, valinit, 0, 1, **kwargs) - self.hline = ax.axhline(valinit, 0, 1, color=initcolor, lw=1) + self.track = Rectangle( + (.25, 0), .5, 1, + transform=ax.transAxes, + facecolor=track_color + ) + ax.add_patch(self.track) + self.poly = ax.axhspan(valmin, valinit, .25, .75, **kwargs) + # Drawing a longer line and clipping it to the track avoids + # pixelation-related asymmetries. + self.hline = ax.axhline(valinit, 0, 1, color=initcolor, lw=1, + clip_path=TransformedPatchPath(self.track)) + handleXY = [[0.5], [valinit]] else: - self.poly = ax.axvspan(valmin, valinit, 0, 1, **kwargs) - self.vline = ax.axvline(valinit, 0, 1, color=initcolor, lw=1) + self.track = Rectangle( + (0, .25), 1, .5, + transform=ax.transAxes, + facecolor=track_color + ) + ax.add_patch(self.track) + self.poly = ax.axvspan(valmin, valinit, .25, .75, **kwargs) + self.vline = ax.axvline(valinit, 0, 1, color=initcolor, lw=1, + clip_path=TransformedPatchPath(self.track)) + handleXY = [[valinit], [0.5]] + self._handle, = ax.plot( + *handleXY, + "o", + **marker_props, + clip_on=False + ) if orientation == 'vertical': self.label = ax.text(0.5, 1.02, label, transform=ax.transAxes, @@ -499,11 +568,13 @@ def set_val(self, val): """ xy = self.poly.xy if self.orientation == 'vertical': - xy[1] = 0, val - xy[2] = 1, val + xy[1] = .25, val + xy[2] = .75, val + self._handle.set_ydata([val]) else: - xy[2] = val, 1 - xy[3] = val, 0 + xy[2] = val, .75 + xy[3] = val, .25 + self._handle.set_xdata([val]) self.poly.xy = xy self.valtext.set_text(self._format(val)) if self.drawon: @@ -536,7 +607,7 @@ class RangeSlider(SliderBase): max of the range via the *val* attribute as a tuple of (min, max). Create a slider that defines a range contained within [*valmin*, *valmax*] - in axes *ax*. For the slider to remain responsive you must maintain a + in Axes *ax*. For the slider to remain responsive you must maintain a reference to it. Call :meth:`on_changed` to connect to the slider event. Attributes @@ -545,6 +616,7 @@ class RangeSlider(SliderBase): Slider value. """ + @_api.make_keyword_only("3.7", name="valinit") def __init__( self, ax, @@ -558,6 +630,8 @@ def __init__( dragging=True, valstep=None, orientation="horizontal", + track_color='lightgrey', + handle_style=None, **kwargs, ): """ @@ -598,18 +672,37 @@ def __init__( orientation : {'horizontal', 'vertical'}, default: 'horizontal' The orientation of the slider. + track_color : color, default: 'lightgrey' + The color of the background track. The track is accessible for + further styling via the *track* attribute. + + handle_style : dict + Properties of the slider handles. Default values are + + ========= ===== ======= ========================================= + Key Value Default Description + ========= ===== ======= ========================================= + facecolor color 'white' The facecolor of the slider handles. + edgecolor color '.75' The edgecolor of the slider handles. + size int 10 The size of the slider handles in points. + ========= ===== ======= ========================================= + + Other values will be transformed as marker{foo} and passed to the + `~.Line2D` constructor. e.g. ``handle_style = {'style'='x'}`` will + result in ``markerstyle = 'x'``. + Notes ----- Additional kwargs are passed on to ``self.poly`` which is the - `~matplotlib.patches.Rectangle` that draws the slider knob. See the - `.Rectangle` documentation for valid property names (``facecolor``, + `~matplotlib.patches.Polygon` that draws the slider knob. See the + `.Polygon` documentation for valid property names (``facecolor``, ``edgecolor``, ``alpha``, etc.). """ super().__init__(ax, orientation, closedmin, closedmax, valmin, valmax, valfmt, dragging, valstep) # Set a value to allow _value_in_bounds() to work. - self.val = [valmin, valmax] + self.val = (valmin, valmax) if valinit is None: # Place at the 25th and 75th percentiles extent = valmax - valmin @@ -619,10 +712,53 @@ def __init__( valinit = self._value_in_bounds(valinit) self.val = valinit self.valinit = valinit + + defaults = {'facecolor': 'white', 'edgecolor': '.75', 'size': 10} + handle_style = {} if handle_style is None else handle_style + marker_props = { + f'marker{k}': v for k, v in {**defaults, **handle_style}.items() + } + if orientation == "vertical": - self.poly = ax.axhspan(valinit[0], valinit[1], 0, 1, **kwargs) + self.track = Rectangle( + (.25, 0), .5, 2, + transform=ax.transAxes, + facecolor=track_color + ) + ax.add_patch(self.track) + poly_transform = self.ax.get_yaxis_transform(which="grid") + handleXY_1 = [.5, valinit[0]] + handleXY_2 = [.5, valinit[1]] else: - self.poly = ax.axvspan(valinit[0], valinit[1], 0, 1, **kwargs) + self.track = Rectangle( + (0, .25), 1, .5, + transform=ax.transAxes, + facecolor=track_color + ) + ax.add_patch(self.track) + poly_transform = self.ax.get_xaxis_transform(which="grid") + handleXY_1 = [valinit[0], .5] + handleXY_2 = [valinit[1], .5] + self.poly = Polygon(np.zeros([5, 2]), **kwargs) + self._update_selection_poly(*valinit) + self.poly.set_transform(poly_transform) + self.poly.get_path()._interpolation_steps = 100 + self.ax.add_patch(self.poly) + self.ax._request_autoscale_view() + self._handles = [ + ax.plot( + *handleXY_1, + "o", + **marker_props, + clip_on=False + )[0], + ax.plot( + *handleXY_2, + "o", + **marker_props, + clip_on=False + )[0] + ] if orientation == "vertical": self.label = ax.text( @@ -661,8 +797,30 @@ def __init__( horizontalalignment="left", ) + self._active_handle = None self.set_val(valinit) + def _update_selection_poly(self, vmin, vmax): + """ + Update the vertices of the *self.poly* slider in-place + to cover the data range *vmin*, *vmax*. + """ + # The vertices are positioned + # 1 ------ 2 + # | | + # 0, 4 ---- 3 + verts = self.poly.xy + if self.orientation == "vertical": + verts[0] = verts[4] = .25, vmin + verts[1] = .25, vmax + verts[2] = .75, vmax + verts[3] = .75, vmin + else: + verts[0] = verts[4] = vmin, .25 + verts[1] = vmin, .75 + verts[2] = vmax, .75 + verts[3] = vmax, .25 + def _min_in_bounds(self, min): """Ensure the new min value is between valmin and self.val[1].""" if min <= self.valmin: @@ -698,6 +856,11 @@ def _update_val_from_pos(self, pos): else: val = self._max_in_bounds(pos) self.set_max(val) + if self._active_handle: + if self.orientation == "vertical": + self._active_handle.set_ydata([val]) + else: + self._active_handle.set_xdata([val]) def _update(self, event): """Update the slider position.""" @@ -716,7 +879,25 @@ def _update(self, event): ): self.drag_active = False event.canvas.release_mouse(self.ax) + self._active_handle = None return + + # determine which handle was grabbed + if self.orientation == "vertical": + handle_index = np.argmin( + np.abs([h.get_ydata()[0] - event.ydata for h in self._handles]) + ) + else: + handle_index = np.argmin( + np.abs([h.get_xdata()[0] - event.xdata for h in self._handles]) + ) + handle = self._handles[handle_index] + + # these checks ensure smooth behavior if the handles swap which one + # has a higher value. i.e. if one is dragged over and past the other. + if handle is not self._active_handle: + self._active_handle = handle + if self.orientation == "vertical": self._update_val_from_pos(event.ydata) else: @@ -764,33 +945,26 @@ def set_val(self, val): ---------- val : tuple or array-like of float """ - val = np.sort(np.asanyarray(val)) - if val.shape != (2,): - raise ValueError( - f"val must have shape (2,) but has shape {val.shape}" - ) - val[0] = self._min_in_bounds(val[0]) - val[1] = self._max_in_bounds(val[1]) - xy = self.poly.xy + val = np.sort(val) + _api.check_shape((2,), val=val) + # Reset value to allow _value_in_bounds() to work. + self.val = (self.valmin, self.valmax) + vmin, vmax = self._value_in_bounds(val) + self._update_selection_poly(vmin, vmax) if self.orientation == "vertical": - xy[0] = 0, val[0] - xy[1] = 0, val[1] - xy[2] = 1, val[1] - xy[3] = 1, val[0] - xy[4] = 0, val[0] + self._handles[0].set_ydata([vmin]) + self._handles[1].set_ydata([vmax]) else: - xy[0] = val[0], 0 - xy[1] = val[0], 1 - xy[2] = val[1], 1 - xy[3] = val[1], 0 - xy[4] = val[0], 0 - self.poly.xy = xy - self.valtext.set_text(self._format(val)) + self._handles[0].set_xdata([vmin]) + self._handles[1].set_xdata([vmax]) + + self.valtext.set_text(self._format((vmin, vmax))) + if self.drawon: self.ax.figure.canvas.draw_idle() - self.val = val + self.val = (vmin, vmax) if self.eventson: - self._observers.process("changed", val) + self._observers.process("changed", (vmin, vmax)) def on_changed(self, func): """ @@ -800,7 +974,7 @@ def on_changed(self, func): ---------- func : callable Function to call when slider is changed. The function - must accept a numpy array with shape (2,) as its argument. + must accept a 2-tuple of floats as its argument. Returns ------- @@ -810,6 +984,11 @@ def on_changed(self, func): return self._observers.connect('changed', lambda val: func(val)) +def _expand_text_props(props): + props = cbook.normalize_kwargs(props, mtext.Text) + return cycler(**props)() if props else itertools.repeat({}) + + class CheckButtons(AxesWidget): r""" A GUI neutral set of check buttons. @@ -822,39 +1001,56 @@ class CheckButtons(AxesWidget): Attributes ---------- ax : `~matplotlib.axes.Axes` - The parent axes for the widget. + The parent Axes for the widget. + labels : list of `.Text` rectangles : list of `.Rectangle` lines : list of (`.Line2D`, `.Line2D`) pairs - List of lines for the x's in the check boxes. These lines exist for + List of lines for the x's in the checkboxes. These lines exist for each box, but have ``set_visible(False)`` when its box is not checked. """ - cnt = _api.deprecated("3.4")(property( # Not real, but close enough. - lambda self: len(self._observers.callbacks['clicked']))) - observers = _api.deprecated("3.4")(property( - lambda self: self._observers.callbacks['clicked'])) - - def __init__(self, ax, labels, actives=None): + def __init__(self, ax, labels, actives=None, *, useblit=True, + label_props=None, frame_props=None, check_props=None): """ Add check buttons to `matplotlib.axes.Axes` instance *ax*. Parameters ---------- ax : `~matplotlib.axes.Axes` - The parent axes for the widget. - + The parent Axes for the widget. labels : list of str The labels of the check buttons. - actives : list of bool, optional The initial check states of the buttons. The list must have the same length as *labels*. If not given, all buttons are unchecked. + useblit : bool, default: True + Use blitting for faster drawing if supported by the backend. + See the tutorial :doc:`/tutorials/advanced/blitting` for details. + label_props : dict, optional + Dictionary of `.Text` properties to be used for the labels. + + .. versionadded:: 3.7 + frame_props : dict, optional + Dictionary of scatter `.Collection` properties to be used for the + check button frame. Defaults (label font size / 2)**2 size, black + edgecolor, no facecolor, and 1.0 linewidth. + + .. versionadded:: 3.7 + check_props : dict, optional + Dictionary of scatter `.Collection` properties to be used for the + check button check. Defaults to (label font size / 2)**2 size, + black color, and 1.0 linewidth. + + .. versionadded:: 3.7 """ super().__init__(ax) + _api.check_isinstance((dict, None), label_props=label_props, + frame_props=frame_props, check_props=check_props) + ax.set_xticks([]) ax.set_yticks([]) ax.set_navigate(False) @@ -862,56 +1058,138 @@ def __init__(self, ax, labels, actives=None): if actives is None: actives = [False] * len(labels) - if len(labels) > 1: - dy = 1. / (len(labels) + 1) - ys = np.linspace(1 - dy, dy, len(labels)) - else: - dy = 0.25 - ys = [0.5] + self._useblit = useblit and self.canvas.supports_blit + self._background = None + + ys = np.linspace(1, 0, len(labels)+2)[1:-1] + + label_props = _expand_text_props(label_props) + self.labels = [ + ax.text(0.25, y, label, transform=ax.transAxes, + horizontalalignment="left", verticalalignment="center", + **props) + for y, label, props in zip(ys, labels, label_props)] + text_size = np.array([text.get_fontsize() for text in self.labels]) / 2 + + frame_props = { + 's': text_size**2, + 'linewidth': 1, + **cbook.normalize_kwargs(frame_props, collections.PathCollection), + 'marker': 's', + 'transform': ax.transAxes, + } + frame_props.setdefault('facecolor', frame_props.get('color', 'none')) + frame_props.setdefault('edgecolor', frame_props.pop('color', 'black')) + self._frames = ax.scatter([0.15] * len(ys), ys, **frame_props) + check_props = { + 'linewidth': 1, + 's': text_size**2, + **cbook.normalize_kwargs(check_props, collections.PathCollection), + 'marker': 'x', + 'transform': ax.transAxes, + 'animated': self._useblit, + } + check_props.setdefault('facecolor', check_props.pop('color', 'black')) + self._checks = ax.scatter([0.15] * len(ys), ys, **check_props) + # The user may have passed custom colours in check_props, so we need to + # create the checks (above), and modify the visibility after getting + # whatever the user set. + self._init_status(actives) - axcolor = ax.get_facecolor() + self.connect_event('button_press_event', self._clicked) + if self._useblit: + self.connect_event('draw_event', self._clear) - self.labels = [] - self.lines = [] - self.rectangles = [] + self._observers = cbook.CallbackRegistry(signals=["clicked"]) - lineparams = {'color': 'k', 'linewidth': 1.25, - 'transform': ax.transAxes, 'solid_capstyle': 'butt'} - for y, label, active in zip(ys, labels, actives): - t = ax.text(0.25, y, label, transform=ax.transAxes, - horizontalalignment='left', - verticalalignment='center') + def _clear(self, event): + """Internal event handler to clear the buttons.""" + if self.ignore(event) or self._changed_canvas(): + return + self._background = self.canvas.copy_from_bbox(self.ax.bbox) + self.ax.draw_artist(self._checks) + if hasattr(self, '_lines'): + for l1, l2 in self._lines: + self.ax.draw_artist(l1) + self.ax.draw_artist(l2) - w, h = dy / 2, dy / 2 - x, y = 0.05, y - h / 2 + def _clicked(self, event): + if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: + return + pclicked = self.ax.transAxes.inverted().transform((event.x, event.y)) + distances = {} + if hasattr(self, "_rectangles"): + for i, (p, t) in enumerate(zip(self._rectangles, self.labels)): + x0, y0 = p.get_xy() + if (t.get_window_extent().contains(event.x, event.y) + or (x0 <= pclicked[0] <= x0 + p.get_width() + and y0 <= pclicked[1] <= y0 + p.get_height())): + distances[i] = np.linalg.norm(pclicked - p.get_center()) + else: + _, frame_inds = self._frames.contains(event) + coords = self._frames.get_offset_transform().transform( + self._frames.get_offsets() + ) + for i, t in enumerate(self.labels): + if (i in frame_inds["ind"] + or t.get_window_extent().contains(event.x, event.y)): + distances[i] = np.linalg.norm(pclicked - coords[i]) + if len(distances) > 0: + closest = min(distances, key=distances.get) + self.set_active(closest) - p = Rectangle(xy=(x, y), width=w, height=h, edgecolor='black', - facecolor=axcolor, transform=ax.transAxes) + def set_label_props(self, props): + """ + Set properties of the `.Text` labels. - l1 = Line2D([x, x + w], [y + h, y], **lineparams) - l2 = Line2D([x, x + w], [y, y + h], **lineparams) + .. versionadded:: 3.7 - l1.set_visible(active) - l2.set_visible(active) - self.labels.append(t) - self.rectangles.append(p) - self.lines.append((l1, l2)) - ax.add_patch(p) - ax.add_line(l1) - ax.add_line(l2) + Parameters + ---------- + props : dict + Dictionary of `.Text` properties to be used for the labels. + """ + _api.check_isinstance(dict, props=props) + props = _expand_text_props(props) + for text, prop in zip(self.labels, props): + text.update(prop) - self.connect_event('button_press_event', self._clicked) + def set_frame_props(self, props): + """ + Set properties of the check button frames. - self._observers = cbook.CallbackRegistry() + .. versionadded:: 3.7 - def _clicked(self, event): - if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: - return - for i, (p, t) in enumerate(zip(self.rectangles, self.labels)): - if (t.get_window_extent().contains(event.x, event.y) or - p.get_window_extent().contains(event.x, event.y)): - self.set_active(i) - break + Parameters + ---------- + props : dict + Dictionary of `.Collection` properties to be used for the check + button frames. + """ + _api.check_isinstance(dict, props=props) + if 's' in props: # Keep API consistent with constructor. + props['sizes'] = np.broadcast_to(props.pop('s'), len(self.labels)) + self._frames.update(props) + + def set_check_props(self, props): + """ + Set properties of the check button checks. + + .. versionadded:: 3.7 + + Parameters + ---------- + props : dict + Dictionary of `.Collection` properties to be used for the check + button check. + """ + _api.check_isinstance(dict, props=props) + if 's' in props: # Keep API consistent with constructor. + props['sizes'] = np.broadcast_to(props.pop('s'), len(self.labels)) + actives = self.get_status() + self._checks.update(props) + # If new colours are supplied, then we must re-apply the status. + self._init_status(actives) def set_active(self, index): """ @@ -932,21 +1210,59 @@ def set_active(self, index): if index not in range(len(self.labels)): raise ValueError(f'Invalid CheckButton index: {index}') - l1, l2 = self.lines[index] - l1.set_visible(not l1.get_visible()) - l2.set_visible(not l2.get_visible()) + invisible = colors.to_rgba('none') + + facecolors = self._checks.get_facecolor() + facecolors[index] = ( + self._active_check_colors[index] + if colors.same_color(facecolors[index], invisible) + else invisible + ) + self._checks.set_facecolor(facecolors) + + if hasattr(self, "_lines"): + l1, l2 = self._lines[index] + l1.set_visible(not l1.get_visible()) + l2.set_visible(not l2.get_visible()) if self.drawon: - self.ax.figure.canvas.draw() + if self._useblit: + if self._background is not None: + self.canvas.restore_region(self._background) + self.ax.draw_artist(self._checks) + if hasattr(self, "_lines"): + for l1, l2 in self._lines: + self.ax.draw_artist(l1) + self.ax.draw_artist(l2) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw() if self.eventson: self._observers.process('clicked', self.labels[index].get_text()) + def _init_status(self, actives): + """ + Initialize properties to match active status. + + The user may have passed custom colours in *check_props* to the + constructor, or to `.set_check_props`, so we need to modify the + visibility after getting whatever the user set. + """ + self._active_check_colors = self._checks.get_facecolor() + if len(self._active_check_colors) == 1: + self._active_check_colors = np.repeat(self._active_check_colors, + len(actives), axis=0) + self._checks.set_facecolor( + [ec if active else "none" + for ec, active in zip(self._active_check_colors, actives)]) + def get_status(self): """ - Return a tuple of the status (True/False) of all of the check buttons. + Return a list of the status (True/False) of all of the check buttons. """ - return [l1.get_visible() for (l1, l2) in self.lines] + return [not colors.same_color(color, colors.to_rgba("none")) + for color in self._checks.get_facecolors()] def on_clicked(self, func): """ @@ -960,6 +1276,60 @@ def disconnect(self, cid): """Remove the observer with connection id *cid*.""" self._observers.disconnect(cid) + @_api.deprecated("3.7", + addendum="Any custom property styling may be lost.") + @property + def rectangles(self): + if not hasattr(self, "_rectangles"): + ys = np.linspace(1, 0, len(self.labels)+2)[1:-1] + dy = 1. / (len(self.labels) + 1) + w, h = dy / 2, dy / 2 + rectangles = self._rectangles = [ + Rectangle(xy=(0.05, ys[i] - h / 2), width=w, height=h, + edgecolor="black", + facecolor="none", + transform=self.ax.transAxes + ) + for i, y in enumerate(ys) + ] + self._frames.set_visible(False) + for rectangle in rectangles: + self.ax.add_patch(rectangle) + if not hasattr(self, "_lines"): + with _api.suppress_matplotlib_deprecation_warning(): + _ = self.lines + return self._rectangles + + @_api.deprecated("3.7", + addendum="Any custom property styling may be lost.") + @property + def lines(self): + if not hasattr(self, "_lines"): + ys = np.linspace(1, 0, len(self.labels)+2)[1:-1] + self._checks.set_visible(False) + dy = 1. / (len(self.labels) + 1) + w, h = dy / 2, dy / 2 + self._lines = [] + current_status = self.get_status() + lineparams = {'color': 'k', 'linewidth': 1.25, + 'transform': self.ax.transAxes, + 'solid_capstyle': 'butt', + 'animated': self._useblit} + for i, y in enumerate(ys): + x, y = 0.05, y - h / 2 + l1 = Line2D([x, x + w], [y + h, y], **lineparams) + l2 = Line2D([x, x + w], [y, y + h], **lineparams) + + l1.set_visible(current_status[i]) + l2.set_visible(current_status[i]) + self._lines.append((l1, l2)) + self.ax.add_line(l1) + self.ax.add_line(l2) + if not hasattr(self, "_rectangles"): + with _api.suppress_matplotlib_deprecation_warning(): + _ = self.rectangles + return self._lines + class TextBox(AxesWidget): """ @@ -975,7 +1345,7 @@ class TextBox(AxesWidget): Attributes ---------- ax : `~matplotlib.axes.Axes` - The parent axes for the widget. + The parent Axes for the widget. label : `.Text` color : color @@ -984,17 +1354,10 @@ class TextBox(AxesWidget): The color of the text box when hovering. """ - params_to_disable = _api.deprecated("3.3")(property( - lambda self: [key for key in mpl.rcParams if 'keymap' in key])) - cnt = _api.deprecated("3.4")(property( # Not real, but close enough. - lambda self: sum(len(d) for d in self._observers.callbacks.values()))) - change_observers = _api.deprecated("3.4")(property( - lambda self: self._observers.callbacks['change'])) - submit_observers = _api.deprecated("3.4")(property( - lambda self: self._observers.callbacks['submit'])) - + @_api.make_keyword_only("3.7", name="color") def __init__(self, ax, label, initial='', - color='.95', hovercolor='1', label_pad=.01): + color='.95', hovercolor='1', label_pad=.01, + textalignment="left"): """ Parameters ---------- @@ -1010,19 +1373,26 @@ def __init__(self, ax, label, initial='', The color of the box when the mouse is over it. label_pad : float The distance between the label and the right side of the textbox. + textalignment : {'left', 'center', 'right'} + The horizontal location of the text. """ super().__init__(ax) - self.DIST_FROM_LEFT = .05 + self._text_position = _api.check_getitem( + {"left": 0.05, "center": 0.5, "right": 0.95}, + textalignment=textalignment) self.label = ax.text( -label_pad, 0.5, label, transform=ax.transAxes, verticalalignment='center', horizontalalignment='right') + + # TextBox's text object should not parse mathtext at all. self.text_disp = self.ax.text( - self.DIST_FROM_LEFT, 0.5, initial, transform=self.ax.transAxes, - verticalalignment='center', horizontalalignment='left') + self._text_position, 0.5, initial, transform=self.ax.transAxes, + verticalalignment='center', horizontalalignment=textalignment, + parse_math=False) - self._observers = cbook.CallbackRegistry() + self._observers = cbook.CallbackRegistry(signals=["change", "submit"]) ax.set( xlim=(0, 1), ylim=(0, 1), # s.t. cursor appears from first click. @@ -1058,17 +1428,27 @@ def _rendercursor(self): # 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: + if self.ax.figure._get_renderer() is None: self.ax.figure.canvas.draw() text = self.text_disp.get_text() # Save value before overwriting it. widthtext = text[:self.cursor_index] + + bb_text = self.text_disp.get_window_extent() 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 + bb_widthtext = self.text_disp.get_window_extent() + + if bb_text.y0 == bb_text.y1: # Restoring the height if no text. + bb_text.y0 -= bb_widthtext.height / 2 + bb_text.y1 += bb_widthtext.height / 2 + elif not widthtext: # Keep width to 0. + bb_text.x1 = bb_text.x0 + else: # Move the cursor using width of bb_widthtext. + bb_text.x1 = bb_text.x0 + bb_widthtext.width + self.cursor.set( - segments=[[(bb.x1, bb.y0), (bb.x1, bb.y1)]], visible=True) + segments=[[(bb_text.x1, bb_text.y0), (bb_text.x1, bb_text.y1)]], + visible=True) self.text_disp.set_text(text) self.ax.figure.canvas.draw() @@ -1126,7 +1506,8 @@ def set_val(self, val): self._observers.process('change', self.text) self._observers.process('submit', self.text) - def begin_typing(self, x): + @_api.delete_parameter("3.7", "x") + def begin_typing(self, x=None): self.capturekeystrokes = True # Disable keypress shortcuts, which may otherwise cause the figure to # be saved, closed, etc., until the user stops typing. The way to @@ -1139,7 +1520,7 @@ def begin_typing(self, x): # If using toolmanager, lock keypresses, and plan to release the # lock when typing stops. toolmanager.keypresslock(self) - stack.push(toolmanager.keypresslock.release, self) + stack.callback(toolmanager.keypresslock.release, self) else: # If not using toolmanager, disable all keypress-related rcParams. # Avoid spurious warnings if keymaps are getting deprecated. @@ -1162,17 +1543,6 @@ def stop_typing(self): # call it once we've already done our cleanup. self._observers.process('submit', self.text) - def position_cursor(self, x): - # now, we have to figure out where the cursor goes. - # approximate it based on assuming all characters the same length - if len(self.text) == 0: - self.cursor_index = 0 - else: - bb = self.text_disp.get_window_extent() - ratio = np.clip((x - bb.x0) / bb.width, 0, 1) - self.cursor_index = int(len(self.text) * ratio) - self._rendercursor() - def _click(self, event): if self.ignore(event): return @@ -1184,8 +1554,9 @@ def _click(self, event): if event.canvas.mouse_grabber != self.ax: event.canvas.grab_mouse(self.ax) if not self.capturekeystrokes: - self.begin_typing(event.x) - self.position_cursor(event.x) + self.begin_typing() + self.cursor_index = self.text_disp._char_index_at(event.x) + self._rendercursor() def _resize(self, event): self.stop_typing() @@ -1233,7 +1604,7 @@ class RadioButtons(AxesWidget): Attributes ---------- ax : `~matplotlib.axes.Axes` - The parent axes for the widget. + The parent Axes for the widget. activecolor : color The color of the selected button. labels : list of `.Text` @@ -1244,81 +1615,192 @@ class RadioButtons(AxesWidget): The label text of the currently selected button. """ - def __init__(self, ax, labels, active=0, activecolor='blue'): + def __init__(self, ax, labels, active=0, activecolor=None, *, + useblit=True, label_props=None, radio_props=None): """ Add radio buttons to an `~.axes.Axes`. Parameters ---------- ax : `~matplotlib.axes.Axes` - The axes to add the buttons to. + The Axes to add the buttons to. labels : list of str The button labels. active : int The index of the initially selected button. activecolor : color - The color of the selected button. + The color of the selected button. The default is ``'blue'`` if not + specified here or in *radio_props*. + useblit : bool, default: True + Use blitting for faster drawing if supported by the backend. + See the tutorial :doc:`/tutorials/advanced/blitting` for details. + label_props : dict or list of dict, optional + Dictionary of `.Text` properties to be used for the labels. + + .. versionadded:: 3.7 + radio_props : dict, optional + Dictionary of scatter `.Collection` properties to be used for the + radio buttons. Defaults to (label font size / 2)**2 size, black + edgecolor, and *activecolor* facecolor (when active). + + .. note:: + If a facecolor is supplied in *radio_props*, it will override + *activecolor*. This may be used to provide an active color per + button. + + .. versionadded:: 3.7 """ super().__init__(ax) - self.activecolor = activecolor - self.value_selected = None + + _api.check_isinstance((dict, None), label_props=label_props, + radio_props=radio_props) + + radio_props = cbook.normalize_kwargs(radio_props, + collections.PathCollection) + if activecolor is not None: + if 'facecolor' in radio_props: + _api.warn_external( + 'Both the *activecolor* parameter and the *facecolor* ' + 'key in the *radio_props* parameter has been specified. ' + '*activecolor* will be ignored.') + else: + activecolor = 'blue' # Default. + + self._activecolor = activecolor + self.value_selected = labels[active] ax.set_xticks([]) ax.set_yticks([]) ax.set_navigate(False) - dy = 1. / (len(labels) + 1) - ys = np.linspace(1 - dy, dy, len(labels)) - cnt = 0 - axcolor = ax.get_facecolor() - - # scale the radius of the circle with the spacing between each one - circle_radius = dy / 2 - 0.01 - # default to hard-coded value if the radius becomes too large - circle_radius = min(circle_radius, 0.05) - - self.labels = [] - self.circles = [] - for y, label in zip(ys, labels): - t = ax.text(0.25, y, label, transform=ax.transAxes, - horizontalalignment='left', - verticalalignment='center') - - if cnt == active: - self.value_selected = label - facecolor = activecolor - else: - facecolor = axcolor - - p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black', - facecolor=facecolor, transform=ax.transAxes) - self.labels.append(t) - self.circles.append(p) - ax.add_patch(p) - cnt += 1 + ys = np.linspace(1, 0, len(labels) + 2)[1:-1] + + self._useblit = useblit and self.canvas.supports_blit + self._background = None + + label_props = _expand_text_props(label_props) + self.labels = [ + ax.text(0.25, y, label, transform=ax.transAxes, + horizontalalignment="left", verticalalignment="center", + **props) + for y, label, props in zip(ys, labels, label_props)] + text_size = np.array([text.get_fontsize() for text in self.labels]) / 2 + + radio_props = { + 's': text_size**2, + **radio_props, + 'marker': 'o', + 'transform': ax.transAxes, + 'animated': self._useblit, + } + radio_props.setdefault('edgecolor', radio_props.get('color', 'black')) + radio_props.setdefault('facecolor', + radio_props.pop('color', activecolor)) + self._buttons = ax.scatter([.15] * len(ys), ys, **radio_props) + # The user may have passed custom colours in radio_props, so we need to + # create the radios, and modify the visibility after getting whatever + # the user set. + self._active_colors = self._buttons.get_facecolor() + if len(self._active_colors) == 1: + self._active_colors = np.repeat(self._active_colors, len(labels), + axis=0) + self._buttons.set_facecolor( + [activecolor if i == active else "none" + for i, activecolor in enumerate(self._active_colors)]) self.connect_event('button_press_event', self._clicked) + if self._useblit: + self.connect_event('draw_event', self._clear) - self._observers = cbook.CallbackRegistry() + self._observers = cbook.CallbackRegistry(signals=["clicked"]) - cnt = _api.deprecated("3.4")(property( # Not real, but close enough. - lambda self: len(self._observers.callbacks['clicked']))) - observers = _api.deprecated("3.4")(property( - lambda self: self._observers.callbacks['clicked'])) + def _clear(self, event): + """Internal event handler to clear the buttons.""" + if self.ignore(event) or self._changed_canvas(): + return + self._background = self.canvas.copy_from_bbox(self.ax.bbox) + self.ax.draw_artist(self._buttons) + if hasattr(self, "_circles"): + for circle in self._circles: + self.ax.draw_artist(circle) def _clicked(self, event): if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: return pclicked = self.ax.transAxes.inverted().transform((event.x, event.y)) + _, inds = self._buttons.contains(event) + coords = self._buttons.get_offset_transform().transform( + self._buttons.get_offsets()) distances = {} - for i, (p, t) in enumerate(zip(self.circles, self.labels)): - if (t.get_window_extent().contains(event.x, event.y) - or np.linalg.norm(pclicked - p.center) < p.radius): - distances[i] = np.linalg.norm(pclicked - p.center) + if hasattr(self, "_circles"): # Remove once circles is removed. + for i, (p, t) in enumerate(zip(self._circles, self.labels)): + if (t.get_window_extent().contains(event.x, event.y) + or np.linalg.norm(pclicked - p.center) < p.radius): + distances[i] = np.linalg.norm(pclicked - p.center) + else: + for i, t in enumerate(self.labels): + if (i in inds["ind"] + or t.get_window_extent().contains(event.x, event.y)): + distances[i] = np.linalg.norm(pclicked - coords[i]) if len(distances) > 0: closest = min(distances, key=distances.get) self.set_active(closest) + def set_label_props(self, props): + """ + Set properties of the `.Text` labels. + + .. versionadded:: 3.7 + + Parameters + ---------- + props : dict + Dictionary of `.Text` properties to be used for the labels. + """ + _api.check_isinstance(dict, props=props) + props = _expand_text_props(props) + for text, prop in zip(self.labels, props): + text.update(prop) + + def set_radio_props(self, props): + """ + Set properties of the `.Text` labels. + + .. versionadded:: 3.7 + + Parameters + ---------- + props : dict + Dictionary of `.Collection` properties to be used for the radio + buttons. + """ + _api.check_isinstance(dict, props=props) + if 's' in props: # Keep API consistent with constructor. + props['sizes'] = np.broadcast_to(props.pop('s'), len(self.labels)) + self._buttons.update(props) + self._active_colors = self._buttons.get_facecolor() + if len(self._active_colors) == 1: + self._active_colors = np.repeat(self._active_colors, + len(self.labels), axis=0) + self._buttons.set_facecolor( + [activecolor if text.get_text() == self.value_selected else "none" + for text, activecolor in zip(self.labels, self._active_colors)]) + + @property + def activecolor(self): + return self._activecolor + + @activecolor.setter + def activecolor(self, activecolor): + colors._check_color_like(activecolor=activecolor) + self._activecolor = activecolor + self.set_radio_props({'facecolor': activecolor}) + # Make sure the deprecated version is updated. + # Remove once circles is removed. + labels = [label.get_text() for label in self.labels] + with cbook._setattr_cm(self, eventson=False): + self.set_active(labels.index(self.value_selected)) + def set_active(self, index): """ Select button with number *index*. @@ -1327,18 +1809,27 @@ def set_active(self, index): """ if index not in range(len(self.labels)): raise ValueError(f'Invalid RadioButton index: {index}') - self.value_selected = self.labels[index].get_text() - - for i, p in enumerate(self.circles): - if i == index: - color = self.activecolor - else: - color = self.ax.get_facecolor() - p.set_facecolor(color) - + button_facecolors = self._buttons.get_facecolor() + button_facecolors[:] = colors.to_rgba("none") + button_facecolors[index] = colors.to_rgba(self._active_colors[index]) + self._buttons.set_facecolor(button_facecolors) + if hasattr(self, "_circles"): # Remove once circles is removed. + for i, p in enumerate(self._circles): + p.set_facecolor(self.activecolor if i == index else "none") + if self.drawon and self._useblit: + self.ax.draw_artist(p) if self.drawon: - self.ax.figure.canvas.draw() + if self._useblit: + if self._background is not None: + self.canvas.restore_region(self._background) + self.ax.draw_artist(self._buttons) + if hasattr(self, "_circles"): + for p in self._circles: + self.ax.draw_artist(p) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw() if self.eventson: self._observers.process('clicked', self.labels[index].get_text()) @@ -1355,6 +1846,23 @@ def disconnect(self, cid): """Remove the observer with connection id *cid*.""" self._observers.disconnect(cid) + @_api.deprecated("3.7", + addendum="Any custom property styling may be lost.") + @property + def circles(self): + if not hasattr(self, "_circles"): + radius = min(.5 / (len(self.labels) + 1) - .01, .05) + circles = self._circles = [ + Circle(xy=self._buttons.get_offsets()[i], edgecolor="black", + facecolor=self._buttons.get_facecolor()[i], + radius=radius, transform=self.ax.transAxes, + animated=self._useblit) + for i in range(len(self.labels))] + self._buttons.set_visible(False) + for circle in circles: + self.ax.add_patch(circle) + return self._circles + class SubplotTool(Widget): """ @@ -1381,8 +1889,8 @@ def __init__(self, targetfig, toolfig): # The last subplot, removed below, keeps space for the "Reset" button. for name, ax in zip(names, toolfig.subplots(len(names) + 1)): ax.set_navigate(False) - slider = Slider(ax, name, - 0, 1, getattr(targetfig.subplotpars, name)) + slider = Slider(ax, name, 0, 1, + valinit=getattr(targetfig.subplotpars, name)) slider.on_changed(self._on_slider_changed) self._sliders.append(slider) toolfig.axes[-1].remove() @@ -1402,11 +1910,7 @@ def __init__(self, targetfig, toolfig): bax = toolfig.add_axes([0.8, 0.05, 0.15, 0.075]) self.buttonreset = Button(bax, 'Reset') - - # During reset there can be a temporary invalid state depending on the - # order of the reset so we turn off validation for the resetting - with cbook._setattr_cm(toolfig.subplotpars, validate=False): - self.buttonreset.on_clicked(self._on_reset) + self.buttonreset.on_clicked(self._on_reset) def _on_slider_changed(self, _): self.targetfig.subplots_adjust( @@ -1417,71 +1921,24 @@ def _on_slider_changed(self, _): def _on_reset(self, event): with ExitStack() as stack: - # Temporarily disable drawing on self and self's sliders. + # Temporarily disable drawing on self and self's sliders, and + # disconnect slider events (as the subplotparams can be temporarily + # invalid, depending on the order in which they are restored). stack.enter_context(cbook._setattr_cm(self, drawon=False)) for slider in self._sliders: - stack.enter_context(cbook._setattr_cm(slider, drawon=False)) + stack.enter_context( + cbook._setattr_cm(slider, drawon=False, eventson=False)) # Reset the slider to the initial position. for slider in self._sliders: slider.reset() - # Draw the canvas. if self.drawon: - event.canvas.draw() - self.targetfig.canvas.draw() - - axleft = _api.deprecated("3.3")( - property(lambda self: self.sliderleft.ax)) - axright = _api.deprecated("3.3")( - property(lambda self: self.sliderright.ax)) - axbottom = _api.deprecated("3.3")( - property(lambda self: self.sliderbottom.ax)) - axtop = _api.deprecated("3.3")( - property(lambda self: self.slidertop.ax)) - axwspace = _api.deprecated("3.3")( - property(lambda self: self.sliderwspace.ax)) - axhspace = _api.deprecated("3.3")( - property(lambda self: self.sliderhspace.ax)) - - @_api.deprecated("3.3") - def funcleft(self, val): - self.targetfig.subplots_adjust(left=val) - if self.drawon: - self.targetfig.canvas.draw() - - @_api.deprecated("3.3") - def funcright(self, val): - self.targetfig.subplots_adjust(right=val) - if self.drawon: - self.targetfig.canvas.draw() - - @_api.deprecated("3.3") - def funcbottom(self, val): - self.targetfig.subplots_adjust(bottom=val) - if self.drawon: - self.targetfig.canvas.draw() - - @_api.deprecated("3.3") - def functop(self, val): - self.targetfig.subplots_adjust(top=val) - if self.drawon: - self.targetfig.canvas.draw() - - @_api.deprecated("3.3") - def funcwspace(self, val): - self.targetfig.subplots_adjust(wspace=val) - if self.drawon: - self.targetfig.canvas.draw() - - @_api.deprecated("3.3") - def funchspace(self, val): - self.targetfig.subplots_adjust(hspace=val) - if self.drawon: - self.targetfig.canvas.draw() + event.canvas.draw() # Redraw the subplottool canvas. + self._on_slider_changed(None) # Apply changes to the target window. class Cursor(AxesWidget): """ - A crosshair cursor that spans the axes and moves with mouse cursor. + A crosshair cursor that spans the Axes and moves with mouse cursor. For the cursor to remain responsive you must keep a reference to it. @@ -1495,6 +1952,7 @@ class Cursor(AxesWidget): Whether to draw the vertical line. useblit : bool, default: False Use blitting for faster drawing if supported by the backend. + See the tutorial :doc:`/tutorials/advanced/blitting` for details. Other Parameters ---------------- @@ -1506,7 +1964,7 @@ class Cursor(AxesWidget): -------- See :doc:`/gallery/widgets/cursor`. """ - + @_api.make_keyword_only("3.7", "horizOn") def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, **lineprops): super().__init__(ax) @@ -1529,12 +1987,10 @@ def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, def clear(self, event): """Internal event handler to clear the cursor.""" - if self.ignore(event): + if self.ignore(event) or self._changed_canvas(): return if self.useblit: self.background = self.canvas.copy_from_bbox(self.ax.bbox) - self.linev.set_visible(False) - self.lineh.set_visible(False) def onmove(self, event): """Internal event handler to draw the cursor when the mouse moves.""" @@ -1551,15 +2007,15 @@ def onmove(self, event): self.needclear = False return self.needclear = True - if not self.visible: - return + self.linev.set_xdata((event.xdata, event.xdata)) + self.linev.set_visible(self.visible and self.vertOn) self.lineh.set_ydata((event.ydata, event.ydata)) - self.linev.set_visible(self.visible and self.vertOn) self.lineh.set_visible(self.visible and self.horizOn) - self._update() + if self.visible and (self.vertOn or self.horizOn): + self._update() def _update(self): if self.useblit: @@ -1576,152 +2032,193 @@ def _update(self): class MultiCursor(Widget): """ Provide a vertical (default) and/or horizontal line cursor shared between - multiple axes. + multiple Axes. For the cursor to remain responsive you must keep a reference to it. - Example usage:: + Parameters + ---------- + canvas : object + This parameter is entirely unused and only kept for back-compatibility. + + axes : list of `matplotlib.axes.Axes` + The `~.axes.Axes` to attach the cursor to. + + useblit : bool, default: True + Use blitting for faster drawing if supported by the backend. + See the tutorial :doc:`/tutorials/advanced/blitting` + for details. - from matplotlib.widgets import MultiCursor - import matplotlib.pyplot as plt - import numpy as np + horizOn : bool, default: False + Whether to draw the horizontal line. - fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True) - t = np.arange(0.0, 2.0, 0.01) - ax1.plot(t, np.sin(2*np.pi*t)) - ax2.plot(t, np.sin(4*np.pi*t)) + vertOn : bool, default: True + Whether to draw the vertical line. - multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1, - horizOn=False, vertOn=True) - plt.show() + Other Parameters + ---------------- + **lineprops + `.Line2D` properties that control the appearance of the lines. + See also `~.Axes.axhline`. + Examples + -------- + See :doc:`/gallery/widgets/multicursor`. """ + + @_api.make_keyword_only("3.6", "useblit") def __init__(self, canvas, axes, useblit=True, horizOn=False, vertOn=True, **lineprops): + # canvas is stored only to provide the deprecated .canvas attribute; + # once it goes away the unused argument won't need to be stored at all. + self._canvas = canvas - self.canvas = canvas self.axes = axes self.horizOn = horizOn self.vertOn = vertOn + self._canvas_infos = { + ax.figure.canvas: {"cids": [], "background": None} for ax in axes} + xmin, xmax = axes[-1].get_xlim() ymin, ymax = axes[-1].get_ylim() xmid = 0.5 * (xmin + xmax) ymid = 0.5 * (ymin + ymax) self.visible = True - self.useblit = useblit and self.canvas.supports_blit - self.background = None - self.needclear = False + self.useblit = ( + useblit + and all(canvas.supports_blit for canvas in self._canvas_infos)) if self.useblit: lineprops['animated'] = True - if vertOn: - self.vlines = [ax.axvline(xmid, visible=False, **lineprops) - for ax in axes] - else: - self.vlines = [] - - if horizOn: - self.hlines = [ax.axhline(ymid, visible=False, **lineprops) - for ax in axes] - else: - self.hlines = [] + self.vlines = [ax.axvline(xmid, visible=False, **lineprops) + for ax in axes] + self.hlines = [ax.axhline(ymid, visible=False, **lineprops) + for ax in axes] self.connect() + canvas = _api.deprecate_privatize_attribute("3.6") + background = _api.deprecated("3.6")(lambda self: ( + self._backgrounds[self.axes[0].figure.canvas] if self.axes else None)) + needclear = _api.deprecated("3.7")(lambda self: False) + def connect(self): """Connect events.""" - self._cidmotion = self.canvas.mpl_connect('motion_notify_event', - self.onmove) - self._ciddraw = self.canvas.mpl_connect('draw_event', self.clear) + for canvas, info in self._canvas_infos.items(): + info["cids"] = [ + canvas.mpl_connect('motion_notify_event', self.onmove), + canvas.mpl_connect('draw_event', self.clear), + ] def disconnect(self): """Disconnect events.""" - self.canvas.mpl_disconnect(self._cidmotion) - self.canvas.mpl_disconnect(self._ciddraw) + for canvas, info in self._canvas_infos.items(): + for cid in info["cids"]: + canvas.mpl_disconnect(cid) + info["cids"].clear() def clear(self, event): """Clear the cursor.""" if self.ignore(event): return if self.useblit: - self.background = ( - self.canvas.copy_from_bbox(self.canvas.figure.bbox)) - for line in self.vlines + self.hlines: - line.set_visible(False) + for canvas, info in self._canvas_infos.items(): + # someone has switched the canvas on us! This happens if + # `savefig` needs to save to a format the previous backend did + # not support (e.g. saving a figure using an Agg based backend + # saved to a vector format). + if canvas is not canvas.figure.canvas: + continue + info["background"] = canvas.copy_from_bbox(canvas.figure.bbox) def onmove(self, event): - if self.ignore(event): - return - if event.inaxes not in self.axes: - return - if not self.canvas.widgetlock.available(self): - return - self.needclear = True - if not self.visible: + if (self.ignore(event) + or event.inaxes not in self.axes + or not event.canvas.widgetlock.available(self)): return - if self.vertOn: - for line in self.vlines: - line.set_xdata((event.xdata, event.xdata)) - line.set_visible(self.visible) - if self.horizOn: - for line in self.hlines: - line.set_ydata((event.ydata, event.ydata)) - line.set_visible(self.visible) - self._update() + for line in self.vlines: + line.set_xdata((event.xdata, event.xdata)) + line.set_visible(self.visible and self.vertOn) + for line in self.hlines: + line.set_ydata((event.ydata, event.ydata)) + line.set_visible(self.visible and self.horizOn) + if self.visible and (self.vertOn or self.horizOn): + self._update() def _update(self): if self.useblit: - if self.background is not None: - self.canvas.restore_region(self.background) + for canvas, info in self._canvas_infos.items(): + if info["background"]: + canvas.restore_region(info["background"]) if self.vertOn: for ax, line in zip(self.axes, self.vlines): ax.draw_artist(line) if self.horizOn: for ax, line in zip(self.axes, self.hlines): ax.draw_artist(line) - self.canvas.blit() + for canvas in self._canvas_infos: + canvas.blit() else: - self.canvas.draw_idle() + for canvas in self._canvas_infos: + canvas.draw_idle() class _SelectorWidget(AxesWidget): def __init__(self, ax, onselect, useblit=False, button=None, - state_modifier_keys=None): + state_modifier_keys=None, use_data_coordinates=False): super().__init__(ax) - self.visible = True + self._visible = True self.onselect = onselect self.useblit = useblit and self.canvas.supports_blit self.connect_default_events() - self.state_modifier_keys = dict(move=' ', clear='escape', - square='shift', center='control') - self.state_modifier_keys.update(state_modifier_keys or {}) + self._state_modifier_keys = dict(move=' ', clear='escape', + square='shift', center='control', + rotate='r') + self._state_modifier_keys.update(state_modifier_keys or {}) + self._use_data_coordinates = use_data_coordinates self.background = None - self.artists = [] if isinstance(button, Integral): self.validButtons = [button] else: self.validButtons = button + # Set to True when a selection is completed, otherwise is False + self._selection_completed = False + # will save the data (position at mouseclick) - self.eventpress = None + self._eventpress = None # will save the data (pos. at mouserelease) - self.eventrelease = None + self._eventrelease = None self._prev_event = None - self.state = set() + self._state = set() + + state_modifier_keys = _api.deprecate_privatize_attribute("3.6") def set_active(self, active): super().set_active(active) if active: self.update_background(None) + def _get_animated_artists(self): + """ + Convenience method to get all animated artists of the figure containing + this widget, excluding those already present in self.artists. + The returned tuple is not sorted by 'z_order': z_order sorting is + valid only when considering all artists and not only a subset of all + artists. + """ + return tuple(a for ax_ in self.ax.get_figure().get_axes() + for a in ax_.get_children() + if a.get_animated() and a not in self.artists) + def update_background(self, event): """Force an update of the background.""" # If you add a call to `ignore` here, you'll want to check edge case: @@ -1731,16 +2228,22 @@ def update_background(self, event): # Make sure that widget artists don't get accidentally included in the # background, by re-rendering the background if needed (and then # re-re-rendering the canvas with the visible widget artists). - needs_redraw = any(artist.get_visible() for artist in self.artists) + # We need to remove all artists which will be drawn when updating + # the selector: if we have animated artists in the figure, it is safer + # to redrawn by default, in case they have updated by the callback + # zorder needs to be respected when redrawing + artists = sorted(self.artists + self._get_animated_artists(), + key=lambda a: a.get_zorder()) + needs_redraw = any(artist.get_visible() for artist in artists) with ExitStack() as stack: if needs_redraw: - for artist in self.artists: - stack.callback(artist.set_visible, artist.get_visible()) - artist.set_visible(False) + for artist in artists: + stack.enter_context(artist._cm_set(visible=False)) self.canvas.draw() self.background = self.canvas.copy_from_bbox(self.ax.bbox) if needs_redraw: - self.update() + for artist in artists: + self.ax.draw_artist(artist) def connect_default_events(self): """Connect the major canvas events to methods.""" @@ -1767,29 +2270,36 @@ def ignore(self, event): and event.button not in self.validButtons): return True # If no button was pressed yet ignore the event if it was out - # of the axes - if self.eventpress is None: + # of the Axes + if self._eventpress is None: return event.inaxes != self.ax # If a button was pressed, check if the release-button is the same. - if event.button == self.eventpress.button: + if event.button == self._eventpress.button: return False # If a button was pressed, check if the release-button is the same. return (event.inaxes != self.ax or - event.button != self.eventpress.button) + event.button != self._eventpress.button) def update(self): """Draw using blit() or draw_idle(), depending on ``self.useblit``.""" - if not self.ax.get_visible(): - return False + if (not self.ax.get_visible() or + self.ax.figure._get_renderer() is None): + return if self.useblit: if self.background is not None: self.canvas.restore_region(self.background) - for artist in self.artists: + else: + self.update_background(None) + # We need to draw all artists, which are not included in the + # background, therefore we also draw self._get_animated_artists() + # and we make sure that we respect z_order + artists = sorted(self.artists + self._get_animated_artists(), + key=lambda a: a.get_zorder()) + for artist in artists: self.ax.draw_artist(artist) self.canvas.blit(self.ax.bbox) else: self.canvas.draw_idle() - return False def _get_data(self, event): """Get the xdata and ydata for event, with limits.""" @@ -1819,13 +2329,13 @@ def press(self, event): """Button press handler and validator.""" if not self.ignore(event): event = self._clean_event(event) - self.eventpress = event + self._eventpress = event self._prev_event = event key = event.key or '' key = key.replace('ctrl', 'control') # move state is locked in on a button press - if key == self.state_modifier_keys['move']: - self.state.add('move') + if key == self._state_modifier_keys['move']: + self._state.add('move') self._press(event) return True return False @@ -1835,13 +2345,13 @@ def _press(self, event): def release(self, event): """Button release event handler and validator.""" - if not self.ignore(event) and self.eventpress: + if not self.ignore(event) and self._eventpress: event = self._clean_event(event) - self.eventrelease = event + self._eventrelease = event self._release(event) - self.eventpress = None - self.eventrelease = None - self.state.discard('move') + self._eventpress = None + self._eventrelease = None + self._state.discard('move') return True return False @@ -1850,7 +2360,7 @@ def _release(self, event): def onmove(self, event): """Cursor move event handler and validator.""" - if not self.ignore(event) and self.eventpress: + if not self.ignore(event) and self._eventpress: event = self._clean_event(event) self._onmove(event) return True @@ -1872,14 +2382,20 @@ def on_key_press(self, event): if self.active: key = event.key or '' key = key.replace('ctrl', 'control') - if key == self.state_modifier_keys['clear']: - for artist in self.artists: - artist.set_visible(False) - self.update() + if key == self._state_modifier_keys['clear']: + self.clear() return - for (state, modifier) in self.state_modifier_keys.items(): - if modifier in key: - self.state.add(state) + for (state, modifier) in self._state_modifier_keys.items(): + if modifier in key.split('+'): + # 'rotate' is changing _state on press and is not removed + # from _state when releasing + if state == 'rotate': + if state in self._state: + self._state.discard(state) + else: + self._state.add(state) + else: + self._state.add(state) self._on_key_press(event) def _on_key_press(self, event): @@ -1889,59 +2405,210 @@ def on_key_release(self, event): """Key release event handler and validator.""" if self.active: key = event.key or '' - for (state, modifier) in self.state_modifier_keys.items(): - if modifier in key: - self.state.discard(state) + for (state, modifier) in self._state_modifier_keys.items(): + # 'rotate' is changing _state on press and is not removed + # from _state when releasing + if modifier in key.split('+') and state != 'rotate': + self._state.discard(state) self._on_key_release(event) def _on_key_release(self, event): """Key release event handler.""" def set_visible(self, visible): - """Set the visibility of our artists.""" - self.visible = visible + """Set the visibility of the selector artists.""" + self._visible = visible for artist in self.artists: artist.set_visible(visible) + def get_visible(self): + """Get the visibility of the selector artists.""" + return self._visible -class SpanSelector(_SelectorWidget): - """ - Visually select a min/max range on a single axis and call a function with - those values. - - To guarantee that the selector remains responsive, keep a reference to it. + @property + def visible(self): + return self.get_visible() - In order to turn off the SpanSelector, set ``span_selector.active`` to - False. To turn it back on, set it to True. + @visible.setter + def visible(self, visible): + _api.warn_deprecated("3.6", alternative="set_visible") + self.set_visible(visible) - Parameters - ---------- - ax : `matplotlib.axes.Axes` + def clear(self): + """Clear the selection and set the selector ready to make a new one.""" + self._clear_without_update() + self.update() - onselect : func(min, max), min/max are floats + def _clear_without_update(self): + self._selection_completed = False + self.set_visible(False) - direction : {"horizontal", "vertical"} - The direction along which to draw the span selector. + @property + def artists(self): + """Tuple of the artists of the selector.""" + handles_artists = getattr(self, '_handles_artists', ()) + return (self._selection_artist,) + handles_artists - minspan : float, default: None - If selection is less than *minspan*, do not call *onselect*. + def set_props(self, **props): + """ + Set the properties of the selector artist. See the `props` argument + in the selector docstring to know which properties are supported. + """ + artist = self._selection_artist + props = cbook.normalize_kwargs(props, artist) + artist.set(**props) + if self.useblit: + self.update() + self._props.update(props) - useblit : bool, default: False - If True, use the backend-dependent blitting features for faster - canvas updates. + def set_handle_props(self, **handle_props): + """ + Set the properties of the handles selector artist. See the + `handle_props` argument in the selector docstring to know which + properties are supported. + """ + if not hasattr(self, '_handles_artists'): + raise NotImplementedError("This selector doesn't have handles.") - rectprops : dict, default: None + artist = self._handles_artists[0] + handle_props = cbook.normalize_kwargs(handle_props, artist) + for handle in self._handles_artists: + handle.set(**handle_props) + if self.useblit: + self.update() + self._handle_props.update(handle_props) + + def _validate_state(self, state): + supported_state = [ + key for key, value in self._state_modifier_keys.items() + if key != 'clear' and value != 'not-applicable' + ] + _api.check_in_list(supported_state, state=state) + + def add_state(self, state): + """ + Add a state to define the widget's behavior. See the + `state_modifier_keys` parameters for details. + + Parameters + ---------- + state : str + Must be a supported state of the selector. See the + `state_modifier_keys` parameters for details. + + Raises + ------ + ValueError + When the state is not supported by the selector. + + """ + self._validate_state(state) + self._state.add(state) + + def remove_state(self, state): + """ + Remove a state to define the widget's behavior. See the + `state_modifier_keys` parameters for details. + + Parameters + ---------- + state : str + Must be a supported state of the selector. See the + `state_modifier_keys` parameters for details. + + Raises + ------ + ValueError + When the state is not supported by the selector. + + """ + self._validate_state(state) + self._state.remove(state) + + +class SpanSelector(_SelectorWidget): + """ + Visually select a min/max range on a single axis and call a function with + those values. + + To guarantee that the selector remains responsive, keep a reference to it. + + In order to turn off the SpanSelector, set ``span_selector.active`` to + False. To turn it back on, set it to True. + + Press and release events triggered at the same coordinates outside the + selection will clear the selector, except when + ``ignore_event_outside=True``. + + Parameters + ---------- + ax : `matplotlib.axes.Axes` + + onselect : callable + A callback function that is called after a release event and the + selection is created, changed or removed. + It must have the signature:: + + def on_select(min: float, max: float) -> Any + + direction : {"horizontal", "vertical"} + The direction along which to draw the span selector. + + minspan : float, default: 0 + If selection is less than or equal to *minspan*, the selection is + removed (when already existing) or cancelled. + + useblit : bool, default: False + If True, use the backend-dependent blitting features for faster + canvas updates. See the tutorial :doc:`/tutorials/advanced/blitting` + for details. + + props : dict, optional Dictionary of `matplotlib.patches.Patch` properties. + Default: + + ``dict(facecolor='red', alpha=0.5)`` onmove_callback : func(min, max), min/max are floats, default: None Called on mouse move while the span is being selected. span_stays : bool, default: False If True, the span stays visible after the mouse is released. + Deprecated, use *interactive* instead. - button : `.MouseButton` or list of `.MouseButton` + interactive : bool, default: False + Whether to draw a set of handles that allow interaction with the + widget after it is drawn. + + button : `.MouseButton` or list of `.MouseButton`, default: all buttons The mouse buttons which activate the span selector. + handle_props : dict, default: None + Properties of the handle lines at the edges of the span. Only used + when *interactive* is True. See `matplotlib.lines.Line2D` for valid + properties. + + grab_range : float, default: 10 + Distance in pixels within which the interactive tool handles can be + activated. + + state_modifier_keys : dict, optional + Keyboard modifiers which affect the widget's behavior. Values + amend the defaults, which are: + + - "clear": Clear the current shape, default: "escape". + + drag_from_anywhere : bool, default: False + If `True`, the widget can be moved by clicking anywhere within + its bounds. + + ignore_event_outside : bool, default: False + If `True`, the event triggered outside the span selector will be + ignored. + + snap_values : 1D array-like, optional + Snap the selector edges to the given values. + Examples -------- >>> import matplotlib.pyplot as plt @@ -1950,43 +2617,69 @@ class SpanSelector(_SelectorWidget): >>> ax.plot([1, 2, 3], [10, 50, 100]) >>> def onselect(vmin, vmax): ... print(vmin, vmax) - >>> rectprops = dict(facecolor='blue', alpha=0.5) >>> span = mwidgets.SpanSelector(ax, onselect, 'horizontal', - ... rectprops=rectprops) + ... props=dict(facecolor='blue', alpha=0.5)) >>> fig.show() See also: :doc:`/gallery/widgets/span_selector` """ - def __init__(self, ax, onselect, direction, minspan=None, useblit=False, - rectprops=None, onmove_callback=None, span_stays=False, - button=None): - - super().__init__(ax, onselect, useblit=useblit, button=button) + @_api.make_keyword_only("3.7", name="minspan") + def __init__(self, ax, onselect, direction, minspan=0, useblit=False, + props=None, onmove_callback=None, interactive=False, + button=None, handle_props=None, grab_range=10, + state_modifier_keys=None, drag_from_anywhere=False, + ignore_event_outside=False, snap_values=None): + + if state_modifier_keys is None: + state_modifier_keys = dict(clear='escape', + square='not-applicable', + center='not-applicable', + rotate='not-applicable') + super().__init__(ax, onselect, useblit=useblit, button=button, + state_modifier_keys=state_modifier_keys) - if rectprops is None: - rectprops = dict(facecolor='red', alpha=0.5) + if props is None: + props = dict(facecolor='red', alpha=0.5) - rectprops['animated'] = self.useblit + props['animated'] = self.useblit - _api.check_in_list(['horizontal', 'vertical'], direction=direction) self.direction = direction + self._extents_on_press = None + self.snap_values = snap_values - self.rect = None - self.pressv = None + # self._pressv is deprecated and we don't use it internally anymore + # but we maintain it until it is removed + self._pressv = None - self.rectprops = rectprops + self._props = props self.onmove_callback = onmove_callback self.minspan = minspan - self.span_stays = span_stays - # Needed when dragging out of axes - self.prev = (0, 0) + self.grab_range = grab_range + self._interactive = interactive + self._edge_handles = None + self.drag_from_anywhere = drag_from_anywhere + self.ignore_event_outside = ignore_event_outside # Reset canvas so that `new_axes` connects events. self.canvas = None self.new_axes(ax) + # Setup handles + self._handle_props = { + 'color': props.get('facecolor', 'r'), + **cbook.normalize_kwargs(handle_props, Line2D)} + + if self._interactive: + self._edge_order = ['min', 'max'] + self._setup_edge_handles(self._handle_props) + + self._active_handle = None + + # prev attribute is deprecated but we still need to maintain it + self._prev = (0, 0) + def new_axes(self, ax): """Set SpanSelector to operate on a new Axes.""" self.ax = ax @@ -1997,123 +2690,386 @@ def new_axes(self, ax): self.canvas = ax.figure.canvas self.connect_default_events() + # Reset + self._selection_completed = False + if self.direction == 'horizontal': trans = ax.get_xaxis_transform() w, h = 0, 1 else: trans = ax.get_yaxis_transform() w, h = 1, 0 - self.rect = Rectangle((0, 0), w, h, - transform=trans, - visible=False, - **self.rectprops) - if self.span_stays: - self.stay_rect = Rectangle((0, 0), w, h, - transform=trans, - visible=False, - **self.rectprops) - self.stay_rect.set_animated(False) - self.ax.add_patch(self.stay_rect) - - self.ax.add_patch(self.rect) - self.artists = [self.rect] + rect_artist = Rectangle((0, 0), w, h, + transform=trans, + visible=False, + **self._props) - def ignore(self, event): + self.ax.add_patch(rect_artist) + self._selection_artist = rect_artist + + def _setup_edge_handles(self, props): + # Define initial position using the axis bounds to keep the same bounds + if self.direction == 'horizontal': + positions = self.ax.get_xbound() + else: + positions = self.ax.get_ybound() + self._edge_handles = ToolLineHandles(self.ax, positions, + direction=self.direction, + line_props=props, + useblit=self.useblit) + + @property + def _handles_artists(self): + if self._edge_handles is not None: + return self._edge_handles.artists + else: + return () + + def _set_cursor(self, enabled): + """Update the canvas cursor based on direction of the selector.""" + if enabled: + cursor = (backend_tools.Cursors.RESIZE_HORIZONTAL + if self.direction == 'horizontal' else + backend_tools.Cursors.RESIZE_VERTICAL) + else: + cursor = backend_tools.Cursors.POINTER + + self.ax.figure.canvas.set_cursor(cursor) + + def connect_default_events(self): # docstring inherited - return super().ignore(event) or not self.visible + super().connect_default_events() + if getattr(self, '_interactive', False): + self.connect_event('motion_notify_event', self._hover) def _press(self, event): """Button press event handler.""" - self.rect.set_visible(self.visible) - if self.span_stays: - self.stay_rect.set_visible(False) - # really force a draw so that the stay rect is not in - # the blit background - if self.useblit: - self.canvas.draw() - xdata, ydata = self._get_data(event) - if self.direction == 'horizontal': - self.pressv = xdata + self._set_cursor(True) + if self._interactive and self._selection_artist.get_visible(): + self._set_active_handle(event) else: - self.pressv = ydata + self._active_handle = None + + if self._active_handle is None or not self._interactive: + # Clear previous rectangle before drawing new rectangle. + self.update() + + v = event.xdata if self.direction == 'horizontal' else event.ydata + # self._pressv and self._prev are deprecated but we still need to + # maintain them + self._pressv = v + self._prev = self._get_data(event) + + if self._active_handle is None and not self.ignore_event_outside: + # when the press event outside the span, we initially set the + # visibility to False and extents to (v, v) + # update will be called when setting the extents + self._visible = False + self.extents = v, v + # We need to set the visibility back, so the span selector will be + # drawn when necessary (span width > 0) + self._visible = True + else: + self.set_visible(True) - self._set_span_xy(event) return False + @property + def direction(self): + """Direction of the span selector: 'vertical' or 'horizontal'.""" + return self._direction + + @direction.setter + def direction(self, direction): + """Set the direction of the span selector.""" + _api.check_in_list(['horizontal', 'vertical'], direction=direction) + if hasattr(self, '_direction') and direction != self._direction: + # remove previous artists + self._selection_artist.remove() + if self._interactive: + self._edge_handles.remove() + self._direction = direction + self.new_axes(self.ax) + if self._interactive: + self._setup_edge_handles(self._handle_props) + else: + self._direction = direction + def _release(self, event): """Button release event handler.""" - if self.pressv is None: - return + self._set_cursor(False) + # self._pressv is deprecated but we still need to maintain it + self._pressv = None - self.rect.set_visible(False) + if not self._interactive: + self._selection_artist.set_visible(False) - if self.span_stays: - self.stay_rect.set_x(self.rect.get_x()) - self.stay_rect.set_y(self.rect.get_y()) - self.stay_rect.set_width(self.rect.get_width()) - self.stay_rect.set_height(self.rect.get_height()) - self.stay_rect.set_visible(True) + if (self._active_handle is None and self._selection_completed and + self.ignore_event_outside): + return - self.canvas.draw_idle() - vmin = self.pressv - xdata, ydata = self._get_data(event) - if self.direction == 'horizontal': - vmax = xdata or self.prev[0] + vmin, vmax = self.extents + span = vmax - vmin + + if span <= self.minspan: + # Remove span and set self._selection_completed = False + self.set_visible(False) + if self._selection_completed: + # Call onselect, only when the span is already existing + self.onselect(vmin, vmax) + self._selection_completed = False else: - vmax = ydata or self.prev[1] + self.onselect(vmin, vmax) + self._selection_completed = True + + self.update() + + self._active_handle = None - if vmin > vmax: - vmin, vmax = vmax, vmin - span = vmax - vmin - if self.minspan is not None and span < self.minspan: - return - self.onselect(vmin, vmax) - self.pressv = None return False + def _hover(self, event): + """Update the canvas cursor if it's over a handle.""" + if self.ignore(event): + return + + if self._active_handle is not None or not self._selection_completed: + # Do nothing if button is pressed and a handle is active, which may + # occur with drag_from_anywhere=True. + # Do nothing if selection is not completed, which occurs when + # a selector has been cleared + return + + _, e_dist = self._edge_handles.closest(event.x, event.y) + self._set_cursor(e_dist <= self.grab_range) + def _onmove(self, event): """Motion notify event handler.""" - if self.pressv is None: - return - self._set_span_xy(event) + # self._prev are deprecated but we still need to maintain it + self._prev = self._get_data(event) - if self.onmove_callback is not None: - vmin = self.pressv - xdata, ydata = self._get_data(event) - if self.direction == 'horizontal': - vmax = xdata or self.prev[0] - else: - vmax = ydata or self.prev[1] + v = event.xdata if self.direction == 'horizontal' else event.ydata + if self.direction == 'horizontal': + vpress = self._eventpress.xdata + else: + vpress = self._eventpress.ydata + + # move existing span + # When "dragging from anywhere", `self._active_handle` is set to 'C' + # (match notation used in the RectangleSelector) + if self._active_handle == 'C' and self._extents_on_press is not None: + vmin, vmax = self._extents_on_press + dv = v - vpress + vmin += dv + vmax += dv + # resize an existing shape + elif self._active_handle and self._active_handle != 'C': + vmin, vmax = self._extents_on_press + if self._active_handle == 'min': + vmin = v + else: + vmax = v + # new shape + else: + # Don't create a new span if there is already one when + # ignore_event_outside=True + if self.ignore_event_outside and self._selection_completed: + return + vmin, vmax = vpress, v if vmin > vmax: vmin, vmax = vmax, vmin + + self.extents = vmin, vmax + + if self.onmove_callback is not None: self.onmove_callback(vmin, vmax) - self.update() return False - def _set_span_xy(self, event): - """Set the span coordinates.""" - x, y = self._get_data(event) - if x is None: - return + def _draw_shape(self, vmin, vmax): + if vmin > vmax: + vmin, vmax = vmax, vmin + if self.direction == 'horizontal': + self._selection_artist.set_x(vmin) + self._selection_artist.set_width(vmax - vmin) + else: + self._selection_artist.set_y(vmin) + self._selection_artist.set_height(vmax - vmin) + + def _set_active_handle(self, event): + """Set active handle based on the location of the mouse event.""" + # Note: event.xdata/ydata in data coordinates, event.x/y in pixels + e_idx, e_dist = self._edge_handles.closest(event.x, event.y) - self.prev = x, y + # Prioritise center handle over other handles + # Use 'C' to match the notation used in the RectangleSelector + if 'move' in self._state: + self._active_handle = 'C' + elif e_dist > self.grab_range: + # Not close to any handles + self._active_handle = None + if self.drag_from_anywhere and self._contains(event): + # Check if we've clicked inside the region + self._active_handle = 'C' + self._extents_on_press = self.extents + else: + self._active_handle = None + return + else: + # Closest to an edge handle + self._active_handle = self._edge_order[e_idx] + + # Save coordinates of rectangle at the start of handle movement. + self._extents_on_press = self.extents + + def _contains(self, event): + """Return True if event is within the patch.""" + return self._selection_artist.contains(event, radius=0)[0] + + @staticmethod + def _snap(values, snap_values): + """Snap values to a given array values (snap_values).""" + # take into account machine precision + eps = np.min(np.abs(np.diff(snap_values))) * 1e-12 + return tuple( + snap_values[np.abs(snap_values - v + np.sign(v) * eps).argmin()] + for v in values) + + @property + def extents(self): + """Return extents of the span selector.""" if self.direction == 'horizontal': - v = x + vmin = self._selection_artist.get_x() + vmax = vmin + self._selection_artist.get_width() else: - v = y + vmin = self._selection_artist.get_y() + vmax = vmin + self._selection_artist.get_height() + return vmin, vmax + + @extents.setter + def extents(self, extents): + # Update displayed shape + if self.snap_values is not None: + extents = tuple(self._snap(extents, self.snap_values)) + self._draw_shape(*extents) + if self._interactive: + # Update displayed handles + self._edge_handles.set_data(self.extents) + self.set_visible(self._visible) + self.update() + + +class ToolLineHandles: + """ + Control handles for canvas tools. + + Parameters + ---------- + ax : `matplotlib.axes.Axes` + Matplotlib Axes where tool handles are displayed. + positions : 1D array + Positions of handles in data coordinates. + direction : {"horizontal", "vertical"} + Direction of handles, either 'vertical' or 'horizontal' + line_props : dict, optional + Additional line properties. See `matplotlib.lines.Line2D`. + useblit : bool, default: True + Whether to use blitting for faster drawing (if supported by the + backend). See the tutorial :doc:`/tutorials/advanced/blitting` + for details. + """ + + @_api.make_keyword_only("3.7", "line_props") + def __init__(self, ax, positions, direction, line_props=None, + useblit=True): + self.ax = ax + + _api.check_in_list(['horizontal', 'vertical'], direction=direction) + self._direction = direction + + line_props = { + **(line_props if line_props is not None else {}), + 'visible': False, + 'animated': useblit, + } + + line_fun = ax.axvline if self.direction == 'horizontal' else ax.axhline - minv, maxv = v, self.pressv - if minv > maxv: - minv, maxv = maxv, minv + self._artists = [line_fun(p, **line_props) for p in positions] + + @property + def artists(self): + return tuple(self._artists) + + @property + def positions(self): + """Positions of the handle in data coordinates.""" + method = 'get_xdata' if self.direction == 'horizontal' else 'get_ydata' + return [getattr(line, method)()[0] for line in self.artists] + + @property + def direction(self): + """Direction of the handle: 'vertical' or 'horizontal'.""" + return self._direction + + def set_data(self, positions): + """ + Set x- or y-positions of handles, depending on if the lines are + vertical or horizontal. + + Parameters + ---------- + positions : tuple of length 2 + Set the positions of the handle in data coordinates + """ + method = 'set_xdata' if self.direction == 'horizontal' else 'set_ydata' + for line, p in zip(self.artists, positions): + getattr(line, method)([p, p]) + + def set_visible(self, value): + """Set the visibility state of the handles artist.""" + for artist in self.artists: + artist.set_visible(value) + + def set_animated(self, value): + """Set the animated state of the handles artist.""" + for artist in self.artists: + artist.set_animated(value) + + def remove(self): + """Remove the handles artist from the figure.""" + for artist in self._artists: + artist.remove() + + def closest(self, x, y): + """ + Return index and pixel distance to closest handle. + + Parameters + ---------- + x, y : float + x, y position from which the distance will be calculated to + determinate the closest handle + + Returns + ------- + index, distance : index of the handle and its distance from + position x, y + """ if self.direction == 'horizontal': - self.rect.set_x(minv) - self.rect.set_width(maxv - minv) + p_pts = np.array([ + self.ax.transData.transform((p, 0))[0] for p in self.positions + ]) + dist = abs(p_pts - x) else: - self.rect.set_y(minv) - self.rect.set_height(maxv - minv) + p_pts = np.array([ + self.ax.transData.transform((0, p))[1] for p in self.positions + ]) + dist = abs(p_pts - y) + index = np.argmin(dist) + return index, dist[index] class ToolHandles: @@ -2123,15 +3079,20 @@ class ToolHandles: Parameters ---------- ax : `matplotlib.axes.Axes` - Matplotlib axes where tool handles are displayed. + Matplotlib Axes where tool handles are displayed. x, y : 1D arrays Coordinates of control handles. - marker : str + marker : str, default: 'o' Shape of marker used to display handle. See `matplotlib.pyplot.plot`. - marker_props : dict + marker_props : dict, optional Additional marker properties. See `matplotlib.lines.Line2D`. + useblit : bool, default: True + Whether to use blitting for faster drawing (if supported by the + backend). See the tutorial :doc:`/tutorials/advanced/blitting` + for details. """ + @_api.make_keyword_only("3.7", "marker") def __init__(self, ax, x, y, marker='o', marker_props=None, useblit=True): self.ax = ax props = {'marker': marker, 'markersize': 7, 'markerfacecolor': 'w', @@ -2140,7 +3101,6 @@ def __init__(self, ax, x, y, marker='o', marker_props=None, useblit=True): **cbook.normalize_kwargs(marker_props, Line2D._alias_map)} self._markers = Line2D(x, y, animated=useblit, **props) self.ax.add_line(self._markers) - self.artist = self._markers @property def x(self): @@ -2150,6 +3110,10 @@ def x(self): def y(self): return self._markers.get_ydata() + @property + def artists(self): + return (self._markers, ) + def set_data(self, pts, y=None): """Set x and y positions of handles.""" if y is not None: @@ -2174,272 +3138,413 @@ def closest(self, x, y): return min_index, dist[min_index] -class RectangleSelector(_SelectorWidget): - """ - Select a rectangular region of an axes. - - For the cursor to remain responsive you must keep a reference to it. - - Examples - -------- - :doc:`/gallery/widgets/rectangle_selector` - """ - - _shape_klass = Rectangle - - def __init__(self, ax, onselect, drawtype='box', - minspanx=0, minspany=0, useblit=False, - lineprops=None, rectprops=None, spancoords='data', - button=None, maxdist=10, marker_props=None, - interactive=False, state_modifier_keys=None): - r""" - Parameters - ---------- - ax : `~matplotlib.axes.Axes` - The parent axes for the widget. - - onselect : function - A callback function that is called after a selection is completed. - It must have the signature:: - - def onselect(eclick: MouseEvent, erelease: MouseEvent) - - where *eclick* and *erelease* are the mouse click and release - `.MouseEvent`\s that start and complete the selection. - - drawtype : {"box", "line", "none"}, default: "box" - Whether to draw the full rectangle box, the diagonal line of the - rectangle, or nothing at all. - - minspanx : float, default: 0 - Selections with an x-span less than *minspanx* are ignored. +_RECTANGLESELECTOR_PARAMETERS_DOCSTRING = \ + r""" + Parameters + ---------- + ax : `~matplotlib.axes.Axes` + The parent axes for the widget. - minspany : float, default: 0 - Selections with an y-span less than *minspany* are ignored. + onselect : function + A callback function that is called after a release event and the + selection is created, changed or removed. + It must have the signature:: - useblit : bool, default: False - Whether to use blitting for faster drawing (if supported by the - backend). + def onselect(eclick: MouseEvent, erelease: MouseEvent) - lineprops : dict, optional - Properties with which the line is drawn, if ``drawtype == "line"``. - Default:: + where *eclick* and *erelease* are the mouse click and release + `.MouseEvent`\s that start and complete the selection. - dict(color="black", linestyle="-", linewidth=2, alpha=0.5) + minspanx : float, default: 0 + Selections with an x-span less than or equal to *minspanx* are removed + (when already existing) or cancelled. - rectprops : dict, optional - Properties with which the rectangle is drawn, if ``drawtype == - "box"``. Default:: + minspany : float, default: 0 + Selections with an y-span less than or equal to *minspanx* are removed + (when already existing) or cancelled. - dict(facecolor="red", edgecolor="black", alpha=0.2, fill=True) + useblit : bool, default: False + Whether to use blitting for faster drawing (if supported by the + backend). See the tutorial :doc:`/tutorials/advanced/blitting` + for details. + + props : dict, optional + Properties with which the __ARTIST_NAME__ is drawn. See + `matplotlib.patches.Patch` for valid properties. + Default: + + ``dict(facecolor='red', edgecolor='black', alpha=0.2, fill=True)`` + + spancoords : {"data", "pixels"}, default: "data" + Whether to interpret *minspanx* and *minspany* in data or in pixel + coordinates. + + button : `.MouseButton`, list of `.MouseButton`, default: all buttons + Button(s) that trigger rectangle selection. + + grab_range : float, default: 10 + Distance in pixels within which the interactive tool handles can be + activated. + + handle_props : dict, optional + Properties with which the interactive handles (marker artists) are + drawn. See the marker arguments in `matplotlib.lines.Line2D` for valid + properties. Default values are defined in ``mpl.rcParams`` except for + the default value of ``markeredgecolor`` which will be the same as the + ``edgecolor`` property in *props*. + + interactive : bool, default: False + Whether to draw a set of handles that allow interaction with the + widget after it is drawn. + + state_modifier_keys : dict, optional + Keyboard modifiers which affect the widget's behavior. Values + amend the defaults, which are: + + - "move": Move the existing shape, default: no modifier. + - "clear": Clear the current shape, default: "escape". + - "square": Make the shape square, default: "shift". + - "center": change the shape around its center, default: "ctrl". + - "rotate": Rotate the shape around its center between -45° and 45°, + default: "r". + + "square" and "center" can be combined. The square shape can be defined + in data or display coordinates as determined by the + ``use_data_coordinates`` argument specified when creating the selector. + + drag_from_anywhere : bool, default: False + If `True`, the widget can be moved by clicking anywhere within + its bounds. + + ignore_event_outside : bool, default: False + If `True`, the event triggered outside the span selector will be + ignored. + + use_data_coordinates : bool, default: False + If `True`, the "square" shape of the selector is defined in + data coordinates instead of display coordinates. + """ - spancoords : {"data", "pixels"}, default: "data" - Whether to interpret *minspanx* and *minspany* in data or in pixel - coordinates. - button : `.MouseButton`, list of `.MouseButton`, default: all buttons - Button(s) that trigger rectangle selection. +@_docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( + '__ARTIST_NAME__', 'rectangle')) +class RectangleSelector(_SelectorWidget): + """ + Select a rectangular region of an Axes. - maxdist : float, default: 10 - Distance in pixels within which the interactive tool handles can be - activated. + For the cursor to remain responsive you must keep a reference to it. - marker_props : dict - Properties with which the interactive handles are drawn. Currently - not implemented and ignored. + Press and release events triggered at the same coordinates outside the + selection will clear the selector, except when + ``ignore_event_outside=True``. - interactive : bool, default: False - Whether to draw a set of handles that allow interaction with the - widget after it is drawn. + %s - state_modifier_keys : dict, optional - Keyboard modifiers which affect the widget's behavior. Values - amend the defaults. + Examples + -------- + >>> import matplotlib.pyplot as plt + >>> import matplotlib.widgets as mwidgets + >>> fig, ax = plt.subplots() + >>> ax.plot([1, 2, 3], [10, 50, 100]) + >>> def onselect(eclick, erelease): + ... print(eclick.xdata, eclick.ydata) + ... print(erelease.xdata, erelease.ydata) + >>> props = dict(facecolor='blue', alpha=0.5) + >>> rect = mwidgets.RectangleSelector(ax, onselect, interactive=True, + ... props=props) + >>> fig.show() + >>> rect.add_state('square') - - "move": Move the existing shape, default: no modifier. - - "clear": Clear the current shape, default: "escape". - - "square": Makes the shape square, default: "shift". - - "center": Make the initial point the center of the shape, - default: "ctrl". + See also: :doc:`/gallery/widgets/rectangle_selector` + """ - "square" and "center" can be combined. - """ + def __init__(self, ax, onselect, *, minspanx=0, minspany=0, useblit=False, + props=None, spancoords='data', button=None, grab_range=10, + handle_props=None, interactive=False, + state_modifier_keys=None, drag_from_anywhere=False, + ignore_event_outside=False, use_data_coordinates=False): super().__init__(ax, onselect, useblit=useblit, button=button, - state_modifier_keys=state_modifier_keys) - - self.to_draw = None - self.visible = True - self.interactive = interactive - - if drawtype == 'none': # draw a line but make it invisible - drawtype = 'line' - self.visible = False - - if drawtype == 'box': - if rectprops is None: - rectprops = dict(facecolor='red', edgecolor='black', - alpha=0.2, fill=True) - rectprops['animated'] = self.useblit - self.rectprops = rectprops - self.to_draw = self._shape_klass((0, 0), 0, 1, visible=False, - **self.rectprops) - self.ax.add_patch(self.to_draw) - if drawtype == 'line': - if lineprops is None: - lineprops = dict(color='black', linestyle='-', - linewidth=2, alpha=0.5) - lineprops['animated'] = self.useblit - self.lineprops = lineprops - self.to_draw = Line2D([0, 0], [0, 0], visible=False, - **self.lineprops) - self.ax.add_line(self.to_draw) + state_modifier_keys=state_modifier_keys, + use_data_coordinates=use_data_coordinates) + + self._interactive = interactive + self.drag_from_anywhere = drag_from_anywhere + self.ignore_event_outside = ignore_event_outside + self._rotation = 0.0 + self._aspect_ratio_correction = 1.0 + + # State to allow the option of an interactive selector that can't be + # interactively drawn. This is used in PolygonSelector as an + # interactive bounding box to allow the polygon to be easily resized + self._allow_creation = True + + if props is None: + props = dict(facecolor='red', edgecolor='black', + alpha=0.2, fill=True) + self._props = {**props, 'animated': self.useblit} + self._visible = self._props.pop('visible', self._visible) + to_draw = self._init_shape(**self._props) + self.ax.add_patch(to_draw) + + self._selection_artist = to_draw + self._set_aspect_ratio_correction() self.minspanx = minspanx self.minspany = minspany _api.check_in_list(['data', 'pixels'], spancoords=spancoords) self.spancoords = spancoords - self.drawtype = drawtype - - self.maxdist = maxdist - if rectprops is None: - props = dict(markeredgecolor='r') - else: - props = dict(markeredgecolor=rectprops.get('edgecolor', 'r')) - props.update(cbook.normalize_kwargs(marker_props, Line2D._alias_map)) - self._corner_order = ['NW', 'NE', 'SE', 'SW'] - xc, yc = self.corners - self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props, - useblit=self.useblit) + self.grab_range = grab_range - self._edge_order = ['W', 'N', 'E', 'S'] - xe, ye = self.edge_centers - self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', - marker_props=props, - useblit=self.useblit) + if self._interactive: + self._handle_props = { + 'markeredgecolor': (self._props or {}).get( + 'edgecolor', 'black'), + **cbook.normalize_kwargs(handle_props, Line2D)} - xc, yc = self.center - self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', - marker_props=props, - useblit=self.useblit) + self._corner_order = ['SW', 'SE', 'NE', 'NW'] + xc, yc = self.corners + self._corner_handles = ToolHandles(self.ax, xc, yc, + marker_props=self._handle_props, + useblit=self.useblit) - self.active_handle = None + self._edge_order = ['W', 'S', 'E', 'N'] + xe, ye = self.edge_centers + self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', + marker_props=self._handle_props, + useblit=self.useblit) - self.artists = [self.to_draw, self._center_handle.artist, - self._corner_handles.artist, - self._edge_handles.artist] + xc, yc = self.center + self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', + marker_props=self._handle_props, + useblit=self.useblit) - if not self.interactive: - self.artists = [self.to_draw] + self._active_handle = None self._extents_on_press = None + @property + def _handles_artists(self): + return (*self._center_handle.artists, *self._corner_handles.artists, + *self._edge_handles.artists) + + def _init_shape(self, **props): + return Rectangle((0, 0), 0, 1, visible=False, + rotation_point='center', **props) + def _press(self, event): """Button press event handler.""" # make the drawn box/line visible get the click-coordinates, # button, ... - if self.interactive and self.to_draw.get_visible(): + if self._interactive and self._selection_artist.get_visible(): self._set_active_handle(event) else: - self.active_handle = None + self._active_handle = None - if self.active_handle is None or not self.interactive: + if ((self._active_handle is None or not self._interactive) and + self._allow_creation): # Clear previous rectangle before drawing new rectangle. self.update() - if not self.interactive: + if (self._active_handle is None and not self.ignore_event_outside and + self._allow_creation): x = event.xdata y = event.ydata + self._visible = False self.extents = x, x, y, y + self._visible = True + else: + self.set_visible(True) - self.set_visible(self.visible) + self._extents_on_press = self.extents + self._rotation_on_press = self._rotation + self._set_aspect_ratio_correction() + + return False def _release(self, event): """Button release event handler.""" - if not self.interactive: - self.to_draw.set_visible(False) + if not self._interactive: + self._selection_artist.set_visible(False) + + if (self._active_handle is None and self._selection_completed and + self.ignore_event_outside): + return # update the eventpress and eventrelease with the resulting extents - x1, x2, y1, y2 = self.extents - self.eventpress.xdata = x1 - self.eventpress.ydata = y1 + x0, x1, y0, y1 = self.extents + self._eventpress.xdata = x0 + self._eventpress.ydata = y0 + xy0 = self.ax.transData.transform([x0, y0]) + self._eventpress.x, self._eventpress.y = xy0 + + self._eventrelease.xdata = x1 + self._eventrelease.ydata = y1 xy1 = self.ax.transData.transform([x1, y1]) - self.eventpress.x, self.eventpress.y = xy1 - - self.eventrelease.xdata = x2 - self.eventrelease.ydata = y2 - xy2 = self.ax.transData.transform([x2, y2]) - self.eventrelease.x, self.eventrelease.y = xy2 + self._eventrelease.x, self._eventrelease.y = xy1 # calculate dimensions of box or line if self.spancoords == 'data': - spanx = abs(self.eventpress.xdata - self.eventrelease.xdata) - spany = abs(self.eventpress.ydata - self.eventrelease.ydata) + spanx = abs(self._eventpress.xdata - self._eventrelease.xdata) + spany = abs(self._eventpress.ydata - self._eventrelease.ydata) elif self.spancoords == 'pixels': - spanx = abs(self.eventpress.x - self.eventrelease.x) - spany = abs(self.eventpress.y - self.eventrelease.y) + spanx = abs(self._eventpress.x - self._eventrelease.x) + spany = abs(self._eventpress.y - self._eventrelease.y) else: _api.check_in_list(['data', 'pixels'], spancoords=self.spancoords) # check if drawn distance (if it exists) is not too small in # either x or y-direction - if (self.drawtype != 'none' - and (self.minspanx is not None and spanx < self.minspanx - or self.minspany is not None and spany < self.minspany)): - for artist in self.artists: - artist.set_visible(False) - self.update() - return + if spanx <= self.minspanx or spany <= self.minspany: + if self._selection_completed: + # Call onselect, only when the selection is already existing + self.onselect(self._eventpress, self._eventrelease) + self._clear_without_update() + else: + self.onselect(self._eventpress, self._eventrelease) + self._selection_completed = True - # call desired function - self.onselect(self.eventpress, self.eventrelease) self.update() + self._active_handle = None + self._extents_on_press = None return False def _onmove(self, event): - """Motion notify event handler.""" - # resize an existing shape - if self.active_handle and self.active_handle != 'C': - x1, x2, y1, y2 = self._extents_on_press - if self.active_handle in ['E', 'W'] + self._corner_order: - x2 = event.xdata - if self.active_handle in ['N', 'S'] + self._corner_order: - y2 = event.ydata - - # move existing shape - elif (('move' in self.state or self.active_handle == 'C') - and self._extents_on_press is not None): - x1, x2, y1, y2 = self._extents_on_press - dx = event.xdata - self.eventpress.xdata - dy = event.ydata - self.eventpress.ydata + """ + Motion notify event handler. + + This can do one of four things: + - Translate + - Rotate + - Re-size + - Continue the creation of a new shape + """ + eventpress = self._eventpress + # The calculations are done for rotation at zero: we apply inverse + # transformation to events except when we rotate and move + state = self._state + rotate = ('rotate' in state and + self._active_handle in self._corner_order) + move = self._active_handle == 'C' + resize = self._active_handle and not move + + if resize: + inv_tr = self._get_rotation_transform().inverted() + event.xdata, event.ydata = inv_tr.transform( + [event.xdata, event.ydata]) + eventpress.xdata, eventpress.ydata = inv_tr.transform( + [eventpress.xdata, eventpress.ydata] + ) + + dx = event.xdata - eventpress.xdata + dy = event.ydata - eventpress.ydata + # refmax is used when moving the corner handle with the square state + # and is the maximum between refx and refy + refmax = None + if self._use_data_coordinates: + refx, refy = dx, dy + else: + # Get dx/dy in display coordinates + refx = event.x - eventpress.x + refy = event.y - eventpress.y + + x0, x1, y0, y1 = self._extents_on_press + # rotate an existing shape + if rotate: + # calculate angle abc + a = np.array([eventpress.xdata, eventpress.ydata]) + b = np.array(self.center) + c = np.array([event.xdata, event.ydata]) + angle = (np.arctan2(c[1]-b[1], c[0]-b[0]) - + np.arctan2(a[1]-b[1], a[0]-b[0])) + self.rotation = np.rad2deg(self._rotation_on_press + angle) + + elif resize: + size_on_press = [x1 - x0, y1 - y0] + center = [x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2] + + # Keeping the center fixed + if 'center' in state: + # hh, hw are half-height and half-width + if 'square' in state: + # when using a corner, find which reference to use + if self._active_handle in self._corner_order: + refmax = max(refx, refy, key=abs) + if self._active_handle in ['E', 'W'] or refmax == refx: + hw = event.xdata - center[0] + hh = hw / self._aspect_ratio_correction + else: + hh = event.ydata - center[1] + hw = hh * self._aspect_ratio_correction + else: + hw = size_on_press[0] / 2 + hh = size_on_press[1] / 2 + # cancel changes in perpendicular direction + if self._active_handle in ['E', 'W'] + self._corner_order: + hw = abs(event.xdata - center[0]) + if self._active_handle in ['N', 'S'] + self._corner_order: + hh = abs(event.ydata - center[1]) + + x0, x1, y0, y1 = (center[0] - hw, center[0] + hw, + center[1] - hh, center[1] + hh) + + else: + # change sign of relative changes to simplify calculation + # Switch variables so that x1 and/or y1 are updated on move + if 'W' in self._active_handle: + x0 = x1 + if 'S' in self._active_handle: + y0 = y1 + if self._active_handle in ['E', 'W'] + self._corner_order: + x1 = event.xdata + if self._active_handle in ['N', 'S'] + self._corner_order: + y1 = event.ydata + if 'square' in state: + # when using a corner, find which reference to use + if self._active_handle in self._corner_order: + refmax = max(refx, refy, key=abs) + if self._active_handle in ['E', 'W'] or refmax == refx: + sign = np.sign(event.ydata - y0) + y1 = y0 + sign * abs(x1 - x0) / \ + self._aspect_ratio_correction + else: + sign = np.sign(event.xdata - x0) + x1 = x0 + sign * abs(y1 - y0) * \ + self._aspect_ratio_correction + + elif move: + x0, x1, y0, y1 = self._extents_on_press + dx = event.xdata - eventpress.xdata + dy = event.ydata - eventpress.ydata + x0 += dx x1 += dx - x2 += dx + y0 += dy y1 += dy - y2 += dy - # new shape else: - center = [self.eventpress.xdata, self.eventpress.ydata] - center_pix = [self.eventpress.x, self.eventpress.y] + # Create a new shape + self._rotation = 0 + # Don't create a new rectangle if there is already one when + # ignore_event_outside=True + if ((self.ignore_event_outside and self._selection_completed) or + not self._allow_creation): + return + center = [eventpress.xdata, eventpress.ydata] dx = (event.xdata - center[0]) / 2. dy = (event.ydata - center[1]) / 2. # square shape - if 'square' in self.state: - dx_pix = abs(event.x - center_pix[0]) - dy_pix = abs(event.y - center_pix[1]) - if not dx_pix: - return - maxd = max(abs(dx_pix), abs(dy_pix)) - if abs(dx_pix) < maxd: - dx *= maxd / (abs(dx_pix) + 1e-6) - if abs(dy_pix) < maxd: - dy *= maxd / (abs(dy_pix) + 1e-6) + if 'square' in state: + refmax = max(refx, refy, key=abs) + if refmax == refx: + dy = np.sign(dy) * abs(dx) / self._aspect_ratio_correction + else: + dx = np.sign(dx) * abs(dy) * self._aspect_ratio_correction # from center - if 'center' in self.state: + if 'center' in state: dx *= 2 dy *= 2 @@ -2448,52 +3553,71 @@ def _onmove(self, event): center[0] += dx center[1] += dy - x1, x2, y1, y2 = (center[0] - dx, center[0] + dx, + x0, x1, y0, y1 = (center[0] - dx, center[0] + dx, center[1] - dy, center[1] + dy) - self.extents = x1, x2, y1, y2 + self.extents = x0, x1, y0, y1 @property def _rect_bbox(self): - if self.drawtype == 'box': - x0 = self.to_draw.get_x() - y0 = self.to_draw.get_y() - width = self.to_draw.get_width() - height = self.to_draw.get_height() - return x0, y0, width, height + return self._selection_artist.get_bbox().bounds + + def _set_aspect_ratio_correction(self): + aspect_ratio = self.ax._get_aspect_ratio() + self._selection_artist._aspect_ratio_correction = aspect_ratio + if self._use_data_coordinates: + self._aspect_ratio_correction = 1 else: - x, y = self.to_draw.get_data() - x0, x1 = min(x), max(x) - y0, y1 = min(y), max(y) - return x0, y0, x1 - x0, y1 - y0 + self._aspect_ratio_correction = aspect_ratio + + def _get_rotation_transform(self): + aspect_ratio = self.ax._get_aspect_ratio() + return Affine2D().translate(-self.center[0], -self.center[1]) \ + .scale(1, aspect_ratio) \ + .rotate(self._rotation) \ + .scale(1, 1 / aspect_ratio) \ + .translate(*self.center) @property def corners(self): - """Corners of rectangle from lower left, moving clockwise.""" + """ + Corners of rectangle in data coordinates from lower left, + moving clockwise. + """ x0, y0, width, height = self._rect_bbox xc = x0, x0 + width, x0 + width, x0 yc = y0, y0, y0 + height, y0 + height - return xc, yc + transform = self._get_rotation_transform() + coords = transform.transform(np.array([xc, yc]).T).T + return coords[0], coords[1] @property def edge_centers(self): - """Midpoint of rectangle edges from left, moving anti-clockwise.""" + """ + Midpoint of rectangle edges in data coordinates from left, + moving anti-clockwise. + """ x0, y0, width, height = self._rect_bbox w = width / 2. h = height / 2. xe = x0, x0 + w, x0 + width, x0 + w ye = y0 + h, y0, y0 + h, y0 + height - return xe, ye + transform = self._get_rotation_transform() + coords = transform.transform(np.array([xe, ye]).T).T + return coords[0], coords[1] @property def center(self): - """Center of rectangle.""" + """Center of rectangle in data coordinates.""" x0, y0, width, height = self._rect_bbox return x0 + width / 2., y0 + height / 2. @property def extents(self): - """Return (xmin, xmax, ymin, ymax).""" + """ + Return (xmin, xmax, ymin, ymax) in data coordinates as defined by the + bounding box before rotation. + """ x0, y0, width, height = self._rect_bbox xmin, xmax = sorted([x0, x0 + width]) ymin, ymax = sorted([y0, y0 + height]) @@ -2502,15 +3626,34 @@ def extents(self): @extents.setter def extents(self, extents): # Update displayed shape - self.draw_shape(extents) - # Update displayed handles - self._corner_handles.set_data(*self.corners) - self._edge_handles.set_data(*self.edge_centers) - self._center_handle.set_data(*self.center) - self.set_visible(self.visible) + self._draw_shape(extents) + if self._interactive: + # Update displayed handles + self._corner_handles.set_data(*self.corners) + self._edge_handles.set_data(*self.edge_centers) + x, y = self.center + self._center_handle.set_data([x], [y]) + self.set_visible(self._visible) self.update() - def draw_shape(self, extents): + @property + def rotation(self): + """ + Rotation in degree in interval [-45°, 45°]. The rotation is limited in + range to keep the implementation simple. + """ + return np.rad2deg(self._rotation) + + @rotation.setter + def rotation(self, value): + # Restrict to a limited range of rotation [-45°, 45°] to avoid changing + # order of handles + if -45 <= value and value <= 45: + self._rotation = np.deg2rad(value) + # call extents setter to draw shape and update handles positions + self.extents = self.extents + + def _draw_shape(self, extents): x0, x1, y0, y1 = extents xmin, xmax = sorted([x0, x1]) ymin, ymax = sorted([y0, y1]) @@ -2522,14 +3665,11 @@ def draw_shape(self, extents): xmax = min(xmax, xlim[1]) ymax = min(ymax, ylim[1]) - if self.drawtype == 'box': - self.to_draw.set_x(xmin) - self.to_draw.set_y(ymin) - self.to_draw.set_width(xmax - xmin) - self.to_draw.set_height(ymax - ymin) - - elif self.drawtype == 'line': - self.to_draw.set_data([xmin, xmax], [ymin, ymax]) + self._selection_artist.set_x(xmin) + self._selection_artist.set_y(ymin) + self._selection_artist.set_width(xmax - xmin) + self._selection_artist.set_height(ymax - ymin) + self._selection_artist.set_angle(self.rotation) def _set_active_handle(self, event): """Set active handle based on the location of the mouse event.""" @@ -2538,115 +3678,87 @@ def _set_active_handle(self, event): e_idx, e_dist = self._edge_handles.closest(event.x, event.y) m_idx, m_dist = self._center_handle.closest(event.x, event.y) - if 'move' in self.state: - self.active_handle = 'C' - self._extents_on_press = self.extents - + if 'move' in self._state: + self._active_handle = 'C' # Set active handle as closest handle, if mouse click is close enough. - elif m_dist < self.maxdist * 2: - self.active_handle = 'C' - elif c_dist > self.maxdist and e_dist > self.maxdist: - self.active_handle = None - return + elif m_dist < self.grab_range * 2: + # Prioritise center handle over other handles + self._active_handle = 'C' + elif c_dist > self.grab_range and e_dist > self.grab_range: + # Not close to any handles + if self.drag_from_anywhere and self._contains(event): + # Check if we've clicked inside the region + self._active_handle = 'C' + else: + self._active_handle = None + return elif c_dist < e_dist: - self.active_handle = self._corner_order[c_idx] + # Closest to a corner handle + self._active_handle = self._corner_order[c_idx] else: - self.active_handle = self._edge_order[e_idx] + # Closest to an edge handle + self._active_handle = self._edge_order[e_idx] - # Save coordinates of rectangle at the start of handle movement. - x1, x2, y1, y2 = self.extents - # Switch variables so that only x2 and/or y2 are updated on move. - if self.active_handle in ['W', 'SW', 'NW']: - x1, x2 = x2, event.xdata - if self.active_handle in ['N', 'NW', 'NE']: - y1, y2 = y2, event.ydata - self._extents_on_press = x1, x2, y1, y2 + def _contains(self, event): + """Return True if event is within the patch.""" + return self._selection_artist.contains(event, radius=0)[0] @property def geometry(self): """ Return an array of shape (2, 5) containing the x (``RectangleSelector.geometry[1, :]``) and - y (``RectangleSelector.geometry[0, :]``) coordinates - of the four corners of the rectangle starting and ending - in the top left corner. + y (``RectangleSelector.geometry[0, :]``) data coordinates of the four + corners of the rectangle starting and ending in the top left corner. """ - if hasattr(self.to_draw, 'get_verts'): + if hasattr(self._selection_artist, 'get_verts'): xfm = self.ax.transData.inverted() - y, x = xfm.transform(self.to_draw.get_verts()).T + y, x = xfm.transform(self._selection_artist.get_verts()).T return np.array([x, y]) else: - return np.array(self.to_draw.get_data()) + return np.array(self._selection_artist.get_data()) +@_docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( + '__ARTIST_NAME__', 'ellipse')) class EllipseSelector(RectangleSelector): """ - Select an elliptical region of an axes. + Select an elliptical region of an Axes. For the cursor to remain responsive you must keep a reference to it. - Example usage:: + Press and release events triggered at the same coordinates outside the + selection will clear the selector, except when + ``ignore_event_outside=True``. - import numpy as np - import matplotlib.pyplot as plt - from matplotlib.widgets import EllipseSelector - - def onselect(eclick, erelease): - "eclick and erelease are matplotlib events at press and release." - print('startposition: (%f, %f)' % (eclick.xdata, eclick.ydata)) - print('endposition : (%f, %f)' % (erelease.xdata, erelease.ydata)) - print('used button : ', eclick.button) - - def toggle_selector(event): - print(' Key pressed.') - if event.key in ['Q', 'q'] and toggle_selector.ES.active: - print('EllipseSelector deactivated.') - toggle_selector.RS.set_active(False) - if event.key in ['A', 'a'] and not toggle_selector.ES.active: - print('EllipseSelector activated.') - toggle_selector.ES.set_active(True) - - x = np.arange(100.) / 99 - y = np.sin(x) - fig, ax = plt.subplots() - ax.plot(x, y) + %s - toggle_selector.ES = EllipseSelector(ax, onselect, drawtype='line') - fig.canvas.mpl_connect('key_press_event', toggle_selector) - plt.show() + Examples + -------- + :doc:`/gallery/widgets/rectangle_selector` """ - _shape_klass = Ellipse + def _init_shape(self, **props): + return Ellipse((0, 0), 0, 1, visible=False, **props) - def draw_shape(self, extents): - x1, x2, y1, y2 = extents - xmin, xmax = sorted([x1, x2]) - ymin, ymax = sorted([y1, y2]) - center = [x1 + (x2 - x1) / 2., y1 + (y2 - y1) / 2.] + def _draw_shape(self, extents): + x0, x1, y0, y1 = extents + xmin, xmax = sorted([x0, x1]) + ymin, ymax = sorted([y0, y1]) + center = [x0 + (x1 - x0) / 2., y0 + (y1 - y0) / 2.] a = (xmax - xmin) / 2. b = (ymax - ymin) / 2. - if self.drawtype == 'box': - self.to_draw.center = center - self.to_draw.width = 2 * a - self.to_draw.height = 2 * b - else: - rad = np.deg2rad(np.arange(31) * 12) - x = a * np.cos(rad) + center[0] - y = b * np.sin(rad) + center[1] - self.to_draw.set_data(x, y) + self._selection_artist.center = center + self._selection_artist.width = 2 * a + self._selection_artist.height = 2 * b + self._selection_artist.angle = self.rotation @property def _rect_bbox(self): - if self.drawtype == 'box': - x, y = self.to_draw.center - width = self.to_draw.width - height = self.to_draw.height - return x - width / 2., y - height / 2., width, height - else: - x, y = self.to_draw.get_data() - x0, x1 = min(x), max(x) - y0, y1 = min(y), max(y) - return x0, y0, x1 - x0, y1 - y0 + x, y = self._selection_artist.center + width = self._selection_artist.width + height = self._selection_artist.height + return x - width / 2., y - height / 2., width, height class LassoSelector(_SelectorWidget): @@ -2660,7 +3772,7 @@ class LassoSelector(_SelectorWidget): In contrast to `Lasso`, `LassoSelector` is written with an interface similar to `RectangleSelector` and `SpanSelector`, and will continue to - interact with the axes until disconnected. + interact with the Axes until disconnected. Example usage:: @@ -2674,94 +3786,139 @@ def onselect(verts): Parameters ---------- ax : `~matplotlib.axes.Axes` - The parent axes for the widget. + The parent Axes for the widget. onselect : function Whenever the lasso is released, the *onselect* function is called and passed the vertices of the selected path. + useblit : bool, default: True + Whether to use blitting for faster drawing (if supported by the + backend). See the tutorial :doc:`/tutorials/advanced/blitting` + for details. + props : dict, optional + Properties with which the line is drawn, see `matplotlib.lines.Line2D` + for valid properties. Default values are defined in ``mpl.rcParams``. button : `.MouseButton` or list of `.MouseButton`, optional The mouse buttons used for rectangle selection. Default is ``None``, which corresponds to all buttons. """ - def __init__(self, ax, onselect=None, useblit=True, lineprops=None, - button=None): + @_api.make_keyword_only("3.7", name="useblit") + def __init__(self, ax, onselect, useblit=True, props=None, button=None): super().__init__(ax, onselect, useblit=useblit, button=button) self.verts = None - if lineprops is None: - lineprops = dict() - # self.useblit may be != useblit, if the canvas doesn't support blit. - lineprops.update(animated=self.useblit, visible=False) - self.line = Line2D([], [], **lineprops) - self.ax.add_line(self.line) - self.artists = [self.line] - - def onpress(self, event): - self.press(event) + props = { + **(props if props is not None else {}), + # Note that self.useblit may be != useblit, if the canvas doesn't + # support blitting. + 'animated': self.useblit, 'visible': False, + } + line = Line2D([], [], **props) + self.ax.add_line(line) + self._selection_artist = line def _press(self, event): self.verts = [self._get_data(event)] - self.line.set_visible(True) - - def onrelease(self, event): - self.release(event) + self._selection_artist.set_visible(True) def _release(self, event): if self.verts is not None: self.verts.append(self._get_data(event)) self.onselect(self.verts) - self.line.set_data([[], []]) - self.line.set_visible(False) + self._selection_artist.set_data([[], []]) + self._selection_artist.set_visible(False) self.verts = None def _onmove(self, event): if self.verts is None: return self.verts.append(self._get_data(event)) - - self.line.set_data(list(zip(*self.verts))) + self._selection_artist.set_data(list(zip(*self.verts))) self.update() class PolygonSelector(_SelectorWidget): """ - Select a polygon region of an axes. + Select a polygon region of an Axes. Place vertices with each mouse click, and make the selection by completing - the polygon (clicking on the first vertex). Hold the *ctrl* key and click - and drag a vertex to reposition it (the *ctrl* key is not necessary if the - polygon has already been completed). Hold the *shift* key and click and - drag anywhere in the axes to move all vertices. Press the *esc* key to - start a new polygon. + the polygon (clicking on the first vertex). Once drawn individual vertices + can be moved by clicking and dragging with the left mouse button, or + removed by clicking the right mouse button. + + In addition, the following modifier keys can be used: + + - Hold *ctrl* and click and drag a vertex to reposition it before the + polygon has been completed. + - Hold the *shift* key and click and drag anywhere in the Axes to move + all vertices. + - Press the *esc* key to start a new polygon. For the selector to remain responsive you must keep a reference to it. Parameters ---------- ax : `~matplotlib.axes.Axes` - The parent axes for the widget. + 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 ``(xdata, ydata)`` tuples. + 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)``. + Whether to use blitting for faster drawing (if supported by the + backend). See the tutorial :doc:`/tutorials/advanced/blitting` + for details. + + props : dict, optional + Properties with which the line is drawn, see `matplotlib.lines.Line2D` + for valid properties. + Default: + + ``dict(color='k', linestyle='-', linewidth=2, alpha=0.5)`` + + handle_props : dict, optional Artist properties for the markers drawn at the vertices of the polygon. - vertex_select_radius : float, default: 15px + See the marker arguments in `matplotlib.lines.Line2D` for valid + properties. Default values are defined in ``mpl.rcParams`` except for + the default value of ``markeredgecolor`` which will be the same as the + ``color`` property in *props*. + + grab_range : float, default: 10 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 mouse click is within *grab_range* pixels of the vertex. + + draw_bounding_box : bool, optional + If `True`, a bounding box will be drawn around the polygon selector + once it is complete. This box can be used to move and resize the + selector. + + box_handle_props : dict, optional + Properties to set for the box handles. See the documentation for the + *handle_props* argument to `RectangleSelector` for more info. + + box_props : dict, optional + Properties to set for the box. See the documentation for the *props* + argument to `RectangleSelector` for more info. Examples -------- + :doc:`/gallery/widgets/polygon_selector_simple` :doc:`/gallery/widgets/polygon_selector_demo` + + Notes + ----- + If only one point remains after removing points, the selector reverts to an + incomplete state and you can start drawing a new polygon from the existing + point. """ + @_api.make_keyword_only("3.7", name="useblit") def __init__(self, ax, onselect, useblit=False, - lineprops=None, markerprops=None, vertex_select_radius=15): + props=None, handle_props=None, grab_range=10, *, + draw_bounding_box=False, box_handle_props=None, + box_props=None): # The state modifiers 'move', 'square', and 'center' are expected by # _SelectorWidget but are not supported by PolygonSelector # Note: could not use the existing 'move' state modifier in-place of @@ -2770,64 +3927,158 @@ def __init__(self, ax, onselect, useblit=False, state_modifier_keys = dict(clear='escape', move_vertex='control', move_all='shift', move='not-applicable', square='not-applicable', - center='not-applicable') + center='not-applicable', + rotate='not-applicable') super().__init__(ax, onselect, useblit=useblit, state_modifier_keys=state_modifier_keys) - self._xs, self._ys = [0], [0] - self._polygon_completed = False + self._xys = [(0, 0)] - if lineprops is None: - lineprops = dict(color='k', linestyle='-', linewidth=2, alpha=0.5) - lineprops['animated'] = self.useblit - self.line = Line2D(self._xs, self._ys, **lineprops) - self.ax.add_line(self.line) + if props is None: + props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5) + self._props = {**props, 'animated': self.useblit} + self._selection_artist = line = Line2D([], [], **self._props) + self.ax.add_line(line) - if markerprops is None: - markerprops = dict(markeredgecolor='k', - markerfacecolor=lineprops.get('color', 'k')) - self._polygon_handles = ToolHandles(self.ax, self._xs, self._ys, + if handle_props is None: + handle_props = dict(markeredgecolor='k', + markerfacecolor=self._props.get('color', 'k')) + self._handle_props = handle_props + self._polygon_handles = ToolHandles(self.ax, [], [], useblit=self.useblit, - marker_props=markerprops) + marker_props=self._handle_props) self._active_handle_idx = -1 - self.vertex_select_radius = vertex_select_radius + self.grab_range = grab_range - self.artists = [self.line, self._polygon_handles.artist] self.set_visible(True) + self._draw_box = draw_bounding_box + self._box = None + + if box_handle_props is None: + box_handle_props = {} + self._box_handle_props = self._handle_props.update(box_handle_props) + self._box_props = box_props + + def _get_bbox(self): + return self._selection_artist.get_bbox() + + def _add_box(self): + self._box = RectangleSelector(self.ax, + onselect=lambda *args, **kwargs: None, + useblit=self.useblit, + grab_range=self.grab_range, + handle_props=self._box_handle_props, + props=self._box_props, + interactive=True) + self._box._state_modifier_keys.pop('rotate') + self._box.connect_event('motion_notify_event', self._scale_polygon) + self._update_box() + # Set state that prevents the RectangleSelector from being created + # by the user + self._box._allow_creation = False + self._box._selection_completed = True + self._draw_polygon() + + def _remove_box(self): + if self._box is not None: + self._box.set_visible(False) + self._box = None + + def _update_box(self): + # Update selection box extents to the extents of the polygon + if self._box is not None: + bbox = self._get_bbox() + self._box.extents = [bbox.x0, bbox.x1, bbox.y0, bbox.y1] + # Save a copy + self._old_box_extents = self._box.extents + + def _scale_polygon(self, event): + """ + Scale the polygon selector points when the bounding box is moved or + scaled. + + This is set as a callback on the bounding box RectangleSelector. + """ + if not self._selection_completed: + return + + if self._old_box_extents == self._box.extents: + return + + # Create transform from old box to new box + x1, y1, w1, h1 = self._box._rect_bbox + old_bbox = self._get_bbox() + t = (transforms.Affine2D() + .translate(-old_bbox.x0, -old_bbox.y0) + .scale(1 / old_bbox.width, 1 / old_bbox.height) + .scale(w1, h1) + .translate(x1, y1)) + + # Update polygon verts. Must be a list of tuples for consistency. + new_verts = [(x, y) for x, y in t.transform(np.array(self.verts))] + self._xys = [*new_verts, new_verts[0]] + self._draw_polygon() + self._old_box_extents = self._box.extents + + @property + def _handles_artists(self): + return self._polygon_handles.artists + + def _remove_vertex(self, i): + """Remove vertex with index i.""" + if (len(self._xys) > 2 and + self._selection_completed and + i in (0, len(self._xys) - 1)): + # If selecting the first or final vertex, remove both first and + # last vertex as they are the same for a closed polygon + self._xys.pop(0) + self._xys.pop(-1) + # Close the polygon again by appending the new first vertex to the + # end + self._xys.append(self._xys[0]) + else: + self._xys.pop(i) + if len(self._xys) <= 2: + # If only one point left, return to incomplete state to let user + # start drawing again + self._selection_completed = False + self._remove_box() def _press(self, event): """Button press event handler.""" # Check for selection of a tool handle. - if ((self._polygon_completed or 'move_vertex' in self.state) - and len(self._xs) > 0): + if ((self._selection_completed or 'move_vertex' in self._state) + and len(self._xys) > 0): h_idx, h_dist = self._polygon_handles.closest(event.x, event.y) - if h_dist < self.vertex_select_radius: + if h_dist < self.grab_range: self._active_handle_idx = h_idx # Save the vertex positions at the time of the press event (needed to # support the 'move_all' state modifier). - self._xs_at_press, self._ys_at_press = self._xs.copy(), self._ys.copy() + self._xys_at_press = self._xys.copy() def _release(self, event): """Button release event handler.""" # Release active tool handle. if self._active_handle_idx >= 0: + if event.button == 3: + self._remove_vertex(self._active_handle_idx) + self._draw_polygon() self._active_handle_idx = -1 # Complete the polygon. - elif (len(self._xs) > 3 - and self._xs[-1] == self._xs[0] - and self._ys[-1] == self._ys[0]): - self._polygon_completed = True + elif len(self._xys) > 3 and self._xys[-1] == self._xys[0]: + self._selection_completed = True + if self._draw_box and self._box is None: + self._add_box() # Place new vertex. - elif (not self._polygon_completed - and 'move_all' not in self.state - and 'move_vertex' not in self.state): - self._xs.insert(-1, event.xdata) - self._ys.insert(-1, event.ydata) + elif (not self._selection_completed + and 'move_all' not in self._state + and 'move_vertex' not in self._state): + self._xys.insert(-1, (event.xdata, event.ydata)) - if self._polygon_completed: + if self._selection_completed: self.onselect(self.verts) def onmove(self, event): @@ -2835,7 +4086,7 @@ def onmove(self, event): # Method overrides _SelectorWidget.onmove because the polygon selector # needs to process the move callback even if there is no button press. # _SelectorWidget.onmove include logic to ignore move event if - # eventpress is None. + # _eventpress is None. if not self.ignore(event): event = self._clean_event(event) self._onmove(event) @@ -2847,36 +4098,36 @@ def _onmove(self, event): # Move the active vertex (ToolHandle). if self._active_handle_idx >= 0: idx = self._active_handle_idx - self._xs[idx], self._ys[idx] = event.xdata, event.ydata + self._xys[idx] = event.xdata, event.ydata # Also update the end of the polygon line if the first vertex is # the active handle and the polygon is completed. - if idx == 0 and self._polygon_completed: - self._xs[-1], self._ys[-1] = event.xdata, event.ydata + if idx == 0 and self._selection_completed: + self._xys[-1] = event.xdata, event.ydata # Move all vertices. - elif 'move_all' in self.state and self.eventpress: - dx = event.xdata - self.eventpress.xdata - dy = event.ydata - self.eventpress.ydata - for k in range(len(self._xs)): - self._xs[k] = self._xs_at_press[k] + dx - self._ys[k] = self._ys_at_press[k] + dy + elif 'move_all' in self._state and self._eventpress: + dx = event.xdata - self._eventpress.xdata + dy = event.ydata - self._eventpress.ydata + for k in range(len(self._xys)): + x_at_press, y_at_press = self._xys_at_press[k] + self._xys[k] = x_at_press + dx, y_at_press + dy # Do nothing if completed or waiting for a move. - elif (self._polygon_completed - or 'move_vertex' in self.state or 'move_all' in self.state): + elif (self._selection_completed + or 'move_vertex' in self._state or 'move_all' in self._state): return # Position pending vertex. else: # Calculate distance to the start vertex. - x0, y0 = self.line.get_transform().transform((self._xs[0], - self._ys[0])) + x0, y0 = \ + self._selection_artist.get_transform().transform(self._xys[0]) v0_dist = np.hypot(x0 - event.x, y0 - event.y) # Lock on to the start vertex if near it and ready to complete. - if len(self._xs) > 3 and v0_dist < self.vertex_select_radius: - self._xs[-1], self._ys[-1] = self._xs[0], self._ys[0] + if len(self._xys) > 3 and v0_dist < self.grab_range: + self._xys[-1] = self._xys[0] else: - self._xs[-1], self._ys[-1] = event.xdata, event.ydata + self._xys[-1] = event.xdata, event.ydata self._draw_polygon() @@ -2884,48 +4135,65 @@ def _on_key_press(self, event): """Key press event handler.""" # Remove the pending vertex if entering the 'move_vertex' or # 'move_all' mode - if (not self._polygon_completed - and ('move_vertex' in self.state or 'move_all' in self.state)): - self._xs, self._ys = self._xs[:-1], self._ys[:-1] + if (not self._selection_completed + and ('move_vertex' in self._state or + 'move_all' in self._state)): + self._xys.pop() self._draw_polygon() def _on_key_release(self, event): """Key release event handler.""" # Add back the pending vertex if leaving the 'move_vertex' or # 'move_all' mode (by checking the released key) - if (not self._polygon_completed + if (not self._selection_completed and - (event.key == self.state_modifier_keys.get('move_vertex') - or event.key == self.state_modifier_keys.get('move_all'))): - self._xs.append(event.xdata) - self._ys.append(event.ydata) + (event.key == self._state_modifier_keys.get('move_vertex') + or event.key == self._state_modifier_keys.get('move_all'))): + self._xys.append((event.xdata, event.ydata)) self._draw_polygon() # Reset the polygon if the released key is the 'clear' key. - elif event.key == self.state_modifier_keys.get('clear'): + elif event.key == self._state_modifier_keys.get('clear'): event = self._clean_event(event) - self._xs, self._ys = [event.xdata], [event.ydata] - self._polygon_completed = False + self._xys = [(event.xdata, event.ydata)] + self._selection_completed = False + self._remove_box() self.set_visible(True) def _draw_polygon(self): """Redraw the polygon based on the new vertex positions.""" - self.line.set_data(self._xs, self._ys) + xs, ys = zip(*self._xys) if self._xys else ([], []) + self._selection_artist.set_data(xs, ys) + self._update_box() # Only show one tool handle at the start and end vertex of the polygon # if the polygon is completed or the user is locked on to the start # vertex. - if (self._polygon_completed - or (len(self._xs) > 3 - and self._xs[-1] == self._xs[0] - and self._ys[-1] == self._ys[0])): - self._polygon_handles.set_data(self._xs[:-1], self._ys[:-1]) + if (self._selection_completed + or (len(self._xys) > 3 + and self._xys[-1] == self._xys[0])): + self._polygon_handles.set_data(xs[:-1], ys[:-1]) else: - self._polygon_handles.set_data(self._xs, self._ys) + self._polygon_handles.set_data(xs, ys) self.update() @property def verts(self): """The polygon vertices, as a list of ``(x, y)`` pairs.""" - return list(zip(self._xs[:-1], self._ys[:-1])) + return self._xys[:-1] + + @verts.setter + def verts(self, xys): + """ + Set the polygon vertices. + + This will remove any preexisting vertices, creating a complete polygon + with the new vertices. + """ + self._xys = [*xys, xys[0]] + self._selection_completed = True + self.set_visible(True) + if self._draw_box and self._box is None: + self._add_box() + self._draw_polygon() class Lasso(AxesWidget): @@ -2941,15 +4209,20 @@ class Lasso(AxesWidget): Parameters ---------- ax : `~matplotlib.axes.Axes` - The parent axes for the widget. + The parent Axes for the widget. xy : (float, float) Coordinates of the start of the lasso. callback : callable Whenever the lasso is released, the *callback* function is called and passed the vertices of the selected path. + useblit : bool, default: True + Whether to use blitting for faster drawing (if supported by the + backend). See the tutorial :doc:`/tutorials/advanced/blitting` + for details. """ - def __init__(self, ax, xy, callback=None, useblit=True): + @_api.make_keyword_only("3.7", name="useblit") + def __init__(self, ax, xy, callback, useblit=True): super().__init__(ax) self.useblit = useblit and self.canvas.supports_blit @@ -2971,7 +4244,7 @@ def onrelease(self, event): self.verts.append((event.xdata, event.ydata)) if len(self.verts) > 2: self.callback(self.verts) - self.ax.lines.remove(self.line) + self.line.remove() self.verts = None self.disconnect_events() diff --git a/lib/mpl_toolkits/axes_grid/__init__.py b/lib/mpl_toolkits/axes_grid/__init__.py deleted file mode 100644 index 9e76d10ecbed..000000000000 --- a/lib/mpl_toolkits/axes_grid/__init__.py +++ /dev/null @@ -1,11 +0,0 @@ -from . import axes_size as Size -from .axes_divider import Divider, SubplotDivider, make_axes_locatable -from .axes_grid import Grid, ImageGrid, AxesGrid -#from axes_divider import make_axes_locatable -from matplotlib import _api -_api.warn_deprecated(since='2.1', - name='mpl_toolkits.axes_grid', - alternative='mpl_toolkits.axes_grid1 and' - ' mpl_toolkits.axisartist, which provide' - ' the same functionality', - obj_type='module') diff --git a/lib/mpl_toolkits/axes_grid/anchored_artists.py b/lib/mpl_toolkits/axes_grid/anchored_artists.py deleted file mode 100644 index f486805d98c6..000000000000 --- a/lib/mpl_toolkits/axes_grid/anchored_artists.py +++ /dev/null @@ -1,6 +0,0 @@ -from matplotlib.offsetbox import AnchoredOffsetbox, AuxTransformBox, VPacker,\ - TextArea, AnchoredText, DrawingArea, AnnotationBbox - -from mpl_toolkits.axes_grid1.anchored_artists import \ - AnchoredDrawingArea, AnchoredAuxTransformBox, \ - AnchoredEllipse, AnchoredSizeBar diff --git a/lib/mpl_toolkits/axes_grid/angle_helper.py b/lib/mpl_toolkits/axes_grid/angle_helper.py deleted file mode 100644 index fa14e7f5897b..000000000000 --- a/lib/mpl_toolkits/axes_grid/angle_helper.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axisartist.angle_helper import * diff --git a/lib/mpl_toolkits/axes_grid/axes_divider.py b/lib/mpl_toolkits/axes_grid/axes_divider.py deleted file mode 100644 index 33b4e512c9a0..000000000000 --- a/lib/mpl_toolkits/axes_grid/axes_divider.py +++ /dev/null @@ -1,3 +0,0 @@ -from mpl_toolkits.axes_grid1.axes_divider import ( - AxesDivider, AxesLocator, Divider, SubplotDivider, make_axes_locatable) -from mpl_toolkits.axisartist.axislines import Axes diff --git a/lib/mpl_toolkits/axes_grid/axes_grid.py b/lib/mpl_toolkits/axes_grid/axes_grid.py deleted file mode 100644 index 893b7800ccde..000000000000 --- a/lib/mpl_toolkits/axes_grid/axes_grid.py +++ /dev/null @@ -1,2 +0,0 @@ -from mpl_toolkits.axisartist.axes_grid import ( - AxesGrid, CbarAxes, Grid, ImageGrid) diff --git a/lib/mpl_toolkits/axes_grid/axes_rgb.py b/lib/mpl_toolkits/axes_grid/axes_rgb.py deleted file mode 100644 index 30f7e42743f9..000000000000 --- a/lib/mpl_toolkits/axes_grid/axes_rgb.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axisartist.axes_rgb import * diff --git a/lib/mpl_toolkits/axes_grid/axes_size.py b/lib/mpl_toolkits/axes_grid/axes_size.py deleted file mode 100644 index 742f7fe6347c..000000000000 --- a/lib/mpl_toolkits/axes_grid/axes_size.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axes_grid1.axes_size import * diff --git a/lib/mpl_toolkits/axes_grid/axis_artist.py b/lib/mpl_toolkits/axes_grid/axis_artist.py deleted file mode 100644 index 11180b10bfef..000000000000 --- a/lib/mpl_toolkits/axes_grid/axis_artist.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axisartist.axis_artist import * diff --git a/lib/mpl_toolkits/axes_grid/axisline_style.py b/lib/mpl_toolkits/axes_grid/axisline_style.py deleted file mode 100644 index 0c846e22afa0..000000000000 --- a/lib/mpl_toolkits/axes_grid/axisline_style.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axisartist.axisline_style import * diff --git a/lib/mpl_toolkits/axes_grid/axislines.py b/lib/mpl_toolkits/axes_grid/axislines.py deleted file mode 100644 index a8ceb9cc28ad..000000000000 --- a/lib/mpl_toolkits/axes_grid/axislines.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axisartist.axislines import * diff --git a/lib/mpl_toolkits/axes_grid/clip_path.py b/lib/mpl_toolkits/axes_grid/clip_path.py deleted file mode 100644 index 5b92d9ae57f6..000000000000 --- a/lib/mpl_toolkits/axes_grid/clip_path.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axisartist.clip_path import * diff --git a/lib/mpl_toolkits/axes_grid/floating_axes.py b/lib/mpl_toolkits/axes_grid/floating_axes.py deleted file mode 100644 index de8ebb7367be..000000000000 --- a/lib/mpl_toolkits/axes_grid/floating_axes.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axisartist.floating_axes import * diff --git a/lib/mpl_toolkits/axes_grid/grid_finder.py b/lib/mpl_toolkits/axes_grid/grid_finder.py deleted file mode 100644 index 6cdec87a7f40..000000000000 --- a/lib/mpl_toolkits/axes_grid/grid_finder.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axisartist.grid_finder import * diff --git a/lib/mpl_toolkits/axes_grid/grid_helper_curvelinear.py b/lib/mpl_toolkits/axes_grid/grid_helper_curvelinear.py deleted file mode 100644 index ebb3edf139f5..000000000000 --- a/lib/mpl_toolkits/axes_grid/grid_helper_curvelinear.py +++ /dev/null @@ -1 +0,0 @@ -from mpl_toolkits.axisartist.grid_helper_curvelinear import * diff --git a/lib/mpl_toolkits/axes_grid/inset_locator.py b/lib/mpl_toolkits/axes_grid/inset_locator.py deleted file mode 100644 index 9d656e6edaf7..000000000000 --- a/lib/mpl_toolkits/axes_grid/inset_locator.py +++ /dev/null @@ -1,4 +0,0 @@ -from mpl_toolkits.axes_grid1.inset_locator import InsetPosition, \ - AnchoredSizeLocator, \ - AnchoredZoomLocator, BboxPatch, BboxConnector, BboxConnectorPatch, \ - inset_axes, zoomed_inset_axes, mark_inset diff --git a/lib/mpl_toolkits/axes_grid/parasite_axes.py b/lib/mpl_toolkits/axes_grid/parasite_axes.py deleted file mode 100644 index d988d704c344..000000000000 --- a/lib/mpl_toolkits/axes_grid/parasite_axes.py +++ /dev/null @@ -1,12 +0,0 @@ -from matplotlib import _api -from mpl_toolkits.axes_grid1.parasite_axes import ( - host_axes_class_factory, parasite_axes_class_factory, - parasite_axes_auxtrans_class_factory, subplot_class_factory) -from mpl_toolkits.axisartist.axislines import Axes - - -ParasiteAxes = parasite_axes_class_factory(Axes) -HostAxes = host_axes_class_factory(Axes) -SubplotHost = subplot_class_factory(HostAxes) -with _api.suppress_matplotlib_deprecation_warning(): - ParasiteAxesAuxTrans = parasite_axes_auxtrans_class_factory(ParasiteAxes) diff --git a/lib/mpl_toolkits/axes_grid1/__init__.py b/lib/mpl_toolkits/axes_grid1/__init__.py index 0f359c9e01f3..c55302485e3f 100644 --- a/lib/mpl_toolkits/axes_grid1/__init__.py +++ b/lib/mpl_toolkits/axes_grid1/__init__.py @@ -1,5 +1,10 @@ from . import axes_size as Size from .axes_divider import Divider, SubplotDivider, make_axes_locatable -from .axes_grid import Grid, ImageGrid, AxesGrid +from .axes_grid import AxesGrid, Grid, ImageGrid from .parasite_axes import host_subplot, host_axes + +__all__ = ["Size", + "Divider", "SubplotDivider", "make_axes_locatable", + "AxesGrid", "Grid", "ImageGrid", + "host_subplot", "host_axes"] diff --git a/lib/mpl_toolkits/axes_grid1/anchored_artists.py b/lib/mpl_toolkits/axes_grid1/anchored_artists.py index b3e88c909a8f..7638a75d924a 100644 --- a/lib/mpl_toolkits/axes_grid1/anchored_artists.py +++ b/lib/mpl_toolkits/axes_grid1/anchored_artists.py @@ -14,7 +14,7 @@ def __init__(self, width, height, xdescent, ydescent, loc, pad=0.4, borderpad=0.5, prop=None, frameon=True, **kwargs): """ - An anchored container with a fixed size and fillable DrawingArea. + An anchored container with a fixed size and fillable `.DrawingArea`. Artists added to the *drawing_area* will have their coordinates interpreted as pixels. Any transformations set on the artists will be @@ -23,50 +23,36 @@ def __init__(self, width, height, xdescent, ydescent, Parameters ---------- width, height : float - width and height of the container, in pixels. - + Width and height of the container, in pixels. xdescent, ydescent : float - descent of the container in the x- and y- direction, in pixels. - - loc : int - Location of this artist. Valid location codes are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 - + Descent of the container in the x- and y- direction, in pixels. + loc : str + Location of this artist. Valid locations are + 'upper left', 'upper center', 'upper right', + 'center left', 'center', 'center right', + 'lower left', 'lower center', 'lower right'. + For backward compatibility, numeric values are accepted as well. + See the parameter *loc* of `.Legend` for details. pad : float, default: 0.4 Padding around the child objects, in fraction of the font size. - borderpad : float, default: 0.5 Border padding, in fraction of the font size. - - prop : `matplotlib.font_manager.FontProperties`, optional + prop : `~matplotlib.font_manager.FontProperties`, optional Font property used as a reference for paddings. - frameon : bool, default: True - If True, draw a box around this artists. - + If True, draw a box around this artist. **kwargs - Keyworded arguments to pass to - :class:`matplotlib.offsetbox.AnchoredOffsetbox`. + Keyword arguments forwarded to `.AnchoredOffsetbox`. Attributes ---------- - drawing_area : `matplotlib.offsetbox.DrawingArea` + drawing_area : `~matplotlib.offsetbox.DrawingArea` A container for artists to display. Examples -------- To display blue and red circles of different sizes in the upper right - of an axes *ax*: + of an Axes *ax*: >>> ada = AnchoredDrawingArea(20, 20, 0, 0, ... loc='upper right', frameon=False) @@ -95,43 +81,30 @@ def __init__(self, transform, loc, Parameters ---------- - transform : `matplotlib.transforms.Transform` + transform : `~matplotlib.transforms.Transform` The transformation object for the coordinate system in use, i.e., :attr:`matplotlib.axes.Axes.transData`. - - loc : int - Location of this artist. Valid location codes are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 - + loc : str + Location of this artist. Valid locations are + 'upper left', 'upper center', 'upper right', + 'center left', 'center', 'center right', + 'lower left', 'lower center', 'lower right'. + For backward compatibility, numeric values are accepted as well. + See the parameter *loc* of `.Legend` for details. pad : float, default: 0.4 Padding around the child objects, in fraction of the font size. - borderpad : float, default: 0.5 Border padding, in fraction of the font size. - - prop : `matplotlib.font_manager.FontProperties`, optional + prop : `~matplotlib.font_manager.FontProperties`, optional Font property used as a reference for paddings. - frameon : bool, default: True - If True, draw a box around this artists. - + If True, draw a box around this artist. **kwargs - Keyworded arguments to pass to - :class:`matplotlib.offsetbox.AnchoredOffsetbox`. + Keyword arguments forwarded to `.AnchoredOffsetbox`. Attributes ---------- - drawing_area : `matplotlib.offsetbox.AuxTransformBox` + drawing_area : `~matplotlib.offsetbox.AuxTransformBox` A container for artists to display. Examples @@ -159,55 +132,39 @@ def __init__(self, transform, width, height, angle, loc, Parameters ---------- - transform : `matplotlib.transforms.Transform` + transform : `~matplotlib.transforms.Transform` The transformation object for the coordinate system in use, i.e., :attr:`matplotlib.axes.Axes.transData`. - width, height : float Width and height of the ellipse, given in coordinates of *transform*. - angle : float Rotation of the ellipse, in degrees, anti-clockwise. - - loc : int - Location of this size bar. Valid location codes are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 - - pad : float, optional - Padding around the ellipse, in fraction of the font size. Defaults - to 0.1. - + loc : str + Location of the ellipse. Valid locations are + 'upper left', 'upper center', 'upper right', + 'center left', 'center', 'center right', + 'lower left', 'lower center', 'lower right'. + For backward compatibility, numeric values are accepted as well. + See the parameter *loc* of `.Legend` for details. + pad : float, default: 0.1 + Padding around the ellipse, in fraction of the font size. borderpad : float, default: 0.1 Border padding, in fraction of the font size. - frameon : bool, default: True If True, draw a box around the ellipse. - - prop : `matplotlib.font_manager.FontProperties`, optional + prop : `~matplotlib.font_manager.FontProperties`, optional Font property used as a reference for paddings. - **kwargs - Keyworded arguments to pass to - :class:`matplotlib.offsetbox.AnchoredOffsetbox`. + Keyword arguments forwarded to `.AnchoredOffsetbox`. Attributes ---------- - ellipse : `matplotlib.patches.Ellipse` + ellipse : `~matplotlib.patches.Ellipse` Ellipse patch drawn. """ self._box = AuxTransformBox(transform) - self.ellipse = Ellipse((0, 0), width, height, angle) + self.ellipse = Ellipse((0, 0), width, height, angle=angle) self._box.add_artist(self.ellipse) super().__init__(loc, pad=pad, borderpad=borderpad, child=self._box, @@ -225,79 +182,58 @@ def __init__(self, transform, size, label, loc, Parameters ---------- - transform : `matplotlib.transforms.Transform` + transform : `~matplotlib.transforms.Transform` The transformation object for the coordinate system in use, i.e., :attr:`matplotlib.axes.Axes.transData`. - size : float Horizontal length of the size bar, given in coordinates of *transform*. - label : str Label to display. - - loc : int - Location of this size bar. Valid location codes are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 - + loc : str + Location of the size bar. Valid locations are + 'upper left', 'upper center', 'upper right', + 'center left', 'center', 'center right', + 'lower left', 'lower center', 'lower right'. + For backward compatibility, numeric values are accepted as well. + See the parameter *loc* of `.Legend` for details. pad : float, default: 0.1 Padding around the label and size bar, in fraction of the font size. - borderpad : float, default: 0.1 Border padding, in fraction of the font size. - sep : float, default: 2 Separation between the label and the size bar, in points. - frameon : bool, default: True If True, draw a box around the horizontal bar and label. - size_vertical : float, default: 0 Vertical length of the size bar, given in coordinates of *transform*. - color : str, default: 'black' Color for the size bar and label. - label_top : bool, default: False If True, the label will be over the size bar. - - fontproperties : `matplotlib.font_manager.FontProperties`, optional + fontproperties : `~matplotlib.font_manager.FontProperties`, optional Font properties for the label text. - fill_bar : bool, optional - If True and if size_vertical is nonzero, the size bar will + If True and if *size_vertical* is nonzero, the size bar will be filled in with the color specified by the size bar. Defaults to True if *size_vertical* is greater than zero and False otherwise. - **kwargs - Keyworded arguments to pass to - :class:`matplotlib.offsetbox.AnchoredOffsetbox`. + Keyword arguments forwarded to `.AnchoredOffsetbox`. Attributes ---------- - size_bar : `matplotlib.offsetbox.AuxTransformBox` + size_bar : `~matplotlib.offsetbox.AuxTransformBox` Container for the size bar. - - txt_label : `matplotlib.offsetbox.TextArea` + txt_label : `~matplotlib.offsetbox.TextArea` Container for the label of the size bar. Notes ----- - If *prop* is passed as a keyworded argument, but *fontproperties* is - not, then *prop* is be assumed to be the intended *fontproperties*. + If *prop* is passed as a keyword argument, but *fontproperties* is + not, then *prop* is assumed to be the intended *fontproperties*. Using both *prop* and *fontproperties* is not supported. Examples @@ -354,8 +290,8 @@ def __init__(self, transform, size, label, loc, class AnchoredDirectionArrows(AnchoredOffsetbox): def __init__(self, transform, label_x, label_y, length=0.15, - fontsize=0.08, loc=2, angle=0, aspect_ratio=1, pad=0.4, - borderpad=0.4, frameon=False, color='w', alpha=1, + fontsize=0.08, loc='upper left', angle=0, aspect_ratio=1, + pad=0.4, borderpad=0.4, frameon=False, color='w', alpha=1, sep_x=0.01, sep_y=0, fontproperties=None, back_length=0.15, head_width=10, head_length=15, tail_width=2, text_props=None, arrow_props=None, @@ -365,100 +301,71 @@ def __init__(self, transform, label_x, label_y, length=0.15, Parameters ---------- - transform : `matplotlib.transforms.Transform` + transform : `~matplotlib.transforms.Transform` The transformation object for the coordinate system in use, i.e., :attr:`matplotlib.axes.Axes.transAxes`. - label_x, label_y : str Label text for the x and y arrows - length : float, default: 0.15 Length of the arrow, given in coordinates of *transform*. - fontsize : float, default: 0.08 Size of label strings, given in coordinates of *transform*. - - loc : int, default: 2 - Location of the direction arrows. Valid location codes are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 - + loc : str, default: 'upper left' + Location of the arrow. Valid locations are + 'upper left', 'upper center', 'upper right', + 'center left', 'center', 'center right', + 'lower left', 'lower center', 'lower right'. + For backward compatibility, numeric values are accepted as well. + See the parameter *loc* of `.Legend` for details. angle : float, default: 0 The angle of the arrows in degrees. - 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, default: 0.4 Padding around the labels and arrows, in fraction of the font size. - borderpad : float, default: 0.4 Border padding, in fraction of the font size. - frameon : bool, default: False If True, draw a box around the arrows and labels. - color : str, default: 'white' Color for the arrows and labels. - alpha : float, default: 1 Alpha values of the arrows and labels - 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 + fontproperties : `~matplotlib.font_manager.FontProperties`, optional Font properties for the label text. - back_length : float, default: 0.15 Fraction of the arrow behind the arrow crossing. - head_width : float, default: 10 - Width of arrow head, sent to ArrowStyle. - + Width of arrow head, sent to `.ArrowStyle`. head_length : float, default: 15 - Length of arrow head, sent to ArrowStyle. - + Length of arrow head, sent to `.ArrowStyle`. tail_width : float, default: 2 - Width of arrow tail, sent to ArrowStyle. - + Width of arrow tail, sent to `.ArrowStyle`. text_props, arrow_props : dict - Properties of the text and arrows, passed to - `.textpath.TextPath` and `.patches.FancyArrowPatch`. - + Properties of the text and arrows, passed to `.TextPath` and + `.FancyArrowPatch`. **kwargs - Keyworded arguments to pass to - :class:`matplotlib.offsetbox.AnchoredOffsetbox`. + Keyword arguments forwarded to `.AnchoredOffsetbox`. Attributes ---------- - arrow_x, arrow_y : `matplotlib.patches.FancyArrowPatch` + arrow_x, arrow_y : `~matplotlib.patches.FancyArrowPatch` Arrow x and y - - text_path_x, text_path_y : `matplotlib.textpath.TextPath` + text_path_x, text_path_y : `~matplotlib.text.TextPath` Path for arrow labels - - p_x, p_y : `matplotlib.patches.PathPatch` + p_x, p_y : `~matplotlib.patches.PathPatch` Patch for arrow labels - - box : `matplotlib.offsetbox.AuxTransformBox` + box : `~matplotlib.offsetbox.AuxTransformBox` Container for the arrows and labels. Notes ----- If *prop* is passed as a keyword argument, but *fontproperties* is - not, then *prop* is be assumed to be the intended *fontproperties*. + not, then *prop* is assumed to be the intended *fontproperties*. Using both *prop* and *fontproperties* is not supported. Examples diff --git a/lib/mpl_toolkits/axes_grid1/axes_divider.py b/lib/mpl_toolkits/axes_grid1/axes_divider.py index b50bb5ccdeac..2a6bd0d5da08 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_divider.py +++ b/lib/mpl_toolkits/axes_grid1/axes_divider.py @@ -4,9 +4,9 @@ import numpy as np +import matplotlib as mpl from matplotlib import _api -from matplotlib.axes import SubplotBase -from matplotlib.gridspec import SubplotSpec, GridSpec +from matplotlib.gridspec import SubplotSpec import matplotlib.transforms as mtransforms from . import axes_size as Size @@ -38,7 +38,8 @@ def __init__(self, fig, pos, horizontal, vertical, aspect : bool 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'} + anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', \ +'NW', 'W'} Placement of the reduced rectangle, when *aspect* is True. """ @@ -47,43 +48,17 @@ def __init__(self, fig, pos, horizontal, vertical, self._horizontal = horizontal self._vertical = vertical self._anchor = anchor + self.set_anchor(anchor) self._aspect = aspect self._xrefindex = 0 self._yrefindex = 0 self._locator = None def get_horizontal_sizes(self, renderer): - return [s.get_size(renderer) for s in self.get_horizontal()] + return np.array([s.get_size(renderer) for s in self.get_horizontal()]) def get_vertical_sizes(self, renderer): - return [s.get_size(renderer) for s in self.get_vertical()] - - def get_vsize_hsize(self): - vsize = Size.AddList(self.get_vertical()) - hsize = Size.AddList(self.get_horizontal()) - return vsize, hsize - - @staticmethod - def _calc_k(l, total_size): - - rs_sum, as_sum = 0., 0. - - for _rs, _as in l: - rs_sum += _rs - as_sum += _as - - if rs_sum != 0.: - k = (total_size - as_sum) / rs_sum - return k - else: - return 0. - - @staticmethod - def _calc_offsets(l, k): - offsets = [0.] - for _rs, _as in l: - offsets.append(offsets[-1] + _rs*k + _as) - return offsets + return np.array([s.get_size(renderer) for s in self.get_vertical()]) def set_position(self, pos): """ @@ -104,26 +79,20 @@ def set_anchor(self, anchor): """ Parameters ---------- - anchor : {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', 'NW', 'W'} - anchor position - - ===== ============ - value description - ===== ============ - 'C' Center - 'SW' bottom left - 'S' bottom - 'SE' bottom right - 'E' right - 'NE' top right - 'N' top - 'NW' top left - 'W' left - ===== ============ - - """ - if len(anchor) != 2: + anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', \ +'NW', 'W'} + Either an (*x*, *y*) pair of relative coordinates (0 is left or + bottom, 1 is right or top), 'C' (center), or a cardinal direction + ('SW', southwest, is bottom left, etc.). + + See Also + -------- + .Axes.set_anchor + """ + if isinstance(anchor, str): _api.check_in_list(mtransforms.Bbox.coefs, anchor=anchor) + elif not isinstance(anchor, (tuple, list)) or len(anchor) != 2: + raise TypeError("anchor must be str or 2-tuple") self._anchor = anchor def get_anchor(self): @@ -180,40 +149,52 @@ def get_position_runtime(self, ax, renderer): else: return self._locator(ax, renderer).bounds + @staticmethod + def _calc_k(sizes, total): + # sizes is a (n, 2) array of (rel_size, abs_size); this method finds + # the k factor such that sum(rel_size * k + abs_size) == total. + rel_sum, abs_sum = sizes.sum(0) + return (total - abs_sum) / rel_sum if rel_sum else 0 + + @staticmethod + def _calc_offsets(sizes, k): + # Apply k factors to (n, 2) sizes array of (rel_size, abs_size); return + # the resulting cumulative offset positions. + return np.cumsum([0, *(sizes @ [k, 1])]) + def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): """ Parameters ---------- nx, nx1 : int - Integers specifying the column-position of the - cell. When *nx1* is None, a single *nx*-th column is - specified. Otherwise location of columns spanning between *nx* - to *nx1* (but excluding *nx1*-th column) is specified. + Integers specifying the column-position of the cell. When *nx1* is + None, a single *nx*-th column is specified. Otherwise, the + location of columns spanning between *nx* to *nx1* (but excluding + *nx1*-th column) is specified. ny, ny1 : int Same as *nx* and *nx1*, but for row positions. axes renderer """ - figW, figH = self._fig.get_size_inches() + fig_w, fig_h = self._fig.bbox.size / self._fig.dpi x, y, w, h = self.get_position_runtime(axes, renderer) hsizes = self.get_horizontal_sizes(renderer) vsizes = self.get_vertical_sizes(renderer) - k_h = self._calc_k(hsizes, figW*w) - k_v = self._calc_k(vsizes, figH*h) + k_h = self._calc_k(hsizes, fig_w * w) + k_v = self._calc_k(vsizes, fig_h * h) if self.get_aspect(): k = min(k_h, k_v) ox = self._calc_offsets(hsizes, k) oy = self._calc_offsets(vsizes, k) - ww = (ox[-1] - ox[0]) / figW - hh = (oy[-1] - oy[0]) / figH + ww = (ox[-1] - ox[0]) / fig_w + hh = (oy[-1] - oy[0]) / fig_h pb = mtransforms.Bbox.from_bounds(x, y, w, h) pb1 = mtransforms.Bbox.from_bounds(x, y, ww, hh) - pb1_anchored = pb1.anchored(self.get_anchor(), pb) - x0, y0 = pb1_anchored.x0, pb1_anchored.y0 + x0, y0 = pb1.anchored(self.get_anchor(), pb).p0 else: ox = self._calc_offsets(hsizes, k_h) @@ -221,32 +202,37 @@ def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): x0, y0 = x, y if nx1 is None: - nx1 = nx + 1 + nx1 = -1 if ny1 is None: - ny1 = ny + 1 + ny1 = -1 - x1, w1 = x0 + ox[nx] / figW, (ox[nx1] - ox[nx]) / figW - y1, h1 = y0 + oy[ny] / figH, (oy[ny1] - oy[ny]) / figH + x1, w1 = x0 + ox[nx] / fig_w, (ox[nx1] - ox[nx]) / fig_w + y1, h1 = y0 + oy[ny] / fig_h, (oy[ny1] - oy[ny]) / fig_h return mtransforms.Bbox.from_bounds(x1, y1, w1, h1) def new_locator(self, nx, ny, nx1=None, ny1=None): """ - Return a new `AxesLocator` for the specified cell. + Return a new `.AxesLocator` for the specified cell. Parameters ---------- nx, nx1 : int Integers specifying the column-position of the cell. When *nx1* is None, a single *nx*-th column is - specified. Otherwise location of columns spanning between *nx* + specified. Otherwise, location of columns spanning between *nx* to *nx1* (but excluding *nx1*-th column) is specified. ny, ny1 : int Same as *nx* and *nx1*, but for row positions. """ - return AxesLocator(self, nx, ny, nx1, ny1) + return AxesLocator( + self, nx, ny, + nx1 if nx1 is not None else nx + 1, + ny1 if ny1 is not None else ny + 1) def append_size(self, position, size): + _api.check_in_list(["left", "right", "bottom", "top"], + position=position) if position == "left": self._horizontal.insert(0, size) self._xrefindex += 1 @@ -255,37 +241,44 @@ def append_size(self, position, size): elif position == "bottom": self._vertical.insert(0, size) self._yrefindex += 1 - elif position == "top": + else: # 'top' self._vertical.append(size) - else: - _api.check_in_list(["left", "right", "bottom", "top"], - position=position) def add_auto_adjustable_area(self, use_axes, pad=0.1, adjust_dirs=None): + """ + Add auto-adjustable padding around *use_axes* to take their decorations + (title, labels, ticks, ticklabels) into account during layout. + + Parameters + ---------- + use_axes : `~matplotlib.axes.Axes` or list of `~matplotlib.axes.Axes` + The Axes whose decorations are taken into account. + pad : float, optional + Additional padding in inches. + adjust_dirs : list of {"left", "right", "bottom", "top"}, optional + The sides where padding is added; defaults to all four sides. + """ if adjust_dirs is None: adjust_dirs = ["left", "right", "bottom", "top"] - from .axes_size import Padded, SizeFromFunc, GetExtentHelper for d in adjust_dirs: - helper = GetExtentHelper(use_axes, d) - size = SizeFromFunc(helper) - padded_size = Padded(size, pad) # pad in inch - self.append_size(d, padded_size) + self.append_size(d, Size._AxesDecorationsSize(use_axes, d) + pad) class AxesLocator: """ - A simple callable object, initialized with AxesDivider class, - returns the position and size of the given cell. + A callable object which returns the position and size of a given + `.AxesDivider` cell. """ + def __init__(self, axes_divider, nx, ny, nx1=None, ny1=None): """ Parameters ---------- - axes_divider : AxesDivider + axes_divider : `~mpl_toolkits.axes_grid1.axes_divider.AxesDivider` nx, nx1 : int Integers specifying the column-position of the cell. When *nx1* is None, a single *nx*-th column is - specified. Otherwise location of columns spanning between *nx* + specified. Otherwise, location of columns spanning between *nx* to *nx1* (but excluding *nx1*-th column) is specified. ny, ny1 : int Same as *nx* and *nx1*, but for row positions. @@ -298,9 +291,9 @@ def __init__(self, axes_divider, nx, ny, nx1=None, ny1=None): self._nx, self._ny = nx - _xrefindex, ny - _yrefindex if nx1 is None: - nx1 = nx + 1 + nx1 = len(self._axes_divider) if ny1 is None: - ny1 = ny + 1 + ny1 = len(self._axes_divider[0]) self._nx1 = nx1 - _xrefindex self._ny1 = ny1 - _yrefindex @@ -318,10 +311,7 @@ def __call__(self, axes, renderer): renderer) def get_subplotspec(self): - if hasattr(self._axes_divider, "get_subplotspec"): - return self._axes_divider.get_subplotspec() - else: - return None + return self._axes_divider.get_subplotspec() class SubplotDivider(Divider): @@ -334,7 +324,7 @@ def __init__(self, fig, *args, horizontal=None, vertical=None, """ Parameters ---------- - fig : `matplotlib.figure.Figure` + fig : `~matplotlib.figure.Figure` *args : tuple (*nrows*, *ncols*, *index*) or int The array of subplots in the figure has dimensions ``(nrows, @@ -356,30 +346,6 @@ def get_position(self): """Return the bounds of the subplot box.""" return self.get_subplotspec().get_position(self.figure).bounds - @_api.deprecated("3.4") - @property - def figbox(self): - return self.get_subplotspec().get_position(self.figure) - - @_api.deprecated("3.4") - def update_params(self): - pass - - @_api.deprecated( - "3.4", alternative="get_subplotspec", - addendum="(get_subplotspec returns a SubplotSpec instance.)") - def get_geometry(self): - """Get the subplot geometry, e.g., (2, 2, 3).""" - rows, cols, num1, num2 = self.get_subplotspec().get_geometry() - return rows, cols, num1 + 1 # for compatibility - - @_api.deprecated("3.4", alternative="set_subplotspec") - def change_geometry(self, numrows, numcols, num): - """Change subplot geometry, e.g., from (1, 1, 1) to (2, 2, 3).""" - self._subplotspec = GridSpec(numrows, numcols)[num-1] - self.update_params() - self.set_position(self.figbox) - def get_subplotspec(self): """Get the SubplotSpec instance.""" return self._subplotspec @@ -392,7 +358,7 @@ def set_subplotspec(self, subplotspec): class AxesDivider(Divider): """ - Divider based on the pre-existing axes. + Divider based on the preexisting axes. """ def __init__(self, axes, xref=None, yref=None): @@ -420,40 +386,20 @@ def __init__(self, axes, xref=None, yref=None): def _get_new_axes(self, *, axes_class=None, **kwargs): axes = self._axes if axes_class is None: - if isinstance(axes, SubplotBase): - axes_class = axes._axes_class - else: - axes_class = type(axes) + axes_class = type(axes) return axes_class(axes.get_figure(), axes.get_position(original=True), **kwargs) def new_horizontal(self, size, pad=None, pack_start=False, **kwargs): """ - Add a new axes on the right (or left) side of the main axes. + Helper method for ``append_axes("left")`` and ``append_axes("right")``. - Parameters - ---------- - size : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str - A width of the axes. If float or string is given, *from_any* - function is used to create the size, with *ref_size* set to AxesX - instance of the current axes. - pad : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str - Pad between the axes. It takes same argument as *size*. - pack_start : bool - If False, the new axes is appended at the end - of the list, i.e., it became the right-most axes. If True, it is - inserted at the start of the list, and becomes the left-most axes. - **kwargs - All extra keywords arguments are passed to the created axes. - If *axes_class* is given, the new axes will be created as an - instance of the given class. Otherwise, the same class of the - main axes will be used. + See the documentation of `append_axes` for more details. + + :meta private: """ if pad is None: - _api.warn_deprecated( - "3.2", message="In a future version, 'pad' will default to " - "rcParams['figure.subplot.wspace']. Set pad=0 to keep the " - "old behavior.") + pad = mpl.rcParams["figure.subplot.wspace"] * self._xref if pad: if not isinstance(pad, Size._Base): pad = Size.from_any(pad, fraction_ref=self._xref) @@ -478,31 +424,14 @@ def new_horizontal(self, size, pad=None, pack_start=False, **kwargs): def new_vertical(self, size, pad=None, pack_start=False, **kwargs): """ - Add a new axes on the top (or bottom) side of the main axes. + Helper method for ``append_axes("top")`` and ``append_axes("bottom")``. - Parameters - ---------- - size : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str - A height of the axes. If float or string is given, *from_any* - function is used to create the size, with *ref_size* set to AxesX - instance of the current axes. - pad : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str - Pad between the axes. It takes same argument as *size*. - pack_start : bool - If False, the new axes is appended at the end - of the list, i.e., it became the right-most axes. If True, it is - inserted at the start of the list, and becomes the left-most axes. - **kwargs - All extra keywords arguments are passed to the created axes. - If *axes_class* is given, the new axes will be created as an - instance of the given class. Otherwise, the same class of the - main axes will be used. + See the documentation of `append_axes` for more details. + + :meta private: """ if pad is None: - _api.warn_deprecated( - "3.2", message="In a future version, 'pad' will default to " - "rcParams['figure.subplot.hspace']. Set pad=0 to keep the " - "old behavior.") + pad = mpl.rcParams["figure.subplot.hspace"] * self._yref if pad: if not isinstance(pad, Size._Base): pad = Size.from_any(pad, fraction_ref=self._yref) @@ -520,35 +449,43 @@ def new_vertical(self, size, pad=None, pack_start=False, **kwargs): else: self._vertical.append(size) locator = self.new_locator( - nx=self._xrefindex, ny=len(self._vertical)-1) + nx=self._xrefindex, ny=len(self._vertical) - 1) ax = self._get_new_axes(**kwargs) ax.set_axes_locator(locator) return ax - def append_axes(self, position, size, pad=None, add_to_figure=True, + def append_axes(self, position, size, pad=None, *, axes_class=None, **kwargs): """ - Create an axes at the given *position* with the same height - (or width) of the main axes. + Add a new axes on a given side of the main axes. - *position* - ["left"|"right"|"bottom"|"top"] - - *size* and *pad* should be axes_grid.axes_size compatible. + Parameters + ---------- + position : {"left", "right", "bottom", "top"} + Where the new axes is positioned relative to the main axes. + size : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str + The axes width or height. float or str arguments are interpreted + as ``axes_size.from_any(size, AxesX())`` for left or + right axes, and likewise with ``AxesY`` for bottom or top axes. + pad : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str + Padding between the axes. float or str arguments are interpreted + as for *size*. Defaults to :rc:`figure.subplot.wspace` times the + main Axes width (left or right axes) or :rc:`figure.subplot.hspace` + times the main Axes height (bottom or top axes). + axes_class : subclass type of `~.axes.Axes`, optional + The type of the new axes. Defaults to the type of the main axes. + **kwargs + All extra keywords arguments are passed to the created axes. """ - if position == "left": - ax = self.new_horizontal(size, pad, pack_start=True, **kwargs) - elif position == "right": - ax = self.new_horizontal(size, pad, pack_start=False, **kwargs) - elif position == "bottom": - ax = self.new_vertical(size, pad, pack_start=True, **kwargs) - elif position == "top": - ax = self.new_vertical(size, pad, pack_start=False, **kwargs) - else: - _api.check_in_list(["left", "right", "bottom", "top"], - position=position) - if add_to_figure: - self._fig.add_axes(ax) + create_axes, pack_start = _api.check_getitem({ + "left": (self.new_horizontal, True), + "right": (self.new_horizontal, False), + "bottom": (self.new_vertical, True), + "top": (self.new_vertical, False), + }, position=position) + ax = create_axes( + size, pad, pack_start=pack_start, axes_class=axes_class, **kwargs) + self._fig.add_axes(ax) return ax def get_aspect(self): @@ -575,168 +512,116 @@ def get_anchor(self): return self._anchor def get_subplotspec(self): - if hasattr(self._axes, "get_subplotspec"): - return self._axes.get_subplotspec() - else: - return None + return self._axes.get_subplotspec() + + +# Helper for HBoxDivider/VBoxDivider. +# The variable names are written for a horizontal layout, but the calculations +# work identically for vertical layouts. +def _locate(x, y, w, h, summed_widths, equal_heights, fig_w, fig_h, anchor): + + total_width = fig_w * w + max_height = fig_h * h + + # Determine the k factors. + n = len(equal_heights) + eq_rels, eq_abss = equal_heights.T + sm_rels, sm_abss = summed_widths.T + A = np.diag([*eq_rels, 0]) + A[:n, -1] = -1 + A[-1, :-1] = sm_rels + B = [*(-eq_abss), total_width - sm_abss.sum()] + # A @ K = B: This finds factors {k_0, ..., k_{N-1}, H} so that + # eq_rel_i * k_i + eq_abs_i = H for all i: all axes have the same height + # sum(sm_rel_i * k_i + sm_abs_i) = total_width: fixed total width + # (foo_rel_i * k_i + foo_abs_i will end up being the size of foo.) + *karray, height = np.linalg.solve(A, B) + if height > max_height: # Additionally, upper-bound the height. + karray = (max_height - eq_abss) / eq_rels + + # Compute the offsets corresponding to these factors. + ox = np.cumsum([0, *(sm_rels * karray + sm_abss)]) + ww = (ox[-1] - ox[0]) / fig_w + h0_rel, h0_abs = equal_heights[0] + hh = (karray[0]*h0_rel + h0_abs) / fig_h + pb = mtransforms.Bbox.from_bounds(x, y, w, h) + pb1 = mtransforms.Bbox.from_bounds(x, y, ww, hh) + x0, y0 = pb1.anchored(anchor, pb).p0 + + return x0, y0, ox, hh class HBoxDivider(SubplotDivider): + """ + A `.SubplotDivider` for laying out axes horizontally, while ensuring that + they have equal heights. - @staticmethod - def _determine_karray(equivalent_sizes, appended_sizes, - max_equivalent_size, - total_appended_size): - - n = len(equivalent_sizes) - eq_rs, eq_as = np.asarray(equivalent_sizes).T - ap_rs, ap_as = np.asarray(appended_sizes).T - A = np.zeros((n + 1, n + 1)) - B = np.zeros(n + 1) - np.fill_diagonal(A[:n, :n], eq_rs) - A[:n, -1] = -1 - A[-1, :-1] = ap_rs - B[:n] = -eq_as - B[-1] = total_appended_size - sum(ap_as) - - karray_H = np.linalg.solve(A, B) # A @ K = B - karray = karray_H[:-1] - H = karray_H[-1] - - if H > max_equivalent_size: - karray = (max_equivalent_size - eq_as) / eq_rs - return karray - - @staticmethod - def _calc_offsets(appended_sizes, karray): - offsets = [0.] - for (r, a), k in zip(appended_sizes, karray): - offsets.append(offsets[-1] + r*k + a) - return offsets + Examples + -------- + .. plot:: gallery/axes_grid1/demo_axes_hbox_divider.py + """ def new_locator(self, nx, nx1=None): """ - Create a new `AxesLocator` for the specified cell. + Create a new `.AxesLocator` for the specified cell. Parameters ---------- nx, nx1 : int Integers specifying the column-position of the cell. When *nx1* is None, a single *nx*-th column is - specified. Otherwise location of columns spanning between *nx* + specified. Otherwise, location of columns spanning between *nx* to *nx1* (but excluding *nx1*-th column) is specified. - ny, ny1 : int - Same as *nx* and *nx1*, but for row positions. """ - return AxesLocator(self, nx, 0, nx1, None) - - def _locate(self, x, y, w, h, - y_equivalent_sizes, x_appended_sizes, - figW, figH): - equivalent_sizes = y_equivalent_sizes - appended_sizes = x_appended_sizes - - max_equivalent_size = figH * h - total_appended_size = figW * w - karray = self._determine_karray(equivalent_sizes, appended_sizes, - max_equivalent_size, - total_appended_size) - - ox = self._calc_offsets(appended_sizes, karray) - - ww = (ox[-1] - ox[0]) / figW - ref_h = equivalent_sizes[0] - hh = (karray[0]*ref_h[0] + ref_h[1]) / figH - pb = mtransforms.Bbox.from_bounds(x, y, w, h) - pb1 = mtransforms.Bbox.from_bounds(x, y, ww, hh) - pb1_anchored = pb1.anchored(self.get_anchor(), pb) - x0, y0 = pb1_anchored.x0, pb1_anchored.y0 - - return x0, y0, ox, hh + return AxesLocator(self, nx, 0, nx1 if nx1 is not None else nx + 1, 1) def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): - """ - Parameters - ---------- - axes_divider : AxesDivider - nx, nx1 : int - Integers specifying the column-position of the - cell. When *nx1* is None, a single *nx*-th column is - specified. Otherwise location of columns spanning between *nx* - to *nx1* (but excluding *nx1*-th column) is specified. - ny, ny1 : int - Same as *nx* and *nx1*, but for row positions. - axes - renderer - """ - - figW, figH = self._fig.get_size_inches() + # docstring inherited + fig_w, fig_h = self._fig.bbox.size / self._fig.dpi x, y, w, h = self.get_position_runtime(axes, renderer) - - y_equivalent_sizes = self.get_vertical_sizes(renderer) - x_appended_sizes = self.get_horizontal_sizes(renderer) - x0, y0, ox, hh = self._locate(x, y, w, h, - y_equivalent_sizes, x_appended_sizes, - figW, figH) + summed_ws = self.get_horizontal_sizes(renderer) + equal_hs = self.get_vertical_sizes(renderer) + x0, y0, ox, hh = _locate( + x, y, w, h, summed_ws, equal_hs, fig_w, fig_h, self.get_anchor()) if nx1 is None: - nx1 = nx + 1 - - x1, w1 = x0 + ox[nx] / figW, (ox[nx1] - ox[nx]) / figW + nx1 = -1 + x1, w1 = x0 + ox[nx] / fig_w, (ox[nx1] - ox[nx]) / fig_w y1, h1 = y0, hh - return mtransforms.Bbox.from_bounds(x1, y1, w1, h1) -class VBoxDivider(HBoxDivider): +class VBoxDivider(SubplotDivider): """ - The Divider class whose rectangle area is specified as a subplot geometry. + A `.SubplotDivider` for laying out axes vertically, while ensuring that + they have equal widths. """ def new_locator(self, ny, ny1=None): """ - Create a new `AxesLocator` for the specified cell. + Create a new `.AxesLocator` for the specified cell. Parameters ---------- ny, ny1 : int Integers specifying the row-position of the cell. When *ny1* is None, a single *ny*-th row is - specified. Otherwise location of rows spanning between *ny* + specified. Otherwise, location of rows spanning between *ny* to *ny1* (but excluding *ny1*-th row) is specified. """ - return AxesLocator(self, 0, ny, None, ny1) + return AxesLocator(self, 0, ny, 1, ny1 if ny1 is not None else ny + 1) def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None): - """ - Parameters - ---------- - axes_divider : AxesDivider - nx, nx1 : int - Integers specifying the column-position of the - cell. When *nx1* is None, a single *nx*-th column is - specified. Otherwise location of columns spanning between *nx* - to *nx1* (but excluding *nx1*-th column) is specified. - ny, ny1 : int - Same as *nx* and *nx1*, but for row positions. - axes - renderer - """ - - figW, figH = self._fig.get_size_inches() + # docstring inherited + fig_w, fig_h = self._fig.bbox.size / self._fig.dpi x, y, w, h = self.get_position_runtime(axes, renderer) - - x_equivalent_sizes = self.get_horizontal_sizes(renderer) - y_appended_sizes = self.get_vertical_sizes(renderer) - - y0, x0, oy, ww = self._locate(y, x, h, w, - x_equivalent_sizes, y_appended_sizes, - figH, figW) + summed_hs = self.get_vertical_sizes(renderer) + equal_ws = self.get_horizontal_sizes(renderer) + y0, x0, oy, ww = _locate( + y, x, h, w, summed_hs, equal_ws, fig_h, fig_w, self.get_anchor()) if ny1 is None: - ny1 = ny + 1 - + ny1 = -1 x1, w1 = x0, ww - y1, h1 = y0 + oy[ny] / figH, (oy[ny1] - oy[ny]) / figH - + y1, h1 = y0 + oy[ny] / fig_h, (oy[ny1] - oy[ny]) / fig_h return mtransforms.Bbox.from_bounds(x1, y1, w1, h1) @@ -748,15 +633,20 @@ def make_axes_locatable(axes): return divider -def make_axes_area_auto_adjustable(ax, - use_axes=None, pad=0.1, - adjust_dirs=None): +def make_axes_area_auto_adjustable( + ax, use_axes=None, pad=0.1, adjust_dirs=None): + """ + Add auto-adjustable padding around *ax* to take its decorations (title, + labels, ticks, ticklabels) into account during layout, using + `.Divider.add_auto_adjustable_area`. + + By default, padding is determined from the decorations of *ax*. + Pass *use_axes* to consider the decorations of other Axes instead. + """ if adjust_dirs is None: adjust_dirs = ["left", "right", "bottom", "top"] divider = make_axes_locatable(ax) - if use_axes is None: use_axes = ax - divider.add_auto_adjustable_area(use_axes=use_axes, pad=pad, adjust_dirs=adjust_dirs) diff --git a/lib/mpl_toolkits/axes_grid1/axes_grid.py b/lib/mpl_toolkits/axes_grid1/axes_grid.py index 295a03ec801f..1d6d6265e86f 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_grid.py +++ b/lib/mpl_toolkits/axes_grid1/axes_grid.py @@ -3,8 +3,7 @@ import numpy as np -import matplotlib as mpl -from matplotlib import _api +from matplotlib import _api, cbook from matplotlib.gridspec import SubplotSpec from .axes_divider import Size, SubplotDivider, Divider @@ -21,57 +20,34 @@ def _tick_only(ax, bottom_on, left_on): class CbarAxesBase: def __init__(self, *args, orientation, **kwargs): self.orientation = orientation - self._default_label_on = True - self._locator = None # deprecated. super().__init__(*args, **kwargs) def colorbar(self, mappable, *, ticks=None, **kwargs): - - if self.orientation in ["top", "bottom"]: - orientation = "horizontal" - else: - orientation = "vertical" - - cb = mpl.colorbar.Colorbar( - self, mappable, orientation=orientation, ticks=ticks, **kwargs) - self._cbid = mappable.colorbar_cid # deprecated in 3.3. - self._locator = cb.locator # deprecated in 3.3. - - self._config_axes() + orientation = ( + "horizontal" if self.orientation in ["top", "bottom"] else + "vertical") + cb = self.figure.colorbar(mappable, cax=self, orientation=orientation, + ticks=ticks, **kwargs) return cb - cbid = _api.deprecate_privatize_attribute( - "3.3", alternative="mappable.colorbar_cid") - locator = _api.deprecate_privatize_attribute( - "3.3", alternative=".colorbar().locator") - - def _config_axes(self): - """Make an axes patch and outline.""" - ax = self - ax.set_navigate(False) - ax.axis[:].toggle(all=False) - b = self._default_label_on - ax.axis[self.orientation].toggle(all=b) - def toggle_label(self, b): - self._default_label_on = b axis = self.axis[self.orientation] axis.toggle(ticklabels=b, label=b) def cla(self): + orientation = self.orientation super().cla() - self._config_axes() + self.orientation = orientation -class CbarAxes(CbarAxesBase, Axes): - pass +_cbaraxes_class_factory = cbook._make_class_factory(CbarAxesBase, "Cbar{}") class Grid: """ A grid of Axes. - In Matplotlib, the axes location (and size) is specified in normalized + In Matplotlib, the Axes location (and size) is specified in normalized figure coordinates. This may not be ideal for images that needs to be displayed with a given aspect ratio; for example, it is difficult to display multiple images of a same size with some fixed padding between @@ -80,20 +56,18 @@ class Grid: _defaultAxesClass = Axes - @_api.delete_parameter("3.3", "add_all") def __init__(self, fig, rect, nrows_ncols, ngrids=None, direction="row", axes_pad=0.02, - add_all=True, + *, share_all=False, share_x=True, share_y=True, label_mode="L", axes_class=None, - *, aspect=False, ): """ @@ -101,22 +75,22 @@ def __init__(self, fig, ---------- fig : `.Figure` The parent figure. - rect : (float, float, float, float) or int - The axes position, as a ``(left, bottom, width, height)`` tuple or - as a three-digit subplot position code (e.g., "121"). + rect : (float, float, float, float), (int, int, int), int, or \ + `~.SubplotSpec` + The axes position, as a ``(left, bottom, width, height)`` tuple, + as a three-digit subplot position code (e.g., ``(1, 2, 1)`` or + ``121``), or as a `~.SubplotSpec`. nrows_ncols : (int, int) Number of rows and columns in the grid. ngrids : int or None, default: None If not None, only the first *ngrids* axes in the grid are created. direction : {"row", "column"}, default: "row" Whether axes are created in row-major ("row by row") or - column-major order ("column by column"). + column-major order ("column by column"). This also affects the + order in which axes are accessed using indexing (``grid[index]``). axes_pad : float or (float, float), default: 0.02 Padding or (horizontal padding, vertical padding) between axes, in inches. - add_all : bool, default: True - Whether to add the axes to the figure using `.Figure.add_axes`. - This parameter is deprecated. share_all : bool, default: False Whether all axes share their x- and y-axis. Overrides *share_x* and *share_y*. @@ -124,13 +98,14 @@ def __init__(self, fig, Whether all axes of a column share their x-axis. share_y : bool, default: True Whether all axes of a row share their y-axis. - label_mode : {"L", "1", "all"}, default: "L" + label_mode : {"L", "1", "all", "keep"}, default: "L" Determines which axes will get tick labels: - "L": All axes on the left column get vertical tick labels; all axes on the bottom row get horizontal tick labels. - "1": Only the bottom left axes is labelled. - - "all": all axes are labelled. + - "all": All axes are labelled. + - "keep": Do not do anything. axes_class : subclass of `matplotlib.axes.Axes`, default: None aspect : bool, default: False @@ -143,7 +118,8 @@ def __init__(self, fig, ngrids = self._nrows * self._ncols else: if not 0 < ngrids <= self._nrows * self._ncols: - raise Exception("") + raise ValueError( + "ngrids must be positive and not larger than nrows*ncols") self.ngrids = ngrids @@ -160,14 +136,14 @@ def __init__(self, fig, axes_class = functools.partial(cls, **kwargs) kw = dict(horizontal=[], vertical=[], aspect=aspect) - if isinstance(rect, (str, Number, SubplotSpec)): + if isinstance(rect, (Number, SubplotSpec)): self._divider = SubplotDivider(fig, rect, **kw) elif len(rect) == 3: self._divider = SubplotDivider(fig, *rect, **kw) elif len(rect) == 4: self._divider = Divider(fig, rect, **kw) else: - raise Exception("") + raise TypeError("Incorrect rect format") rect = self._divider.get_position() @@ -181,16 +157,16 @@ def __init__(self, fig, sharey = axes_array[row, 0] if share_y else None axes_array[row, col] = axes_class( fig, rect, sharex=sharex, sharey=sharey) - self.axes_all = axes_array.ravel().tolist() + self.axes_all = axes_array.ravel( + order="C" if self._direction == "row" else "F").tolist() self.axes_column = axes_array.T.tolist() self.axes_row = axes_array.tolist() self.axes_llc = self.axes_column[0][-1] self._init_locators() - if add_all: - for ax in self.axes_all: - fig.add_axes(ax) + for ax in self.axes_all: + fig.add_axes(ax) self.set_label_mode(label_mode) @@ -282,13 +258,14 @@ def set_label_mode(self, mode): Parameters ---------- - mode : {"L", "1", "all"} + mode : {"L", "1", "all", "keep"} The label mode: - "L": All axes on the left column get vertical tick labels; all axes on the bottom row get horizontal tick labels. - "1": Only the bottom left axes is labelled. - - "all": all axes are labelled. + - "all": All axes are labelled. + - "keep": Do not do anything. """ if mode == "all": for ax in self.axes_all: @@ -316,6 +293,16 @@ def set_label_mode(self, mode): ax = self.axes_llc _tick_only(ax, bottom_on=False, left_on=False) + else: + # Use _api.check_in_list at the top of the method when deprecation + # period expires + if mode != 'keep': + _api.warn_deprecated( + '3.7', name="Grid label_mode", + message='Passing an undefined label_mode is deprecated ' + 'since %(since)s and will become an error ' + '%(removal)s. To silence this warning, pass ' + '"keep", which gives the same behaviour.') def get_divider(self): return self._divider @@ -326,23 +313,17 @@ def set_axes_locator(self, locator): def get_axes_locator(self): return self._divider.get_locator() - def get_vsize_hsize(self): - return self._divider.get_vsize_hsize() - class ImageGrid(Grid): # docstring inherited - _defaultCbarAxesClass = CbarAxes - - @_api.delete_parameter("3.3", "add_all") def __init__(self, fig, rect, nrows_ncols, ngrids=None, direction="row", axes_pad=0.02, - add_all=True, + *, share_all=False, aspect=True, label_mode="L", @@ -372,9 +353,6 @@ def __init__(self, fig, axes_pad : float or (float, float), default: 0.02in Padding or (horizontal padding, vertical padding) between axes, in inches. - add_all : bool, default: True - Whether to add the axes to the figure using `.Figure.add_axes`. - This parameter is deprecated. share_all : bool, default: False Whether all axes share their x- and y-axis. aspect : bool, default: True @@ -403,28 +381,24 @@ def __init__(self, fig, to associated *cbar_axes*. axes_class : subclass of `matplotlib.axes.Axes`, default: None """ + _api.check_in_list(["each", "single", "edge", None], + cbar_mode=cbar_mode) + _api.check_in_list(["left", "right", "bottom", "top"], + cbar_location=cbar_location) self._colorbar_mode = cbar_mode self._colorbar_location = cbar_location self._colorbar_pad = cbar_pad self._colorbar_size = cbar_size # The colorbar axes are created in _init_locators(). - if add_all: - super().__init__( - fig, rect, nrows_ncols, ngrids, - direction=direction, axes_pad=axes_pad, - share_all=share_all, share_x=True, share_y=True, aspect=aspect, - label_mode=label_mode, axes_class=axes_class) - else: # Only show deprecation in that case. - super().__init__( - fig, rect, nrows_ncols, ngrids, - direction=direction, axes_pad=axes_pad, add_all=add_all, - share_all=share_all, share_x=True, share_y=True, aspect=aspect, - label_mode=label_mode, axes_class=axes_class) - - if add_all: - for ax in self.cbar_axes: - fig.add_axes(ax) + super().__init__( + fig, rect, nrows_ncols, ngrids, + direction=direction, axes_pad=axes_pad, + share_all=share_all, share_x=True, share_y=True, aspect=aspect, + label_mode=label_mode, axes_class=axes_class) + + for ax in self.cbar_axes: + fig.add_axes(ax) if cbar_set_cax: if self._colorbar_mode == "single": @@ -451,7 +425,7 @@ def _init_locators(self): else: self._colorbar_pad = self._vert_pad_size.fixed_size self.cbar_axes = [ - self._defaultCbarAxesClass( + _cbaraxes_class_factory(self._defaultAxesClass)( self.axes_all[0].figure, self._divider.get_position(), orientation=self._colorbar_location) for _ in range(self.ngrids)] diff --git a/lib/mpl_toolkits/axes_grid1/axes_rgb.py b/lib/mpl_toolkits/axes_grid1/axes_rgb.py index b8cdf4dde7f1..f0672b7a8b29 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_rgb.py +++ b/lib/mpl_toolkits/axes_grid1/axes_rgb.py @@ -1,17 +1,22 @@ import numpy as np -from matplotlib import _api from .axes_divider import make_axes_locatable, Size from .mpl_axes import Axes -@_api.delete_parameter("3.3", "add_all") -def make_rgb_axes(ax, pad=0.01, axes_class=None, add_all=True, **kwargs): +def make_rgb_axes(ax, pad=0.01, axes_class=None, **kwargs): """ Parameters ---------- - pad : float - Fraction of the axes height. + ax : `~matplotlib.axes.Axes` + Axes instance to create the RGB Axes in. + pad : float, optional + Fraction of the Axes height to pad. + axes_class : `matplotlib.axes.Axes` or None, optional + Axes class to use for the R, G, and B Axes. If None, use + the same class as *ax*. + **kwargs : + Forwarded to *axes_class* init for the R, G, and B Axes. """ divider = make_axes_locatable(ax) @@ -28,10 +33,7 @@ def make_rgb_axes(ax, pad=0.01, axes_class=None, add_all=True, **kwargs): ax_rgb = [] if axes_class is None: - try: - axes_class = ax._axes_class - except AttributeError: - axes_class = type(ax) + axes_class = type(ax) for ny in [4, 2, 0]: ax1 = axes_class(ax.get_figure(), ax.get_position(original=True), @@ -48,72 +50,60 @@ def make_rgb_axes(ax, pad=0.01, axes_class=None, add_all=True, **kwargs): ax_rgb.append(ax1) - if add_all: - fig = ax.get_figure() - for ax1 in ax_rgb: - fig.add_axes(ax1) + fig = ax.get_figure() + for ax1 in ax_rgb: + fig.add_axes(ax1) return ax_rgb -@_api.deprecated("3.3", alternative="ax.imshow(np.dstack([r, g, b]))") -def imshow_rgb(ax, r, g, b, **kwargs): - return ax.imshow(np.dstack([r, g, b]), **kwargs) - - class RGBAxes: """ - 4-panel imshow (RGB, R, G, B). + 4-panel `~.Axes.imshow` (RGB, R, G, B). - Layout: + Layout:: - +---------------+-----+ - | | R | - + +-----+ - | RGB | G | - + +-----+ - | | B | - +---------------+-----+ + ┌───────────────┬─────┠+ │ │ R │ + │ ├─────┤ + │ RGB │ G │ + │ ├─────┤ + │ │ B │ + └───────────────┴─────┘ Subclasses can override the ``_defaultAxesClass`` attribute. + By default RGBAxes uses `.mpl_axes.Axes`. Attributes ---------- RGB : ``_defaultAxesClass`` - The axes object for the three-channel imshow. + The Axes object for the three-channel `~.Axes.imshow`. R : ``_defaultAxesClass`` - The axes object for the red channel imshow. + The Axes object for the red channel `~.Axes.imshow`. G : ``_defaultAxesClass`` - The axes object for the green channel imshow. + The Axes object for the green channel `~.Axes.imshow`. B : ``_defaultAxesClass`` - The axes object for the blue channel imshow. + The Axes object for the blue channel `~.Axes.imshow`. """ _defaultAxesClass = Axes - @_api.delete_parameter("3.3", "add_all") - def __init__(self, *args, pad=0, add_all=True, **kwargs): + def __init__(self, *args, pad=0, **kwargs): """ Parameters ---------- pad : float, default: 0 - fraction of the axes height to put as padding. - add_all : bool, default: True - Whether to add the {rgb, r, g, b} axes to the figure. - This parameter is deprecated. - axes_class : matplotlib.axes.Axes - + Fraction of the Axes height to put as padding. + axes_class : `~matplotlib.axes.Axes` + Axes class to use. If not provided, ``_defaultAxesClass`` is used. *args - Unpacked into axes_class() init for RGB + Forwarded to *axes_class* init for the RGB Axes **kwargs - Unpacked into axes_class() init for RGB, R, G, B axes + Forwarded to *axes_class* init for the RGB, R, G, and B Axes """ axes_class = kwargs.pop("axes_class", self._defaultAxesClass) self.RGB = ax = axes_class(*args, **kwargs) - if add_all: - ax.get_figure().add_axes(ax) - else: - kwargs["add_all"] = add_all # only show deprecation in that case + ax.get_figure().add_axes(ax) self.R, self.G, self.B = make_rgb_axes( ax, pad=pad, axes_class=axes_class, **kwargs) # Set the line color and ticks for the axes. @@ -121,13 +111,6 @@ def __init__(self, *args, pad=0, add_all=True, **kwargs): ax1.axis[:].line.set_color("w") ax1.axis[:].major_ticks.set_markeredgecolor("w") - @_api.deprecated("3.3") - def add_RGB_to_figure(self): - """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}. @@ -136,15 +119,15 @@ def imshow_rgb(self, r, g, b, **kwargs): ---------- r, g, b : array-like The red, green, and blue arrays. - kwargs : imshow kwargs - kwargs get unpacked into the imshow calls for the four images. + **kwargs : + Forwarded to `~.Axes.imshow` calls for the four images. Returns ------- - rgb : matplotlib.image.AxesImage - r : matplotlib.image.AxesImage - g : matplotlib.image.AxesImage - b : matplotlib.image.AxesImage + rgb : `~matplotlib.image.AxesImage` + r : `~matplotlib.image.AxesImage` + g : `~matplotlib.image.AxesImage` + b : `~matplotlib.image.AxesImage` """ if not (r.shape == g.shape == b.shape): raise ValueError( @@ -161,8 +144,3 @@ def imshow_rgb(self, r, g, b, **kwargs): im_g = self.G.imshow(G, **kwargs) im_b = self.B.imshow(B, **kwargs) return im_rgb, im_r, im_g, im_b - - -@_api.deprecated("3.3", alternative="RGBAxes") -class RGBAxesBase(RGBAxes): - pass diff --git a/lib/mpl_toolkits/axes_grid1/axes_size.py b/lib/mpl_toolkits/axes_grid1/axes_size.py index 87bc2419e1b4..cb800210fb79 100644 --- a/lib/mpl_toolkits/axes_grid1/axes_size.py +++ b/lib/mpl_toolkits/axes_grid1/axes_size.py @@ -1,6 +1,6 @@ """ Provides classes of simple units that will be used with AxesDivider -class (or others) to determine the size of each axes. The unit +class (or others) to determine the size of each Axes. The unit classes define `get_size` method that returns a tuple of two floats, meaning relative and absolute sizes, respectively. @@ -16,7 +16,6 @@ class (or others) to determine the size of each axes. The unit class _Base: - def __rmul__(self, other): return Fraction(other, self) @@ -38,6 +37,8 @@ def get_size(self, renderer): return a_rel_size + b_rel_size, a_abs_size + b_abs_size +@_api.deprecated( + "3.6", alternative="sum(sizes, start=Fixed(0))") class AddList(_Base): def __init__(self, add_list): self._list = add_list @@ -203,9 +204,10 @@ def get_size(self, renderer): return rel_size, abs_size +@_api.deprecated("3.6", alternative="size + pad") class Padded(_Base): """ - Return a instance where the absolute part of *size* is + Return an instance where the absolute part of *size* is increase by the amount of *pad*. """ @@ -237,6 +239,7 @@ def from_any(size, fraction_ref=None): raise ValueError("Unknown format") +@_api.deprecated("3.6") class SizeFromFunc(_Base): def __init__(self, func): self._func = func @@ -251,6 +254,7 @@ def get_size(self, renderer): return rel_size, abs_size +@_api.deprecated("3.6") class GetExtentHelper: _get_func_map = { "left": lambda self, axes_bbox: axes_bbox.xmin - self.xmin, @@ -270,3 +274,31 @@ def __call__(self, renderer): ax.bbox) for ax in self._ax_list] return max(vl) + + +class _AxesDecorationsSize(_Base): + """ + Fixed size, corresponding to the size of decorations on a given Axes side. + """ + + _get_size_map = { + "left": lambda tight_bb, axes_bb: axes_bb.xmin - tight_bb.xmin, + "right": lambda tight_bb, axes_bb: tight_bb.xmax - axes_bb.xmax, + "bottom": lambda tight_bb, axes_bb: axes_bb.ymin - tight_bb.ymin, + "top": lambda tight_bb, axes_bb: tight_bb.ymax - axes_bb.ymax, + } + + def __init__(self, ax, direction): + self._get_size = _api.check_getitem( + self._get_size_map, direction=direction) + self._ax_list = [ax] if isinstance(ax, Axes) else ax + + def get_size(self, renderer): + sz = max([ + self._get_size(ax.get_tightbbox(renderer, call_axes_locator=False), + ax.bbox) + for ax in self._ax_list]) + dpi = renderer.points_to_pixels(72) + abs_size = sz / dpi + rel_size = 0 + return rel_size, abs_size diff --git a/lib/mpl_toolkits/axes_grid1/inset_locator.py b/lib/mpl_toolkits/axes_grid1/inset_locator.py index 4af827e60f47..0771efd71fa0 100644 --- a/lib/mpl_toolkits/axes_grid1/inset_locator.py +++ b/lib/mpl_toolkits/axes_grid1/inset_locator.py @@ -2,7 +2,7 @@ A collection of functions and objects for creating or placing inset axes. """ -from matplotlib import _api, docstring +from matplotlib import _api, _docstring from matplotlib.offsetbox import AnchoredOffsetbox from matplotlib.patches import Patch, Rectangle from matplotlib.path import Path @@ -14,7 +14,7 @@ class InsetPosition: - @docstring.dedent_interpd + @_docstring.dedent_interpd def __init__(self, parent, lbwh): """ An object for positioning an inset axes. @@ -24,7 +24,7 @@ def __init__(self, parent, lbwh): Parameters ---------- - parent : `matplotlib.axes.Axes` + parent : `~matplotlib.axes.Axes` Axes to use for normalizing coordinates. lbwh : iterable of four floats @@ -38,12 +38,12 @@ def __init__(self, parent, lbwh): Examples -------- The following bounds the inset axes to a box with 20%% of the parent - axes's height and 40%% of the width. The size of the axes specified + axes height and 40%% of the width. The size of the axes specified ([0, 0, 1, 1]) ensures that the axes completely fills the bounding box: >>> parent_axes = plt.gca() >>> ax_ins = plt.axes([0, 0, 1, 1]) - >>> ip = InsetPosition(ax, [0.5, 0.1, 0.4, 0.2]) + >>> ip = InsetPosition(parent_axes, [0.5, 0.1, 0.4, 0.2]) >>> ax_ins.set_axes_locator(ip) """ self.parent = parent @@ -70,18 +70,11 @@ def draw(self, renderer): def __call__(self, ax, renderer): self.axes = ax - - fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) - self._update_offset_func(renderer, fontsize) - - width, height, xdescent, ydescent = self.get_extent(renderer) - - px, py = self.get_offset(width, height, 0, 0, renderer) - bbox_canvas = Bbox.from_bounds(px, py, width, height) - tr = ax.figure.transFigure.inverted() - bb = TransformedBbox(bbox_canvas, tr) - - return bb + bbox = self.get_window_extent(renderer) + px, py = self.get_offset(bbox.width, bbox.height, 0, 0, renderer) + bbox_canvas = Bbox.from_bounds(px, py, bbox.width, bbox.height) + tr = ax.figure.transSubfigure.inverted() + return TransformedBbox(bbox_canvas, tr) class AnchoredSizeLocator(AnchoredLocatorBase): @@ -95,7 +88,7 @@ def __init__(self, bbox_to_anchor, x_size, y_size, loc, self.x_size = Size.from_any(x_size) self.y_size = Size.from_any(y_size) - def get_extent(self, renderer): + def get_bbox(self, renderer): bbox = self.get_bbox_to_anchor() dpi = renderer.points_to_pixels(72.) @@ -104,12 +97,10 @@ def get_extent(self, renderer): r, a = self.y_size.get_size(renderer) height = bbox.height * r + a * dpi - xd, yd = 0, 0 - fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) pad = self.pad * fontsize - return width + 2 * pad, height + 2 * pad, xd + pad, yd + pad + return Bbox.from_bounds(0, 0, width, height).padded(pad) class AnchoredZoomLocator(AnchoredLocatorBase): @@ -125,30 +116,31 @@ def __init__(self, parent_axes, zoom, loc, bbox_to_anchor, None, loc, borderpad=borderpad, bbox_transform=bbox_transform) - def get_extent(self, renderer): - bb = TransformedBbox(self.axes.viewLim, self.parent_axes.transData) + def get_bbox(self, renderer): + bb = self.parent_axes.transData.transform_bbox(self.axes.viewLim) fontsize = renderer.points_to_pixels(self.prop.get_size_in_points()) pad = self.pad * fontsize - return (abs(bb.width * self.zoom) + 2 * pad, - abs(bb.height * self.zoom) + 2 * pad, - pad, pad) + return ( + Bbox.from_bounds( + 0, 0, abs(bb.width * self.zoom), abs(bb.height * self.zoom)) + .padded(pad)) class BboxPatch(Patch): - @docstring.dedent_interpd + @_docstring.dedent_interpd def __init__(self, bbox, **kwargs): """ Patch showing the shape bounded by a Bbox. Parameters ---------- - bbox : `matplotlib.transforms.Bbox` + bbox : `~matplotlib.transforms.Bbox` Bbox to use for the extents of this patch. **kwargs Patch properties. Valid arguments include: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ if "transform" in kwargs: raise ValueError("transform should not be set") @@ -160,32 +152,15 @@ def __init__(self, bbox, **kwargs): def get_path(self): # docstring inherited x0, y0, x1, y1 = self.bbox.extents - return Path([(x0, y0), (x1, y0), (x1, y1), (x0, y1), (x0, y0)], - closed=True) + return Path._create_closed([(x0, y0), (x1, y0), (x1, y1), (x0, y1)]) class BboxConnector(Patch): @staticmethod def get_bbox_edge_pos(bbox, loc): """ - Helper function to obtain the location of a corner of a bbox - - Parameters - ---------- - bbox : `matplotlib.transforms.Bbox` - - loc : {1, 2, 3, 4} - Corner of *bbox*. Valid values are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4 - - Returns - ------- - x, y : float - Coordinates of the corner specified by *loc*. + Return the ``(x, y)`` coordinates of corner *loc* of *bbox*; parameters + behave as documented for the `.BboxConnector` constructor. """ x0, y0, x1, y1 = bbox.extents if loc == 1: @@ -200,35 +175,9 @@ def get_bbox_edge_pos(bbox, loc): @staticmethod def connect_bbox(bbox1, bbox2, loc1, loc2=None): """ - Helper function to obtain a Path from one bbox to another. - - Parameters - ---------- - bbox1, bbox2 : `matplotlib.transforms.Bbox` - Bounding boxes to connect. - - loc1 : {1, 2, 3, 4} - Corner of *bbox1* to use. Valid values are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4 - - loc2 : {1, 2, 3, 4}, optional - Corner of *bbox2* to use. If None, defaults to *loc1*. - Valid values are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4 - - Returns - ------- - path : `matplotlib.path.Path` - A line segment from the *loc1* corner of *bbox1* to the *loc2* - corner of *bbox2*. + Construct a `.Path` connecting corner *loc1* of *bbox1* to corner + *loc2* of *bbox2*, where parameters behave as documented as for the + `.BboxConnector` constructor. """ if isinstance(bbox1, Rectangle): bbox1 = TransformedBbox(Bbox.unit(), bbox1.get_transform()) @@ -240,37 +189,30 @@ def connect_bbox(bbox1, bbox2, loc1, loc2=None): x2, y2 = BboxConnector.get_bbox_edge_pos(bbox2, loc2) return Path([[x1, y1], [x2, y2]]) - @docstring.dedent_interpd + @_docstring.dedent_interpd def __init__(self, bbox1, bbox2, loc1, loc2=None, **kwargs): """ Connect two bboxes with a straight line. Parameters ---------- - bbox1, bbox2 : `matplotlib.transforms.Bbox` + bbox1, bbox2 : `~matplotlib.transforms.Bbox` Bounding boxes to connect. - loc1 : {1, 2, 3, 4} - Corner of *bbox1* to draw the line. Valid values are:: + loc1, loc2 : {1, 2, 3, 4} + Corner of *bbox1* and *bbox2* to draw the line. Valid values are:: 'upper right' : 1, 'upper left' : 2, 'lower left' : 3, 'lower right' : 4 - loc2 : {1, 2, 3, 4}, optional - Corner of *bbox2* to draw the line. If None, defaults to *loc1*. - Valid values are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4 + *loc2* is optional and defaults to *loc1*. **kwargs Patch properties for the line drawn. Valid arguments include: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ if "transform" in kwargs: raise ValueError("transform should not be set") @@ -293,7 +235,7 @@ def get_path(self): class BboxConnectorPatch(BboxConnector): - @docstring.dedent_interpd + @_docstring.dedent_interpd def __init__(self, bbox1, bbox2, loc1a, loc2a, loc1b, loc2b, **kwargs): """ Connect two bboxes with a quadrilateral. @@ -305,21 +247,13 @@ def __init__(self, bbox1, bbox2, loc1a, loc2a, loc1b, loc2b, **kwargs): Parameters ---------- - bbox1, bbox2 : `matplotlib.transforms.Bbox` + bbox1, bbox2 : `~matplotlib.transforms.Bbox` Bounding boxes to connect. - loc1a, loc2a : {1, 2, 3, 4} - Corners of *bbox1* and *bbox2* to draw the first line. - Valid values are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4 - - loc1b, loc2b : {1, 2, 3, 4} - Corners of *bbox1* and *bbox2* to draw the second line. - Valid values are:: + loc1a, loc2a, loc1b, loc2b : {1, 2, 3, 4} + The first line connects corners *loc1a* of *bbox1* and *loc2a* of + *bbox2*; the second line connects corners *loc1b* of *bbox1* and + *loc2b* of *bbox2*. Valid values are:: 'upper right' : 1, 'upper left' : 2, @@ -329,7 +263,7 @@ def __init__(self, bbox1, bbox2, loc1a, loc2a, loc1b, loc2b, **kwargs): **kwargs Patch properties for the line drawn: - %(Patch_kwdoc)s + %(Patch:kwdoc)s """ if "transform" in kwargs: raise ValueError("transform should not be set") @@ -347,16 +281,15 @@ def get_path(self): def _add_inset_axes(parent_axes, inset_axes): - """Helper function to add an inset axes and disable navigation in it""" + """Helper function to add an inset axes and disable navigation in it.""" parent_axes.figure.add_axes(inset_axes) inset_axes.set_navigate(False) -@docstring.dedent_interpd +@_docstring.dedent_interpd def inset_axes(parent_axes, width, height, loc='upper right', bbox_to_anchor=None, bbox_transform=None, - axes_class=None, - axes_kwargs=None, + axes_class=None, axes_kwargs=None, borderpad=0.5): """ Create an inset axes with a given width and height. @@ -364,7 +297,7 @@ def inset_axes(parent_axes, width, height, loc='upper right', Both sizes used can be specified either in inches or percentage. For example,:: - inset_axes(parent_axes, width='40%%', height='30%%', loc=3) + inset_axes(parent_axes, width='40%%', height='30%%', loc='lower left') creates in inset axes in the lower left corner of *parent_axes* which spans over 30%% in height and 40%% in width of the *parent_axes*. Since the usage @@ -401,24 +334,18 @@ def inset_axes(parent_axes, width, height, loc='upper right', the size in inches, e.g. *width=1.3*. If a string is provided, it is the size in relative units, e.g. *width='40%%'*. By default, i.e. if neither *bbox_to_anchor* nor *bbox_transform* are specified, those - are relative to the parent_axes. Otherwise they are to be understood + 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, default: 1 - Location to place the inset axes. The valid locations are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 - - bbox_to_anchor : tuple or `matplotlib.transforms.BboxBase`, optional + loc : str, default: 'upper right' + Location to place the inset axes. Valid locations are + 'upper left', 'upper center', 'upper right', + 'center left', 'center', 'center right', + 'lower left', 'lower center', 'lower right'. + For backward compatibility, numeric values are accepted as well. + See the parameter *loc* of `.Legend` for details. + + bbox_to_anchor : tuple or `~matplotlib.transforms.BboxBase`, optional Bbox that the inset axes will be anchored to. If None, a tuple of (0, 0, 1, 1) is used if *bbox_transform* is set to *parent_axes.transAxes* or *parent_axes.figure.transFigure*. @@ -432,7 +359,7 @@ def inset_axes(parent_axes, width, height, loc='upper right', a *bbox_transform*. This might often be the axes transform *parent_axes.transAxes*. - bbox_transform : `matplotlib.transforms.Transform`, optional + bbox_transform : `~matplotlib.transforms.Transform`, optional Transformation for the bbox that contains the inset axes. If None, a `.transforms.IdentityTransform` is used. The value of *bbox_to_anchor* (or the return value of its get_points method) @@ -441,15 +368,14 @@ def inset_axes(parent_axes, width, height, loc='upper right', You may provide *bbox_to_anchor* in some normalized coordinate, and give an appropriate transform (e.g., *parent_axes.transAxes*). - axes_class : `matplotlib.axes.Axes` type, optional - If specified, the inset axes created will be created with this class's - constructor. + axes_class : `~matplotlib.axes.Axes` type, default: `.HostAxes` + The type of the newly created inset axes. axes_kwargs : dict, optional - Keyworded arguments to pass to the constructor of the inset axes. + Keyword arguments to pass to the constructor of the inset axes. Valid arguments include: - %(Axes_kwdoc)s + %(Axes:kwdoc)s borderpad : float, default: 0.5 Padding between inset axes and the bbox_to_anchor. @@ -464,12 +390,10 @@ def inset_axes(parent_axes, width, height, loc='upper right', if axes_class is None: axes_class = HostAxes - if axes_kwargs is None: - inset_axes = axes_class(parent_axes.figure, parent_axes.get_position()) - else: - inset_axes = axes_class(parent_axes.figure, parent_axes.get_position(), - **axes_kwargs) + axes_kwargs = {} + inset_axes = axes_class(parent_axes.figure, parent_axes.get_position(), + **axes_kwargs) if bbox_transform in [parent_axes.transAxes, parent_axes.figure.transFigure]: @@ -502,11 +426,10 @@ def inset_axes(parent_axes, width, height, loc='upper right', return inset_axes -@docstring.dedent_interpd +@_docstring.dedent_interpd def zoomed_inset_axes(parent_axes, zoom, loc='upper right', bbox_to_anchor=None, bbox_transform=None, - axes_class=None, - axes_kwargs=None, + axes_class=None, axes_kwargs=None, borderpad=0.5): """ Create an anchored inset axes by scaling a parent axes. For usage, also see @@ -514,29 +437,23 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', Parameters ---------- - parent_axes : `matplotlib.axes.Axes` + parent_axes : `~matplotlib.axes.Axes` Axes to place the inset axes. zoom : float - Scaling factor of the data axes. *zoom* > 1 will enlargen the + Scaling factor of the data axes. *zoom* > 1 will enlarge the coordinates (i.e., "zoomed in"), while *zoom* < 1 will shrink the coordinates (i.e., "zoomed out"). - loc : int or str, default: 'upper right' - Location to place the inset axes. The valid locations are:: - - 'upper right' : 1, - 'upper left' : 2, - 'lower left' : 3, - 'lower right' : 4, - 'right' : 5, - 'center left' : 6, - 'center right' : 7, - 'lower center' : 8, - 'upper center' : 9, - 'center' : 10 - - bbox_to_anchor : tuple or `matplotlib.transforms.BboxBase`, optional + loc : str, default: 'upper right' + Location to place the inset axes. Valid locations are + 'upper left', 'upper center', 'upper right', + 'center left', 'center', 'center right', + 'lower left', 'lower center', 'lower right'. + For backward compatibility, numeric values are accepted as well. + See the parameter *loc* of `.Legend` for details. + + bbox_to_anchor : tuple or `~matplotlib.transforms.BboxBase`, optional Bbox that the inset axes will be anchored to. If None, *parent_axes.bbox* is used. If a tuple, can be either [left, bottom, width, height], or [left, bottom]. @@ -547,7 +464,7 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', also specify a *bbox_transform*. This might often be the axes transform *parent_axes.transAxes*. - bbox_transform : `matplotlib.transforms.Transform`, optional + bbox_transform : `~matplotlib.transforms.Transform`, optional Transformation for the bbox that contains the inset axes. If None, a `.transforms.IdentityTransform` is used (i.e. pixel coordinates). This is useful when not providing any argument to @@ -558,15 +475,14 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', *bbox_to_anchor* will use *parent_axes.bbox*, the units of which are in display (pixel) coordinates. - axes_class : `matplotlib.axes.Axes` type, optional - If specified, the inset axes created will be created with this class's - constructor. + axes_class : `~matplotlib.axes.Axes` type, default: `.HostAxes` + The type of the newly created inset axes. axes_kwargs : dict, optional - Keyworded arguments to pass to the constructor of the inset axes. + Keyword arguments to pass to the constructor of the inset axes. Valid arguments include: - %(Axes_kwdoc)s + %(Axes:kwdoc)s borderpad : float, default: 0.5 Padding between inset axes and the bbox_to_anchor. @@ -581,12 +497,10 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', if axes_class is None: axes_class = HostAxes - if axes_kwargs is None: - inset_axes = axes_class(parent_axes.figure, parent_axes.get_position()) - else: - inset_axes = axes_class(parent_axes.figure, parent_axes.get_position(), - **axes_kwargs) + axes_kwargs = {} + inset_axes = axes_class(parent_axes.figure, parent_axes.get_position(), + **axes_kwargs) axes_locator = AnchoredZoomLocator(parent_axes, zoom=zoom, loc=loc, bbox_to_anchor=bbox_to_anchor, @@ -599,7 +513,23 @@ def zoomed_inset_axes(parent_axes, zoom, loc='upper right', return inset_axes -@docstring.dedent_interpd +class _TransformedBboxWithCallback(TransformedBbox): + """ + Variant of `.TransformBbox` which calls *callback* before returning points. + + Used by `.mark_inset` to unstale the parent axes' viewlim as needed. + """ + + def __init__(self, *args, callback, **kwargs): + super().__init__(*args, **kwargs) + self._callback = callback + + def get_points(self): + self._callback() + return super().get_points() + + +@_docstring.dedent_interpd def mark_inset(parent_axes, inset_axes, loc1, loc2, **kwargs): """ Draw a box to mark the location of an area represented by an inset axes. @@ -610,10 +540,10 @@ def mark_inset(parent_axes, inset_axes, loc1, loc2, **kwargs): Parameters ---------- - parent_axes : `matplotlib.axes.Axes` + parent_axes : `~matplotlib.axes.Axes` Axes which contains the area of the inset axes. - inset_axes : `matplotlib.axes.Axes` + inset_axes : `~matplotlib.axes.Axes` The inset axes. loc1, loc2 : {1, 2, 3, 4} @@ -623,17 +553,19 @@ def mark_inset(parent_axes, inset_axes, loc1, loc2, **kwargs): **kwargs Patch properties for the lines and box drawn: - %(Patch_kwdoc)s + %(Patch:kwdoc)s Returns ------- - pp : `matplotlib.patches.Patch` + pp : `~matplotlib.patches.Patch` The patch drawn to represent the area of the inset axes. - p1, p2 : `matplotlib.patches.Patch` + p1, p2 : `~matplotlib.patches.Patch` The patches connecting two corners of the inset axes and its area. """ - rect = TransformedBbox(inset_axes.viewLim, parent_axes.transData) + rect = _TransformedBboxWithCallback( + inset_axes.viewLim, parent_axes.transData, + callback=parent_axes._unstale_viewLim) if 'fill' in kwargs: pp = BboxPatch(rect, **kwargs) diff --git a/lib/mpl_toolkits/axes_grid1/mpl_axes.py b/lib/mpl_toolkits/axes_grid1/mpl_axes.py index 3bb73770a07b..51c8748758cb 100644 --- a/lib/mpl_toolkits/axes_grid1/mpl_axes.py +++ b/lib/mpl_toolkits/axes_grid1/mpl_axes.py @@ -40,9 +40,14 @@ def __getitem__(self, k): def __call__(self, *v, **kwargs): return maxes.Axes.axis(self.axes, *v, **kwargs) - def _init_axis_artists(self, axes=None): - if axes is None: - axes = self + @property + def axis(self): + return self._axislines + + def clear(self): + # docstring inherited + super().clear() + # Init axis artists. self._axislines = self.AxisDict(self) self._axislines.update( bottom=SimpleAxisArtist(self.xaxis, 1, self.spines["bottom"]), @@ -50,14 +55,6 @@ def _init_axis_artists(self, axes=None): left=SimpleAxisArtist(self.yaxis, 1, self.spines["left"]), right=SimpleAxisArtist(self.yaxis, 2, self.spines["right"])) - @property - def axis(self): - return self._axislines - - def cla(self): - super().cla() - self._init_axis_artists() - class SimpleAxisArtist(Artist): def __init__(self, axis, axisnum, spine): @@ -115,14 +112,11 @@ def toggle(self, all=None, ticks=None, ticklabels=None, label=None): if label is not None: _label = label - tickOn = "tick%dOn" % self._axisnum - labelOn = "label%dOn" % self._axisnum - if _ticks is not None: - tickparam = {tickOn: _ticks} + tickparam = {f"tick{self._axisnum}On": _ticks} self._axis.set_tick_params(**tickparam) if _ticklabels is not None: - tickparam = {labelOn: _ticklabels} + tickparam = {f"label{self._axisnum}On": _ticklabels} self._axis.set_tick_params(**tickparam) if _label is not None: diff --git a/lib/mpl_toolkits/axes_grid1/parasite_axes.py b/lib/mpl_toolkits/axes_grid1/parasite_axes.py index b7ee444077f7..b8781a18c22b 100644 --- a/lib/mpl_toolkits/axes_grid1/parasite_axes.py +++ b/lib/mpl_toolkits/axes_grid1/parasite_axes.py @@ -1,9 +1,6 @@ -import functools - -from matplotlib import _api +from matplotlib import _api, cbook import matplotlib.artist as martist import matplotlib.transforms as mtransforms -from matplotlib.axes import subplot_class_factory from matplotlib.transforms import Bbox from .mpl_axes import Axes @@ -18,15 +15,14 @@ def __init__(self, parent_axes, aux_transform=None, kwargs["frameon"] = False super().__init__(parent_axes.figure, parent_axes._position, **kwargs) - def cla(self): - super().cla() + def clear(self): + super().clear() martist.setp(self.get_children(), visible=False) self._get_lines = self._parent_axes._get_lines - - def get_images_artists(self): - artists = {a for a in self.get_children() if a.get_visible()} - images = {a for a in self.images if a.get_visible()} - return list(images), list(artists - images) + self._parent_axes.callbacks._connect_picklable( + "xlim_changed", self._sync_lims) + self._parent_axes.callbacks._connect_picklable( + "ylim_changed", self._sync_lims) def pick(self, mouseevent): # This most likely goes to Artist.pick (depending on axes_class given @@ -60,179 +56,92 @@ def set_viewlim_mode(self, mode): def get_viewlim_mode(self): return self._viewlim_mode - @_api.deprecated("3.4", alternative="apply_aspect") - def update_viewlim(self): - return self._update_viewlim - - def _update_viewlim(self): # Inline after deprecation elapses. - viewlim = self._parent_axes.viewLim.frozen() + def _sync_lims(self, parent): + viewlim = parent.viewLim.frozen() mode = self.get_viewlim_mode() if mode is None: pass elif mode == "equal": - self.axes.viewLim.set(viewlim) + self.viewLim.set(viewlim) elif mode == "transform": - self.axes.viewLim.set( - viewlim.transformed(self.transAux.inverted())) + self.viewLim.set(viewlim.transformed(self.transAux.inverted())) else: _api.check_in_list([None, "equal", "transform"], mode=mode) - def apply_aspect(self, position=None): - self._update_viewlim() - super().apply_aspect() - # end of aux_transform support -@functools.lru_cache(None) -def parasite_axes_class_factory(axes_class=None): - if axes_class is None: - _api.warn_deprecated( - "3.3", message="Support for passing None to " - "parasite_axes_class_factory is deprecated since %(since)s and " - "will be removed %(removal)s; explicitly pass the default Axes " - "class instead.") - axes_class = Axes - - return type("%sParasite" % axes_class.__name__, - (ParasiteAxesBase, axes_class), {}) - - +parasite_axes_class_factory = cbook._make_class_factory( + ParasiteAxesBase, "{}Parasite") ParasiteAxes = parasite_axes_class_factory(Axes) -@_api.deprecated("3.4", alternative="ParasiteAxesBase") -class ParasiteAxesAuxTransBase: - def __init__(self, parent_axes, aux_transform, viewlim_mode=None, - **kwargs): - # Explicit wrapper for deprecation to work. - super().__init__(parent_axes, aux_transform, - viewlim_mode=viewlim_mode, **kwargs) - - def _set_lim_and_transforms(self): - self.transAxes = self._parent_axes.transAxes - self.transData = self.transAux + self._parent_axes.transData - self._xaxis_transform = mtransforms.blended_transform_factory( - self.transData, self.transAxes) - self._yaxis_transform = mtransforms.blended_transform_factory( - self.transAxes, self.transData) - - def set_viewlim_mode(self, mode): - _api.check_in_list([None, "equal", "transform"], mode=mode) - self._viewlim_mode = mode - - def get_viewlim_mode(self): - return self._viewlim_mode - - @_api.deprecated("3.4", alternative="apply_aspect") - def update_viewlim(self): - return self._update_viewlim() - - def _update_viewlim(self): # Inline after deprecation elapses. - viewlim = self._parent_axes.viewLim.frozen() - mode = self.get_viewlim_mode() - if mode is None: - pass - elif mode == "equal": - self.axes.viewLim.set(viewlim) - elif mode == "transform": - self.axes.viewLim.set( - viewlim.transformed(self.transAux.inverted())) - else: - _api.check_in_list([None, "equal", "transform"], mode=mode) - - def apply_aspect(self, position=None): - self._update_viewlim() - super().apply_aspect() - - -@_api.deprecated("3.4", alternative="parasite_axes_class_factory") -@functools.lru_cache(None) -def parasite_axes_auxtrans_class_factory(axes_class=None): - if axes_class is None: - _api.warn_deprecated( - "3.3", message="Support for passing None to " - "parasite_axes_auxtrans_class_factory is deprecated since " - "%(since)s and will be removed %(removal)s; explicitly pass the " - "default ParasiteAxes class instead.") - parasite_axes_class = ParasiteAxes - elif not issubclass(axes_class, ParasiteAxesBase): - parasite_axes_class = parasite_axes_class_factory(axes_class) - else: - parasite_axes_class = axes_class - return type("%sParasiteAuxTrans" % parasite_axes_class.__name__, - (ParasiteAxesAuxTransBase, parasite_axes_class), - {'name': 'parasite_axes'}) - - -# Also deprecated. -with _api.suppress_matplotlib_deprecation_warning(): - ParasiteAxesAuxTrans = parasite_axes_auxtrans_class_factory(ParasiteAxes) - - class HostAxesBase: def __init__(self, *args, **kwargs): self.parasites = [] super().__init__(*args, **kwargs) - def get_aux_axes(self, tr=None, viewlim_mode="equal", axes_class=Axes): + def get_aux_axes( + self, tr=None, viewlim_mode="equal", axes_class=None, **kwargs): """ Add a parasite axes to this host. Despite this method's name, this should actually be thought of as an ``add_parasite_axes`` method. - *tr* may be `.Transform`, in which case the following relation will - hold: ``parasite.transData = tr + host.transData``. Alternatively, it - may be None (the default), no special relationship will hold between - the parasite's and the host's ``transData``. + .. versionchanged:: 3.7 + Defaults to same base axes class as host axes. + + Parameters + ---------- + tr : `~matplotlib.transforms.Transform` or None, default: None + If a `.Transform`, the following relation will hold: + ``parasite.transData = tr + host.transData``. + If None, the parasite's and the host's ``transData`` are unrelated. + viewlim_mode : {"equal", "transform", None}, default: "equal" + How the parasite's view limits are set: directly equal to the + parent axes ("equal"), equal after application of *tr* + ("transform"), or independently (None). + axes_class : subclass type of `~matplotlib.axes.Axes`, optional + The `~.axes.Axes` subclass that is instantiated. If None, the base + class of the host axes is used. + kwargs + Other parameters are forwarded to the parasite axes constructor. """ + if axes_class is None: + axes_class = self._base_axes_class parasite_axes_class = parasite_axes_class_factory(axes_class) - ax2 = parasite_axes_class(self, tr, viewlim_mode=viewlim_mode) + ax2 = parasite_axes_class( + self, tr, viewlim_mode=viewlim_mode, **kwargs) # note that ax2.transData == tr + ax1.transData # Anything you draw in ax2 will match the ticks and grids of ax1. self.parasites.append(ax2) ax2._remove_method = self.parasites.remove return ax2 - def _get_legend_handles(self, legend_handler_map=None): - all_handles = super()._get_legend_handles() - for ax in self.parasites: - all_handles.extend(ax._get_legend_handles(legend_handler_map)) - return all_handles - def draw(self, renderer): + orig_children_len = len(self._children) - orig_artists = list(self.artists) - orig_images = list(self.images) - - if hasattr(self, "get_axes_locator"): - locator = self.get_axes_locator() - if locator: - pos = locator(self, renderer) - self.set_position(pos, which="active") - self.apply_aspect(pos) - else: - self.apply_aspect() + locator = self.get_axes_locator() + if locator: + pos = locator(self, renderer) + self.set_position(pos, which="active") + self.apply_aspect(pos) else: self.apply_aspect() rect = self.get_position() - for ax in self.parasites: ax.apply_aspect(rect) - images, artists = ax.get_images_artists() - self.images.extend(images) - self.artists.extend(artists) + self._children.extend(ax.get_children()) super().draw(renderer) - self.artists = orig_artists - self.images = orig_images + del self._children[orig_children_len:] - def cla(self): + def clear(self): + super().clear() for ax in self.parasites: - ax.cla() - super().cla() + ax.clear() def pick(self, mouseevent): super().pick(mouseevent) @@ -290,7 +199,7 @@ def _add_twin_axes(self, axes_class, **kwargs): *kwargs* are forwarded to the parasite axes constructor. """ if axes_class is None: - axes_class = self._get_base_axes() + axes_class = self._base_axes_class ax = parasite_axes_class_factory(axes_class)(self, **kwargs) self.parasites.append(ax) ax._remove_method = self._remove_any_twin @@ -306,7 +215,7 @@ def _remove_any_twin(self, ax): self.axis[tuple(restore)].set_visible(True) self.axis[tuple(restore)].toggle(ticklabels=False, label=False) - def get_tightbbox(self, renderer, call_axes_locator=True, + def get_tightbbox(self, renderer=None, call_axes_locator=True, bbox_extra_artists=None): bbs = [ *[ax.get_tightbbox(renderer, call_axes_locator=call_axes_locator) @@ -317,31 +226,9 @@ def get_tightbbox(self, renderer, call_axes_locator=True, return Bbox.union([b for b in bbs if b.width != 0 or b.height != 0]) -@functools.lru_cache(None) -def host_axes_class_factory(axes_class=None): - if axes_class is None: - _api.warn_deprecated( - "3.3", message="Support for passing None to host_axes_class is " - "deprecated since %(since)s and will be removed %(removed)s; " - "explicitly pass the default Axes class instead.") - axes_class = Axes - - def _get_base_axes(self): - return axes_class - - return type("%sHostAxes" % axes_class.__name__, - (HostAxesBase, axes_class), - {'_get_base_axes': _get_base_axes}) - - -def host_subplot_class_factory(axes_class): - host_axes_class = host_axes_class_factory(axes_class) - subplot_host_class = subplot_class_factory(host_axes_class) - return subplot_host_class - - -HostAxes = host_axes_class_factory(Axes) -SubplotHost = subplot_class_factory(HostAxes) +host_axes_class_factory = host_subplot_class_factory = \ + cbook._make_class_factory(HostAxesBase, "{}HostAxes", "_base_axes_class") +HostAxes = SubplotHost = host_axes_class_factory(Axes) def host_axes(*args, axes_class=Axes, figure=None, **kwargs): @@ -350,12 +237,12 @@ def host_axes(*args, axes_class=Axes, figure=None, **kwargs): Parameters ---------- - figure : `matplotlib.figure.Figure` + figure : `~matplotlib.figure.Figure` Figure to which the axes will be added. Defaults to the current figure `.pyplot.gcf()`. *args, **kwargs - Will be passed on to the underlying ``Axes`` object creation. + Will be passed on to the underlying `~.axes.Axes` object creation. """ import matplotlib.pyplot as plt host_axes_class = host_axes_class_factory(axes_class) @@ -363,28 +250,7 @@ def host_axes(*args, axes_class=Axes, figure=None, **kwargs): figure = plt.gcf() ax = host_axes_class(figure, *args, **kwargs) figure.add_axes(ax) - plt.draw_if_interactive() return ax -def host_subplot(*args, axes_class=Axes, figure=None, **kwargs): - """ - Create a subplot that can act as a host to parasitic axes. - - Parameters - ---------- - figure : `matplotlib.figure.Figure` - Figure to which the subplot will be added. Defaults to the current - figure `.pyplot.gcf()`. - - *args, **kwargs - Will be passed on to the underlying ``Axes`` object creation. - """ - import matplotlib.pyplot as plt - host_subplot_class = host_subplot_class_factory(axes_class) - if figure is None: - figure = plt.gcf() - ax = host_subplot_class(figure, *args, **kwargs) - figure.add_subplot(ax) - plt.draw_if_interactive() - return ax +host_subplot = host_axes diff --git a/lib/mpl_toolkits/tests/__init__.py b/lib/mpl_toolkits/axes_grid1/tests/__init__.py similarity index 100% rename from lib/mpl_toolkits/tests/__init__.py rename to lib/mpl_toolkits/axes_grid1/tests/__init__.py diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/anchored_direction_arrows.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/anchored_direction_arrows.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/anchored_direction_arrows.png rename to lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/anchored_direction_arrows.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/anchored_direction_arrows_many_args.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/anchored_direction_arrows_many_args.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/anchored_direction_arrows_many_args.png rename to lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/anchored_direction_arrows_many_args.png diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/anchored_locator_base_call.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/anchored_locator_base_call.png new file mode 100644 index 000000000000..31c63d7df718 Binary files /dev/null and b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/anchored_locator_base_call.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/fill_facecolor.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/fill_facecolor.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/fill_facecolor.png rename to lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/fill_facecolor.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/image_grid.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/image_grid.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/image_grid.png rename to lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/image_grid.png diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/image_grid_each_left_label_mode_all.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/image_grid_each_left_label_mode_all.png new file mode 100644 index 000000000000..23abe8b9649d Binary files /dev/null and b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/image_grid_each_left_label_mode_all.png differ diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/image_grid_single_bottom_label_mode_1.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/image_grid_single_bottom_label_mode_1.png new file mode 100644 index 000000000000..1a0f4cd1fc9a Binary files /dev/null and b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/image_grid_single_bottom_label_mode_1.png differ diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/imagegrid_cbar_mode.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/imagegrid_cbar_mode.png new file mode 100644 index 000000000000..eb16727ed407 Binary files /dev/null and b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/imagegrid_cbar_mode.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/inset_axes.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inset_axes.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/inset_axes.png rename to lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inset_axes.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/inset_locator.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inset_locator.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/inset_locator.png rename to lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inset_locator.png diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png new file mode 100644 index 000000000000..e8676cfd6c95 Binary files /dev/null and b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/insetposition.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/inverted_zoomed_axes.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inverted_zoomed_axes.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/inverted_zoomed_axes.png rename to lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/inverted_zoomed_axes.png diff --git a/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/rgb_axes.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/rgb_axes.png new file mode 100644 index 000000000000..5cf6dc7e35c0 Binary files /dev/null and b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/rgb_axes.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/twin_axes_empty_and_removed.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/twin_axes_empty_and_removed.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/twin_axes_empty_and_removed.png rename to lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/twin_axes_empty_and_removed.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/zoomed_axes.png b/lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/zoomed_axes.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axes_grid1/zoomed_axes.png rename to lib/mpl_toolkits/axes_grid1/tests/baseline_images/test_axes_grid1/zoomed_axes.png diff --git a/lib/mpl_toolkits/axes_grid1/tests/conftest.py b/lib/mpl_toolkits/axes_grid1/tests/conftest.py new file mode 100644 index 000000000000..61c2de3e07ba --- /dev/null +++ b/lib/mpl_toolkits/axes_grid1/tests/conftest.py @@ -0,0 +1,2 @@ +from matplotlib.testing.conftest import (mpl_test_settings, # noqa + pytest_configure, pytest_unconfigure) diff --git a/lib/mpl_toolkits/tests/test_axes_grid1.py b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py similarity index 60% rename from lib/mpl_toolkits/tests/test_axes_grid1.py rename to lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py index 0e1602e81212..8b84f0c4e671 100644 --- a/lib/mpl_toolkits/tests/test_axes_grid1.py +++ b/lib/mpl_toolkits/axes_grid1/tests/test_axes_grid1.py @@ -1,22 +1,29 @@ from itertools import product import platform -import matplotlib +import matplotlib as mpl import matplotlib.pyplot as plt -from matplotlib import cbook +import matplotlib.ticker as mticker +from matplotlib import _api, cbook from matplotlib.backend_bases import MouseEvent from matplotlib.colors import LogNorm from matplotlib.transforms import Bbox, TransformedBbox from matplotlib.testing.decorators import ( - image_comparison, remove_ticks_and_titles) + check_figures_equal, image_comparison, remove_ticks_and_titles) from mpl_toolkits.axes_grid1 import ( - axes_size as Size, host_subplot, make_axes_locatable, AxesGrid, ImageGrid) + axes_size as Size, + host_subplot, make_axes_locatable, + Grid, AxesGrid, ImageGrid) from mpl_toolkits.axes_grid1.anchored_artists import ( AnchoredSizeBar, AnchoredDirectionArrows) -from mpl_toolkits.axes_grid1.axes_divider import HBoxDivider +from mpl_toolkits.axes_grid1.axes_divider import ( + Divider, HBoxDivider, make_axes_area_auto_adjustable, SubplotDivider, + VBoxDivider) +from mpl_toolkits.axes_grid1.axes_rgb import RGBAxes from mpl_toolkits.axes_grid1.inset_locator import ( - zoomed_inset_axes, mark_inset, inset_axes, BboxConnectorPatch) + zoomed_inset_axes, mark_inset, inset_axes, BboxConnectorPatch, + InsetPosition) import mpl_toolkits.axes_grid1.mpl_axes import pytest @@ -36,7 +43,6 @@ def test_divider_append_axes(): "right": divider.append_axes("right", 1.2, pad=0.1, sharey=ax), } fig.canvas.draw() - renderer = fig.canvas.get_renderer() bboxes = {k: axs[k].get_window_extent() for k in axs} dpi = fig.dpi assert bboxes["top"].height == pytest.approx(1.2 * dpi) @@ -53,12 +59,13 @@ def test_divider_append_axes(): assert bboxes["top"].x1 == bboxes["main"].x1 == bboxes["bottom"].x1 -@image_comparison(['twin_axes_empty_and_removed'], extensions=["png"], tol=1) +# Update style when regenerating the test image +@image_comparison(['twin_axes_empty_and_removed'], extensions=["png"], tol=1, + style=('classic', '_classic_test_patch')) def test_twin_axes_empty_and_removed(): # Purely cosmetic font changes (avoid overlap) - matplotlib.rcParams.update({"font.size": 8}) - matplotlib.rcParams.update({"xtick.labelsize": 8}) - matplotlib.rcParams.update({"ytick.labelsize": 8}) + mpl.rcParams.update( + {"font.size": 8, "xtick.labelsize": 8, "ytick.labelsize": 8}) generators = ["twinx", "twiny", "twin"] modifiers = ["", "host invisible", "twin removed", "twin invisible", "twin removed\nhost invisible"] @@ -98,6 +105,18 @@ def test_axesgrid_colorbar_log_smoketest(): grid.cbar_axes[0].colorbar(im) +def test_inset_colorbar_tight_layout_smoketest(): + fig, ax = plt.subplots(1, 1) + pts = ax.scatter([0, 1], [0, 1], c=[1, 5]) + + cax = inset_axes(ax, width="3%", height="70%") + plt.colorbar(pts, cax=cax) + + with pytest.warns(UserWarning, match="This figure includes Axes"): + # Will warn, but not raise an error + plt.tight_layout() + + @image_comparison(['inset_locator.png'], style='default', remove_text=True) def test_inset_locator(): fig, ax = plt.subplots(figsize=[5, 4]) @@ -110,7 +129,6 @@ def test_inset_locator(): ny, nx = Z.shape Z2[30:30+ny, 30:30+nx] = Z - # extent = [-3, 4, -4, 3] ax.imshow(Z2, extent=extent, interpolation="nearest", origin="lower") @@ -152,7 +170,6 @@ def test_inset_axes(): ny, nx = Z.shape Z2[30:30+ny, 30:30+nx] = Z - # extent = [-3, 4, -4, 3] ax.imshow(Z2, extent=extent, interpolation="nearest", origin="lower") @@ -194,23 +211,22 @@ def test_inset_axes_complete(): ins = inset_axes(ax, width=2., height=2., borderpad=0) fig.canvas.draw() assert_array_almost_equal( - ins.get_position().extents, - np.array(((0.9*figsize[0]-2.)/figsize[0], - (0.9*figsize[1]-2.)/figsize[1], 0.9, 0.9))) + ins.get_position().extents, + [(0.9*figsize[0]-2.)/figsize[0], (0.9*figsize[1]-2.)/figsize[1], + 0.9, 0.9]) ins = inset_axes(ax, width="40%", height="30%", borderpad=0) fig.canvas.draw() assert_array_almost_equal( - ins.get_position().extents, - np.array((.9-.8*.4, .9-.8*.3, 0.9, 0.9))) + ins.get_position().extents, [.9-.8*.4, .9-.8*.3, 0.9, 0.9]) ins = inset_axes(ax, width=1., height=1.2, bbox_to_anchor=(200, 100), loc=3, borderpad=0) fig.canvas.draw() assert_array_almost_equal( - ins.get_position().extents, - np.array((200./dpi/figsize[0], 100./dpi/figsize[1], - (200./dpi+1)/figsize[0], (100./dpi+1.2)/figsize[1]))) + ins.get_position().extents, + [200/dpi/figsize[0], 100/dpi/figsize[1], + (200/dpi+1)/figsize[0], (100/dpi+1.2)/figsize[1]]) ins1 = inset_axes(ax, width="35%", height="60%", loc=3, borderpad=1) ins2 = inset_axes(ax, width="100%", height="100%", @@ -307,7 +323,9 @@ def test_fill_facecolor(): mark_inset(ax[3], axins, loc1=2, loc2=4, fc="g", ec="0.5", fill=False) -@image_comparison(['zoomed_axes.png', 'inverted_zoomed_axes.png']) +# Update style when regenerating the test image +@image_comparison(['zoomed_axes.png', 'inverted_zoomed_axes.png'], + style=('classic', '_classic_test_patch')) def test_zooming_with_inverted_axes(): fig, ax = plt.subplots() ax.plot([1, 2, 3], [1, 2, 3]) @@ -322,8 +340,10 @@ def test_zooming_with_inverted_axes(): inset_ax.axis([1.4, 1.1, 1.4, 1.1]) +# Update style when regenerating the test image @image_comparison(['anchored_direction_arrows.png'], - tol=0 if platform.machine() == 'x86_64' else 0.01) + tol=0 if platform.machine() == 'x86_64' else 0.01, + style=('classic', '_classic_test_patch')) def test_anchored_direction_arrows(): fig, ax = plt.subplots() ax.imshow(np.zeros((10, 10)), interpolation='nearest') @@ -332,7 +352,9 @@ def test_anchored_direction_arrows(): ax.add_artist(simple_arrow) -@image_comparison(['anchored_direction_arrows_many_args.png']) +# Update style when regenerating the test image +@image_comparison(['anchored_direction_arrows_many_args.png'], + style=('classic', '_classic_test_patch')) def test_anchored_direction_arrows_many_args(): fig, ax = plt.subplots() ax.imshow(np.ones((10, 10))) @@ -348,12 +370,56 @@ def test_anchored_direction_arrows_many_args(): def test_axes_locatable_position(): fig, ax = plt.subplots() divider = make_axes_locatable(ax) - cax = divider.append_axes('right', size='5%', pad='2%') + with mpl.rc_context({"figure.subplot.wspace": 0.02}): + cax = divider.append_axes('right', size='5%') fig.canvas.draw() assert np.isclose(cax.get_position(original=False).width, 0.03621495327102808) +@image_comparison(['image_grid_each_left_label_mode_all.png'], style='mpl20', + savefig_kwarg={'bbox_inches': 'tight'}) +def test_image_grid_each_left_label_mode_all(): + imdata = np.arange(100).reshape((10, 10)) + + fig = plt.figure(1, (3, 3)) + grid = ImageGrid(fig, (1, 1, 1), nrows_ncols=(3, 2), axes_pad=(0.5, 0.3), + cbar_mode="each", cbar_location="left", cbar_size="15%", + label_mode="all") + # 3-tuple rect => SubplotDivider + assert isinstance(grid.get_divider(), SubplotDivider) + assert grid.get_axes_pad() == (0.5, 0.3) + assert grid.get_aspect() # True by default for ImageGrid + for ax, cax in zip(grid, grid.cbar_axes): + im = ax.imshow(imdata, interpolation='none') + cax.colorbar(im) + + +@image_comparison(['image_grid_single_bottom_label_mode_1.png'], style='mpl20', + savefig_kwarg={'bbox_inches': 'tight'}) +def test_image_grid_single_bottom(): + imdata = np.arange(100).reshape((10, 10)) + + fig = plt.figure(1, (2.5, 1.5)) + grid = ImageGrid(fig, (0, 0, 1, 1), nrows_ncols=(1, 3), + axes_pad=(0.2, 0.15), cbar_mode="single", + cbar_location="bottom", cbar_size="10%", label_mode="1") + # 4-tuple rect => Divider, isinstance will give True for SubplotDivider + assert type(grid.get_divider()) is Divider + for i in range(3): + im = grid[i].imshow(imdata, interpolation='none') + grid.cbar_axes[0].colorbar(im) + + +def test_image_grid_label_mode_deprecation_warning(): + imdata = np.arange(9).reshape((3, 3)) + + fig = plt.figure() + with pytest.warns(_api.MatplotlibDeprecationWarning, + match="Passing an undefined label_mode"): + grid = ImageGrid(fig, (0, 0, 1, 1), (2, 1), label_mode="foo") + + @image_comparison(['image_grid.png'], remove_text=True, style='mpl20', savefig_kwarg={'bbox_inches': 'tight'}) @@ -363,10 +429,9 @@ def test_image_grid(): fig = plt.figure(1, (4, 4)) grid = ImageGrid(fig, 111, nrows_ncols=(2, 2), axes_pad=0.1) - + assert grid.get_axes_pad() == (0.1, 0.1) for i in range(4): grid[i].imshow(im, interpolation='nearest') - grid[i].set_title('test {0}{0}'.format(i)) def test_gettightbbox(): @@ -466,7 +531,208 @@ def test_hbox_divider(): assert p2.width / p1.width == pytest.approx((4 / 5) ** 2) +def test_vbox_divider(): + arr1 = np.arange(20).reshape((4, 5)) + arr2 = np.arange(20).reshape((5, 4)) + + fig, (ax1, ax2) = plt.subplots(1, 2) + ax1.imshow(arr1) + ax2.imshow(arr2) + + pad = 0.5 # inches. + divider = VBoxDivider( + fig, 111, # Position of combined axes. + horizontal=[Size.AxesX(ax1), Size.Scaled(1), Size.AxesX(ax2)], + vertical=[Size.AxesY(ax1), Size.Fixed(pad), Size.AxesY(ax2)]) + ax1.set_axes_locator(divider.new_locator(0)) + ax2.set_axes_locator(divider.new_locator(2)) + + fig.canvas.draw() + p1 = ax1.get_position() + p2 = ax2.get_position() + assert p1.width == p2.width + assert p1.height / p2.height == pytest.approx((4 / 5) ** 2) + + def test_axes_class_tuple(): fig = plt.figure() axes_class = (mpl_toolkits.axes_grid1.mpl_axes.Axes, {}) gr = AxesGrid(fig, 111, nrows_ncols=(1, 1), axes_class=axes_class) + + +def test_grid_axes_lists(): + """Test Grid axes_all, axes_row and axes_column relationship.""" + fig = plt.figure() + grid = Grid(fig, 111, (2, 3), direction="row") + assert_array_equal(grid, grid.axes_all) + assert_array_equal(grid.axes_row, np.transpose(grid.axes_column)) + assert_array_equal(grid, np.ravel(grid.axes_row), "row") + assert grid.get_geometry() == (2, 3) + grid = Grid(fig, 111, (2, 3), direction="column") + assert_array_equal(grid, np.ravel(grid.axes_column), "column") + + +@pytest.mark.parametrize('direction', ('row', 'column')) +def test_grid_axes_position(direction): + """Test positioning of the axes in Grid.""" + fig = plt.figure() + grid = Grid(fig, 111, (2, 2), direction=direction) + loc = [ax.get_axes_locator() for ax in np.ravel(grid.axes_row)] + assert loc[1]._nx > loc[0]._nx and loc[2]._ny < loc[0]._ny + assert loc[0]._nx == loc[2]._nx and loc[0]._ny == loc[1]._ny + assert loc[3]._nx == loc[1]._nx and loc[3]._ny == loc[2]._ny + + +@pytest.mark.parametrize('rect, ngrids, error, message', ( + ((1, 1), None, TypeError, "Incorrect rect format"), + (111, -1, ValueError, "ngrids must be positive"), + (111, 7, ValueError, "ngrids must be positive"), +)) +def test_grid_errors(rect, ngrids, error, message): + fig = plt.figure() + with pytest.raises(error, match=message): + Grid(fig, rect, (2, 3), ngrids=ngrids) + + +@pytest.mark.parametrize('anchor, error, message', ( + (None, TypeError, "anchor must be str"), + ("CC", ValueError, "'CC' is not a valid value for anchor"), + ((1, 1, 1), TypeError, "anchor must be str"), +)) +def test_divider_errors(anchor, error, message): + fig = plt.figure() + with pytest.raises(error, match=message): + Divider(fig, [0, 0, 1, 1], [Size.Fixed(1)], [Size.Fixed(1)], + anchor=anchor) + + +@check_figures_equal(extensions=["png"]) +def test_mark_inset_unstales_viewlim(fig_test, fig_ref): + inset, full = fig_test.subplots(1, 2) + full.plot([0, 5], [0, 5]) + inset.set(xlim=(1, 2), ylim=(1, 2)) + # Check that mark_inset unstales full's viewLim before drawing the marks. + mark_inset(full, inset, 1, 4) + + inset, full = fig_ref.subplots(1, 2) + full.plot([0, 5], [0, 5]) + inset.set(xlim=(1, 2), ylim=(1, 2)) + mark_inset(full, inset, 1, 4) + # Manually unstale the full's viewLim. + fig_ref.canvas.draw() + + +def test_auto_adjustable(): + fig = plt.figure() + ax = fig.add_axes([0, 0, 1, 1]) + pad = 0.1 + make_axes_area_auto_adjustable(ax, pad=pad) + fig.canvas.draw() + tbb = ax.get_tightbbox() + assert tbb.x0 == pytest.approx(pad * fig.dpi) + assert tbb.x1 == pytest.approx(fig.bbox.width - pad * fig.dpi) + assert tbb.y0 == pytest.approx(pad * fig.dpi) + assert tbb.y1 == pytest.approx(fig.bbox.height - pad * fig.dpi) + + +# Update style when regenerating the test image +@image_comparison(['rgb_axes.png'], remove_text=True, + style=('classic', '_classic_test_patch')) +def test_rgb_axes(): + fig = plt.figure() + ax = RGBAxes(fig, (0.1, 0.1, 0.8, 0.8), pad=0.1) + rng = np.random.default_rng(19680801) + r = rng.random((5, 5)) + g = rng.random((5, 5)) + b = rng.random((5, 5)) + ax.imshow_rgb(r, g, b, interpolation='none') + + +# Update style when regenerating the test image +@image_comparison(['insetposition.png'], remove_text=True, + style=('classic', '_classic_test_patch')) +def test_insetposition(): + fig, ax = plt.subplots(figsize=(2, 2)) + ax_ins = plt.axes([0, 0, 1, 1]) + ip = InsetPosition(ax, [0.2, 0.25, 0.5, 0.4]) + ax_ins.set_axes_locator(ip) + + +# The original version of this test relied on mpl_toolkits's slightly different +# colorbar implementation; moving to matplotlib's own colorbar implementation +# caused the small image comparison error. +@image_comparison(['imagegrid_cbar_mode.png'], + remove_text=True, style='mpl20', tol=0.3) +def test_imagegrid_cbar_mode_edge(): + # Remove this line when this test image is regenerated. + plt.rcParams['pcolormesh.snap'] = False + + X, Y = np.meshgrid(np.linspace(0, 6, 30), np.linspace(0, 6, 30)) + arr = np.sin(X) * np.cos(Y) + 1j*(np.sin(3*Y) * np.cos(Y/2.)) + + fig = plt.figure(figsize=(18, 9)) + + positions = (241, 242, 243, 244, 245, 246, 247, 248) + directions = ['row']*4 + ['column']*4 + cbar_locations = ['left', 'right', 'top', 'bottom']*2 + + for position, direction, location in zip( + positions, directions, cbar_locations): + grid = ImageGrid(fig, position, + nrows_ncols=(2, 2), + direction=direction, + cbar_location=location, + cbar_size='20%', + cbar_mode='edge') + ax1, ax2, ax3, ax4, = grid + + ax1.imshow(arr.real, cmap='nipy_spectral') + ax2.imshow(arr.imag, cmap='hot') + ax3.imshow(np.abs(arr), cmap='jet') + ax4.imshow(np.arctan2(arr.imag, arr.real), cmap='hsv') + + # In each row/column, the "first" colorbars must be overwritten by the + # "second" ones. To achieve this, clear out the axes first. + for ax in grid: + ax.cax.cla() + cb = ax.cax.colorbar(ax.images[0]) + + +def test_imagegrid(): + fig = plt.figure() + grid = ImageGrid(fig, 111, nrows_ncols=(1, 1)) + ax = grid[0] + im = ax.imshow([[1, 2]], norm=mpl.colors.LogNorm()) + cb = ax.cax.colorbar(im) + assert isinstance(cb.locator, mticker.LogLocator) + + +def test_removal(): + import matplotlib.pyplot as plt + import mpl_toolkits.axisartist as AA + fig = plt.figure() + ax = host_subplot(111, axes_class=AA.Axes, figure=fig) + col = ax.fill_between(range(5), 0, range(5)) + fig.canvas.draw() + col.remove() + fig.canvas.draw() + + +@image_comparison(['anchored_locator_base_call.png'], style="mpl20") +def test_anchored_locator_base_call(): + fig = plt.figure(figsize=(3, 3)) + fig1, fig2 = fig.subfigures(nrows=2, ncols=1) + + ax = fig1.subplots() + ax.set(aspect=1, xlim=(-15, 15), ylim=(-20, 5)) + ax.set(xticks=[], yticks=[]) + + Z = cbook.get_sample_data( + "axes_grid/bivariate_normal.npy", np_load=True + ) + extent = (-3, 4, -4, 3) + + axins = zoomed_inset_axes(ax, zoom=2, loc="upper left") + axins.set(xticks=[], yticks=[]) + + axins.imshow(Z, extent=extent, origin="lower") diff --git a/lib/mpl_toolkits/axisartist/__init__.py b/lib/mpl_toolkits/axisartist/__init__.py index 0699b0165d84..47242cf7f0c5 100644 --- a/lib/mpl_toolkits/axisartist/__init__.py +++ b/lib/mpl_toolkits/axisartist/__init__.py @@ -1,4 +1,3 @@ -from matplotlib import _api from .axislines import ( Axes, AxesZero, AxisArtistHelper, AxisArtistHelperRectlinear, GridHelperBase, GridHelperRectlinear, Subplot, SubplotZero) @@ -6,12 +5,9 @@ from .grid_helper_curvelinear import GridHelperCurveLinear from .floating_axes import FloatingAxes, FloatingSubplot from mpl_toolkits.axes_grid1.parasite_axes import ( - host_axes_class_factory, parasite_axes_class_factory, - parasite_axes_auxtrans_class_factory, subplot_class_factory) + host_axes_class_factory, parasite_axes_class_factory) ParasiteAxes = parasite_axes_class_factory(Axes) HostAxes = host_axes_class_factory(Axes) -SubplotHost = subplot_class_factory(HostAxes) -with _api.suppress_matplotlib_deprecation_warning(): - ParasiteAxesAuxTrans = parasite_axes_auxtrans_class_factory(ParasiteAxes) +SubplotHost = HostAxes diff --git a/lib/mpl_toolkits/axisartist/angle_helper.py b/lib/mpl_toolkits/axisartist/angle_helper.py index 041bf443b766..1786cd70bcdb 100644 --- a/lib/mpl_toolkits/axisartist/angle_helper.py +++ b/lib/mpl_toolkits/axisartist/angle_helper.py @@ -1,7 +1,6 @@ import numpy as np import math -from matplotlib import _api from mpl_toolkits.axisartist.grid_finder import ExtremeFinderSimple @@ -141,20 +140,10 @@ def select_step360(v1, v2, nv, include_last=True, threshold_factor=3600): class LocatorBase: - @_api.rename_parameter("3.3", "den", "nbins") def __init__(self, nbins, include_last=True): self.nbins = nbins self._include_last = include_last - @_api.deprecated("3.3", alternative="nbins") - @property - def den(self): - return self.nbins - - @den.setter - def den(self, v): - self.nbins = v - def set_params(self, nbins=None): if nbins is not None: self.nbins = int(nbins) @@ -295,7 +284,7 @@ def __call__(self, direction, factor, values): return r else: # factor > 3600. - return [r"$%s^{\circ}$" % (str(v),) for v in ss*values] + return [r"$%s^{\circ}$" % v for v in ss*values] class FormatterHMS(FormatterDMS): diff --git a/lib/mpl_toolkits/axisartist/axes_divider.py b/lib/mpl_toolkits/axisartist/axes_divider.py index 3b177838f896..a01d4e27df93 100644 --- a/lib/mpl_toolkits/axisartist/axes_divider.py +++ b/lib/mpl_toolkits/axisartist/axes_divider.py @@ -1,2 +1,2 @@ -from mpl_toolkits.axes_grid1.axes_divider import ( +from mpl_toolkits.axes_grid1.axes_divider import ( # noqa Divider, AxesLocator, SubplotDivider, AxesDivider, make_axes_locatable) diff --git a/lib/mpl_toolkits/axisartist/axes_grid.py b/lib/mpl_toolkits/axisartist/axes_grid.py index 15c715b72896..27877a238b7d 100644 --- a/lib/mpl_toolkits/axisartist/axes_grid.py +++ b/lib/mpl_toolkits/axisartist/axes_grid.py @@ -2,17 +2,12 @@ from .axislines import Axes -class CbarAxes(axes_grid_orig.CbarAxesBase, Axes): - pass - - class Grid(axes_grid_orig.Grid): _defaultAxesClass = Axes class ImageGrid(axes_grid_orig.ImageGrid): _defaultAxesClass = Axes - _defaultCbarAxesClass = CbarAxes AxesGrid = ImageGrid diff --git a/lib/mpl_toolkits/axisartist/axes_rgb.py b/lib/mpl_toolkits/axisartist/axes_rgb.py index 429843f6f4b9..20c1f7fd233b 100644 --- a/lib/mpl_toolkits/axisartist/axes_rgb.py +++ b/lib/mpl_toolkits/axisartist/axes_rgb.py @@ -1,7 +1,11 @@ -from mpl_toolkits.axes_grid1.axes_rgb import ( - make_rgb_axes, imshow_rgb, RGBAxes as _RGBAxes) +from mpl_toolkits.axes_grid1.axes_rgb import ( # noqa + make_rgb_axes, RGBAxes as _RGBAxes) from .axislines import Axes class RGBAxes(_RGBAxes): + """ + Subclass of `~.axes_grid1.axes_rgb.RGBAxes` with + ``_defaultAxesClass`` = `.axislines.Axes`. + """ _defaultAxesClass = Axes diff --git a/lib/mpl_toolkits/axisartist/axis_artist.py b/lib/mpl_toolkits/axisartist/axis_artist.py index 21d9eff8ea34..74df999ef24e 100644 --- a/lib/mpl_toolkits/axisartist/axis_artist.py +++ b/lib/mpl_toolkits/axisartist/axis_artist.py @@ -43,11 +43,11 @@ ticklabel), which gives 0 for bottom axis. =================== ====== ======== ====== ======== -Parameter left bottom right top +Property left bottom right top =================== ====== ======== ====== ======== -ticklabels location left right right left +ticklabel location left right right left axislabel location left right right left -ticklabels angle 90 0 -90 180 +ticklabel angle 90 0 -90 180 axislabel angle 180 0 0 180 ticklabel va center baseline center baseline axislabel va center top center bottom @@ -75,10 +75,11 @@ import numpy as np -from matplotlib import _api, rcParams +import matplotlib as mpl +from matplotlib import _api, cbook import matplotlib.artist as martist +import matplotlib.colors as mcolors import matplotlib.text as mtext - from matplotlib.collections import LineCollection from matplotlib.lines import Line2D from matplotlib.patches import PathPatch @@ -105,13 +106,13 @@ def get_attribute_from_ref_artist(self, attr_name): class Ticks(AttributeCopier, Line2D): """ - Ticks are derived from Line2D, and note that ticks themselves + Ticks are derived from `.Line2D`, and note that ticks themselves are markers. Thus, you should use set_mec, set_mew, etc. To change the tick size (length), you need to use - set_ticksize. To change the direction of the ticks (ticks are + `set_ticksize`. To change the direction of the ticks (ticks are in opposite direction of ticklabels by default), use - set_tick_out(False). + ``set_tick_out(False)`` """ def __init__(self, ticksize, tick_out=False, *, axis=None, **kwargs): @@ -134,6 +135,14 @@ def get_ref_artist(self): # docstring inherited return self._axis.majorTicks[0].tick1line + def set_color(self, color): + # docstring inherited + # Unlike the base Line2D.set_color, this also supports "auto". + if not cbook._str_equal(color, "auto"): + mcolors._check_color_like(color=color) + self._color = color + self.stale = True + def get_color(self): return self.get_attribute_from_ref_artist("color") @@ -193,8 +202,8 @@ def draw(self, renderer): class LabelBase(mtext.Text): """ - A base class for AxisLabel and TickLabels. The position and angle - of the text are calculated by to offset_ref_angle, + A base class for `.AxisLabel` and `.TickLabels`. The position and + angle of the text are calculated by the offset_ref_angle, text_ref_angle, and offset_radius attributes. """ @@ -208,26 +217,16 @@ def __init__(self, *args, **kwargs): self.set_rotation_mode("anchor") self._text_follow_ref_angle = True - def _set_ref_angle(self, a): - self._ref_angle = a - - def _get_ref_angle(self): - return self._ref_angle - - def _get_text_ref_angle(self): + @property + def _text_ref_angle(self): if self._text_follow_ref_angle: - return self._get_ref_angle()+90 + return self._ref_angle + 90 else: - return 0 # self.get_ref_angle() - - def _get_offset_ref_angle(self): - return self._get_ref_angle() - - def _set_offset_radius(self, offset_radius): - self._offset_radius = offset_radius + return 0 - def _get_offset_radius(self): - return self._offset_radius + @property + def _offset_ref_angle(self): + return self._ref_angle _get_opposite_direction = {"left": "right", "right": "left", @@ -241,31 +240,30 @@ def draw(self, renderer): # save original and adjust some properties tr = self.get_transform() angle_orig = self.get_rotation() - text_ref_angle = self._get_text_ref_angle() - offset_ref_angle = self._get_offset_ref_angle() - theta = np.deg2rad(offset_ref_angle) - dd = self._get_offset_radius() + theta = np.deg2rad(self._offset_ref_angle) + dd = self._offset_radius dx, dy = dd * np.cos(theta), dd * np.sin(theta) self.set_transform(tr + Affine2D().translate(dx, dy)) - self.set_rotation(text_ref_angle+angle_orig) + self.set_rotation(self._text_ref_angle + angle_orig) super().draw(renderer) # restore original properties self.set_transform(tr) self.set_rotation(angle_orig) - def get_window_extent(self, renderer): + def get_window_extent(self, renderer=None): + if renderer is None: + renderer = self.figure._get_renderer() + # save original and adjust some properties tr = self.get_transform() angle_orig = self.get_rotation() - text_ref_angle = self._get_text_ref_angle() - offset_ref_angle = self._get_offset_ref_angle() - theta = np.deg2rad(offset_ref_angle) - dd = self._get_offset_radius() + theta = np.deg2rad(self._offset_ref_angle) + dd = self._offset_radius dx, dy = dd * np.cos(theta), dd * np.sin(theta) self.set_transform(tr + Affine2D().translate(dx, dy)) - self.set_rotation(text_ref_angle+angle_orig) + self.set_rotation(self._text_ref_angle + angle_orig) bbox = super().get_window_extent(renderer).frozen() # restore original properties self.set_transform(tr) @@ -276,17 +274,17 @@ def get_window_extent(self, renderer): class AxisLabel(AttributeCopier, LabelBase): """ - Axis Label. Derived from Text. The position of the text is updated + Axis label. Derived from `.Text`. The position of the text is updated in the fly, so changing text position has no effect. Otherwise, the - properties can be changed as a normal Text. + properties can be changed as a normal `.Text`. - To change the pad between ticklabels and axis label, use set_pad. + To change the pad between tick labels and axis label, use `set_pad`. """ def __init__(self, *args, axis_direction="bottom", axis=None, **kwargs): self._axis = axis self._pad = 5 - self._extra_pad = 0 + self._external_pad = 0 # in pixels LabelBase.__init__(self, *args, **kwargs) self.set_axis_direction(axis_direction) @@ -295,7 +293,12 @@ def set_pad(self, pad): Set the internal pad in points. The actual pad will be the sum of the internal pad and the - external pad (the latter is set automatically by the AxisArtist). + external pad (the latter is set automatically by the `.AxisArtist`). + + Parameters + ---------- + pad : float + The internal pad in points. """ self._pad = pad @@ -307,19 +310,12 @@ def get_pad(self): """ return self._pad - def _set_external_pad(self, p): - """Set external pad in pixels.""" - self._extra_pad = p - - def _get_external_pad(self): - """Return external pad in pixels.""" - return self._extra_pad - def get_ref_artist(self): # docstring inherited return self._axis.get_label() def get_text(self): + # docstring inherited t = super().get_text() if t == "__from_axes__": return self._axis.get_label().get_text() @@ -331,6 +327,13 @@ def get_text(self): top=("bottom", "center")) def set_default_alignment(self, d): + """ + Set the default alignment. See `set_axis_direction` for details. + + Parameters + ---------- + d : {"left", "bottom", "right", "top"} + """ va, ha = _api.check_getitem(self._default_alignments, d=d) self.set_va(va) self.set_ha(ha) @@ -341,6 +344,13 @@ def set_default_alignment(self, d): top=180) def set_default_angle(self, d): + """ + Set the default angle. See `set_axis_direction` for details. + + Parameters + ---------- + d : {"left", "bottom", "right", "top"} + """ self.set_rotation(_api.check_getitem(self._default_angles, d=d)) def set_axis_direction(self, d): @@ -349,7 +359,7 @@ def set_axis_direction(self, d): according to the matplotlib convention. ===================== ========== ========= ========== ========== - property left bottom right top + Property left bottom right top ===================== ========== ========= ========== ========== axislabel angle 180 0 0 180 axislabel va center top center bottom @@ -359,6 +369,10 @@ def set_axis_direction(self, d): Note that the text angles are actually relative to (90 + angle of the direction to the ticklabel), which gives 0 for bottom axis. + + Parameters + ---------- + d : {"left", "bottom", "right", "top"} """ self.set_default_alignment(d) self.set_default_angle(d) @@ -370,19 +384,19 @@ def draw(self, renderer): if not self.get_visible(): return - pad = renderer.points_to_pixels(self.get_pad()) - r = self._get_external_pad() + pad - self._set_offset_radius(r) + self._offset_radius = \ + self._external_pad + renderer.points_to_pixels(self.get_pad()) super().draw(renderer) - def get_window_extent(self, renderer): + def get_window_extent(self, renderer=None): + if renderer is None: + renderer = self.figure._get_renderer() if not self.get_visible(): return - pad = renderer.points_to_pixels(self.get_pad()) - r = self._get_external_pad() + pad - self._set_offset_radius(r) + r = self._external_pad + renderer.points_to_pixels(self.get_pad()) + self._offset_radius = r bb = super().get_window_extent(renderer) @@ -391,14 +405,14 @@ def get_window_extent(self, renderer): class TickLabels(AxisLabel): # mtext.Text """ - Tick Labels. While derived from Text, this single artist draws all - ticklabels. As in AxisLabel, the position of the text is updated + Tick labels. While derived from `.Text`, this single artist draws all + ticklabels. As in `.AxisLabel`, the position of the text is updated in the fly, so changing text position has no effect. Otherwise, - the properties can be changed as a normal Text. Unlike the - ticklabels of the mainline matplotlib, properties of single - ticklabel alone cannot modified. + the properties can be changed as a normal `.Text`. Unlike the + ticklabels of the mainline Matplotlib, properties of a single + ticklabel alone cannot be modified. - To change the pad between ticks and ticklabels, use set_pad. + To change the pad between ticks and ticklabels, use `~.AxisLabel.set_pad`. """ def __init__(self, *, axis_direction="bottom", **kwargs): @@ -413,14 +427,14 @@ def get_ref_artist(self): def set_axis_direction(self, label_direction): """ Adjust the text angle and text alignment of ticklabels - according to the matplotlib convention. + according to the Matplotlib convention. The *label_direction* must be one of [left, right, bottom, top]. ===================== ========== ========= ========== ========== - property left bottom right top + Property left bottom right top ===================== ========== ========= ========== ========== - ticklabels angle 90 0 -90 180 + ticklabel angle 90 0 -90 180 ticklabel va center baseline center baseline ticklabel ha right center right center ===================== ========== ========= ========== ========== @@ -428,6 +442,11 @@ def set_axis_direction(self, label_direction): Note that the text angles are actually relative to (90 + angle of the direction to the ticklabel), which gives 0 for bottom axis. + + Parameters + ---------- + label_direction : {"left", "bottom", "right", "top"} + """ self.set_default_alignment(label_direction) self.set_default_angle(label_direction) @@ -505,20 +524,19 @@ def _get_ticklabels_offsets(self, renderer, label_direction): def draw(self, renderer): if not self.get_visible(): - self._axislabel_pad = self._get_external_pad() + self._axislabel_pad = self._external_pad return r, total_width = self._get_ticklabels_offsets(renderer, self._axis_direction) - pad = (self._get_external_pad() - + renderer.points_to_pixels(self.get_pad())) - self._set_offset_radius(r+pad) + pad = self._external_pad + renderer.points_to_pixels(self.get_pad()) + self._offset_radius = r + pad for (x, y), a, l in self._locs_angles_labels: if not l.strip(): continue - self._set_ref_angle(a) # + add_angle + self._ref_angle = a self.set_x(x) self.set_y(y) self.set_text(l) @@ -530,10 +548,12 @@ def draw(self, renderer): def set_locs_angles_labels(self, locs_angles_labels): self._locs_angles_labels = locs_angles_labels - def get_window_extents(self, renderer): + def get_window_extents(self, renderer=None): + if renderer is None: + renderer = self.figure._get_renderer() if not self.get_visible(): - self._axislabel_pad = self._get_external_pad() + self._axislabel_pad = self._external_pad return [] bboxes = [] @@ -541,12 +561,11 @@ def get_window_extents(self, renderer): r, total_width = self._get_ticklabels_offsets(renderer, self._axis_direction) - pad = self._get_external_pad() + \ - renderer.points_to_pixels(self.get_pad()) - self._set_offset_radius(r+pad) + pad = self._external_pad + renderer.points_to_pixels(self.get_pad()) + self._offset_radius = r + pad for (x, y), a, l in self._locs_angles_labels: - self._set_ref_angle(a) # + add_angle + self._ref_angle = a self.set_x(x) self.set_y(y) self.set_text(l) @@ -578,10 +597,16 @@ def get_texts_widths_heights_descents(self, renderer): class GridlinesCollection(LineCollection): def __init__(self, *args, which="major", axis="both", **kwargs): """ + Collection of grid lines. + Parameters ---------- which : {"major", "minor"} + Which grid to consider. axis : {"both", "x", "y"} + Which axis to consider. + *args, **kwargs : + Passed to `.LineCollection`. """ self._which = which self._axis = axis @@ -589,38 +614,51 @@ def __init__(self, *args, which="major", axis="both", **kwargs): self.set_grid_helper(None) def set_which(self, which): + """ + Select major or minor grid lines. + + Parameters + ---------- + which : {"major", "minor"} + """ self._which = which def set_axis(self, axis): + """ + Select axis. + + Parameters + ---------- + axis : {"both", "x", "y"} + """ self._axis = axis def set_grid_helper(self, grid_helper): + """ + Set grid helper. + + Parameters + ---------- + grid_helper : `.GridHelperBase` subclass + """ self._grid_helper = grid_helper def draw(self, renderer): if self._grid_helper is not None: self._grid_helper.update_lim(self.axes) gl = self._grid_helper.get_gridlines(self._which, self._axis) - if gl: - self.set_segments([np.transpose(l) for l in gl]) - else: - self.set_segments([]) + self.set_segments([np.transpose(l) for l in gl]) super().draw(renderer) class AxisArtist(martist.Artist): """ An artist which draws axis (a line along which the n-th axes coord - is constant) line, ticks, ticklabels, and axis label. + is constant) line, ticks, tick labels, and axis label. """ zorder = 2.5 - @_api.deprecated("3.4") - @_api.classproperty - def ZORDER(cls): - return cls.zorder - @property def LABELPAD(self): return self.label.get_pad() @@ -640,7 +678,7 @@ def __init__(self, axes, axes : `mpl_toolkits.axisartist.axislines.Axes` helper : `~mpl_toolkits.axisartist.axislines.AxisArtistHelper` """ - #axes is also used to follow the axis attribute (tick color, etc). + # axes is also used to follow the axis attribute (tick color, etc). super().__init__(**kwargs) @@ -673,27 +711,22 @@ def __init__(self, axes, self._axislabel_add_angle = 0. self.set_axis_direction(axis_direction) - @_api.deprecated("3.3") - @property - def dpi_transform(self): - return Affine2D().scale(1 / 72) + self.axes.figure.dpi_scale_trans - # axis direction def set_axis_direction(self, axis_direction): """ - Adjust the direction, text angle, text alignment of - ticklabels, labels following the matplotlib convention for - the rectangle axes. + Adjust the direction, text angle, and text alignment of tick labels + and axis labels following the Matplotlib convention for the rectangle + axes. The *axis_direction* must be one of [left, right, bottom, top]. ===================== ========== ========= ========== ========== - property left bottom right top + Property left bottom right top ===================== ========== ========= ========== ========== - ticklabels location "-" "+" "+" "-" - axislabel location "-" "+" "+" "-" - ticklabels angle 90 0 -90 180 + ticklabel direction "-" "+" "+" "-" + axislabel direction "-" "+" "+" "-" + ticklabel angle 90 0 -90 180 ticklabel va center baseline center baseline ticklabel ha right center right center axislabel angle 180 0 0 180 @@ -705,6 +738,10 @@ def set_axis_direction(self, axis_direction): the increasing coordinate. Also, the text angles are actually relative to (90 + angle of the direction to the ticklabel), which gives 0 for bottom axis. + + Parameters + ---------- + axis_direction : {"left", "bottom", "right", "top"} """ self.major_ticklabels.set_axis_direction(axis_direction) self.label.set_axis_direction(axis_direction) @@ -718,9 +755,9 @@ def set_axis_direction(self, axis_direction): def set_ticklabel_direction(self, tick_direction): r""" - Adjust the direction of the ticklabel. + Adjust the direction of the tick labels. - Note that the *label_direction*\s '+' and '-' are relative to the + Note that the *tick_direction*\s '+' and '-' are relative to the direction of the increasing coordinate. Parameters @@ -737,7 +774,7 @@ def invert_ticklabel_direction(self): def set_axislabel_direction(self, label_direction): r""" - Adjust the direction of the axislabel. + Adjust the direction of the axis label. Note that the *label_direction*\s '+' and '-' are relative to the direction of the increasing coordinate. @@ -777,6 +814,7 @@ def set_axisline_style(self, axisline_style=None, **kwargs): Examples -------- The following two commands are equal: + >>> set_axisline_style("->,size=1.5") >>> set_axisline_style("->", size=1.5) """ @@ -805,11 +843,11 @@ def _init_line(self): if axisline_style is None: self.line = PathPatch( self._axis_artist_helper.get_line(self.axes), - color=rcParams['axes.edgecolor'], + color=mpl.rcParams['axes.edgecolor'], fill=False, - linewidth=rcParams['axes.linewidth'], - capstyle=rcParams['lines.solid_capstyle'], - joinstyle=rcParams['lines.solid_joinstyle'], + linewidth=mpl.rcParams['axes.linewidth'], + capstyle=mpl.rcParams['lines.solid_capstyle'], + joinstyle=mpl.rcParams['lines.solid_joinstyle'], transform=tran) else: self.line = axisline_style(self, transform=tran) @@ -828,14 +866,16 @@ def _init_ticks(self, **kwargs): self.major_ticks = Ticks( kwargs.get( - "major_tick_size", rcParams[f"{axis_name}tick.major.size"]), + "major_tick_size", + mpl.rcParams[f"{axis_name}tick.major.size"]), axis=self.axis, transform=trans) self.minor_ticks = Ticks( kwargs.get( - "minor_tick_size", rcParams[f"{axis_name}tick.minor.size"]), + "minor_tick_size", + mpl.rcParams[f"{axis_name}tick.minor.size"]), axis=self.axis, transform=trans) - size = rcParams[f"{axis_name}tick.labelsize"] + size = mpl.rcParams[f"{axis_name}tick.labelsize"] self.major_ticklabels = TickLabels( axis=self.axis, axis_direction=self._axis_direction, @@ -843,7 +883,7 @@ def _init_ticks(self, **kwargs): transform=trans, fontsize=size, pad=kwargs.get( - "major_tick_pad", rcParams[f"{axis_name}tick.major.pad"]), + "major_tick_pad", mpl.rcParams[f"{axis_name}tick.major.pad"]), ) self.minor_ticklabels = TickLabels( axis=self.axis, @@ -852,7 +892,7 @@ def _init_ticks(self, **kwargs): transform=trans, fontsize=size, pad=kwargs.get( - "minor_tick_pad", rcParams[f"{axis_name}tick.minor.pad"]), + "minor_tick_pad", mpl.rcParams[f"{axis_name}tick.minor.pad"]), ) def _get_tick_info(self, tick_iter): @@ -877,19 +917,21 @@ def _get_tick_info(self, tick_iter): return ticks_loc_angle, ticklabels_loc_angle_label - def _update_ticks(self, renderer): + def _update_ticks(self, renderer=None): # set extra pad for major and minor ticklabels: use ticksize of # majorticks even for minor ticks. not clear what is best. + if renderer is None: + renderer = self.figure._get_renderer() + dpi_cor = renderer.points_to_pixels(1.) if self.major_ticks.get_visible() and self.major_ticks.get_tick_out(): - self.major_ticklabels._set_external_pad( - self.major_ticks._ticksize * dpi_cor) - self.minor_ticklabels._set_external_pad( - self.major_ticks._ticksize * dpi_cor) + ticklabel_pad = self.major_ticks._ticksize * dpi_cor + self.major_ticklabels._external_pad = ticklabel_pad + self.minor_ticklabels._external_pad = ticklabel_pad else: - self.major_ticklabels._set_external_pad(0) - self.minor_ticklabels._set_external_pad(0) + self.major_ticklabels._external_pad = 0 + self.minor_ticklabels._external_pad = 0 majortick_iter, minortick_iter = \ self._axis_artist_helper.get_tick_iterators(self.axes) @@ -925,7 +967,7 @@ def _init_offsetText(self, direction): "", xy=(x, y), xycoords="axes fraction", xytext=(0, 0), textcoords="offset points", - color=rcParams['xtick.color'], + color=mpl.rcParams['xtick.color'], horizontalalignment=ha, verticalalignment=va, ) self.offsetText.set_transform(IdentityTransform()) @@ -949,8 +991,8 @@ def _init_label(self, **kwargs): self.label = AxisLabel( 0, 0, "__from_axes__", color="auto", - fontsize=kwargs.get("labelsize", rcParams['axes.labelsize']), - fontweight=rcParams['axes.labelweight'], + fontsize=kwargs.get("labelsize", mpl.rcParams['axes.labelsize']), + fontweight=mpl.rcParams['axes.labelweight'], axis=self.axis, transform=tr, axis_direction=self._axis_direction, @@ -975,7 +1017,7 @@ def _update_label(self, renderer): axislabel_pad = max(self.major_ticklabels._axislabel_pad, self.minor_ticklabels._axislabel_pad) - self.label._set_external_pad(axislabel_pad) + self.label._external_pad = axislabel_pad xy, angle_tangent = \ self._axis_artist_helper.get_axislabel_pos_angle(self.axes) @@ -985,7 +1027,7 @@ def _update_label(self, renderer): angle_label = angle_tangent - 90 x, y = xy - self.label._set_ref_angle(angle_label+self._axislabel_add_angle) + self.label._ref_angle = angle_label + self._axislabel_add_angle self.label.set(x=x, y=y) def _draw_label(self, renderer): @@ -993,19 +1035,26 @@ def _draw_label(self, renderer): self.label.draw(renderer) def set_label(self, s): + # docstring inherited self.label.set_text(s) - def get_tightbbox(self, renderer): + def get_tightbbox(self, renderer=None): if not self.get_visible(): return self._axis_artist_helper.update_lim(self.axes) self._update_ticks(renderer) self._update_label(renderer) + + self.line.set_path(self._axis_artist_helper.get_line(self.axes)) + if self.get_axisline_style() is not None: + self.line.set_line_mutation_scale(self.major_ticklabels.get_size()) + bb = [ *self.major_ticklabels.get_window_extents(renderer), *self.minor_ticklabels.get_window_extents(renderer), self.label.get_window_extent(renderer), self.offsetText.get_window_extent(renderer), + self.line.get_window_extent(renderer), ] bb = [b for b in bb if b and (b.width != 0 or b.height != 0)] if bb: @@ -1039,7 +1088,7 @@ def toggle(self, all=None, ticks=None, ticklabels=None, label=None): To turn all on but (axis) label off :: - axis.toggle(all=True, label=False)) + axis.toggle(all=True, label=False) """ if all: diff --git a/lib/mpl_toolkits/axisartist/axisline_style.py b/lib/mpl_toolkits/axisartist/axisline_style.py index 80f3ce58eb48..5ae188021bb8 100644 --- a/lib/mpl_toolkits/axisartist/axisline_style.py +++ b/lib/mpl_toolkits/axisartist/axisline_style.py @@ -1,17 +1,19 @@ +""" +Provides classes to style the axis lines. +""" import math import numpy as np +import matplotlib as mpl from matplotlib.patches import _Style, FancyArrowPatch -from matplotlib.transforms import IdentityTransform from matplotlib.path import Path +from matplotlib.transforms import IdentityTransform class _FancyAxislineStyle: class SimpleArrow(FancyArrowPatch): - """ - The artist class that will be returned for SimpleArrow style. - """ + """The artist class that will be returned for SimpleArrow style.""" _ARROW_STYLE = "->" def __init__(self, axis_artist, line_path, transform, @@ -56,10 +58,10 @@ def set_path(self, path): def draw(self, renderer): """ Draw the axis line. - 1) transform the path to the display coordinate. - 2) extend the path to make a room for arrow - 3) update the path of the FancyArrowPatch. - 4) draw + 1) Transform the path to the display coordinate. + 2) Extend the path to make a room for arrow. + 3) Update the path of the FancyArrowPatch. + 4) Draw. """ path_in_disp = self._line_transform.transform_path(self._line_path) mutation_size = self.get_mutation_scale() # line_mutation_scale() @@ -68,18 +70,31 @@ def draw(self, renderer): self._path_original = extended_path FancyArrowPatch.draw(self, renderer) + def get_window_extent(self, renderer=None): + + path_in_disp = self._line_transform.transform_path(self._line_path) + mutation_size = self.get_mutation_scale() # line_mutation_scale() + extended_path = self._extend_path(path_in_disp, + mutation_size=mutation_size) + self._path_original = extended_path + return FancyArrowPatch.get_window_extent(self, renderer) + class FilledArrow(SimpleArrow): - """ - The artist class that will be returned for SimpleArrow style. - """ + """The artist class that will be returned for FilledArrow style.""" _ARROW_STYLE = "-|>" + def __init__(self, axis_artist, line_path, transform, + line_mutation_scale, facecolor): + super().__init__(axis_artist, line_path, transform, + line_mutation_scale) + self.set_facecolor(facecolor) + class AxislineStyle(_Style): """ A container class which defines style classes for AxisArtists. - An instance of any axisline style class is an callable object, + An instance of any axisline style class is a callable object, whose call signature is :: __call__(self, axis_artist, path, transform) @@ -144,6 +159,35 @@ def new_line(self, axis_artist, transform): _style_list["->"] = SimpleArrow class FilledArrow(SimpleArrow): + """ + An arrow with a filled head. + """ + ArrowAxisClass = _FancyAxislineStyle.FilledArrow + def __init__(self, size=1, facecolor=None): + """ + Parameters + ---------- + size : float + Size of the arrow as a fraction of the ticklabel size. + facecolor : color, default: :rc:`axes.edgecolor` + Fill color. + + .. versionadded:: 3.7 + """ + + if facecolor is None: + facecolor = mpl.rcParams['axes.edgecolor'] + self.size = size + self._facecolor = facecolor + super().__init__(size=size) + + def new_line(self, axis_artist, transform): + linepath = Path([(0, 0), (0, 1)]) + axisline = self.ArrowAxisClass(axis_artist, linepath, transform, + line_mutation_scale=self.size, + facecolor=self._facecolor) + return axisline + _style_list["-|>"] = FilledArrow diff --git a/lib/mpl_toolkits/axisartist/axislines.py b/lib/mpl_toolkits/axisartist/axislines.py index c49610b93831..36c1a004b78d 100644 --- a/lib/mpl_toolkits/axisartist/axislines.py +++ b/lib/mpl_toolkits/axisartist/axislines.py @@ -20,8 +20,8 @@ instance responsible to draw left y-axis. The default Axes.axis contains "bottom", "left", "top" and "right". -AxisArtist can be considered as a container artist and -has following children artists which will draw ticks, labels, etc. +AxisArtist can be considered as a container artist and has the following +children artists which will draw ticks, labels, etc. * line * major_ticks, major_ticklabels @@ -41,104 +41,105 @@ import numpy as np -from matplotlib import _api, rcParams +import matplotlib as mpl +from matplotlib import _api import matplotlib.axes as maxes from matplotlib.path import Path from mpl_toolkits.axes_grid1 import mpl_axes -from .axisline_style import AxislineStyle +from .axisline_style import AxislineStyle # noqa from .axis_artist import AxisArtist, GridlinesCollection class AxisArtistHelper: """ - AxisArtistHelper should define - following method with given APIs. Note that the first axes argument - will be axes attribute of the caller artist.:: + Axis helpers should define the methods listed below. The *axes* argument + will be the axes attribute of the caller artist. + :: - # LINE (spinal line?) - - def get_line(self, axes): - # path : Path - return path + # Construct the spine. def get_line_transform(self, axes): - # ... - # trans : transform - return trans + return transform - # LABEL + def get_line(self, axes): + return path - def get_label_pos(self, axes): - # x, y : position - return (x, y), trans + # Construct the label. + def get_axislabel_transform(self, axes): + return transform - def get_label_offset_transform(self, - axes, - pad_points, fontprops, renderer, - bboxes, - ): - # va : vertical alignment - # ha : horizontal alignment - # a : angle - return trans, va, ha, a + def get_axislabel_pos_angle(self, axes): + return (x, y), angle - # TICK + # Construct the ticks. def get_tick_transform(self, axes): - return trans + return transform def get_tick_iterators(self, axes): - # iter : iterable object that yields (c, angle, l) where - # c, angle, l is position, tick angle, and label - + # A pair of iterables (one for major ticks, one for minor ticks) + # that yield (tick_position, tick_angle, tick_label). return iter_major, iter_minor """ class _Base: """Base class for axis helper.""" - def __init__(self): - self.delta1, self.delta2 = 0.00001, 0.00001 def update_lim(self, axes): pass - class Fixed(_Base): - """Helper class for a fixed (in the axes coordinate) axis.""" + delta1 = _api.deprecated("3.6")( + property(lambda self: 0.00001, lambda self, value: None)) + delta2 = _api.deprecated("3.6")( + property(lambda self: 0.00001, lambda self, value: None)) - _default_passthru_pt = dict(left=(0, 0), - right=(1, 0), - bottom=(0, 0), - top=(0, 1)) - - def __init__(self, loc, nth_coord=None): + def _to_xy(self, values, const): """ - nth_coord = along which coordinate value varies - in 2D, nth_coord = 0 -> x axis, nth_coord = 1 -> y axis - """ - _api.check_in_list(["left", "right", "bottom", "top"], loc=loc) - self._loc = loc + Create a (*values.shape, 2)-shape array representing (x, y) pairs. - if nth_coord is None: - if loc in ["left", "right"]: - nth_coord = 1 - elif loc in ["bottom", "top"]: - nth_coord = 0 + *values* go into the coordinate determined by ``self.nth_coord``. + The other coordinate is filled with the constant *const*. - self.nth_coord = nth_coord + Example:: - super().__init__() + >>> self.nth_coord = 0 + >>> self._to_xy([1, 2, 3], const=0) + array([[1, 0], + [2, 0], + [3, 0]]) + """ + if self.nth_coord == 0: + return np.stack(np.broadcast_arrays(values, const), axis=-1) + elif self.nth_coord == 1: + return np.stack(np.broadcast_arrays(const, values), axis=-1) + else: + raise ValueError("Unexpected nth_coord") - self.passthru_pt = self._default_passthru_pt[loc] + class Fixed(_Base): + """Helper class for a fixed (in the axes coordinate) axis.""" - _verts = np.array([[0., 0.], - [1., 1.]]) - fixed_coord = 1 - nth_coord - _verts[:, fixed_coord] = self.passthru_pt[fixed_coord] + passthru_pt = _api.deprecated("3.7")(property( + lambda self: {"left": (0, 0), "right": (1, 0), + "bottom": (0, 0), "top": (0, 1)}[self._loc])) + def __init__(self, loc, nth_coord=None): + """``nth_coord = 0``: x-axis; ``nth_coord = 1``: y-axis.""" + self.nth_coord = ( + nth_coord if nth_coord is not None else + _api.check_getitem( + {"bottom": 0, "top": 0, "left": 1, "right": 1}, loc=loc)) + if (nth_coord == 0 and loc not in ["left", "right"] + or nth_coord == 1 and loc not in ["bottom", "top"]): + _api.warn_deprecated( + "3.7", message=f"{loc=!r} is incompatible with " + "{nth_coord=}; support is deprecated since %(since)s") + self._loc = loc + self._pos = {"bottom": 0, "top": 1, "left": 0, "right": 1}[loc] + super().__init__() # axis line in transAxes - self._path = Path(_verts) + self._path = Path(self._to_xy((0, 1), const=self._pos)) def get_nth_coord(self): return self.nth_coord @@ -204,38 +205,31 @@ def __init__(self, axes, loc, nth_coord=None): def get_tick_iterators(self, axes): """tick_loc, tick_angle, tick_label""" - - loc = self._loc - - if loc in ["bottom", "top"]: + if self._loc in ["bottom", "top"]: angle_normal, angle_tangent = 90, 0 - else: + else: # "left", "right" angle_normal, angle_tangent = 0, 90 major = self.axis.major - majorLocs = major.locator() - majorLabels = major.formatter.format_ticks(majorLocs) + major_locs = major.locator() + major_labels = major.formatter.format_ticks(major_locs) minor = self.axis.minor - minorLocs = minor.locator() - minorLabels = minor.formatter.format_ticks(minorLocs) + minor_locs = minor.locator() + minor_labels = minor.formatter.format_ticks(minor_locs) tick_to_axes = self.get_tick_transform(axes) - axes.transAxes def _f(locs, labels): - for x, l in zip(locs, labels): - - c = list(self.passthru_pt) # copy - c[self.nth_coord] = x - + for loc, label in zip(locs, labels): + c = self._to_xy(loc, const=self._pos) # check if the tick point is inside axes c2 = tick_to_axes.transform(c) - if (0 - self.delta1 - <= c2[self.nth_coord] - <= 1 + self.delta2): - yield c, angle_normal, angle_tangent, l + if mpl.transforms._interval_contains_close( + (0, 1), c2[self.nth_coord]): + yield c, angle_normal, angle_tangent, label - return _f(majorLocs, majorLabels), _f(minorLocs, minorLabels) + return _f(major_locs, major_labels), _f(minor_locs, minor_labels) class Floating(AxisArtistHelper.Floating): def __init__(self, axes, nth_coord, @@ -245,15 +239,10 @@ def __init__(self, axes, nth_coord, self.axis = [axes.xaxis, axes.yaxis][self.nth_coord] def get_line(self, axes): - _verts = np.array([[0., 0.], - [1., 1.]]) - fixed_coord = 1 - self.nth_coord data_to_axes = axes.transData - axes.transAxes p = data_to_axes.transform([self._value, self._value]) - _verts[:, fixed_coord] = p[fixed_coord] - - return Path(_verts) + return Path(self._to_xy((0, 1), const=p[fixed_coord])) def get_line_transform(self, axes): return axes.transAxes @@ -268,13 +257,12 @@ def get_axislabel_pos_angle(self, axes): get_label_transform() returns a transform of (transAxes+offset) """ angle = [0, 90][self.nth_coord] - _verts = [0.5, 0.5] fixed_coord = 1 - self.nth_coord data_to_axes = axes.transData - axes.transAxes p = data_to_axes.transform([self._value, self._value]) - _verts[fixed_coord] = p[fixed_coord] - if 0 <= _verts[fixed_coord] <= 1: - return _verts, angle + verts = self._to_xy(0.5, const=p[fixed_coord]) + if 0 <= verts[fixed_coord] <= 1: + return verts, angle else: return None, None @@ -289,64 +277,53 @@ def get_tick_iterators(self, axes): angle_normal, angle_tangent = 0, 90 major = self.axis.major - majorLocs = major.locator() - majorLabels = major.formatter.format_ticks(majorLocs) + major_locs = major.locator() + major_labels = major.formatter.format_ticks(major_locs) minor = self.axis.minor - minorLocs = minor.locator() - minorLabels = minor.formatter.format_ticks(minorLocs) + minor_locs = minor.locator() + minor_labels = minor.formatter.format_ticks(minor_locs) data_to_axes = axes.transData - axes.transAxes def _f(locs, labels): - for x, l in zip(locs, labels): - c = [self._value, self._value] - c[self.nth_coord] = x + for loc, label in zip(locs, labels): + c = self._to_xy(loc, const=self._value) c1, c2 = data_to_axes.transform(c) - if (0 <= c1 <= 1 and 0 <= c2 <= 1 - and 0 - self.delta1 - <= [c1, c2][self.nth_coord] - <= 1 + self.delta2): - yield c, angle_normal, angle_tangent, l + if 0 <= c1 <= 1 and 0 <= c2 <= 1: + yield c, angle_normal, angle_tangent, label - return _f(majorLocs, majorLabels), _f(minorLocs, minorLabels) + return _f(major_locs, major_labels), _f(minor_locs, minor_labels) class GridHelperBase: def __init__(self): - self._force_update = True # Remove together with invalidate()/valid(). self._old_limits = None super().__init__() def update_lim(self, axes): x1, x2 = axes.get_xlim() y1, y2 = axes.get_ylim() - if self._force_update or self._old_limits != (x1, x2, y1, y2): + if self._old_limits != (x1, x2, y1, y2): self._update_grid(x1, y1, x2, y2) - self._force_update = False self._old_limits = (x1, x2, y1, y2) def _update_grid(self, x1, y1, x2, y2): """Cache relevant computations when the axes limits have changed.""" - @_api.deprecated("3.4") - def invalidate(self): - self._force_update = True - - @_api.deprecated("3.4") - def valid(self): - return not self._force_update - def get_gridlines(self, which, axis): """ Return list of grid lines as a list of paths (list of points). - *which* : "major" or "minor" - *axis* : "both", "x" or "y" + Parameters + ---------- + which : {"both", "major", "minor"} + axis : {"both", "x", "y"} """ return [] + @_api.deprecated("3.6") def new_gridlines(self, ax): """ Create and return a new GridlineCollection instance. @@ -355,10 +332,10 @@ def new_gridlines(self, ax): *axis* : "both", "x" or "y" """ - gridlines = GridlinesCollection(None, transform=ax.transData, - colors=rcParams['grid.color'], - linestyles=rcParams['grid.linestyle'], - linewidths=rcParams['grid.linewidth']) + gridlines = GridlinesCollection( + None, transform=ax.transData, colors=mpl.rcParams['grid.color'], + linestyles=mpl.rcParams['grid.linestyle'], + linewidths=mpl.rcParams['grid.linewidth']) ax._set_artist_props(gridlines) gridlines.set_grid_helper(self) @@ -421,23 +398,27 @@ def get_gridlines(self, which="major", axis="both"): """ Return list of gridline coordinates in data coordinates. - *which* : "major" or "minor" - *axis* : "both", "x" or "y" + Parameters + ---------- + which : {"both", "major", "minor"} + axis : {"both", "x", "y"} """ + _api.check_in_list(["both", "major", "minor"], which=which) + _api.check_in_list(["both", "x", "y"], axis=axis) gridlines = [] - if axis in ["both", "x"]: + if axis in ("both", "x"): locs = [] y1, y2 = self.axes.get_ylim() - if which in ["both", "major"]: + if which in ("both", "major"): locs.extend(self.axes.xaxis.major.locator()) - if which in ["both", "minor"]: + if which in ("both", "minor"): locs.extend(self.axes.xaxis.minor.locator()) for x in locs: gridlines.append([[x, x], [y1, y2]]) - if axis in ["both", "y"]: + if axis in ("both", "y"): x1, x2 = self.axes.get_xlim() locs = [] if self.axes.yaxis._major_tick_kw["gridOn"]: @@ -477,25 +458,11 @@ def toggle_axisline(self, b=None): self.xaxis.set_visible(True) self.yaxis.set_visible(True) - def _init_axis_artists(self, axes=None): - if axes is None: - axes = self - - self._axislines = mpl_axes.Axes.AxisDict(self) - new_fixed_axis = self.get_grid_helper().new_fixed_axis - for loc in ["bottom", "top", "left", "right"]: - self._axislines[loc] = new_fixed_axis(loc=loc, axes=axes, - axis_direction=loc) - - for axisline in [self._axislines["top"], self._axislines["right"]]: - axisline.label.set_visible(False) - axisline.major_ticklabels.set_visible(False) - axisline.minor_ticklabels.set_visible(False) - @property def axis(self): return self._axislines + @_api.deprecated("3.6") def new_gridlines(self, grid_helper=None): """ Create and return a new GridlineCollection instance. @@ -510,40 +477,53 @@ def new_gridlines(self, grid_helper=None): gridlines = grid_helper.new_gridlines(self) return gridlines - def _init_gridlines(self, grid_helper=None): - # It is done inside the cla. - self.gridlines = self.new_gridlines(grid_helper) + def clear(self): + # docstring inherited - def cla(self): - # gridlines need to b created before cla() since cla calls grid() - self._init_gridlines() - super().cla() + # Init gridlines before clear() as clear() calls grid(). + self.gridlines = gridlines = GridlinesCollection( + None, transform=self.transData, + colors=mpl.rcParams['grid.color'], + linestyles=mpl.rcParams['grid.linestyle'], + linewidths=mpl.rcParams['grid.linewidth']) + self._set_artist_props(gridlines) + gridlines.set_grid_helper(self.get_grid_helper()) - # the clip_path should be set after Axes.cla() since that's - # when a patch is created. - self.gridlines.set_clip_path(self.axes.patch) + super().clear() - self._init_axis_artists() + # clip_path is set after Axes.clear(): that's when a patch is created. + gridlines.set_clip_path(self.axes.patch) + + # Init axis artists. + self._axislines = mpl_axes.Axes.AxisDict(self) + new_fixed_axis = self.get_grid_helper().new_fixed_axis + self._axislines.update({ + loc: new_fixed_axis(loc=loc, axes=self, axis_direction=loc) + for loc in ["bottom", "top", "left", "right"]}) + for axisline in [self._axislines["top"], self._axislines["right"]]: + axisline.label.set_visible(False) + axisline.major_ticklabels.set_visible(False) + axisline.minor_ticklabels.set_visible(False) def get_grid_helper(self): return self._grid_helper - def grid(self, b=None, which='major', axis="both", **kwargs): + def grid(self, visible=None, which='major', axis="both", **kwargs): """ Toggle the gridlines, and optionally set the properties of the lines. """ # 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) + super().grid(visible, which=which, axis=axis, **kwargs) if not self._axisline_on: return - if b is None: - 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) + if visible is None: + visible = (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=visible) self.gridlines.set(**kwargs) def get_children(self): @@ -554,10 +534,6 @@ def get_children(self): children.extend(super().get_children()) return children - @_api.deprecated("3.4") - def invalidate_grid_helper(self): - self._grid_helper.invalidate() - def new_fixed_axis(self, loc, offset=None): gh = self.get_grid_helper() axis = gh.new_fixed_axis(loc, @@ -576,32 +552,21 @@ def new_floating_axis(self, nth_coord, value, axis_direction="bottom"): return axis -Subplot = maxes.subplot_class_factory(Axes) - - class AxesZero(Axes): - def _init_axis_artists(self): - super()._init_axis_artists() - - new_floating_axis = self._grid_helper.new_floating_axis - xaxis_zero = new_floating_axis(nth_coord=0, - value=0., - axis_direction="bottom", - axes=self) - - xaxis_zero.line.set_clip_path(self.patch) - xaxis_zero.set_visible(False) - self._axislines["xzero"] = xaxis_zero - - yaxis_zero = new_floating_axis(nth_coord=1, - value=0., - axis_direction="left", - axes=self) - - yaxis_zero.line.set_clip_path(self.patch) - yaxis_zero.set_visible(False) - self._axislines["yzero"] = yaxis_zero - - -SubplotZero = maxes.subplot_class_factory(AxesZero) + def clear(self): + super().clear() + new_floating_axis = self.get_grid_helper().new_floating_axis + self._axislines.update( + xzero=new_floating_axis( + nth_coord=0, value=0., axis_direction="bottom", axes=self), + yzero=new_floating_axis( + nth_coord=1, value=0., axis_direction="left", axes=self), + ) + for k in ["xzero", "yzero"]: + self._axislines[k].line.set_clip_path(self.patch) + self._axislines[k].set_visible(False) + + +Subplot = Axes +SubplotZero = AxesZero diff --git a/lib/mpl_toolkits/axisartist/clip_path.py b/lib/mpl_toolkits/axisartist/clip_path.py deleted file mode 100644 index 830dc7fa2aa9..000000000000 --- a/lib/mpl_toolkits/axisartist/clip_path.py +++ /dev/null @@ -1,118 +0,0 @@ -import numpy as np -from math import degrees -from matplotlib import _api -import math - - -def atan2(dy, dx): - if dx == 0 and dy == 0: - _api.warn_external("dx and dy are 0") - return 0 - else: - return math.atan2(dy, dx) - - -# FIXME : The current algorithm seems to return incorrect angle when the line -# ends at the boundary. -def clip(xlines, ylines, x0, clip="right", xdir=True, ydir=True): - - clipped_xlines = [] - clipped_ylines = [] - - _pos_angles = [] - - xsign = 1 if xdir else -1 - ysign = 1 if ydir else -1 - - for x, y in zip(xlines, ylines): - - if clip in ["up", "right"]: - b = (x < x0).astype("i") - db = b[1:] - b[:-1] - else: - b = (x > x0).astype("i") - db = b[1:] - b[:-1] - - if b[0]: - ns = 0 - else: - ns = -1 - segx, segy = [], [] - for (i,) in np.argwhere(db): - c = db[i] - if c == -1: - dx = (x0 - x[i]) - dy = (y[i+1] - y[i]) * (dx / (x[i+1] - x[i])) - y0 = y[i] + dy - clipped_xlines.append(np.concatenate([segx, x[ns:i+1], [x0]])) - clipped_ylines.append(np.concatenate([segy, y[ns:i+1], [y0]])) - ns = -1 - segx, segy = [], [] - - if dx == 0. and dy == 0: - dx = x[i+1] - x[i] - dy = y[i+1] - y[i] - - a = degrees(atan2(ysign*dy, xsign*dx)) - _pos_angles.append((x0, y0, a)) - - elif c == 1: - dx = (x0 - x[i]) - dy = (y[i+1] - y[i]) * (dx / (x[i+1] - x[i])) - y0 = y[i] + dy - segx, segy = [x0], [y0] - ns = i+1 - - if dx == 0. and dy == 0: - dx = x[i+1] - x[i] - dy = y[i+1] - y[i] - - a = degrees(atan2(ysign*dy, xsign*dx)) - _pos_angles.append((x0, y0, a)) - - if ns != -1: - clipped_xlines.append(np.concatenate([segx, x[ns:]])) - clipped_ylines.append(np.concatenate([segy, y[ns:]])) - - return clipped_xlines, clipped_ylines, _pos_angles - - -def clip_line_to_rect(xline, yline, bbox): - - x0, y0, x1, y1 = bbox.extents - - xdir = x1 > x0 - ydir = y1 > y0 - - if x1 > x0: - lx1, ly1, c_right_ = clip([xline], [yline], x1, - clip="right", xdir=xdir, ydir=ydir) - lx2, ly2, c_left_ = clip(lx1, ly1, x0, - clip="left", xdir=xdir, ydir=ydir) - else: - lx1, ly1, c_right_ = clip([xline], [yline], x0, - clip="right", xdir=xdir, ydir=ydir) - lx2, ly2, c_left_ = clip(lx1, ly1, x1, - clip="left", xdir=xdir, ydir=ydir) - - if y1 > y0: - ly3, lx3, c_top_ = clip(ly2, lx2, y1, - clip="right", xdir=ydir, ydir=xdir) - ly4, lx4, c_bottom_ = clip(ly3, lx3, y0, - clip="left", xdir=ydir, ydir=xdir) - else: - ly3, lx3, c_top_ = clip(ly2, lx2, y0, - clip="right", xdir=ydir, ydir=xdir) - ly4, lx4, c_bottom_ = clip(ly3, lx3, y1, - clip="left", xdir=ydir, ydir=xdir) - - c_left = [((x, y), (a + 90) % 180 - 90) for x, y, a in c_left_ - if bbox.containsy(y)] - c_bottom = [((x, y), (90 - a) % 180) for y, x, a in c_bottom_ - if bbox.containsx(x)] - c_right = [((x, y), (a + 90) % 180 + 90) for x, y, a in c_right_ - if bbox.containsy(y)] - c_top = [((x, y), (90 - a) % 180 + 180) for y, x, a in c_top_ - if bbox.containsx(x)] - - return list(zip(lx4, ly4)), [c_left, c_bottom, c_right, c_top] diff --git a/lib/mpl_toolkits/axisartist/floating_axes.py b/lib/mpl_toolkits/axisartist/floating_axes.py index e1958939c7d5..82d0f5a89da7 100644 --- a/lib/mpl_toolkits/axisartist/floating_axes.py +++ b/lib/mpl_toolkits/axisartist/floating_axes.py @@ -9,9 +9,10 @@ import numpy as np +import matplotlib as mpl +from matplotlib import _api, cbook import matplotlib.patches as mpatches from matplotlib.path import Path -import matplotlib.axes as maxes from mpl_toolkits.axes_grid1.parasite_axes import host_axes_class_factory @@ -44,91 +45,58 @@ def __init__(self, grid_helper, side, nth_coord_ticks=None): def update_lim(self, axes): self.grid_helper.update_lim(axes) - self.grid_info = self.grid_helper.grid_info + self._grid_info = self.grid_helper._grid_info def get_tick_iterators(self, axes): """tick_loc, tick_angle, tick_label, (optionally) tick_label""" grid_finder = self.grid_helper.grid_finder - lat_levs, lat_n, lat_factor = self.grid_info["lat_info"] - lon_levs, lon_n, lon_factor = self.grid_info["lon_info"] + lat_levs, lat_n, lat_factor = self._grid_info["lat_info"] + yy0 = lat_levs / lat_factor - lon_levs, lat_levs = np.asarray(lon_levs), np.asarray(lat_levs) - if lat_factor is not None: - yy0 = lat_levs / lat_factor - dy = 0.001 / lat_factor - else: - yy0 = lat_levs - dy = 0.001 - - if lon_factor is not None: - xx0 = lon_levs / lon_factor - dx = 0.001 / lon_factor - else: - xx0 = lon_levs - dx = 0.001 + lon_levs, lon_n, lon_factor = self._grid_info["lon_info"] + xx0 = lon_levs / lon_factor extremes = self.grid_helper._extremes xmin, xmax = sorted(extremes[:2]) ymin, ymax = sorted(extremes[2:]) - def transform_xy(x, y): - x1, y1 = grid_finder.transform_xy(x, y) - x2, y2 = axes.transData.transform(np.array([x1, y1]).T).T - return x2, y2 + def trf_xy(x, y): + trf = grid_finder.get_transform() + axes.transData + return trf.transform(np.column_stack(np.broadcast_arrays(x, y))).T if self.nth_coord == 0: mask = (ymin <= yy0) & (yy0 <= ymax) - yy0 = yy0[mask] - xx0 = np.full_like(yy0, self.value) - xx1, yy1 = transform_xy(xx0, yy0) - - xx00 = xx0.astype(float, copy=True) - xx00[xx0 + dx > xmax] -= dx - xx1a, yy1a = transform_xy(xx00, yy0) - xx1b, yy1b = transform_xy(xx00 + dx, yy0) - - yy00 = yy0.astype(float, copy=True) - yy00[yy0 + dy > ymax] -= dy - xx2a, yy2a = transform_xy(xx0, yy00) - xx2b, yy2b = transform_xy(xx0, yy00 + dy) - - labels = self.grid_info["lat_labels"] - labels = [l for l, m in zip(labels, mask) if m] + (xx1, yy1), (dxx1, dyy1), (dxx2, dyy2) = \ + grid_helper_curvelinear._value_and_jacobian( + trf_xy, self.value, yy0[mask], (xmin, xmax), (ymin, ymax)) + labels = self._grid_info["lat_labels"] elif self.nth_coord == 1: mask = (xmin <= xx0) & (xx0 <= xmax) - xx0 = xx0[mask] - yy0 = np.full_like(xx0, self.value) - xx1, yy1 = transform_xy(xx0, yy0) + (xx1, yy1), (dxx2, dyy2), (dxx1, dyy1) = \ + grid_helper_curvelinear._value_and_jacobian( + trf_xy, xx0[mask], self.value, (xmin, xmax), (ymin, ymax)) + labels = self._grid_info["lon_labels"] - yy00 = yy0.astype(float, copy=True) - yy00[yy0 + dy > ymax] -= dy - xx1a, yy1a = transform_xy(xx0, yy00) - xx1b, yy1b = transform_xy(xx0, yy00 + dy) + labels = [l for l, m in zip(labels, mask) if m] - xx00 = xx0.astype(float, copy=True) - xx00[xx0 + dx > xmax] -= dx - xx2a, yy2a = transform_xy(xx00, yy0) - xx2b, yy2b = transform_xy(xx00 + dx, yy0) + angle_normal = np.arctan2(dyy1, dxx1) + angle_tangent = np.arctan2(dyy2, dxx2) + mm = (dyy1 == 0) & (dxx1 == 0) # points with degenerate normal + angle_normal[mm] = angle_tangent[mm] + np.pi / 2 - labels = self.grid_info["lon_labels"] - labels = [l for l, m in zip(labels, mask) if m] + tick_to_axes = self.get_tick_transform(axes) - axes.transAxes + in_01 = functools.partial( + mpl.transforms._interval_contains_close, (0, 1)) def f1(): - dd = np.arctan2(yy1b - yy1a, xx1b - xx1a) # angle normal - dd2 = np.arctan2(yy2b - yy2a, xx2b - xx2a) # angle tangent - mm = (yy1b - yy1a == 0) & (xx1b - xx1a == 0) # mask not defined dd - dd[mm] = dd2[mm] + np.pi / 2 - - tick_to_axes = self.get_tick_transform(axes) - axes.transAxes - for x, y, d, d2, lab in zip(xx1, yy1, dd, dd2, labels): + for x, y, normal, tangent, lab \ + in zip(xx1, yy1, angle_normal, angle_tangent, labels): c2 = tick_to_axes.transform((x, y)) - delta = 0.00001 - if 0-delta <= c2[0] <= 1+delta and 0-delta <= c2[1] <= 1+delta: - d1, d2 = np.rad2deg([d, d2]) - yield [x, y], d1, d2, lab + if in_01(c2[0]) and in_01(c2[1]): + yield [x, y], *np.rad2deg([normal, tangent]), lab return f1(), iter([]) @@ -138,7 +106,7 @@ def get_line(self, axes): right=("lon_lines0", 1), bottom=("lat_lines0", 0), top=("lat_lines0", 1))[self._side] - xx, yy = self.grid_info[k][v] + xx, yy = self._grid_info[k][v] return Path(np.column_stack([xx, yy])) @@ -229,10 +197,10 @@ def new_fixed_axis(self, loc, # return axis def _update_grid(self, x1, y1, x2, y2): - if self.grid_info is None: - self.grid_info = dict() + if self._grid_info is None: + self._grid_info = dict() - grid_info = self.grid_info + grid_info = self._grid_info grid_finder = self.grid_finder extremes = grid_finder.extreme_finder(grid_finder.inv_transform_xy, @@ -240,31 +208,25 @@ def _update_grid(self, x1, y1, x2, y2): lon_min, lon_max = sorted(extremes[:2]) lat_min, lat_max = sorted(extremes[2:]) + grid_info["extremes"] = lon_min, lon_max, lat_min, lat_max # extremes + lon_levs, lon_n, lon_factor = \ grid_finder.grid_locator1(lon_min, lon_max) + lon_levs = np.asarray(lon_levs) lat_levs, lat_n, lat_factor = \ grid_finder.grid_locator2(lat_min, lat_max) - grid_info["extremes"] = lon_min, lon_max, lat_min, lat_max # extremes + lat_levs = np.asarray(lat_levs) grid_info["lon_info"] = lon_levs, lon_n, lon_factor grid_info["lat_info"] = lat_levs, lat_n, lat_factor - grid_info["lon_labels"] = grid_finder.tick_formatter1("bottom", - lon_factor, - lon_levs) - - grid_info["lat_labels"] = grid_finder.tick_formatter2("bottom", - lat_factor, - lat_levs) + grid_info["lon_labels"] = grid_finder.tick_formatter1( + "bottom", lon_factor, lon_levs) + grid_info["lat_labels"] = grid_finder.tick_formatter2( + "bottom", lat_factor, lat_levs) - if lon_factor is None: - lon_values = np.asarray(lon_levs[:lon_n]) - else: - lon_values = np.asarray(lon_levs[:lon_n]/lon_factor) - if lat_factor is None: - lat_values = np.asarray(lat_levs[:lat_n]) - else: - lat_values = np.asarray(lat_levs[:lat_n]/lat_factor) + lon_values = lon_levs[:lon_n] / lon_factor + lat_values = lat_levs[:lat_n] / lat_factor lon_lines, lat_lines = grid_finder._get_raw_grid_lines( lon_values[(lon_min < lon_values) & (lon_values < lon_max)], @@ -284,89 +246,54 @@ def _update_grid(self, x1, y1, x2, y2): def get_gridlines(self, which="major", axis="both"): grid_lines = [] if axis in ["both", "x"]: - grid_lines.extend(self.grid_info["lon_lines"]) + grid_lines.extend(self._grid_info["lon_lines"]) if axis in ["both", "y"]: - grid_lines.extend(self.grid_info["lat_lines"]) + grid_lines.extend(self._grid_info["lat_lines"]) return grid_lines - def get_boundary(self): - """ - Return (N, 2) array of (x, y) coordinate of the boundary. - """ - x0, x1, y0, y1 = self._extremes - tr = self._aux_trans - - xx = np.linspace(x0, x1, 100) - yy0 = np.full_like(xx, y0) - yy1 = np.full_like(xx, y1) - yy = np.linspace(y0, y1, 100) - xx0 = np.full_like(yy, x0) - xx1 = np.full_like(yy, x1) - - xxx = np.concatenate([xx[:-1], xx1[:-1], xx[-1:0:-1], xx0]) - yyy = np.concatenate([yy0[:-1], yy[:-1], yy1[:-1], yy[::-1]]) - t = tr.transform(np.array([xxx, yyy]).transpose()) - - return t - class FloatingAxesBase: - def __init__(self, *args, **kwargs): - grid_helper = kwargs.get("grid_helper", None) - if grid_helper is None: - raise ValueError("FloatingAxes requires grid_helper argument") - if not hasattr(grid_helper, "get_boundary"): - raise ValueError("grid_helper must implement get_boundary method") - - self._axes_class_floating.__init__(self, *args, **kwargs) - + def __init__(self, *args, grid_helper, **kwargs): + _api.check_isinstance(GridHelperCurveLinear, grid_helper=grid_helper) + super().__init__(*args, grid_helper=grid_helper, **kwargs) self.set_aspect(1.) self.adjust_axes_lim() def _gen_axes_patch(self): # docstring inherited - grid_helper = self.get_grid_helper() - t = grid_helper.get_boundary() - return mpatches.Polygon(t) - - def cla(self): - self._axes_class_floating.cla(self) - # HostAxes.cla(self) - self.patch.set_transform(self.transData) - - patch = self._axes_class_floating._gen_axes_patch(self) - patch.set_figure(self.figure) - patch.set_visible(False) - patch.set_transform(self.transAxes) - - self.patch.set_clip_path(patch) - self.gridlines.set_clip_path(patch) - - self._original_patch = patch + # Using a public API to access _extremes. + (x0, _), (x1, _), (y0, _), (y1, _) = map( + self.get_grid_helper().get_data_boundary, + ["left", "right", "bottom", "top"]) + patch = mpatches.Polygon([(x0, y0), (x1, y0), (x1, y1), (x0, y1)]) + patch.get_path()._interpolation_steps = 100 + return patch + + def clear(self): + super().clear() + self.patch.set_transform( + self.get_grid_helper().grid_finder.get_transform() + + self.transData) + # The original patch is not in the draw tree; it is only used for + # clipping purposes. + orig_patch = super()._gen_axes_patch() + orig_patch.set_figure(self.figure) + orig_patch.set_transform(self.transAxes) + self.patch.set_clip_path(orig_patch) + self.gridlines.set_clip_path(orig_patch) def adjust_axes_lim(self): - grid_helper = self.get_grid_helper() - t = grid_helper.get_boundary() - x, y = t[:, 0], t[:, 1] - - xmin, xmax = min(x), max(x) - ymin, ymax = min(y), max(y) - - dx = (xmax-xmin) / 100 - dy = (ymax-ymin) / 100 - - self.set_xlim(xmin-dx, xmax+dx) - self.set_ylim(ymin-dy, ymax+dy) - - -@functools.lru_cache(None) -def floatingaxes_class_factory(axes_class): - return type("Floating %s" % axes_class.__name__, - (FloatingAxesBase, axes_class), - {'_axes_class_floating': axes_class}) + bbox = self.patch.get_path().get_extents( + # First transform to pixel coords, then to parent data coords. + self.patch.get_transform() - self.transData) + bbox = bbox.expanded(1.02, 1.02) + self.set_xlim(bbox.xmin, bbox.xmax) + self.set_ylim(bbox.ymin, bbox.ymax) +floatingaxes_class_factory = cbook._make_class_factory( + FloatingAxesBase, "Floating{}") FloatingAxes = floatingaxes_class_factory( host_axes_class_factory(axislines.Axes)) -FloatingSubplot = maxes.subplot_class_factory(FloatingAxes) +FloatingSubplot = FloatingAxes diff --git a/lib/mpl_toolkits/axisartist/grid_finder.py b/lib/mpl_toolkits/axisartist/grid_finder.py index 76d7b7016fc2..4468eb65f3e7 100644 --- a/lib/mpl_toolkits/axisartist/grid_finder.py +++ b/lib/mpl_toolkits/axisartist/grid_finder.py @@ -1,8 +1,49 @@ import numpy as np -from matplotlib import _api, ticker as mticker +from matplotlib import ticker as mticker from matplotlib.transforms import Bbox, Transform -from .clip_path import clip_line_to_rect + + +def _find_line_box_crossings(xys, bbox): + """ + Find the points where a polyline crosses a bbox, and the crossing angles. + + Parameters + ---------- + xys : (N, 2) array + The polyline coordinates. + bbox : `.Bbox` + The bounding box. + + Returns + ------- + list of ((float, float), float) + Four separate lists of crossings, for the left, right, bottom, and top + sides of the bbox, respectively. For each list, the entries are the + ``((x, y), ccw_angle_in_degrees)`` of the crossing, where an angle of 0 + means that the polyline is moving to the right at the crossing point. + + The entries are computed by linearly interpolating at each crossing + between the nearest points on either side of the bbox edges. + """ + crossings = [] + dxys = xys[1:] - xys[:-1] + for sl in [slice(None), slice(None, None, -1)]: + us, vs = xys.T[sl] # "this" coord, "other" coord + dus, dvs = dxys.T[sl] + umin, vmin = bbox.min[sl] + umax, vmax = bbox.max[sl] + for u0, inside in [(umin, us > umin), (umax, us < umax)]: + crossings.append([]) + idxs, = (inside[:-1] ^ inside[1:]).nonzero() + for idx in idxs: + v = vs[idx] + (u0 - us[idx]) * dvs[idx] / dus[idx] + if not vmin <= v <= vmax: + continue + crossing = (u0, v)[sl] + theta = np.degrees(np.arctan2(*dxys[idx][::-1])) + crossings[-1].append((crossing, theta)) + return crossings class ExtremeFinderSimple: @@ -51,6 +92,34 @@ def _add_pad(self, x_min, x_max, y_min, y_max): return x_min - dx, x_max + dx, y_min - dy, y_max + dy +class _User2DTransform(Transform): + """A transform defined by two user-set functions.""" + + input_dims = output_dims = 2 + + def __init__(self, forward, backward): + """ + Parameters + ---------- + forward, backward : callable + The forward and backward transforms, taking ``x`` and ``y`` as + separate arguments and returning ``(tr_x, tr_y)``. + """ + # The normal Matplotlib convention would be to take and return an + # (N, 2) array but axisartist uses the transposed version. + super().__init__() + self._forward = forward + self._backward = backward + + def transform_non_affine(self, values): + # docstring inherited + return np.transpose(self._forward(*np.transpose(values))) + + def inverted(self): + # docstring inherited + return type(self)(self._backward, self._forward) + + class GridFinder: def __init__(self, transform, @@ -61,7 +130,7 @@ def __init__(self, tick_formatter2=None): """ transform : transform from the image coordinate (which will be - the transData of the axes to the world coordinate. + the transData of the axes to the world coordinate). or transform = (transform_xy, inv_transform_xy) @@ -82,7 +151,7 @@ def __init__(self, self.grid_locator2 = grid_locator2 self.tick_formatter1 = tick_formatter1 self.tick_formatter2 = tick_formatter2 - self.update_transform(transform) + self.set_transform(transform) def get_grid_info(self, x1, y1, x2, y2): """ @@ -97,7 +166,9 @@ def get_grid_info(self, x1, y1, x2, y2): lon_min, lon_max, lat_min, lat_max = extremes lon_levs, lon_n, lon_factor = self.grid_locator1(lon_min, lon_max) + lon_levs = np.asarray(lon_levs) lat_levs, lat_n, lat_factor = self.grid_locator2(lat_min, lat_max) + lat_levs = np.asarray(lat_levs) lon_values = lon_levs[:lon_n] / lon_factor lat_values = lat_levs[:lat_n] / lat_factor @@ -161,52 +232,49 @@ def _clip_grid_lines_and_find_ticks(self, lines, values, levs, bb): tck_levels = gi["tick_levels"] tck_locs = gi["tick_locs"] for (lx, ly), v, lev in zip(lines, values, levs): - xy, tcks = clip_line_to_rect(lx, ly, bb) - if not xy: - continue + tcks = _find_line_box_crossings(np.column_stack([lx, ly]), bb) gi["levels"].append(v) - gi["lines"].append(xy) + gi["lines"].append([(lx, ly)]) for tck, direction in zip(tcks, - ["left", "bottom", "right", "top"]): + ["left", "right", "bottom", "top"]): for t in tck: tck_levels[direction].append(lev) tck_locs[direction].append(t) return gi - def update_transform(self, aux_trans): - if not isinstance(aux_trans, Transform) and len(aux_trans) != 2: - raise TypeError("'aux_trans' must be either a Transform instance " - "or a pair of callables") - self._aux_transform = aux_trans + def set_transform(self, aux_trans): + if isinstance(aux_trans, Transform): + self._aux_transform = aux_trans + elif len(aux_trans) == 2 and all(map(callable, aux_trans)): + self._aux_transform = _User2DTransform(*aux_trans) + else: + raise TypeError("'aux_trans' must be either a Transform " + "instance or a pair of callables") + + def get_transform(self): + return self._aux_transform + + update_transform = set_transform # backcompat alias. def transform_xy(self, x, y): - aux_trf = self._aux_transform - if isinstance(aux_trf, Transform): - return aux_trf.transform(np.column_stack([x, y])).T - else: - transform_xy, inv_transform_xy = aux_trf - return transform_xy(x, y) + return self._aux_transform.transform(np.column_stack([x, y])).T def inv_transform_xy(self, x, y): - aux_trf = self._aux_transform - if isinstance(aux_trf, Transform): - return aux_trf.inverted().transform(np.column_stack([x, y])).T - else: - transform_xy, inv_transform_xy = aux_trf - return inv_transform_xy(x, y) + return self._aux_transform.inverted().transform( + np.column_stack([x, y])).T - def update(self, **kw): - for k in kw: + def update(self, **kwargs): + for k, v in kwargs.items(): if k in ["extreme_finder", "grid_locator1", "grid_locator2", "tick_formatter1", "tick_formatter2"]: - setattr(self, k, kw[k]) + setattr(self, k, v) else: - raise ValueError("Unknown update property '%s'" % k) + raise ValueError(f"Unknown update property {k!r}") class MaxNLocator(mticker.MaxNLocator): @@ -219,31 +287,20 @@ def __init__(self, nbins=10, steps=None, super().__init__(nbins, steps=steps, integer=integer, symmetric=symmetric, prune=prune) self.create_dummy_axis() - self._factor = 1 def __call__(self, v1, v2): - self.set_bounds(v1 * self._factor, v2 * self._factor) - locs = super().__call__() - return np.array(locs), len(locs), self._factor - - @_api.deprecated("3.3") - def set_factor(self, f): - self._factor = f + locs = super().tick_values(v1, v2) + return np.array(locs), len(locs), 1 # 1: factor (see angle_helper) class FixedLocator: def __init__(self, locs): self._locs = locs - self._factor = 1 def __call__(self, v1, v2): - v1, v2 = sorted([v1 * self._factor, v2 * self._factor]) + v1, v2 = sorted([v1, v2]) locs = np.array([l for l in self._locs if v1 <= l <= v2]) - return locs, len(locs), self._factor - - @_api.deprecated("3.3") - def set_factor(self, f): - self._factor = f + return locs, len(locs), 1 # 1: factor (see angle_helper) # Tick Formatter diff --git a/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py b/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py index bb67dd0d78c8..200c6d89ab92 100644 --- a/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/grid_helper_curvelinear.py @@ -1,10 +1,13 @@ """ An experimental support for curvilinear grid. """ + +import functools from itertools import chain import numpy as np +import matplotlib as mpl from matplotlib.path import Path from matplotlib.transforms import Affine2D, IdentityTransform from .axislines import AxisArtistHelper, GridHelperBase @@ -12,6 +15,31 @@ from .grid_finder import GridFinder +def _value_and_jacobian(func, xs, ys, xlims, ylims): + """ + Compute *func* and its derivatives along x and y at positions *xs*, *ys*, + while ensuring that finite difference calculations don't try to evaluate + values outside of *xlims*, *ylims*. + """ + eps = np.finfo(float).eps ** (1/2) # see e.g. scipy.optimize.approx_fprime + val = func(xs, ys) + # Take the finite difference step in the direction where the bound is the + # furthest; the step size is min of epsilon and distance to that bound. + xlo, xhi = sorted(xlims) + dxlo = xs - xlo + dxhi = xhi - xs + xeps = (np.take([-1, 1], dxhi >= dxlo) + * np.minimum(eps, np.maximum(dxlo, dxhi))) + val_dx = func(xs + xeps, ys) + ylo, yhi = sorted(ylims) + dylo = ys - ylo + dyhi = yhi - ys + yeps = (np.take([-1, 1], dyhi >= dylo) + * np.minimum(eps, np.maximum(dylo, dyhi))) + val_dy = func(xs, ys + yeps) + return (val, (val_dx - val) / xeps, (val_dy - val) / yeps) + + class FixedAxisArtistHelper(AxisArtistHelper.Fixed): """ Helper class for a fixed axis. @@ -31,63 +59,37 @@ def __init__(self, grid_helper, side, nth_coord_ticks=None): self.nth_coord_ticks = nth_coord_ticks self.side = side - self._limits_inverted = False def update_lim(self, axes): self.grid_helper.update_lim(axes) - if self.nth_coord == 0: - xy1, xy2 = axes.get_ylim() - else: - xy1, xy2 = axes.get_xlim() - - if xy1 > xy2: - self._limits_inverted = True - else: - self._limits_inverted = False - - def change_tick_coord(self, coord_number=None): - if coord_number is None: - self.nth_coord_ticks = 1 - self.nth_coord_ticks - elif coord_number in [0, 1]: - self.nth_coord_ticks = coord_number - else: - raise Exception("wrong coord number") - def get_tick_transform(self, axes): return axes.transData def get_tick_iterators(self, axes): """tick_loc, tick_angle, tick_label""" - - g = self.grid_helper - - if self._limits_inverted: + v1, v2 = axes.get_ylim() if self.nth_coord == 0 else axes.get_xlim() + if v1 > v2: # Inverted limits. side = {"left": "right", "right": "left", "top": "bottom", "bottom": "top"}[self.side] else: side = self.side - + g = self.grid_helper ti1 = g.get_tick_iterator(self.nth_coord_ticks, side) ti2 = g.get_tick_iterator(1-self.nth_coord_ticks, side, minor=True) - return chain(ti1, ti2), iter([]) class FloatingAxisArtistHelper(AxisArtistHelper.Floating): - def __init__(self, grid_helper, nth_coord, value, axis_direction=None): """ nth_coord = along which coordinate value varies. nth_coord = 0 -> x axis, nth_coord = 1 -> y axis """ - super().__init__(nth_coord, value) self.value = value self.grid_helper = grid_helper self._extremes = -np.inf, np.inf - - self._get_line_path = None # a method that returns a Path. self._line_num_points = 100 # number of points to create a line def set_extremes(self, e1, e2): @@ -129,10 +131,10 @@ def update_lim(self, axes): yy0 = np.full(self._line_num_points, self.value) xx, yy = grid_finder.transform_xy(xx0, yy0) - self.grid_info = { + self._grid_info = { "extremes": (lon_min, lon_max, lat_min, lat_max), - "lon_info": (lon_levs, lon_n, lon_factor), - "lat_info": (lat_levs, lat_n, lat_factor), + "lon_info": (lon_levs, lon_n, np.asarray(lon_factor)), + "lat_info": (lat_levs, lat_n, np.asarray(lat_factor)), "lon_labels": grid_finder.tick_formatter1( "bottom", lon_factor, lon_levs), "lat_labels": grid_finder.tick_formatter2( @@ -144,31 +146,23 @@ def get_axislabel_transform(self, axes): return Affine2D() # axes.transData def get_axislabel_pos_angle(self, axes): + def trf_xy(x, y): + trf = self.grid_helper.grid_finder.get_transform() + axes.transData + return trf.transform([x, y]).T - extremes = self.grid_info["extremes"] - + xmin, xmax, ymin, ymax = self._grid_info["extremes"] if self.nth_coord == 0: xx0 = self.value - yy0 = (extremes[2] + extremes[3]) / 2 - dxx = 0 - dyy = abs(extremes[2] - extremes[3]) / 1000 + yy0 = (ymin + ymax) / 2 elif self.nth_coord == 1: - xx0 = (extremes[0] + extremes[1]) / 2 + xx0 = (xmin + xmax) / 2 yy0 = self.value - dxx = abs(extremes[0] - extremes[1]) / 1000 - dyy = 0 - - grid_finder = self.grid_helper.grid_finder - (xx1,), (yy1,) = grid_finder.transform_xy([xx0], [yy0]) - - data_to_axes = axes.transData - axes.transAxes - p = data_to_axes.transform([xx1, yy1]) - + xy1, dxy1_dx, dxy1_dy = _value_and_jacobian( + trf_xy, xx0, yy0, (xmin, xmax), (ymin, ymax)) + p = axes.transAxes.inverted().transform(xy1) if 0 <= p[0] <= 1 and 0 <= p[1] <= 1: - xx1c, yy1c = axes.transData.transform([xx1, yy1]) - (xx2,), (yy2,) = grid_finder.transform_xy([xx0 + dxx], [yy0 + dyy]) - xx2c, yy2c = axes.transData.transform([xx2, yy2]) - return (xx1c, yy1c), np.rad2deg(np.arctan2(yy2c-yy1c, xx2c-xx1c)) + d = [dxy1_dy, dxy1_dx][self.nth_coord] + return xy1, np.rad2deg(np.arctan2(*d[::-1])) else: return None, None @@ -178,81 +172,48 @@ def get_tick_transform(self, axes): def get_tick_iterators(self, axes): """tick_loc, tick_angle, tick_label, (optionally) tick_label""" - grid_finder = self.grid_helper.grid_finder - - lat_levs, lat_n, lat_factor = self.grid_info["lat_info"] - lat_levs = np.asarray(lat_levs) + lat_levs, lat_n, lat_factor = self._grid_info["lat_info"] yy0 = lat_levs / lat_factor - dy = 0.01 / lat_factor - lon_levs, lon_n, lon_factor = self.grid_info["lon_info"] - lon_levs = np.asarray(lon_levs) + lon_levs, lon_n, lon_factor = self._grid_info["lon_info"] xx0 = lon_levs / lon_factor - dx = 0.01 / lon_factor e0, e1 = self._extremes - if self.nth_coord == 0: - mask = (e0 <= yy0) & (yy0 <= e1) - #xx0, yy0 = xx0[mask], yy0[mask] - yy0 = yy0[mask] - elif self.nth_coord == 1: - mask = (e0 <= xx0) & (xx0 <= e1) - #xx0, yy0 = xx0[mask], yy0[mask] - xx0 = xx0[mask] - - def transform_xy(x, y): - x1, y1 = grid_finder.transform_xy(x, y) - x2y2 = axes.transData.transform(np.array([x1, y1]).transpose()) - x2, y2 = x2y2.transpose() - return x2, y2 + def trf_xy(x, y): + trf = self.grid_helper.grid_finder.get_transform() + axes.transData + return trf.transform(np.column_stack(np.broadcast_arrays(x, y))).T # find angles if self.nth_coord == 0: - xx0 = np.full_like(yy0, self.value) - - xx1, yy1 = transform_xy(xx0, yy0) - - xx00 = xx0.copy() - xx00[xx0 + dx > e1] -= dx - xx1a, yy1a = transform_xy(xx00, yy0) - xx1b, yy1b = transform_xy(xx00+dx, yy0) - - xx2a, yy2a = transform_xy(xx0, yy0) - xx2b, yy2b = transform_xy(xx0, yy0+dy) - - labels = self.grid_info["lat_labels"] - labels = [l for l, m in zip(labels, mask) if m] + mask = (e0 <= yy0) & (yy0 <= e1) + (xx1, yy1), (dxx1, dyy1), (dxx2, dyy2) = _value_and_jacobian( + trf_xy, self.value, yy0[mask], (-np.inf, np.inf), (e0, e1)) + labels = self._grid_info["lat_labels"] elif self.nth_coord == 1: - yy0 = np.full_like(xx0, self.value) - - xx1, yy1 = transform_xy(xx0, yy0) + mask = (e0 <= xx0) & (xx0 <= e1) + (xx1, yy1), (dxx2, dyy2), (dxx1, dyy1) = _value_and_jacobian( + trf_xy, xx0[mask], self.value, (-np.inf, np.inf), (e0, e1)) + labels = self._grid_info["lon_labels"] - xx1a, yy1a = transform_xy(xx0, yy0) - xx1b, yy1b = transform_xy(xx0, yy0+dy) + labels = [l for l, m in zip(labels, mask) if m] - xx00 = xx0.copy() - xx00[xx0 + dx > e1] -= dx - xx2a, yy2a = transform_xy(xx00, yy0) - xx2b, yy2b = transform_xy(xx00+dx, yy0) + angle_normal = np.arctan2(dyy1, dxx1) + angle_tangent = np.arctan2(dyy2, dxx2) + mm = (dyy1 == 0) & (dxx1 == 0) # points with degenerate normal + angle_normal[mm] = angle_tangent[mm] + np.pi / 2 - labels = self.grid_info["lon_labels"] - labels = [l for l, m in zip(labels, mask) if m] + tick_to_axes = self.get_tick_transform(axes) - axes.transAxes + in_01 = functools.partial( + mpl.transforms._interval_contains_close, (0, 1)) def f1(): - dd = np.arctan2(yy1b-yy1a, xx1b-xx1a) # angle normal - dd2 = np.arctan2(yy2b-yy2a, xx2b-xx2a) # angle tangent - mm = (yy1b == yy1a) & (xx1b == xx1a) # mask where dd not defined - dd[mm] = dd2[mm] + np.pi / 2 - - tick_to_axes = self.get_tick_transform(axes) - axes.transAxes - for x, y, d, d2, lab in zip(xx1, yy1, dd, dd2, labels): + for x, y, normal, tangent, lab \ + in zip(xx1, yy1, angle_normal, angle_tangent, labels): c2 = tick_to_axes.transform((x, y)) - delta = 0.00001 - if 0-delta <= c2[0] <= 1+delta and 0-delta <= c2[1] <= 1+delta: - d1, d2 = np.rad2deg([d, d2]) - yield [x, y], d1, d2, lab + if in_01(c2[0]) and in_01(c2[1]): + yield [x, y], *np.rad2deg([normal, tangent]), lab return f1(), iter([]) @@ -261,16 +222,11 @@ def get_line_transform(self, axes): def get_line(self, axes): self.update_lim(axes) - x, y = self.grid_info["line_xy"] - - if self._get_line_path is None: - return Path(np.column_stack([x, y])) - else: - return self._get_line_path(axes, x, y) + x, y = self._grid_info["line_xy"] + return Path(np.column_stack([x, y])) class GridHelperCurveLinear(GridHelperBase): - def __init__(self, aux_trans, extreme_finder=None, grid_locator1=None, @@ -288,7 +244,7 @@ def __init__(self, aux_trans, e.g., ``x2, y2 = trans(x1, y1)`` """ super().__init__() - self.grid_info = None + self._grid_info = None self._aux_trans = aux_trans self.grid_finder = GridFinder(aux_trans, extreme_finder, @@ -297,10 +253,10 @@ def __init__(self, aux_trans, tick_formatter1, tick_formatter2) - def update_grid_finder(self, aux_trans=None, **kw): + def update_grid_finder(self, aux_trans=None, **kwargs): if aux_trans is not None: self.grid_finder.update_transform(aux_trans) - self.grid_finder.update(**kw) + self.grid_finder.update(**kwargs) self._old_limits = None # Force revalidation. def new_fixed_axis(self, loc, @@ -347,15 +303,15 @@ def new_floating_axis(self, nth_coord, return axisline def _update_grid(self, x1, y1, x2, y2): - self.grid_info = self.grid_finder.get_grid_info(x1, y1, x2, y2) + self._grid_info = self.grid_finder.get_grid_info(x1, y1, x2, y2) def get_gridlines(self, which="major", axis="both"): grid_lines = [] if axis in ["both", "x"]: - for gl in self.grid_info["lon"]["lines"]: + for gl in self._grid_info["lon"]["lines"]: grid_lines.extend(gl) if axis in ["both", "y"]: - for gl in self.grid_info["lat"]["lines"]: + for gl in self._grid_info["lat"]["lines"]: grid_lines.extend(gl) return grid_lines @@ -367,15 +323,15 @@ def get_tick_iterator(self, nth_coord, axis_side, minor=False): lon_or_lat = ["lon", "lat"][nth_coord] if not minor: # major ticks for (xy, a), l in zip( - self.grid_info[lon_or_lat]["tick_locs"][axis_side], - self.grid_info[lon_or_lat]["tick_labels"][axis_side]): + self._grid_info[lon_or_lat]["tick_locs"][axis_side], + self._grid_info[lon_or_lat]["tick_labels"][axis_side]): angle_normal = a yield xy, angle_normal, angle_tangent, l else: for (xy, a), l in zip( - self.grid_info[lon_or_lat]["tick_locs"][axis_side], - self.grid_info[lon_or_lat]["tick_labels"][axis_side]): + self._grid_info[lon_or_lat]["tick_locs"][axis_side], + self._grid_info[lon_or_lat]["tick_labels"][axis_side]): angle_normal = a yield xy, angle_normal, angle_tangent, "" - # for xy, a, l in self.grid_info[lon_or_lat]["ticks"][axis_side]: + # for xy, a, l in self._grid_info[lon_or_lat]["ticks"][axis_side]: # yield xy, a, "" diff --git a/lib/mpl_toolkits/axisartist/parasite_axes.py b/lib/mpl_toolkits/axisartist/parasite_axes.py index b4d1955bb1b5..4ebd6acc03be 100644 --- a/lib/mpl_toolkits/axisartist/parasite_axes.py +++ b/lib/mpl_toolkits/axisartist/parasite_axes.py @@ -1,12 +1,7 @@ -from matplotlib import _api from mpl_toolkits.axes_grid1.parasite_axes import ( - host_axes_class_factory, parasite_axes_class_factory, - parasite_axes_auxtrans_class_factory, subplot_class_factory) + host_axes_class_factory, parasite_axes_class_factory) from .axislines import Axes ParasiteAxes = parasite_axes_class_factory(Axes) -HostAxes = host_axes_class_factory(Axes) -SubplotHost = subplot_class_factory(HostAxes) -with _api.suppress_matplotlib_deprecation_warning(): - ParasiteAxesAuxTrans = parasite_axes_auxtrans_class_factory(ParasiteAxes) +HostAxes = SubplotHost = host_axes_class_factory(Axes) diff --git a/lib/mpl_toolkits/axisartist/tests/__init__.py b/lib/mpl_toolkits/axisartist/tests/__init__.py new file mode 100644 index 000000000000..5b6390f4fe26 --- /dev/null +++ b/lib/mpl_toolkits/axisartist/tests/__init__.py @@ -0,0 +1,10 @@ +from pathlib import Path + + +# Check that the test directories exist +if not (Path(__file__).parent / "baseline_images").exists(): + raise IOError( + 'The baseline image directory does not exist. ' + 'This is most likely because the test data is not installed. ' + 'You may need to install matplotlib from source to get the ' + 'test data.') diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_labelbase.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_labelbase.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_labelbase.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_labelbase.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_ticklabels.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_ticklabels.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_ticklabels.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_ticklabels.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_ticks.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_ticks.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_axis_artist/axis_artist_ticks.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_axis_artist/axis_artist_ticks.png diff --git a/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/ParasiteAxesAuxTrans_meshplot.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/ParasiteAxesAuxTrans_meshplot.png new file mode 100644 index 000000000000..f3d0f67c5ce5 Binary files /dev/null and b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/ParasiteAxesAuxTrans_meshplot.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_axislines/Subplot.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/Subplot.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_axislines/Subplot.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/Subplot.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_axislines/SubplotZero.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/SubplotZero.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_axislines/SubplotZero.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/SubplotZero.png diff --git a/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/axisline_style.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/axisline_style.png new file mode 100644 index 000000000000..31c194bd8af6 Binary files /dev/null and b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/axisline_style.png differ diff --git a/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/axisline_style_size_color.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/axisline_style_size_color.png new file mode 100644 index 000000000000..046928cba3c7 Binary files /dev/null and b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/axisline_style_size_color.png differ diff --git a/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/axisline_style_tight.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/axisline_style_tight.png new file mode 100644 index 000000000000..77314c1695a0 Binary files /dev/null and b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/axisline_style_tight.png differ diff --git a/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/subplotzero_ylabel.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/subplotzero_ylabel.png new file mode 100644 index 000000000000..9dc9e4a1540d Binary files /dev/null and b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_axislines/subplotzero_ylabel.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_floating_axes/curvelinear3.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_floating_axes/curvelinear3.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_floating_axes/curvelinear3.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_floating_axes/curvelinear3.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_floating_axes/curvelinear4.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_floating_axes/curvelinear4.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_floating_axes/curvelinear4.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_floating_axes/curvelinear4.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/axis_direction.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_grid_helper_curvelinear/axis_direction.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/axis_direction.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_grid_helper_curvelinear/axis_direction.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/custom_transform.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_grid_helper_curvelinear/custom_transform.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/custom_transform.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_grid_helper_curvelinear/custom_transform.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/polar_box.png b/lib/mpl_toolkits/axisartist/tests/baseline_images/test_grid_helper_curvelinear/polar_box.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_axisartist_grid_helper_curvelinear/polar_box.png rename to lib/mpl_toolkits/axisartist/tests/baseline_images/test_grid_helper_curvelinear/polar_box.png diff --git a/lib/mpl_toolkits/axisartist/tests/conftest.py b/lib/mpl_toolkits/axisartist/tests/conftest.py new file mode 100644 index 000000000000..61c2de3e07ba --- /dev/null +++ b/lib/mpl_toolkits/axisartist/tests/conftest.py @@ -0,0 +1,2 @@ +from matplotlib.testing.conftest import (mpl_test_settings, # noqa + pytest_configure, pytest_unconfigure) diff --git a/lib/mpl_toolkits/tests/test_axisartist_angle_helper.py b/lib/mpl_toolkits/axisartist/tests/test_angle_helper.py similarity index 100% rename from lib/mpl_toolkits/tests/test_axisartist_angle_helper.py rename to lib/mpl_toolkits/axisartist/tests/test_angle_helper.py diff --git a/lib/mpl_toolkits/tests/test_axisartist_axis_artist.py b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py similarity index 95% rename from lib/mpl_toolkits/tests/test_axisartist_axis_artist.py rename to lib/mpl_toolkits/axisartist/tests/test_axis_artist.py index 1bebbfd9b81d..391fd116ea86 100644 --- a/lib/mpl_toolkits/tests/test_axisartist_axis_artist.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axis_artist.py @@ -34,8 +34,8 @@ def test_labelbase(): ax.plot([0.5], [0.5], "o") label = LabelBase(0.5, 0.5, "Test") - label._set_ref_angle(-90) - label._set_offset_radius(offset_radius=50) + label._ref_angle = -90 + label._offset_radius = 50 label.set_rotation(-90) label.set(ha="center", va="top") ax.add_artist(label) @@ -67,8 +67,8 @@ def test_ticklabels(): ax.plot([0.5], [0.5], "s") axislabel = AxisLabel(0.5, 0.5, "Test") - axislabel._set_offset_radius(20) - axislabel._set_ref_angle(0) + axislabel._offset_radius = 20 + axislabel._ref_angle = 0 axislabel.set_axis_direction("bottom") ax.add_artist(axislabel) diff --git a/lib/mpl_toolkits/tests/test_axisartist_axislines.py b/lib/mpl_toolkits/axisartist/tests/test_axislines.py similarity index 50% rename from lib/mpl_toolkits/tests/test_axisartist_axislines.py rename to lib/mpl_toolkits/axisartist/tests/test_axislines.py index 0502a0f23879..123123069623 100644 --- a/lib/mpl_toolkits/tests/test_axisartist_axislines.py +++ b/lib/mpl_toolkits/axisartist/tests/test_axislines.py @@ -1,14 +1,10 @@ import numpy as np -from matplotlib import _api import matplotlib.pyplot as plt from matplotlib.testing.decorators import image_comparison from matplotlib.transforms import IdentityTransform -from mpl_toolkits.axisartist.axislines import SubplotZero, Subplot -from mpl_toolkits.axisartist import ( - Axes, SubplotHost, ParasiteAxes, ParasiteAxesAuxTrans) - -import pytest +from mpl_toolkits.axisartist.axislines import AxesZero, SubplotZero, Subplot +from mpl_toolkits.axisartist import Axes, SubplotHost @image_comparison(['SubplotZero.png'], style='default') @@ -61,10 +57,9 @@ def test_Axes(): fig.canvas.draw() -@pytest.mark.parametrize('parasite_cls', [ParasiteAxes, ParasiteAxesAuxTrans]) @image_comparison(['ParasiteAxesAuxTrans_meshplot.png'], remove_text=True, style='default', tol=0.075) -def test_ParasiteAxesAuxTrans(parasite_cls): +def test_ParasiteAxesAuxTrans(): # Remove this line when this test image is regenerated. plt.rcParams['pcolormesh.snap'] = False @@ -86,9 +81,7 @@ def test_ParasiteAxesAuxTrans(parasite_cls): ax1 = SubplotHost(fig, 1, 3, i+1) fig.add_subplot(ax1) - with _api.suppress_matplotlib_deprecation_warning(): - ax2 = parasite_cls(ax1, IdentityTransform()) - ax1.parasites.append(ax2) + ax2 = ax1.get_aux_axes(IdentityTransform(), viewlim_mode=None) if name.startswith('pcolor'): getattr(ax2, name)(xx, yy, data[:-1, :-1]) else: @@ -97,3 +90,61 @@ def test_ParasiteAxesAuxTrans(parasite_cls): ax1.set_ylim((0, 5)) ax2.contour(xx, yy, data, colors='k') + + +@image_comparison(['axisline_style.png'], remove_text=True, style='mpl20') +def test_axisline_style(): + fig = plt.figure(figsize=(2, 2)) + ax = fig.add_subplot(axes_class=AxesZero) + ax.axis["xzero"].set_axisline_style("-|>") + ax.axis["xzero"].set_visible(True) + ax.axis["yzero"].set_axisline_style("->") + ax.axis["yzero"].set_visible(True) + + for direction in ("left", "right", "bottom", "top"): + ax.axis[direction].set_visible(False) + + +@image_comparison(['axisline_style_size_color.png'], remove_text=True, + style='mpl20') +def test_axisline_style_size_color(): + fig = plt.figure(figsize=(2, 2)) + ax = fig.add_subplot(axes_class=AxesZero) + ax.axis["xzero"].set_axisline_style("-|>", size=2.0, facecolor='r') + ax.axis["xzero"].set_visible(True) + ax.axis["yzero"].set_axisline_style("->, size=1.5") + ax.axis["yzero"].set_visible(True) + + for direction in ("left", "right", "bottom", "top"): + ax.axis[direction].set_visible(False) + + +@image_comparison(['axisline_style_tight.png'], remove_text=True, + style='mpl20') +def test_axisline_style_tight(): + fig = plt.figure(figsize=(2, 2)) + ax = fig.add_subplot(axes_class=AxesZero) + ax.axis["xzero"].set_axisline_style("-|>", size=5, facecolor='g') + ax.axis["xzero"].set_visible(True) + ax.axis["yzero"].set_axisline_style("->, size=8") + ax.axis["yzero"].set_visible(True) + + for direction in ("left", "right", "bottom", "top"): + ax.axis[direction].set_visible(False) + + fig.tight_layout() + + +@image_comparison(['subplotzero_ylabel.png'], style='mpl20') +def test_subplotzero_ylabel(): + fig = plt.figure() + ax = fig.add_subplot(111, axes_class=SubplotZero) + + ax.set(xlim=(-3, 7), ylim=(-3, 7), xlabel="x", ylabel="y") + + zero_axis = ax.axis["xzero", "yzero"] + zero_axis.set_visible(True) # they are hidden by default + + ax.axis["left", "right", "bottom", "top"].set_visible(False) + + zero_axis.set_axisline_style("->") diff --git a/lib/mpl_toolkits/tests/test_axisartist_floating_axes.py b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py similarity index 58% rename from lib/mpl_toolkits/tests/test_axisartist_floating_axes.py rename to lib/mpl_toolkits/axisartist/tests/test_floating_axes.py index ea319e08caab..d489f492d4d3 100644 --- a/lib/mpl_toolkits/tests/test_axisartist_floating_axes.py +++ b/lib/mpl_toolkits/axisartist/tests/test_floating_axes.py @@ -6,8 +6,7 @@ from matplotlib.testing.decorators import image_comparison from mpl_toolkits.axisartist.axislines import Subplot from mpl_toolkits.axisartist.floating_axes import ( - FloatingSubplot, - GridHelperCurveLinear) + FloatingAxes, GridHelperCurveLinear) from mpl_toolkits.axisartist.grid_finder import FixedLocator from mpl_toolkits.axisartist import angle_helper @@ -18,43 +17,35 @@ def test_subplot(): fig.add_subplot(ax) -@image_comparison(['curvelinear3.png'], style='default', tol=0.01) +# Rather high tolerance to allow ongoing work with floating axes internals; +# remove when image is regenerated. +@image_comparison(['curvelinear3.png'], style='default', tol=5) def test_curvelinear3(): fig = plt.figure(figsize=(5, 5)) tr = (mtransforms.Affine2D().scale(np.pi / 180, 1) + mprojections.PolarAxes.PolarTransform()) - - grid_locator1 = angle_helper.LocatorDMS(15) - tick_formatter1 = angle_helper.FormatterDMS() - - grid_locator2 = FixedLocator([2, 4, 6, 8, 10]) - - grid_helper = GridHelperCurveLinear(tr, - extremes=(0, 360, 10, 3), - grid_locator1=grid_locator1, - grid_locator2=grid_locator2, - tick_formatter1=tick_formatter1, - tick_formatter2=None) - - ax1 = FloatingSubplot(fig, 111, grid_helper=grid_helper) - fig.add_subplot(ax1) + grid_helper = GridHelperCurveLinear( + tr, + extremes=(0, 360, 10, 3), + grid_locator1=angle_helper.LocatorDMS(15), + grid_locator2=FixedLocator([2, 4, 6, 8, 10]), + tick_formatter1=angle_helper.FormatterDMS(), + tick_formatter2=None) + ax1 = fig.add_subplot(axes_class=FloatingAxes, grid_helper=grid_helper) r_scale = 10 tr2 = mtransforms.Affine2D().scale(1, 1 / r_scale) + tr - grid_locator2 = FixedLocator([30, 60, 90]) - grid_helper2 = GridHelperCurveLinear(tr2, - extremes=(0, 360, - 10 * r_scale, 3 * r_scale), - grid_locator2=grid_locator2) + grid_helper2 = GridHelperCurveLinear( + tr2, + extremes=(0, 360, 10 * r_scale, 3 * r_scale), + grid_locator2=FixedLocator([30, 60, 90])) ax1.axis["right"] = axis = grid_helper2.new_fixed_axis("right", axes=ax1) ax1.axis["left"].label.set_text("Test 1") ax1.axis["right"].label.set_text("Test 2") - - for an in ["left", "right"]: - ax1.axis[an].set_visible(False) + ax1.axis["left", "right"].set_visible(False) axis = grid_helper.new_floating_axis(1, 7, axes=ax1, axis_direction="bottom") @@ -72,7 +63,9 @@ def test_curvelinear3(): l.set_clip_path(ax1.patch) -@image_comparison(['curvelinear4.png'], style='default', tol=0.015) +# Rather high tolerance to allow ongoing work with floating axes internals; +# remove when image is regenerated. +@image_comparison(['curvelinear4.png'], style='default', tol=0.9) def test_curvelinear4(): # Remove this line when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 @@ -81,27 +74,18 @@ def test_curvelinear4(): tr = (mtransforms.Affine2D().scale(np.pi / 180, 1) + mprojections.PolarAxes.PolarTransform()) - - grid_locator1 = angle_helper.LocatorDMS(5) - tick_formatter1 = angle_helper.FormatterDMS() - - grid_locator2 = FixedLocator([2, 4, 6, 8, 10]) - - grid_helper = GridHelperCurveLinear(tr, - extremes=(120, 30, 10, 0), - grid_locator1=grid_locator1, - grid_locator2=grid_locator2, - tick_formatter1=tick_formatter1, - tick_formatter2=None) - - ax1 = FloatingSubplot(fig, 111, grid_helper=grid_helper) - fig.add_subplot(ax1) + grid_helper = GridHelperCurveLinear( + tr, + extremes=(120, 30, 10, 0), + grid_locator1=angle_helper.LocatorDMS(5), + grid_locator2=FixedLocator([2, 4, 6, 8, 10]), + tick_formatter1=angle_helper.FormatterDMS(), + tick_formatter2=None) + ax1 = fig.add_subplot(axes_class=FloatingAxes, grid_helper=grid_helper) ax1.axis["left"].label.set_text("Test 1") ax1.axis["right"].label.set_text("Test 2") - - for an in ["top"]: - ax1.axis[an].set_visible(False) + ax1.axis["top"].set_visible(False) axis = grid_helper.new_floating_axis(1, 70, axes=ax1, axis_direction="bottom") diff --git a/lib/mpl_toolkits/axisartist/tests/test_grid_finder.py b/lib/mpl_toolkits/axisartist/tests/test_grid_finder.py new file mode 100644 index 000000000000..6b397675ee10 --- /dev/null +++ b/lib/mpl_toolkits/axisartist/tests/test_grid_finder.py @@ -0,0 +1,34 @@ +import numpy as np +import pytest + +from matplotlib.transforms import Bbox +from mpl_toolkits.axisartist.grid_finder import ( + _find_line_box_crossings, FormatterPrettyPrint, MaxNLocator) + + +def test_find_line_box_crossings(): + x = np.array([-3, -2, -1, 0., 1, 2, 3, 2, 1, 0, -1, -2, -3, 5]) + y = np.arange(len(x)) + bbox = Bbox.from_extents(-2, 3, 2, 12.5) + left, right, bottom, top = _find_line_box_crossings( + np.column_stack([x, y]), bbox) + ((lx0, ly0), la0), ((lx1, ly1), la1), = left + ((rx0, ry0), ra0), ((rx1, ry1), ra1), = right + ((bx0, by0), ba0), = bottom + ((tx0, ty0), ta0), = top + assert (lx0, ly0, la0) == (-2, 11, 135) + assert (lx1, ly1, la1) == pytest.approx((-2., 12.125, 7.125016)) + assert (rx0, ry0, ra0) == (2, 5, 45) + assert (rx1, ry1, ra1) == (2, 7, 135) + assert (bx0, by0, ba0) == (0, 3, 45) + assert (tx0, ty0, ta0) == pytest.approx((1., 12.5, 7.125016)) + + +def test_pretty_print_format(): + locator = MaxNLocator() + locs, nloc, factor = locator(0, 100) + + fmt = FormatterPrettyPrint() + + assert fmt("left", None, locs) == \ + [r'$\mathdefault{%d}$' % (l, ) for l in locs] diff --git a/lib/mpl_toolkits/tests/test_axisartist_grid_helper_curvelinear.py b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py similarity index 89% rename from lib/mpl_toolkits/tests/test_axisartist_grid_helper_curvelinear.py rename to lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py index 9a78a2676adb..037836afec6a 100644 --- a/lib/mpl_toolkits/tests/test_axisartist_grid_helper_curvelinear.py +++ b/lib/mpl_toolkits/axisartist/tests/test_grid_helper_curvelinear.py @@ -1,5 +1,4 @@ import numpy as np -import platform import matplotlib.pyplot as plt from matplotlib.path import Path @@ -7,17 +6,15 @@ from matplotlib.transforms import Affine2D, Transform from matplotlib.testing.decorators import image_comparison -from mpl_toolkits.axes_grid1.parasite_axes import ParasiteAxes from mpl_toolkits.axisartist import SubplotHost -from mpl_toolkits.axes_grid1.parasite_axes import host_subplot_class_factory +from mpl_toolkits.axes_grid1.parasite_axes import host_axes_class_factory from mpl_toolkits.axisartist import angle_helper from mpl_toolkits.axisartist.axislines import Axes from mpl_toolkits.axisartist.grid_helper_curvelinear import \ GridHelperCurveLinear -@image_comparison(['custom_transform.png'], style='default', - tol=0.03 if platform.machine() == 'x86_64' else 0.04) +@image_comparison(['custom_transform.png'], style='default', tol=0.2) def test_custom_transform(): class MyTransform(Transform): input_dims = output_dims = 2 @@ -61,15 +58,14 @@ def inverted(self): fig = plt.figure() - SubplotHost = host_subplot_class_factory(Axes) + SubplotHost = host_axes_class_factory(Axes) tr = MyTransform(1) grid_helper = GridHelperCurveLinear(tr) ax1 = SubplotHost(fig, 1, 1, 1, grid_helper=grid_helper) fig.add_subplot(ax1) - ax2 = ParasiteAxes(ax1, tr, viewlim_mode="equal") - ax1.parasites.append(ax2) + ax2 = ax1.get_aux_axes(tr, viewlim_mode="equal") ax2.plot([3, 6], [5.0, 10.]) ax1.set_aspect(1.) @@ -79,10 +75,9 @@ def inverted(self): ax1.grid(True) -@image_comparison(['polar_box.png'], style='default', - tol={'aarch64': 0.04}.get(platform.machine(), 0.03)) +# Remove tol & kerning_factor when this test image is regenerated. +@image_comparison(['polar_box.png'], style='default', tol=0.27) def test_polar_box(): - # Remove this line when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 fig = plt.figure(figsize=(5, 5)) @@ -130,10 +125,9 @@ def test_polar_box(): axis.get_helper().set_extremes(-180, 90) # A parasite axes with given transform - ax2 = ParasiteAxes(ax1, tr, viewlim_mode="equal") + ax2 = ax1.get_aux_axes(tr, viewlim_mode="equal") assert ax2.transData == tr + ax1.transData # Anything you draw in ax2 will match the ticks and grids of ax1. - ax1.parasites.append(ax2) ax2.plot(np.linspace(0, 30, 50), np.linspace(10, 10, 50)) ax1.set_aspect(1.) @@ -143,9 +137,9 @@ def test_polar_box(): ax1.grid(True) -@image_comparison(['axis_direction.png'], style='default', tol=0.03) +# Remove tol & kerning_factor when this test image is regenerated. +@image_comparison(['axis_direction.png'], style='default', tol=0.12) def test_axis_direction(): - # Remove this line when this test image is regenerated. plt.rcParams['text.kerning_factor'] = 6 fig = plt.figure(figsize=(5, 5)) diff --git a/lib/mpl_toolkits/mplot3d/__init__.py b/lib/mpl_toolkits/mplot3d/__init__.py index 30e682aea018..a089fbd6b70e 100644 --- a/lib/mpl_toolkits/mplot3d/__init__.py +++ b/lib/mpl_toolkits/mplot3d/__init__.py @@ -1 +1,3 @@ from .axes3d import Axes3D + +__all__ = ['Axes3D'] diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 0d49338fc0e5..14511f4a8c2d 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -11,8 +11,10 @@ import numpy as np +from contextlib import contextmanager + from matplotlib import ( - _api, artist, cbook, colors as mcolors, lines, text as mtext, + artist, cbook, colors as mcolors, lines, text as mtext, path as mpath) from matplotlib.collections import ( LineCollection, PolyCollection, PatchCollection, PathCollection) @@ -77,7 +79,7 @@ class Text3D(mtext.Text): Parameters ---------- - x, y, z + x, y, z : float The position of the text. text : str The text string to display. @@ -108,8 +110,8 @@ def set_position_3d(self, xyz, zdir=None): xyz : (float, float, float) The position in 3D space. zdir : {'x', 'y', 'z', None, 3-tuple} - The direction of the text. If unspecified, the zdir will not be - changed. + The direction of the text. If unspecified, the *zdir* will not be + changed. See `.get_dir_vector` for a description of the values. """ super().set_position(xyz[:2]) self.set_z(xyz[2]) @@ -128,6 +130,17 @@ def set_z(self, z): self.stale = True def set_3d_properties(self, z=0, zdir='z'): + """ + Set the *z* position and direction of the text. + + Parameters + ---------- + z : float + The z-position in 3D space. + zdir : {'x', 'y', 'z', 3-tuple} + The direction of the text. Default: 'z'. + See `.get_dir_vector` for a description of the values. + """ self._z = z self._dir_vec = get_dir_vector(zdir) self.stale = True @@ -145,14 +158,24 @@ def draw(self, renderer): mtext.Text.draw(self, renderer) self.stale = False - def get_tightbbox(self, renderer): + def get_tightbbox(self, renderer=None): # Overwriting the 2d Text behavior which is not valid for 3d. # For now, just return None to exclude from layout calculation. return None def text_2d_to_3d(obj, z=0, zdir='z'): - """Convert a Text to a Text3D object.""" + """ + Convert a `.Text` to a `.Text3D` object. + + Parameters + ---------- + z : float + The z-position in 3D space. + zdir : {'x', 'y', 'z', 3-tuple} + The direction of the text. Default: 'z'. + See `.get_dir_vector` for a description of the values. + """ obj.__class__ = Text3D obj.set_3d_properties(z, zdir) @@ -164,15 +187,38 @@ class Line3D(lines.Line2D): def __init__(self, xs, ys, zs, *args, **kwargs): """ - Keyword arguments are passed onto :func:`~matplotlib.lines.Line2D`. + + Parameters + ---------- + xs : array-like + The x-data to be plotted. + ys : array-like + The y-data to be plotted. + zs : array-like + The z-data to be plotted. + + Additional arguments are passed onto :func:`~matplotlib.lines.Line2D`. """ super().__init__([], [], *args, **kwargs) - self._verts3d = xs, ys, zs + self.set_data_3d(xs, ys, zs) def set_3d_properties(self, zs=0, zdir='z'): + """ + Set the *z* position and direction of the line. + + Parameters + ---------- + zs : float or array of floats + The location along the *zdir* axis in 3D space to position the + line. + zdir : {'x', 'y', 'z'} + Plane to plot line orthogonal to. Default: 'z'. + See `.get_dir_vector` for a description of the values. + """ xs = self.get_xdata() ys = self.get_ydata() - zs = np.broadcast_to(zs, xs.shape) + zs = cbook._to_unmasked_float_array(zs).ravel() + zs = np.broadcast_to(zs, len(xs)) self._verts3d = juggle_axes(xs, ys, zs, zdir) self.stale = True @@ -194,9 +240,11 @@ def set_data_3d(self, *args): Accepts x, y, z arguments or a single array-like (x, y, z) """ if len(args) == 1: - self._verts3d = args[0] - else: - self._verts3d = args + args = args[0] + for name, xyz in zip('xyz', args): + if not np.iterable(xyz): + raise RuntimeError(f'{name} must be a sequence') + self._verts3d = args self.stale = True def get_data_3d(self): @@ -220,7 +268,17 @@ def draw(self, renderer): def line_2d_to_3d(line, zs=0, zdir='z'): - """Convert a 2D line to 3D.""" + """ + Convert a `.Line2D` to a `.Line3D` object. + + Parameters + ---------- + zs : float + The location along the *zdir* axis in 3D space to position the line. + zdir : {'x', 'y', 'z'} + Plane to plot line orthogonal to. Default: 'z'. + See `.get_dir_vector` for a description of the values. + """ line.__class__ = Line3D line.set_3d_properties(zs, zdir) @@ -297,8 +355,7 @@ def set_segments(self, segments): self._segments3d = segments super().set_segments([]) - @_api.delete_parameter('3.4', 'renderer') - def do_3d_projection(self, renderer=None): + def do_3d_projection(self): """ Project the points according to renderer matrix. """ @@ -313,17 +370,9 @@ def do_3d_projection(self, renderer=None): minz = min(minz, min(zs)) return minz - @artist.allow_rasterization - @_api.delete_parameter('3.4', 'project', - alternative='Line3DCollection.do_3d_projection') - def draw(self, renderer, project=False): - if project: - self.do_3d_projection() - super().draw(renderer) - def line_collection_2d_to_3d(col, zs=0, zdir='z'): - """Convert a LineCollection to a Line3DCollection object.""" + """Convert a `.LineCollection` to a `.Line3DCollection` object.""" segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir) col.__class__ = Line3DCollection col.set_segments(segments3d) @@ -335,10 +384,34 @@ class Patch3D(Patch): """ def __init__(self, *args, zs=(), zdir='z', **kwargs): + """ + Parameters + ---------- + verts : + zs : float + The location along the *zdir* axis in 3D space to position the + patch. + zdir : {'x', 'y', 'z'} + Plane to plot patch orthogonal to. Default: 'z'. + See `.get_dir_vector` for a description of the values. + """ super().__init__(*args, **kwargs) self.set_3d_properties(zs, zdir) def set_3d_properties(self, verts, zs=0, zdir='z'): + """ + Set the *z* position and direction of the patch. + + Parameters + ---------- + verts : + zs : float + The location along the *zdir* axis in 3D space to position the + patch. + zdir : {'x', 'y', 'z'} + Plane to plot patch orthogonal to. Default: 'z'. + See `.get_dir_vector` for a description of the values. + """ zs = np.broadcast_to(zs, len(verts)) self._segment3d = [juggle_axes(x, y, z, zdir) for ((x, y), z) in zip(verts, zs)] @@ -346,8 +419,7 @@ def set_3d_properties(self, verts, zs=0, zdir='z'): def get_path(self): return self._path2d - @_api.delete_parameter('3.4', 'renderer') - def do_3d_projection(self, renderer=None): + def do_3d_projection(self): s = self._segment3d xs, ys, zs = zip(*s) vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, @@ -362,16 +434,39 @@ class PathPatch3D(Patch3D): """ def __init__(self, path, *, zs=(), zdir='z', **kwargs): + """ + Parameters + ---------- + path : + zs : float + The location along the *zdir* axis in 3D space to position the + path patch. + zdir : {'x', 'y', 'z', 3-tuple} + Plane to plot path patch orthogonal to. Default: 'z'. + See `.get_dir_vector` for a description of the values. + """ # Not super().__init__! Patch.__init__(self, **kwargs) self.set_3d_properties(path, zs, zdir) def set_3d_properties(self, path, zs=0, zdir='z'): + """ + Set the *z* position and direction of the path patch. + + Parameters + ---------- + path : + zs : float + The location along the *zdir* axis in 3D space to position the + path patch. + zdir : {'x', 'y', 'z', 3-tuple} + Plane to plot path patch orthogonal to. Default: 'z'. + See `.get_dir_vector` for a description of the values. + """ Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir) self._code3d = path.codes - @_api.delete_parameter('3.4', 'renderer') - def do_3d_projection(self, renderer=None): + def do_3d_projection(self): s = self._segment3d xs, ys, zs = zip(*s) vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, @@ -389,14 +484,14 @@ def _get_patch_verts(patch): def patch_2d_to_3d(patch, z=0, zdir='z'): - """Convert a Patch to a Patch3D object.""" + """Convert a `.Patch` to a `.Patch3D` object.""" verts = _get_patch_verts(patch) patch.__class__ = Patch3D patch.set_3d_properties(verts, z, zdir) def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'): - """Convert a PathPatch to a PathPatch3D object.""" + """Convert a `.PathPatch` to a `.PathPatch3D` object.""" path = pathpatch.get_path() trans = pathpatch.get_patch_transform() @@ -421,10 +516,9 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): :class:`~matplotlib.collections.PatchCollection`. In addition, keywords *zs=0* and *zdir='z'* are available. - Also, the keyword argument *depthshade* is available to - indicate whether or not to shade the patches in order to - give the appearance of depth (default is *True*). - This is typically desired in scatter plots. + Also, the keyword argument *depthshade* is available to indicate + whether to shade the patches in order to give the appearance of depth + (default is *True*). This is typically desired in scatter plots. """ self._depthshade = depthshade super().__init__(*args, **kwargs) @@ -452,6 +546,19 @@ def set_sort_zpos(self, val): self.stale = True def set_3d_properties(self, zs, zdir): + """ + Set the *z* positions and direction of the patches. + + Parameters + ---------- + zs : float or array of floats + The location or locations to place the patches in the collection + along the *zdir* axis. + zdir : {'x', 'y', 'z'} + Plane to plot patches orthogonal to. + All patches must have the same direction. + See `.get_dir_vector` for a description of the values. + """ # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. self.update_scalarmappable() @@ -466,8 +573,7 @@ def set_3d_properties(self, zs, zdir): self._vzs = None self.stale = True - @_api.delete_parameter('3.4', 'renderer') - def do_3d_projection(self, renderer=None): + def do_3d_projection(self): xs, ys, zs = self._offsets3d vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, self.axes.M) @@ -517,19 +623,20 @@ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): :class:`~matplotlib.collections.PathCollection`. In addition, keywords *zs=0* and *zdir='z'* are available. - Also, the keyword argument *depthshade* is available to - indicate whether or not to shade the patches in order to - give the appearance of depth (default is *True*). - This is typically desired in scatter plots. + Also, the keyword argument *depthshade* is available to indicate + whether to shade the patches in order to give the appearance of depth + (default is *True*). This is typically desired in scatter plots. """ self._depthshade = depthshade self._in_draw = False super().__init__(*args, **kwargs) self.set_3d_properties(zs, zdir) + self._offset_zordered = None def draw(self, renderer): - with cbook._setattr_cm(self, _in_draw=True): - super().draw(renderer) + with self._use_zordered_offset(): + with cbook._setattr_cm(self, _in_draw=True): + super().draw(renderer) def set_sort_zpos(self, val): """Set the position to use for z-sorting.""" @@ -537,6 +644,19 @@ def set_sort_zpos(self, val): self.stale = True def set_3d_properties(self, zs, zdir): + """ + Set the *z* positions and direction of the paths. + + Parameters + ---------- + zs : float or array of floats + The location or locations to place the paths in the collection + along the *zdir* axis. + zdir : {'x', 'y', 'z'} + Plane to plot paths orthogonal to. + All paths must have the same direction. + See `.get_dir_vector` for a description of the values. + """ # Force the collection to initialize the face and edgecolors # just in case it is a scalarmappable with a colormap. self.update_scalarmappable() @@ -558,7 +678,7 @@ def set_3d_properties(self, zs, zdir): # # Grab the current sizes and linewidths to preserve them. self._sizes3d = self._sizes - self._linewidths3d = self._linewidths + self._linewidths3d = np.array(self._linewidths) xs, ys, zs = self._offsets3d # Sort the points based on z coordinates @@ -576,7 +696,7 @@ def set_sizes(self, sizes, dpi=72.0): def set_linewidth(self, lw): super().set_linewidth(lw) if not self._in_draw: - self._linewidth3d = lw + self._linewidths3d = np.array(self._linewidths) def get_depthshade(self): return self._depthshade @@ -594,8 +714,7 @@ def set_depthshade(self, depthshade): self._depthshade = depthshade self.stale = True - @_api.delete_parameter('3.4', 'renderer') - def do_3d_projection(self, renderer=None): + def do_3d_projection(self): xs, ys, zs = self._offsets3d vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, self.axes.M) @@ -616,15 +735,32 @@ def do_3d_projection(self, renderer=None): if len(self._linewidths3d) > 1: self._linewidths = self._linewidths3d[z_markers_idx] + PathCollection.set_offsets(self, np.column_stack((vxs, vys))) + # Re-order items vzs = vzs[z_markers_idx] vxs = vxs[z_markers_idx] vys = vys[z_markers_idx] - PathCollection.set_offsets(self, np.column_stack((vxs, vys))) + # Store ordered offset for drawing purpose + self._offset_zordered = np.column_stack((vxs, vys)) return np.min(vzs) if vzs.size else np.nan + @contextmanager + def _use_zordered_offset(self): + if self._offset_zordered is None: + # Do nothing + yield + else: + # Swap offset with z-ordered offset + old_offset = self._offsets + super().set_offsets(self._offset_zordered) + try: + yield + finally: + self._offsets = old_offset + def _maybe_depth_shade_and_sort_colors(self, color_array): color_array = ( _zalpha(color_array, self._vzs) @@ -649,18 +785,17 @@ def get_edgecolor(self): def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): """ - Convert a :class:`~matplotlib.collections.PatchCollection` into a - :class:`Patch3DCollection` object - (or a :class:`~matplotlib.collections.PathCollection` into a - :class:`Path3DCollection` object). + Convert a `.PatchCollection` into a `.Patch3DCollection` object + (or a `.PathCollection` into a `.Path3DCollection` object). Parameters ---------- - za + zs : float or array of floats The location or locations to place the patches in the collection along the *zdir* axis. Default: 0. - zdir + zdir : {'x', 'y', 'z'} The axis in which to place the patches. Default: "z". + See `.get_dir_vector` for a description of the values. depthshade Whether to shade the patches to give a sense of depth. Default: *True*. @@ -695,16 +830,29 @@ class Poly3DCollection(PolyCollection): triangulation and thus generates consistent surfaces. """ - def __init__(self, verts, *args, zsort='average', **kwargs): + def __init__(self, verts, *args, zsort='average', shade=False, + lightsource=None, **kwargs): """ Parameters ---------- verts : list of (N, 3) array-like - Each element describes a polygon as a sequence of ``N_i`` points - ``(x, y, z)``. + The sequence of polygons [*verts0*, *verts1*, ...] where each + element *verts_i* defines the vertices of polygon *i* as a 2D + array-like of shape (N, 3). zsort : {'average', 'min', 'max'}, default: 'average' The calculation method for the z-order. See `~.Poly3DCollection.set_zsort` for details. + shade : bool, default: False + Whether to shade *facecolors* and *edgecolors*. When activating + *shade*, *facecolors* and/or *edgecolors* must be provided. + + .. versionadded:: 3.7 + + lightsource : `~matplotlib.colors.LightSource` + The lightsource to use when *shade* is True. + + .. versionadded:: 3.7 + *args, **kwargs All other parameters are forwarded to `.PolyCollection`. @@ -713,7 +861,30 @@ def __init__(self, verts, *args, zsort='average', **kwargs): Note that this class does a bit of magic with the _facecolors and _edgecolors properties. """ + if shade: + normals = _generate_normals(verts) + facecolors = kwargs.get('facecolors', None) + if facecolors is not None: + kwargs['facecolors'] = _shade_colors( + facecolors, normals, lightsource + ) + + edgecolors = kwargs.get('edgecolors', None) + if edgecolors is not None: + kwargs['edgecolors'] = _shade_colors( + edgecolors, normals, lightsource + ) + if facecolors is None and edgecolors in None: + raise ValueError( + "You must provide facecolors, edgecolors, or both for " + "shade to work.") super().__init__(verts, *args, **kwargs) + if isinstance(verts, np.ndarray): + if verts.ndim != 3: + raise ValueError('verts must be a list of (N, 3) array-like') + else: + if any(len(np.shape(vert)) != 2 for vert in verts): + raise ValueError('verts must be a list of (N, 3) array-like') self.set_zsort(zsort) self._codes3d = None @@ -750,7 +921,19 @@ def get_vector(self, segments3d): self._segslices = [*map(slice, indices[:-1], indices[1:])] def set_verts(self, verts, closed=True): - """Set 3D vertices.""" + """ + Set 3D vertices. + + Parameters + ---------- + verts : list of (N, 3) array-like + The sequence of polygons [*verts0*, *verts1*, ...] where each + element *verts_i* defines the vertices of polygon *i* as a 2D + array-like of shape (N, 3). + closed : bool, default: True + Whether the polygon should be closed by adding a CLOSEPOLY + connection at the end. + """ self.get_vector(verts) # 2D verts will be updated at draw time super().set_verts([], False) @@ -780,8 +963,7 @@ def set_sort_zpos(self, val): self._sort_zpos = val self.stale = True - @_api.delete_parameter('3.4', 'renderer') - def do_3d_projection(self, renderer=None): + def do_3d_projection(self): """ Perform the 3D projection for this object. """ @@ -793,13 +975,11 @@ def do_3d_projection(self, renderer=None): # # We hold the 3D versions in a fixed order (the order the user # passed in) and sort the 2D version by view depth. - copy_state = self._update_dict['array'] self.update_scalarmappable() - if copy_state: - if self._face_is_mapped: - self._facecolor3d = self._facecolors - if self._edge_is_mapped: - self._edgecolor3d = self._edgecolors + if self._face_is_mapped: + self._facecolor3d = self._facecolors + if self._edge_is_mapped: + self._edgecolor3d = self._edgecolors txs, tys, tzs = proj3d._proj_transform_vec(self._vec, self.axes.M) xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices] @@ -878,14 +1058,35 @@ def set_alpha(self, alpha): self.stale = True def get_facecolor(self): + # docstring inherited + # self._facecolors2d is not initialized until do_3d_projection + if not hasattr(self, '_facecolors2d'): + self.axes.M = self.axes.get_proj() + self.do_3d_projection() return self._facecolors2d def get_edgecolor(self): + # docstring inherited + # self._edgecolors2d is not initialized until do_3d_projection + if not hasattr(self, '_edgecolors2d'): + self.axes.M = self.axes.get_proj() + self.do_3d_projection() return self._edgecolors2d def poly_collection_2d_to_3d(col, zs=0, zdir='z'): - """Convert a PolyCollection to a Poly3DCollection object.""" + """ + Convert a `.PolyCollection` into a `.Poly3DCollection` object. + + Parameters + ---------- + zs : float or array of floats + The location or locations to place the polygons in the collection along + the *zdir* axis. Default: 0. + zdir : {'x', 'y', 'z'} + The axis in which to place the patches. Default: 'z'. + See `.get_dir_vector` for a description of the values. + """ segments_3d, codes = _paths_to_3d_segments_with_codes( col.get_paths(), zs, zdir) col.__class__ = Poly3DCollection @@ -895,9 +1096,10 @@ def poly_collection_2d_to_3d(col, zs=0, zdir='z'): def juggle_axes(xs, ys, zs, zdir): """ - Reorder coordinates so that 2D xs, ys can be plotted in the plane - orthogonal to zdir. zdir is normally x, y or z. However, if zdir - starts with a '-' it is interpreted as a compensation for rotate_axes. + Reorder coordinates so that 2D *xs*, *ys* can be plotted in the plane + orthogonal to *zdir*. *zdir* is normally 'x', 'y' or 'z'. However, if + *zdir* starts with a '-' it is interpreted as a compensation for + `rotate_axes`. """ if zdir == 'x': return zs, xs, ys @@ -911,20 +1113,14 @@ def juggle_axes(xs, ys, zs, zdir): def rotate_axes(xs, ys, zs, zdir): """ - Reorder coordinates so that the axes are rotated with zdir along + Reorder coordinates so that the axes are rotated with *zdir* along the original z axis. Prepending the axis with a '-' does the - inverse transform, so zdir can be x, -x, y, -y, z or -z + inverse transform, so *zdir* can be 'x', '-x', 'y', '-y', 'z' or '-z'. """ - if zdir == 'x': + if zdir in ('x', '-y'): return ys, zs, xs - elif zdir == '-x': - return zs, xs, ys - - elif zdir == 'y': + elif zdir in ('-x', 'y'): return zs, xs, ys - elif zdir == '-y': - return ys, zs, xs - else: return xs, ys, zs @@ -941,3 +1137,84 @@ def _zalpha(colors, zs): sats = 1 - norm(zs) * 0.7 rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4)) return np.column_stack([rgba[:, :3], rgba[:, 3] * sats]) + + +def _generate_normals(polygons): + """ + Compute the normals of a list of polygons, one normal per polygon. + + Normals point towards the viewer for a face with its vertices in + counterclockwise order, following the right hand rule. + + Uses three points equally spaced around the polygon. This method assumes + that the points are in a plane. Otherwise, more than one shade is required, + which is not supported. + + Parameters + ---------- + polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like + A sequence of polygons to compute normals for, which can have + varying numbers of vertices. If the polygons all have the same + number of vertices and array is passed, then the operation will + be vectorized. + + Returns + ------- + normals : (..., 3) array + A normal vector estimated for the polygon. + """ + if isinstance(polygons, np.ndarray): + # optimization: polygons all have the same number of points, so can + # vectorize + n = polygons.shape[-2] + i1, i2, i3 = 0, n//3, 2*n//3 + v1 = polygons[..., i1, :] - polygons[..., i2, :] + v2 = polygons[..., i2, :] - polygons[..., i3, :] + else: + # The subtraction doesn't vectorize because polygons is jagged. + v1 = np.empty((len(polygons), 3)) + v2 = np.empty((len(polygons), 3)) + for poly_i, ps in enumerate(polygons): + n = len(ps) + i1, i2, i3 = 0, n//3, 2*n//3 + v1[poly_i, :] = ps[i1, :] - ps[i2, :] + v2[poly_i, :] = ps[i2, :] - ps[i3, :] + return np.cross(v1, v2) + + +def _shade_colors(color, normals, lightsource=None): + """ + Shade *color* using normal vectors given by *normals*, + assuming a *lightsource* (using default position if not given). + *color* can also be an array of the same length as *normals*. + """ + if lightsource is None: + # chosen for backwards-compatibility + lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712) + + with np.errstate(invalid="ignore"): + shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True)) + @ lightsource.direction) + mask = ~np.isnan(shade) + + if mask.any(): + # convert dot product to allowed shading fractions + in_norm = mcolors.Normalize(-1, 1) + out_norm = mcolors.Normalize(0.3, 1).inverse + + def norm(x): + return out_norm(in_norm(x)) + + shade[~mask] = 0 + + color = mcolors.to_rgba_array(color) + # shape of color should be (M, 4) (where M is number of faces) + # shape of shade should be (M,) + # colors should have final shape of (M, 4) + alpha = color[:, 3] + colors = norm(shade)[:, np.newaxis] * color + colors[:, 3] = alpha + else: + colors = np.asanyarray(color).copy() + + return colors diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 3e4bb9013f29..c180e6f2acc2 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -14,76 +14,109 @@ import functools import itertools import math -from numbers import Integral import textwrap import numpy as np -from matplotlib import _api, artist, cbook, docstring +import matplotlib as mpl +from matplotlib import _api, cbook, _docstring, _preprocess_data +import matplotlib.artist as martist import matplotlib.axes as maxes import matplotlib.collections as mcoll import matplotlib.colors as mcolors +import matplotlib.image as mimage import matplotlib.lines as mlines -import matplotlib.scale as mscale +import matplotlib.patches as mpatches import matplotlib.container as mcontainer import matplotlib.transforms as mtransforms -from matplotlib.axes import Axes, rcParams +from matplotlib.axes import Axes from matplotlib.axes._base import _axis_method_wrapper, _process_plot_format from matplotlib.transforms import Bbox -from matplotlib.tri.triangulation import Triangulation +from matplotlib.tri._triangulation import Triangulation from . import art3d from . import proj3d from . import axis3d -@cbook._define_aliases({ - "xlim3d": ["xlim"], "ylim3d": ["ylim"], "zlim3d": ["zlim"]}) +@_docstring.interpd +@_api.define_aliases({ + "xlim": ["xlim3d"], "ylim": ["ylim3d"], "zlim": ["zlim3d"]}) class Axes3D(Axes): """ - 3D axes object. + 3D Axes object. + + .. note:: + + As a user, you do not instantiate Axes directly, but use Axes creation + methods instead; e.g. from `.pyplot` or `.Figure`: + `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`. """ name = '3d' - _shared_z_axes = cbook.Grouper() + + _axis_names = ("x", "y", "z") + Axes._shared_axes["z"] = cbook.Grouper() + + dist = _api.deprecate_privatize_attribute("3.6") + vvec = _api.deprecate_privatize_attribute("3.7") + eye = _api.deprecate_privatize_attribute("3.7") + sx = _api.deprecate_privatize_attribute("3.7") + sy = _api.deprecate_privatize_attribute("3.7") def __init__( self, fig, rect=None, *args, - azim=-60, elev=30, sharez=None, proj_type='persp', - box_aspect=None, + elev=30, azim=-60, roll=0, sharez=None, proj_type='persp', + box_aspect=None, computed_zorder=True, focal_length=None, **kwargs): """ Parameters ---------- fig : Figure The parent figure. - rect : (float, float, float, float) + rect : tuple (left, bottom, width, height), default: None. The ``(left, bottom, width, height)`` axes position. - azim : float, default: -60 - Azimuthal viewing angle. elev : float, default: 30 - Elevation viewing angle. + The elevation angle in degrees rotates the camera above and below + the x-y plane, with a positive angle corresponding to a location + above the plane. + azim : float, default: -60 + The azimuthal angle in degrees rotates the camera about the z axis, + with a positive angle corresponding to a right-handed rotation. In + other words, a positive azimuth rotates the camera about the origin + from its location along the +x axis towards the +y axis. + roll : float, default: 0 + The roll angle in degrees rotates the camera about the viewing + axis. A positive angle spins the camera clockwise, causing the + scene to rotate counter-clockwise. sharez : Axes3D, optional - Other axes to share z-limits with. + Other Axes to share z-limits with. proj_type : {'persp', 'ortho'} The projection type, default 'persp'. - auto_add_to_figure : bool, default: True - Prior to Matplotlib 3.4 Axes3D would add themselves - to their host Figure on init. Other Axes class do not - do this. - - This behavior is deprecated in 3.4, the default will - change to False in 3.5. The keyword will be undocumented - and a non-False value will be an error in 3.6. + box_aspect : 3-tuple of floats, default: None + Changes the physical dimensions of the Axes3D, such that the ratio + of the axis lengths in display units is x:y:z. + If None, defaults to 4:4:3 + computed_zorder : bool, default: True + If True, the draw order is computed based on the average position + of the `.Artist`\\s along the view direction. + Set to False if you want to manually control the order in which + Artists are drawn on top of each other using their *zorder* + attribute. This can be used for fine-tuning if the automatic order + does not produce the desired result. Note however, that a manual + zorder will only be correct for a limited view angle. If the figure + is rotated by the user, it will look wrong from certain angles. + focal_length : float, default: None + For a projection type of 'persp', the focal length of the virtual + camera. Must be > 0. If None, defaults to 1. + For a projection type of 'ortho', must be set to either None + or infinity (numpy.inf). If None, defaults to infinity. + The focal length can be computed from a desired Field Of View via + the equation: focal_length = 1/tan(FOV/2) **kwargs Other optional keyword arguments: - %(Axes3D_kwdoc)s - - Notes - ----- - .. versionadded:: 1.2.1 - The *sharez* parameter. + %(Axes3D:kwdoc)s """ if rect is None: @@ -91,24 +124,30 @@ def __init__( self.initial_azim = azim self.initial_elev = elev - self.set_proj_type(proj_type) + self.initial_roll = roll + self.set_proj_type(proj_type, focal_length) + self.computed_zorder = computed_zorder self.xy_viewLim = Bbox.unit() self.zz_viewLim = Bbox.unit() self.xy_dataLim = Bbox.unit() + # z-limits are encoded in the x-component of the Bbox, y is un-used self.zz_dataLim = Bbox.unit() - self._stale_viewlim_z = False # inhibit autoscale_view until the axes are defined # they can't be defined until Axes.__init__ has been called - self.view_init(self.initial_elev, self.initial_azim) + self.view_init(self.initial_elev, self.initial_azim, self.initial_roll) self._sharez = sharez if sharez is not None: - self._shared_z_axes.join(self, sharez) + self._shared_axes["z"].join(self, sharez) self._adjustable = 'datalim' - auto_add_to_figure = kwargs.pop('auto_add_to_figure', True) + if kwargs.pop('auto_add_to_figure', False): + raise AttributeError( + 'auto_add_to_figure is no longer supported for Axes3D. ' + 'Use fig.add_axes(ax) instead.' + ) super().__init__( fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs @@ -123,14 +162,12 @@ def __init__( self.fmt_zdata = None self.mouse_init() - self.figure.canvas.callbacks._pickled_cids.update({ - self.figure.canvas.mpl_connect( - 'motion_notify_event', self._on_move), - self.figure.canvas.mpl_connect( - 'button_press_event', self._button_press), - self.figure.canvas.mpl_connect( - 'button_release_event', self._button_release), - }) + self.figure.canvas.callbacks._connect_picklable( + 'motion_notify_event', self._on_move) + self.figure.canvas.callbacks._connect_picklable( + 'button_press_event', self._button_press) + self.figure.canvas.callbacks._connect_picklable( + 'button_release_event', self._button_release) self.set_top_view() self.patch.set_linewidth(0) @@ -142,18 +179,6 @@ def __init__( # for bounding box calculations self.spines[:].set_visible(False) - if auto_add_to_figure: - _api.warn_deprecated( - "3.4", removal="3.6", message="Axes3D(fig) adding itself " - "to the figure is deprecated since %(since)s. " - "Pass the keyword argument auto_add_to_figure=False " - "and use fig.add_axes(ax) to suppress this warning. " - "The default value of auto_add_to_figure will change to " - "False in mpl3.5 and True values will " - "no longer work %(removal)s. This is consistent with " - "other Axes classes.") - fig.add_axes(self) - def set_axis_off(self): self._axis3don = False self.stale = True @@ -164,36 +189,28 @@ def set_axis_on(self): def convert_zunits(self, z): """ - For artists in an axes, if the zaxis has units support, + For artists in an Axes, if the zaxis has units support, convert *z* using zaxis unit type - - .. versionadded:: 1.2.1 - """ return self.zaxis.convert_units(z) def set_top_view(self): # this happens to be the right view for the viewing coordinates # moved up and to the left slightly to fit labels and axes - xdwl = 0.95 / self.dist - xdw = 0.9 / self.dist - ydwl = 0.95 / self.dist - ydw = 0.9 / self.dist - # This is purposely using the 2D Axes's set_xlim and set_ylim, - # because we are trying to place our viewing pane. - super().set_xlim(-xdwl, xdw, auto=None) - super().set_ylim(-ydwl, ydw, auto=None) + xdwl = 0.95 / self._dist + xdw = 0.9 / self._dist + ydwl = 0.95 / self._dist + ydw = 0.9 / self._dist + # Set the viewing pane. + self.viewLim.intervalx = (-xdwl, xdw) + self.viewLim.intervaly = (-ydwl, ydw) + self.stale = True def _init_axis(self): """Init 3D axes; overrides creation of regular X/Y axes.""" - self.xaxis = axis3d.XAxis('x', self.xy_viewLim.intervalx, - self.xy_dataLim.intervalx, self) - self.yaxis = axis3d.YAxis('y', self.xy_viewLim.intervaly, - self.xy_dataLim.intervaly, self) - self.zaxis = axis3d.ZAxis('z', self.zz_viewLim.intervalx, - self.zz_dataLim.intervalx, self) - for ax in self.xaxis, self.yaxis, self.zaxis: - ax.init3d() + self.xaxis = axis3d.XAxis(self) + self.yaxis = axis3d.YAxis(self) + self.zaxis = axis3d.ZAxis(self) def get_zaxis(self): """Return the ``ZAxis`` (`~.axis3d.Axis`) instance.""" @@ -202,35 +219,18 @@ def get_zaxis(self): get_zgridlines = _axis_method_wrapper("zaxis", "get_gridlines") get_zticklines = _axis_method_wrapper("zaxis", "get_ticklines") - w_xaxis = _api.deprecated("3.1", alternative="xaxis", pending=True)( + w_xaxis = _api.deprecated("3.1", alternative="xaxis", removal="3.8")( property(lambda self: self.xaxis)) - w_yaxis = _api.deprecated("3.1", alternative="yaxis", pending=True)( + w_yaxis = _api.deprecated("3.1", alternative="yaxis", removal="3.8")( property(lambda self: self.yaxis)) - w_zaxis = _api.deprecated("3.1", alternative="zaxis", pending=True)( + w_zaxis = _api.deprecated("3.1", alternative="zaxis", removal="3.8")( property(lambda self: self.zaxis)) - def _get_axis_list(self): - return super()._get_axis_list() + (self.zaxis, ) - - def _unstale_viewLim(self): - # We should arrange to store this information once per share-group - # instead of on every axis. - scalex = any(ax._stale_viewlim_x - for ax in self._shared_x_axes.get_siblings(self)) - scaley = any(ax._stale_viewlim_y - for ax in self._shared_y_axes.get_siblings(self)) - scalez = any(ax._stale_viewlim_z - for ax in self._shared_z_axes.get_siblings(self)) - if scalex or scaley or scalez: - for ax in self._shared_x_axes.get_siblings(self): - ax._stale_viewlim_x = False - for ax in self._shared_y_axes.get_siblings(self): - ax._stale_viewlim_y = False - for ax in self._shared_z_axes.get_siblings(self): - ax._stale_viewlim_z = False - self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez) - + @_api.deprecated("3.7") def unit_cube(self, vals=None): + return self._unit_cube(vals) + + def _unit_cube(self, vals=None): minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims() return [(minx, miny, minz), (maxx, miny, minz), @@ -241,15 +241,23 @@ def unit_cube(self, vals=None): (maxx, maxy, maxz), (minx, maxy, maxz)] + @_api.deprecated("3.7") def tunit_cube(self, vals=None, M=None): + return self._tunit_cube(vals, M) + + def _tunit_cube(self, vals=None, M=None): if M is None: M = self.M - xyzs = self.unit_cube(vals) + xyzs = self._unit_cube(vals) tcube = proj3d.proj_points(xyzs, M) return tcube + @_api.deprecated("3.7") def tunit_edges(self, vals=None, M=None): - tc = self.tunit_cube(vals, M) + return self._tunit_edges(vals, M) + + def _tunit_edges(self, vals=None, M=None): + tc = self._tunit_cube(vals, M) edges = [(tc[0], tc[1]), (tc[1], tc[2]), (tc[2], tc[3]), @@ -270,27 +278,22 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): """ Set the aspect ratios. - Axes 3D does not current support any aspect but 'auto' which fills - the axes with the data limits. - - To simulate having equal aspect in data space, set the ratio - of your data limits to match the value of `~.get_box_aspect`. - To control box aspect ratios use `~.Axes3D.set_box_aspect`. - Parameters ---------- - aspect : {'auto'} + aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'} Possible values: ========= ================================================== value description ========= ================================================== 'auto' automatic; fill the position rectangle with data. + 'equal' adapt all the axes to have equal aspect ratios. + 'equalxy' adapt the x and y axes to have equal aspect ratios. + 'equalxz' adapt the x and z axes to have equal aspect ratios. + 'equalyz' adapt the y and z axes to have equal aspect ratios. ========= ================================================== - adjustable : None - Currently ignored by Axes3D - + adjustable : None or {'box', 'datalim'}, optional If not *None*, this defines which parameter will be adjusted to meet the required aspect. See `.set_adjustable` for further details. @@ -298,7 +301,7 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): anchor : None or str or 2-tuple of float, optional If not *None*, this defines where the Axes will be drawn if there is extra space due to aspect constraints. The most common way to - to specify the anchor are abbreviations of cardinal directions: + specify the anchor are abbreviations of cardinal directions: ===== ===================== value description @@ -310,7 +313,7 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): etc. ===== ===================== - See `.set_anchor` for further details. + See `~.Axes.set_anchor` for further details. share : bool, default: False If ``True``, apply the settings to all shared Axes. @@ -319,52 +322,73 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): -------- mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect """ - if aspect != 'auto': - raise NotImplementedError( - "Axes3D currently only supports the aspect argument " - f"'auto'. You passed in {aspect!r}." - ) + _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'), + aspect=aspect) + super().set_aspect( + aspect='auto', adjustable=adjustable, anchor=anchor, share=share) + self._aspect = aspect + + if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): + ax_indices = self._equal_aspect_axis_indices(aspect) + + view_intervals = np.array([self.xaxis.get_view_interval(), + self.yaxis.get_view_interval(), + self.zaxis.get_view_interval()]) + ptp = np.ptp(view_intervals, axis=1) + if self._adjustable == 'datalim': + mean = np.mean(view_intervals, axis=1) + delta = max(ptp[ax_indices]) + scale = self._box_aspect[ptp == delta][0] + deltas = delta * self._box_aspect / scale + + for i, set_lim in enumerate((self.set_xlim3d, + self.set_ylim3d, + self.set_zlim3d)): + if i in ax_indices: + set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.) + else: # 'box' + # Change the box aspect such that the ratio of the length of + # the unmodified axis to the length of the diagonal + # perpendicular to it remains unchanged. + box_aspect = np.array(self._box_aspect) + box_aspect[ax_indices] = ptp[ax_indices] + remaining_ax_indices = {0, 1, 2}.difference(ax_indices) + if remaining_ax_indices: + remaining = remaining_ax_indices.pop() + old_diag = np.linalg.norm(self._box_aspect[ax_indices]) + new_diag = np.linalg.norm(box_aspect[ax_indices]) + box_aspect[remaining] *= new_diag / old_diag + self.set_box_aspect(box_aspect) + + def _equal_aspect_axis_indices(self, aspect): + """ + Get the indices for which of the x, y, z axes are constrained to have + equal aspect ratios. - if share: - axes = {*self._shared_x_axes.get_siblings(self), - *self._shared_y_axes.get_siblings(self), - *self._shared_z_axes.get_siblings(self), - } - else: - axes = {self} - - for ax in axes: - ax._aspect = aspect - ax.stale = True - - if anchor is not None: - self.set_anchor(anchor, share=share) - - def set_anchor(self, anchor, share=False): - # docstring inherited - if not (anchor in mtransforms.Bbox.coefs or len(anchor) == 2): - raise ValueError('anchor must be among %s' % - ', '.join(mtransforms.Bbox.coefs)) - if share: - axes = {*self._shared_x_axes.get_siblings(self), - *self._shared_y_axes.get_siblings(self), - *self._shared_z_axes.get_siblings(self), - } - else: - axes = {self} - for ax in axes: - ax._anchor = anchor - ax.stale = True + Parameters + ---------- + aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'} + See descriptions in docstring for `.set_aspect()`. + """ + ax_indices = [] # aspect == 'auto' + if aspect == 'equal': + ax_indices = [0, 1, 2] + elif aspect == 'equalxy': + ax_indices = [0, 1] + elif aspect == 'equalxz': + ax_indices = [0, 2] + elif aspect == 'equalyz': + ax_indices = [1, 2] + return ax_indices def set_box_aspect(self, aspect, *, zoom=1): """ - Set the axes box aspect. + Set the Axes box aspect. The box aspect is the ratio of height to width in display units for each face of the box when viewed perpendicular to - that face. This is not to be confused with the data aspect - (which for Axes3D is always 'auto'). The default ratios are - 4:4:3 (x:y:z). + that face. This is not to be confused with the data aspect (see + `~.Axes3D.set_aspect`). The default ratios are 4:4:3 (x:y:z). To simulate having equal aspect in data space, set the box aspect to match your data range in each dimension. @@ -376,22 +400,19 @@ def set_box_aspect(self, aspect, *, zoom=1): aspect : 3-tuple of floats or None Changes the physical dimensions of the Axes3D, such that the ratio of the axis lengths in display units is x:y:z. + If None, defaults to (4, 4, 3). - If None, defaults to 4:4:3 - - zoom : float - Control overall size of the Axes3D in the figure. + zoom : float, default: 1 + Control overall size of the Axes3D in the figure. Must be > 0. """ + if zoom <= 0: + raise ValueError(f'Argument zoom = {zoom} must be > 0') + if aspect is None: aspect = np.asarray((4, 4, 3), dtype=float) else: - orig_aspect = aspect aspect = np.asarray(aspect, dtype=float) - if aspect.shape != (3,): - raise ValueError( - "You must pass a 3-tuple that can be cast to floats. " - f"You passed {orig_aspect!r}" - ) + _api.check_shape((3,), aspect=aspect) # default scale tuned to match the mpl32 appearance. aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect) @@ -406,7 +427,7 @@ def apply_aspect(self, position=None): # scales and box/datalim. Those are all irrelevant - all we need to do # is make sure our coordinate system is square. trans = self.get_figure().transSubfigure - bb = mtransforms.Bbox.from_bounds(0, 0, 1, 1).transformed(trans) + bb = mtransforms.Bbox.unit().transformed(trans) # this is the physical aspect of the panel (or figure): fig_aspect = bb.height / bb.width @@ -415,8 +436,10 @@ def apply_aspect(self, position=None): pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect) self._set_position(pb1.anchored(self.get_anchor(), pb), 'active') - @artist.allow_rasterization + @martist.allow_rasterization def draw(self, renderer): + if not self.get_visible(): + return self._unstale_viewLim() # draw the background patch @@ -427,224 +450,96 @@ def draw(self, renderer): # this is duplicated from `axes._base._AxesBase.draw` # but must be called before any of the artist are drawn as # it adjusts the view limits and the size of the bounding box - # of the axes + # of the Axes locator = self.get_axes_locator() - if locator: - pos = locator(self, renderer) - self.apply_aspect(pos) - else: - self.apply_aspect() + self.apply_aspect(locator(self, renderer) if locator else None) # add the projection matrix to the renderer self.M = self.get_proj() - props3d = { - # To raise a deprecation, we need to wrap the attribute in a - # function, but binding that to an instance does not work, as you - # would end up with an instance-specific method. Properties are - # class-level attributes which *are* functions, so we do that - # instead. - # This dictionary comprehension creates deprecated properties for - # the attributes listed below, and they are temporarily attached to - # the _class_ in the `_setattr_cm` call. These can both be removed - # once the deprecation expires - name: _api.deprecated('3.4', name=name, - alternative=f'self.axes.{name}')( - property(lambda self, _value=getattr(self, name): _value)) - for name in ['M', 'vvec', 'eye', 'get_axis_position'] - } - - with cbook._setattr_cm(type(renderer), **props3d): - def do_3d_projection(artist): - """ - Call `do_3d_projection` on an *artist*, and warn if passing - *renderer*. - - For our Artists, never pass *renderer*. For external Artists, - in lieu of more complicated signature parsing, always pass - *renderer* and raise a warning. - """ - - if artist.__module__ == 'mpl_toolkits.mplot3d.art3d': - # Our 3D Artists have deprecated the renderer parameter, so - # avoid passing it to them; call this directly once the - # deprecation has expired. - return artist.do_3d_projection() - - _api.warn_deprecated( - "3.4", - message="The 'renderer' parameter of " - "do_3d_projection() was deprecated in Matplotlib " - "%(since)s and will be removed %(removal)s.") - return artist.do_3d_projection(renderer) - - # Calculate projection of collections and patches and zorder them. - # Make sure they are drawn above the grids. + + collections_and_patches = ( + artist for artist in self._children + if isinstance(artist, (mcoll.Collection, mpatches.Patch)) + and artist.get_visible()) + if self.computed_zorder: + # Calculate projection of collections and patches and zorder + # them. Make sure they are drawn above the grids. zorder_offset = max(axis.get_zorder() - for axis in self._get_axis_list()) + 1 - for i, col in enumerate( - sorted(self.collections, - key=do_3d_projection, - reverse=True)): - col.zorder = zorder_offset + i - for i, patch in enumerate( - sorted(self.patches, - key=do_3d_projection, - reverse=True)): - patch.zorder = zorder_offset + i - - if self._axis3don: - # Draw panes first - for axis in self._get_axis_list(): - axis.draw_pane(renderer) - # Then axes - for axis in self._get_axis_list(): - axis.draw(renderer) - - # Then rest - super().draw(renderer) + for axis in self._axis_map.values()) + 1 + collection_zorder = patch_zorder = zorder_offset + + for artist in sorted(collections_and_patches, + key=lambda artist: artist.do_3d_projection(), + reverse=True): + if isinstance(artist, mcoll.Collection): + artist.zorder = collection_zorder + collection_zorder += 1 + elif isinstance(artist, mpatches.Patch): + artist.zorder = patch_zorder + patch_zorder += 1 + else: + for artist in collections_and_patches: + artist.do_3d_projection() + + if self._axis3don: + # Draw panes first + for axis in self._axis_map.values(): + axis.draw_pane(renderer) + # Then axes + for axis in self._axis_map.values(): + axis.draw(renderer) + + # Then rest + super().draw(renderer) def get_axis_position(self): vals = self.get_w_lims() - tc = self.tunit_cube(vals, self.M) + tc = self._tunit_cube(vals, self.M) xhigh = tc[1][2] > tc[2][2] yhigh = tc[3][2] > tc[2][2] zhigh = tc[0][2] > tc[2][2] return xhigh, yhigh, zhigh - def _unit_change_handler(self, axis_name, event=None): - # docstring inherited - if event is None: # Allow connecting `self._unit_change_handler(name)` - return functools.partial( - self._unit_change_handler, axis_name, event=object()) - _api.check_in_list(self._get_axis_map(), axis_name=axis_name) - self.relim() - self._request_autoscale_view(scalex=(axis_name == "x"), - scaley=(axis_name == "y"), - scalez=(axis_name == "z")) - def update_datalim(self, xys, **kwargs): - pass - - def get_autoscale_on(self): """ - Get whether autoscaling is applied for all axes on plot commands - - .. versionadded:: 1.1.0 - This function was added, but not tested. Please report any bugs. - """ - return super().get_autoscale_on() and self.get_autoscalez_on() - - def get_autoscalez_on(self): - """ - Get whether autoscaling for the z-axis is applied on plot commands - - .. versionadded:: 1.1.0 - This function was added, but not tested. Please report any bugs. + Not implemented in `~mpl_toolkits.mplot3d.axes3d.Axes3D`. """ - return self._autoscaleZon - - def set_autoscale_on(self, b): - """ - Set whether autoscaling is applied on plot commands - - .. versionadded:: 1.1.0 - This function was added, but not tested. Please report any bugs. - - Parameters - ---------- - b : bool - """ - super().set_autoscale_on(b) - self.set_autoscalez_on(b) - - def set_autoscalez_on(self, b): - """ - Set whether autoscaling for the z-axis is applied on plot commands - - .. versionadded:: 1.1.0 + pass - Parameters - ---------- - b : bool - """ - self._autoscaleZon = b - - def set_xmargin(self, m): - # docstring inherited - scalez = self._stale_viewlim_z - super().set_xmargin(m) - # Superclass is 2D and will call _request_autoscale_view with defaults - # for unknown Axis, which would be scalez=True, but it shouldn't be for - # this call, so restore it. - self._stale_viewlim_z = scalez - - def set_ymargin(self, m): - # docstring inherited - scalez = self._stale_viewlim_z - super().set_ymargin(m) - # Superclass is 2D and will call _request_autoscale_view with defaults - # for unknown Axis, which would be scalez=True, but it shouldn't be for - # this call, so restore it. - self._stale_viewlim_z = scalez + get_autoscalez_on = _axis_method_wrapper("zaxis", "_get_autoscale_on") + set_autoscalez_on = _axis_method_wrapper("zaxis", "_set_autoscale_on") def set_zmargin(self, m): """ Set padding of Z data limits prior to autoscaling. - *m* times the data interval will be added to each - end of that interval before it is used in autoscaling. + *m* times the data interval will be added to each end of that interval + before it is used in autoscaling. If *m* is negative, this will clip + the data range instead of expanding it. - accepts: float in range 0 to 1 + For example, if your data is in the range [0, 2], a margin of 0.1 will + result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range + of [0.2, 1.8]. - .. versionadded:: 1.1.0 + Parameters + ---------- + m : float greater than -0.5 """ - if m < 0 or m > 1: - raise ValueError("margin must be in range 0 to 1") + if m <= -0.5: + raise ValueError("margin must be greater than -0.5") self._zmargin = m - self._request_autoscale_view(scalex=False, scaley=False, scalez=True) + self._request_autoscale_view("z") self.stale = True def margins(self, *margins, x=None, y=None, z=None, tight=True): """ - Convenience method to set or retrieve autoscaling margins. - - Call signatures:: - - margins() - - returns xmargin, ymargin, zmargin - - :: - - margins(margin) + Set or retrieve autoscaling margins. - margins(xmargin, ymargin, zmargin) - - margins(x=xmargin, y=ymargin, z=zmargin) - - margins(..., tight=False) - - All forms above set the xmargin, ymargin and zmargin - parameters. All keyword parameters are optional. A single - positional argument specifies xmargin, ymargin and zmargin. - Passing both positional and keyword arguments for xmargin, - ymargin, and/or zmargin is invalid. - - The *tight* parameter - is passed to :meth:`autoscale_view`, which is executed after - a margin is changed; the default here is *True*, on the - assumption that when margins are specified, no additional - padding to match tick marks is usually desired. Setting - *tight* to *None* will preserve the previous setting. - - Specifying any margin changes only the autoscaling; for example, - if *xmargin* is not None, then *xmargin* times the X data - interval will be added to each end of that interval before - it is used in autoscaling. - - .. versionadded:: 1.1.0 + See `.Axes.margins` for full documentation. Because this function + applies to 3D Axes, it also takes a *z* argument, and returns + ``(xmargin, ymargin, zmargin)``. """ - if margins and x is not None and y is not None and z is not None: + if margins and (x is not None or y is not None or z is not None): raise TypeError('Cannot pass both positional and keyword ' 'arguments for x, y, and/or z.') elif len(margins) == 1: @@ -675,12 +570,10 @@ def margins(self, *margins, x=None, y=None, z=None, tight=True): def autoscale(self, enable=True, axis='both', tight=None): """ Convenience method for simple axis view autoscaling. - See :meth:`matplotlib.axes.Axes.autoscale` for full explanation. - Note that this function behaves the same, but for all - three axes. Therefore, 'z' can be passed for *axis*, - and 'both' applies to all three axes. - .. versionadded:: 1.1.0 + See `.Axes.autoscale` for full documentation. Because this function + applies to 3D Axes, *axis* can also be set to 'z', and setting *axis* + to 'both' autoscales all three axes. """ if enable is None: scalex = True @@ -688,74 +581,66 @@ def autoscale(self, enable=True, axis='both', tight=None): scalez = True else: if axis in ['x', 'both']: - self._autoscaleXon = scalex = bool(enable) + self.set_autoscalex_on(bool(enable)) + scalex = self.get_autoscalex_on() else: scalex = False if axis in ['y', 'both']: - self._autoscaleYon = scaley = bool(enable) + self.set_autoscaley_on(bool(enable)) + scaley = self.get_autoscaley_on() else: scaley = False if axis in ['z', 'both']: - self._autoscaleZon = scalez = bool(enable) + self.set_autoscalez_on(bool(enable)) + scalez = self.get_autoscalez_on() else: scalez = False - self._request_autoscale_view(tight=tight, scalex=scalex, scaley=scaley, - scalez=scalez) + if scalex: + self._request_autoscale_view("x", tight=tight) + if scaley: + self._request_autoscale_view("y", tight=tight) + if scalez: + self._request_autoscale_view("z", tight=tight) def auto_scale_xyz(self, X, Y, Z=None, had_data=None): # This updates the bounding boxes as to keep a record as to what the # minimum sized rectangular volume holds the data. - X = np.reshape(X, -1) - Y = np.reshape(Y, -1) - self.xy_dataLim.update_from_data_xy( - np.column_stack([X, Y]), not had_data) + if np.shape(X) == np.shape(Y): + self.xy_dataLim.update_from_data_xy( + np.column_stack([np.ravel(X), np.ravel(Y)]), not had_data) + else: + self.xy_dataLim.update_from_data_x(X, not had_data) + self.xy_dataLim.update_from_data_y(Y, not had_data) if Z is not None: - Z = np.reshape(Z, -1) - self.zz_dataLim.update_from_data_xy( - np.column_stack([Z, Z]), not had_data) + self.zz_dataLim.update_from_data_x(Z, not had_data) # Let autoscale_view figure out how to use this data. self.autoscale_view() - # API could be better, right now this is just to match the old calls to - # autoscale_view() after each plotting method. - def _request_autoscale_view(self, tight=None, scalex=True, scaley=True, - scalez=True): - if tight is not None: - self._tight = tight - if scalex: - self._stale_viewlim_x = True # Else keep old state. - if scaley: - self._stale_viewlim_y = True - if scalez: - self._stale_viewlim_z = True - def autoscale_view(self, tight=None, scalex=True, scaley=True, scalez=True): """ Autoscale the view limits using the data limits. - See :meth:`matplotlib.axes.Axes.autoscale_view` for documentation. - Note that this function applies to the 3D axes, and as such - adds the *scalez* to the function arguments. - .. versionchanged:: 1.1.0 - Function signature was changed to better match the 2D version. - *tight* is now explicitly a kwarg and placed first. - - .. versionchanged:: 1.2.1 - This is now fully functional. + See `.Axes.autoscale_view` for full documentation. Because this + function applies to 3D Axes, it also takes a *scalez* argument. """ # This method looks at the rectangular volume (see above) # of data and decides how to scale the view portal to fit it. if tight is None: - # if image data only just use the datalim - _tight = self._tight or ( - len(self.images) > 0 - and len(self.lines) == len(self.patches) == 0) + _tight = self._tight + if not _tight: + # if image data only just use the datalim + for artist in self._children: + if isinstance(artist, mimage.AxesImage): + _tight = True + elif isinstance(artist, (mlines.Line2D, mpatches.Patch)): + _tight = False + break else: _tight = self._tight = bool(tight) - if scalex and self._autoscaleXon: - self._shared_x_axes.clean() + if scalex and self.get_autoscalex_on(): + self._shared_axes["x"].clean() x0, x1 = self.xy_dataLim.intervalx xlocator = self.xaxis.get_major_locator() x0, x1 = xlocator.nonsingular(x0, x1) @@ -767,8 +652,8 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, x0, x1 = xlocator.view_limits(x0, x1) self.set_xbound(x0, x1) - if scaley and self._autoscaleYon: - self._shared_y_axes.clean() + if scaley and self.get_autoscaley_on(): + self._shared_axes["y"].clean() y0, y1 = self.xy_dataLim.intervaly ylocator = self.yaxis.get_major_locator() y0, y1 = ylocator.nonsingular(y0, y1) @@ -780,8 +665,8 @@ def autoscale_view(self, tight=None, scalex=True, scaley=True, y0, y1 = ylocator.view_limits(y0, y1) self.set_ybound(y0, y1) - if scalez and self._autoscaleZon: - self._shared_z_axes.clean() + if scalez and self.get_autoscalez_on(): + self._shared_axes["z"].clean() z0, z1 = self.zz_dataLim.intervalx zlocator = self.zaxis.get_major_locator() z0, z1 = zlocator.nonsingular(z0, z1) @@ -800,229 +685,49 @@ def get_w_lims(self): minz, maxz = self.get_zlim3d() return minx, maxx, miny, maxy, minz, maxz - def set_xlim3d(self, left=None, right=None, emit=True, auto=False, - *, xmin=None, xmax=None): - """ - Set 3D x limits. - - See :meth:`matplotlib.axes.Axes.set_xlim` for full documentation. - """ - if right is None and np.iterable(left): - left, right = left - if xmin is not None: - if left is not None: - raise TypeError('Cannot pass both `xmin` and `left`') - left = xmin - if xmax is not None: - if right is not None: - raise TypeError('Cannot pass both `xmax` and `right`') - right = xmax - - self._process_unit_info([("x", (left, right))], convert=False) - left = self._validate_converted_limits(left, self.convert_xunits) - right = self._validate_converted_limits(right, self.convert_xunits) - - old_left, old_right = self.get_xlim() - if left is None: - left = old_left - if right is None: - right = old_right - - if left == right: - _api.warn_external( - f"Attempting to set identical left == right == {left} results " - f"in singular transformations; automatically expanding.") - reverse = left > right - left, right = self.xaxis.get_major_locator().nonsingular(left, right) - left, right = self.xaxis.limit_range_for_scale(left, right) - # cast to bool to avoid bad interaction between python 3.8 and np.bool_ - left, right = sorted([left, right], reverse=bool(reverse)) - self.xy_viewLim.intervalx = (left, right) - - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_x_axes.get_siblings(self): - ax._stale_viewlim_x = False - if auto is not None: - self._autoscaleXon = bool(auto) - - if emit: - self.callbacks.process('xlim_changed', self) - # Call all of the other x-axes that are shared with this one - for other in self._shared_x_axes.get_siblings(self): - if other is not self: - other.set_xlim(self.xy_viewLim.intervalx, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return left, right - - def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False, - *, ymin=None, ymax=None): - """ - Set 3D y limits. - - See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation. - """ - if top is None and np.iterable(bottom): - bottom, top = bottom - if ymin is not None: - if bottom is not None: - raise TypeError('Cannot pass both `ymin` and `bottom`') - bottom = ymin - if ymax is not None: - if top is not None: - raise TypeError('Cannot pass both `ymax` and `top`') - top = ymax - - self._process_unit_info([("y", (bottom, top))], convert=False) - bottom = self._validate_converted_limits(bottom, self.convert_yunits) - top = self._validate_converted_limits(top, self.convert_yunits) - - old_bottom, old_top = self.get_ylim() - if bottom is None: - bottom = old_bottom - if top is None: - top = old_top - - if bottom == top: - _api.warn_external( - f"Attempting to set identical bottom == top == {bottom} " - f"results in singular transformations; automatically " - f"expanding.") - swapped = bottom > top - bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top) - bottom, top = self.yaxis.limit_range_for_scale(bottom, top) - if swapped: - bottom, top = top, bottom - self.xy_viewLim.intervaly = (bottom, top) - - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_y_axes.get_siblings(self): - ax._stale_viewlim_y = False - if auto is not None: - self._autoscaleYon = bool(auto) - - if emit: - self.callbacks.process('ylim_changed', self) - # Call all of the other y-axes that are shared with this one - for other in self._shared_y_axes.get_siblings(self): - if other is not self: - other.set_ylim(self.xy_viewLim.intervaly, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return bottom, top - - def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, - *, zmin=None, zmax=None): + # set_xlim, set_ylim are directly inherited from base Axes. + @_api.make_keyword_only("3.6", "emit") + def set_zlim(self, bottom=None, top=None, emit=True, auto=False, + *, zmin=None, zmax=None): """ Set 3D z limits. - See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation + See `.Axes.set_ylim` for full documentation """ if top is None and np.iterable(bottom): bottom, top = bottom if zmin is not None: if bottom is not None: - raise TypeError('Cannot pass both `zmin` and `bottom`') + raise TypeError("Cannot pass both 'bottom' and 'zmin'") bottom = zmin if zmax is not None: if top is not None: - raise TypeError('Cannot pass both `zmax` and `top`') + raise TypeError("Cannot pass both 'top' and 'zmax'") top = zmax + return self.zaxis._set_lim(bottom, top, emit=emit, auto=auto) - self._process_unit_info([("z", (bottom, top))], convert=False) - bottom = self._validate_converted_limits(bottom, self.convert_zunits) - top = self._validate_converted_limits(top, self.convert_zunits) - - old_bottom, old_top = self.get_zlim() - if bottom is None: - bottom = old_bottom - if top is None: - top = old_top - - if bottom == top: - _api.warn_external( - f"Attempting to set identical bottom == top == {bottom} " - f"results in singular transformations; automatically " - f"expanding.") - swapped = bottom > top - bottom, top = self.zaxis.get_major_locator().nonsingular(bottom, top) - bottom, top = self.zaxis.limit_range_for_scale(bottom, top) - if swapped: - bottom, top = top, bottom - self.zz_viewLim.intervalx = (bottom, top) - - # Mark viewlims as no longer stale without triggering an autoscale. - for ax in self._shared_z_axes.get_siblings(self): - ax._stale_viewlim_z = False - if auto is not None: - self._autoscaleZon = bool(auto) - - if emit: - self.callbacks.process('zlim_changed', self) - # Call all of the other y-axes that are shared with this one - for other in self._shared_z_axes.get_siblings(self): - if other is not self: - other.set_zlim(self.zz_viewLim.intervalx, - emit=False, auto=auto) - if other.figure != self.figure: - other.figure.canvas.draw_idle() - self.stale = True - return bottom, top + set_xlim3d = maxes.Axes.set_xlim + set_ylim3d = maxes.Axes.set_ylim + set_zlim3d = set_zlim - def get_xlim3d(self): + def get_xlim(self): + # docstring inherited return tuple(self.xy_viewLim.intervalx) - get_xlim3d.__doc__ = maxes.Axes.get_xlim.__doc__ - if get_xlim3d.__doc__ is not None: - get_xlim3d.__doc__ += """ - .. versionchanged:: 1.1.0 - This function now correctly refers to the 3D x-limits - """ - def get_ylim3d(self): + def get_ylim(self): + # docstring inherited return tuple(self.xy_viewLim.intervaly) - get_ylim3d.__doc__ = maxes.Axes.get_ylim.__doc__ - if get_ylim3d.__doc__ is not None: - get_ylim3d.__doc__ += """ - .. versionchanged:: 1.1.0 - This function now correctly refers to the 3D y-limits. - """ - def get_zlim3d(self): + def get_zlim(self): """Get 3D z limits.""" return tuple(self.zz_viewLim.intervalx) - def get_zscale(self): - """ - Return the zaxis scale string %s - - """ % (", ".join(mscale.get_scale_names())) - return self.zaxis.get_scale() - - # We need to slightly redefine these to pass scalez=False - # to their calls of autoscale_view. - - def set_xscale(self, value, **kwargs): - self.xaxis._set_scale(value, **kwargs) - self.autoscale_view(scaley=False, scalez=False) - self._update_transScale() - self.stale = True - - def set_yscale(self, value, **kwargs): - self.yaxis._set_scale(value, **kwargs) - self.autoscale_view(scalex=False, scalez=False) - self._update_transScale() - self.stale = True - - def set_zscale(self, value, **kwargs): - self.zaxis._set_scale(value, **kwargs) - self.autoscale_view(scalex=False, scaley=False) - self._update_transScale() - self.stale = True + get_zscale = _axis_method_wrapper("zaxis", "get_scale") + # Redefine all three methods to overwrite their docstrings. + set_xscale = _axis_method_wrapper("xaxis", "_set_axes_scale") + set_yscale = _axis_method_wrapper("yaxis", "_set_axes_scale") + set_zscale = _axis_method_wrapper("zaxis", "_set_axes_scale") set_xscale.__doc__, set_yscale.__doc__, set_zscale.__doc__ = map( """ Set the {}-axis scale. @@ -1045,7 +750,7 @@ def set_zscale(self, value, **kwargs): get_zminorticklabels = _axis_method_wrapper("zaxis", "get_minorticklabels") get_zticklabels = _axis_method_wrapper("zaxis", "get_ticklabels") set_zticklabels = _axis_method_wrapper( - "zaxis", "_set_ticklabels", + "zaxis", "set_ticklabels", doc_sub={"Axis.set_ticks": "Axes3D.set_zticks"}) zaxis_date = _axis_method_wrapper("zaxis", "axis_date") @@ -1062,20 +767,56 @@ def clabel(self, *args, **kwargs): """Currently not implemented for 3D axes, and returns *None*.""" return None - def view_init(self, elev=None, azim=None): + def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z"): """ Set the elevation and azimuth of the axes in degrees (not radians). This can be used to rotate the axes programmatically. - 'elev' stores the elevation angle in the z plane (in degrees). - 'azim' stores the azimuth angle in the (x, y) plane (in degrees). - - if 'elev' or 'azim' are None (default), then the initial value - is used which was specified in the :class:`Axes3D` constructor. - """ + To look normal to the primary planes, the following elevation and + azimuth angles can be used. A roll angle of 0, 90, 180, or 270 deg + will rotate these views while keeping the axes at right angles. + + ========== ==== ==== + view plane elev azim + ========== ==== ==== + XY 90 -90 + XZ 0 -90 + YZ 0 0 + -XY -90 90 + -XZ 0 90 + -YZ 0 180 + ========== ==== ==== - self.dist = 10 + Parameters + ---------- + elev : float, default: None + The elevation angle in degrees rotates the camera above the plane + pierced by the vertical axis, with a positive angle corresponding + to a location above that plane. For example, with the default + vertical axis of 'z', the elevation defines the angle of the camera + location above the x-y plane. + If None, then the initial value as specified in the `Axes3D` + constructor is used. + azim : float, default: None + The azimuthal angle in degrees rotates the camera about the + vertical axis, with a positive angle corresponding to a + right-handed rotation. For example, with the default vertical axis + of 'z', a positive azimuth rotates the camera about the origin from + its location along the +x axis towards the +y axis. + If None, then the initial value as specified in the `Axes3D` + constructor is used. + roll : float, default: None + The roll angle in degrees rotates the camera about the viewing + axis. A positive angle spins the camera clockwise, causing the + scene to rotate counter-clockwise. + If None, then the initial value as specified in the `Axes3D` + constructor is used. + vertical_axis : {"z", "x", "y"}, default: "z" + The axis to align vertically. *azim* rotates about this axis. + """ + + self._dist = 10 # The camera distance from origin. Behaves like zoom if elev is None: self.elev = self.initial_elev @@ -1087,65 +828,115 @@ def view_init(self, elev=None, azim=None): else: self.azim = azim - def set_proj_type(self, proj_type): + if roll is None: + self.roll = self.initial_roll + else: + self.roll = roll + + self._vertical_axis = _api.check_getitem( + dict(x=0, y=1, z=2), vertical_axis=vertical_axis + ) + + def set_proj_type(self, proj_type, focal_length=None): """ Set the projection type. Parameters ---------- proj_type : {'persp', 'ortho'} - """ - self._projection = _api.check_getitem({ - 'persp': proj3d.persp_transformation, - 'ortho': proj3d.ortho_transformation, - }, proj_type=proj_type) + The projection type. + focal_length : float, default: None + For a projection type of 'persp', the focal length of the virtual + camera. Must be > 0. If None, defaults to 1. + The focal length can be computed from a desired Field Of View via + the equation: focal_length = 1/tan(FOV/2) + """ + _api.check_in_list(['persp', 'ortho'], proj_type=proj_type) + if proj_type == 'persp': + if focal_length is None: + focal_length = 1 + elif focal_length <= 0: + raise ValueError(f"focal_length = {focal_length} must be " + "greater than 0") + self._focal_length = focal_length + else: # 'ortho': + if focal_length not in (None, np.inf): + raise ValueError(f"focal_length = {focal_length} must be " + f"None for proj_type = {proj_type}") + self._focal_length = np.inf + + def _roll_to_vertical(self, arr): + """Roll arrays to match the different vertical axis.""" + return np.roll(arr, self._vertical_axis - 2) def get_proj(self): """Create the projection matrix from the current viewing position.""" - # elev stores the elevation angle in the z plane - # azim stores the azimuth angle in the x,y plane - # - # dist is the distance of the eye viewing point from the object - # point. - relev, razim = np.pi * self.elev/180, np.pi * self.azim/180 - - xmin, xmax = self.get_xlim3d() - ymin, ymax = self.get_ylim3d() - zmin, zmax = self.get_zlim3d() - - # transform to uniform world coordinates 0-1, 0-1, 0-1 - worldM = proj3d.world_transformation(xmin, xmax, - ymin, ymax, - zmin, zmax, - pb_aspect=self._box_aspect) - - # look into the middle of the new coordinates - R = self._box_aspect / 2 - - xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist - yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist - zp = R[2] + np.sin(relev) * self.dist - E = np.array((xp, yp, zp)) + # Transform to uniform world coordinates 0-1, 0-1, 0-1 + box_aspect = self._roll_to_vertical(self._box_aspect) + worldM = proj3d.world_transformation( + *self.get_xlim3d(), + *self.get_ylim3d(), + *self.get_zlim3d(), + pb_aspect=box_aspect, + ) - self.eye = E - self.vvec = R - E - self.vvec = self.vvec / np.linalg.norm(self.vvec) + # Look into the middle of the world coordinates: + R = 0.5 * box_aspect - if abs(relev) > np.pi/2: - # upside down - V = np.array((0, 0, -1)) + # elev stores the elevation angle in the z plane + # azim stores the azimuth angle in the x,y plane + elev_rad = np.deg2rad(art3d._norm_angle(self.elev)) + azim_rad = np.deg2rad(art3d._norm_angle(self.azim)) + + # Coordinates for a point that rotates around the box of data. + # p0, p1 corresponds to rotating the box only around the + # vertical axis. + # p2 corresponds to rotating the box only around the horizontal + # axis. + p0 = np.cos(elev_rad) * np.cos(azim_rad) + p1 = np.cos(elev_rad) * np.sin(azim_rad) + p2 = np.sin(elev_rad) + + # When changing vertical axis the coordinates changes as well. + # Roll the values to get the same behaviour as the default: + ps = self._roll_to_vertical([p0, p1, p2]) + + # The coordinates for the eye viewing point. The eye is looking + # towards the middle of the box of data from a distance: + eye = R + self._dist * ps + + # vvec, self._vvec and self._eye are unused, remove when deprecated + vvec = R - eye + self._eye = eye + self._vvec = vvec / np.linalg.norm(vvec) + + # Calculate the viewing axes for the eye position + u, v, w = self._calc_view_axes(eye) + self._view_u = u # _view_u is towards the right of the screen + self._view_v = v # _view_v is towards the top of the screen + self._view_w = w # _view_w is out of the screen + + # Generate the view and projection transformation matrices + if self._focal_length == np.inf: + # Orthographic projection + viewM = proj3d._view_transformation_uvw(u, v, w, eye) + projM = proj3d.ortho_transformation(-self._dist, self._dist) else: - V = np.array((0, 0, 1)) - zfront, zback = -self.dist, self.dist - - viewM = proj3d.view_transformation(E, R, V) - projM = self._projection(zfront, zback) + # Perspective projection + # Scale the eye dist to compensate for the focal length zoom effect + eye_focal = R + self._dist * ps * self._focal_length + viewM = proj3d._view_transformation_uvw(u, v, w, eye_focal) + projM = proj3d.persp_transformation(-self._dist, + self._dist, + self._focal_length) + + # Combine all the transformation matrices to get the final projection M0 = np.dot(viewM, worldM) M = np.dot(projM, M0) return M - def mouse_init(self, rotate_btn=1, zoom_btn=3): + def mouse_init(self, rotate_btn=1, pan_btn=2, zoom_btn=3): """ Set the mouse buttons for 3D rotation and zooming. @@ -1153,6 +944,8 @@ def mouse_init(self, rotate_btn=1, zoom_btn=3): ---------- rotate_btn : int or list of int, default: 1 The mouse button or buttons to use for 3D rotation of the axes. + pan_btn : int or list of int, default: 2 + The mouse button or buttons to use to pan the 3D axes. zoom_btn : int or list of int, default: 3 The mouse button or buttons to use to zoom the 3D axes. """ @@ -1161,59 +954,57 @@ def mouse_init(self, rotate_btn=1, zoom_btn=3): # a regular list to avoid comparisons against None # which breaks in recent versions of numpy. self._rotate_btn = np.atleast_1d(rotate_btn).tolist() + self._pan_btn = np.atleast_1d(pan_btn).tolist() self._zoom_btn = np.atleast_1d(zoom_btn).tolist() def disable_mouse_rotation(self): - """Disable mouse buttons for 3D rotation and zooming.""" - self.mouse_init(rotate_btn=[], zoom_btn=[]) + """Disable mouse buttons for 3D rotation, panning, and zooming.""" + self.mouse_init(rotate_btn=[], pan_btn=[], zoom_btn=[]) def can_zoom(self): """ - Return whether this axes supports the zoom box button functionality. - - 3D axes objects do not use the zoom box button. + Return whether this Axes supports the zoom box button functionality. """ - return False + return True def can_pan(self): """ - Return whether this axes supports the pan/zoom button functionality. - - 3D axes objects do not use the pan/zoom button. + Return whether this Axes supports the pan button functionality. """ - return False + return True - def cla(self): - # docstring inherited. + def sharez(self, other): + """ + Share the z-axis with *other*. - super().cla() - self.zaxis.clear() + This is equivalent to passing ``sharez=other`` when constructing the + Axes, and cannot be used if the z-axis is already being shared with + another Axes. + """ + _api.check_isinstance(maxes._base._AxesBase, other=other) + if self._sharez is not None and other is not self._sharez: + raise ValueError("z-axis is already shared") + self._shared_axes["z"].join(self, other) + self._sharez = other + self.zaxis.major = other.zaxis.major # Ticker instances holding + self.zaxis.minor = other.zaxis.minor # locator and formatter. + z0, z1 = other.get_zlim() + self.set_zlim(z0, z1, emit=False, auto=other.get_autoscalez_on()) + self.zaxis._scale = other.zaxis._scale - if self._sharez is not None: - self.zaxis.major = self._sharez.zaxis.major - self.zaxis.minor = self._sharez.zaxis.minor - z0, z1 = self._sharez.get_zlim() - self.set_zlim(z0, z1, emit=False, auto=None) - self.zaxis._set_scale(self._sharez.zaxis.get_scale()) - else: - self.zaxis._set_scale('linear') - try: - self.set_zlim(0, 1) - except TypeError: - pass - - self._autoscaleZon = True - if self._projection is proj3d.ortho_transformation: - self._zmargin = rcParams['axes.zmargin'] + def clear(self): + # docstring inherited. + super().clear() + if self._focal_length == np.inf: + self._zmargin = mpl.rcParams['axes.zmargin'] else: self._zmargin = 0. - - self.grid(rcParams['axes3d.grid']) + self.grid(mpl.rcParams['axes3d.grid']) def _button_press(self, event): if event.inaxes == self: self.button_pressed = event.button - self.sx, self.sy = event.xdata, event.ydata + self._sx, self._sy = event.xdata, event.ydata toolbar = getattr(self.figure.canvas, "toolbar") if toolbar and toolbar._nav_stack() is None: self.figure.canvas.toolbar.push_current() @@ -1221,20 +1012,23 @@ def _button_press(self, event): def _button_release(self, event): self.button_pressed = None toolbar = getattr(self.figure.canvas, "toolbar") - if toolbar: + # backend_bases.release_zoom and backend_bases.release_pan call + # push_current, so check the navigation mode so we don't call it twice + if toolbar and self.get_navigate_mode() is None: self.figure.canvas.toolbar.push_current() def _get_view(self): # docstring inherited return (self.get_xlim(), self.get_ylim(), self.get_zlim(), - self.elev, self.azim) + self.elev, self.azim, self.roll) def _set_view(self, view): # docstring inherited - xlim, ylim, zlim, elev, azim = view + xlim, ylim, zlim, elev, azim, roll = view self.set(xlim=xlim, ylim=ylim, zlim=zlim) self.elev = elev self.azim = azim + self.roll = roll def format_zdata(self, z): """ @@ -1260,14 +1054,19 @@ def format_coord(self, xd, yd): return '' if self.button_pressed in self._rotate_btn: - return 'azimuth={:.0f} deg, elevation={:.0f} deg '.format( - self.azim, self.elev) # ignore xd and yd and display angles instead + norm_elev = art3d._norm_angle(self.elev) + norm_azim = art3d._norm_angle(self.azim) + norm_roll = art3d._norm_angle(self.roll) + return (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, " + f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, " + f"roll={norm_roll:.0f}\N{DEGREE SIGN}" + ).replace("-", "\N{MINUS SIGN}") # nearest edge - p0, p1 = min(self.tunit_edges(), + p0, p1 = min(self._tunit_edges(), key=lambda edge: proj3d._line2d_seg_dist( - edge[0], edge[1], (xd, yd))) + (xd, yd), edge[0][:2], edge[1][:2])) # scale the z value to match x0, y0, z0 = p0 @@ -1288,25 +1087,29 @@ def _on_move(self, event): """ Mouse moving. - By default, button-1 rotates and button-3 zooms; these buttons can be - modified via `mouse_init`. + By default, button-1 rotates, button-2 pans, and button-3 zooms; + these buttons can be modified via `mouse_init`. """ if not self.button_pressed: return + if self.get_navigate_mode() is not None: + # we don't want to rotate if we are zooming/panning + # from the toolbar + return + if self.M is None: return x, y = event.xdata, event.ydata # In case the mouse is out of bounds. - if x is None: + if x is None or event.inaxes != self: return - dx, dy = x - self.sx, y - self.sy + dx, dy = x - self._sx, y - self._sy w = self._pseudo_w h = self._pseudo_h - self.sx, self.sy = x, y # Rotation if self.button_pressed in self._rotate_btn: @@ -1314,46 +1117,207 @@ def _on_move(self, event): # get the x and y pixel coords if dx == 0 and dy == 0: return - self.elev = art3d._norm_angle(self.elev - (dy/h)*180) - self.azim = art3d._norm_angle(self.azim - (dx/w)*180) - self.get_proj() + + roll = np.deg2rad(self.roll) + delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll) + dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll) + self.elev = self.elev + delev + self.azim = self.azim + dazim self.stale = True - self.figure.canvas.draw_idle() - elif self.button_pressed == 2: - # pan view - # get the x and y pixel coords - if dx == 0 and dy == 0: - return - minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() - dx = 1-((w - dx)/w) - dy = 1-((h - dy)/h) - elev, azim = np.deg2rad(self.elev), np.deg2rad(self.azim) - # project xv, yv, zv -> xw, yw, zw - dxx = (maxx-minx)*(dy*np.sin(elev)*np.cos(azim) + dx*np.sin(azim)) - dyy = (maxy-miny)*(-dx*np.cos(azim) + dy*np.sin(elev)*np.sin(azim)) - dzz = (maxz-minz)*(-dy*np.cos(elev)) - # pan - self.set_xlim3d(minx + dxx, maxx + dxx) - self.set_ylim3d(miny + dyy, maxy + dyy) - self.set_zlim3d(minz + dzz, maxz + dzz) - self.get_proj() - self.figure.canvas.draw_idle() + elif self.button_pressed in self._pan_btn: + # Start the pan event with pixel coordinates + px, py = self.transData.transform([self._sx, self._sy]) + self.start_pan(px, py, 2) + # pan view (takes pixel coordinate input) + self.drag_pan(2, None, event.x, event.y) + self.end_pan() # Zoom elif self.button_pressed in self._zoom_btn: - # zoom view - # hmmm..this needs some help from clipping.... - minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() - df = 1-((h - dy)/h) - dx = (maxx-minx)*df - dy = (maxy-miny)*df - dz = (maxz-minz)*df - self.set_xlim3d(minx - dx, maxx + dx) - self.set_ylim3d(miny - dy, maxy + dy) - self.set_zlim3d(minz - dz, maxz + dz) - self.get_proj() - self.figure.canvas.draw_idle() + # zoom view (dragging down zooms in) + scale = h/(h - dy) + self._scale_axis_limits(scale, scale, scale) + + # Store the event coordinates for the next time through. + self._sx, self._sy = x, y + # Always request a draw update at the end of interaction + self.figure.canvas.draw_idle() + + def drag_pan(self, button, key, x, y): + # docstring inherited + + # Get the coordinates from the move event + p = self._pan_start + (xdata, ydata), (xdata_start, ydata_start) = p.trans_inverse.transform( + [(x, y), (p.x, p.y)]) + self._sx, self._sy = xdata, ydata + # Calling start_pan() to set the x/y of this event as the starting + # move location for the next event + self.start_pan(x, y, button) + du, dv = xdata - xdata_start, ydata - ydata_start + dw = 0 + if key == 'x': + dv = 0 + elif key == 'y': + du = 0 + if du == 0 and dv == 0: + return + + # Transform the pan from the view axes to the data axes + R = np.array([self._view_u, self._view_v, self._view_w]) + R = -R / self._box_aspect * self._dist + duvw_projected = R.T @ np.array([du, dv, dw]) + + # Calculate pan distance + minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() + dx = (maxx - minx) * duvw_projected[0] + dy = (maxy - miny) * duvw_projected[1] + dz = (maxz - minz) * duvw_projected[2] + + # Set the new axis limits + self.set_xlim3d(minx + dx, maxx + dx) + self.set_ylim3d(miny + dy, maxy + dy) + self.set_zlim3d(minz + dz, maxz + dz) + + def _calc_view_axes(self, eye): + """ + Get the unit vectors for the viewing axes in data coordinates. + `u` is towards the right of the screen + `v` is towards the top of the screen + `w` is out of the screen + """ + elev_rad = np.deg2rad(art3d._norm_angle(self.elev)) + roll_rad = np.deg2rad(art3d._norm_angle(self.roll)) + + # Look into the middle of the world coordinates + R = 0.5 * self._roll_to_vertical(self._box_aspect) + + # Define which axis should be vertical. A negative value + # indicates the plot is upside down and therefore the values + # have been reversed: + V = np.zeros(3) + V[self._vertical_axis] = -1 if abs(elev_rad) > np.pi/2 else 1 + + u, v, w = proj3d._view_axes(eye, R, V, roll_rad) + return u, v, w + + def _set_view_from_bbox(self, bbox, direction='in', + mode=None, twinx=False, twiny=False): + """ + Zoom in or out of the bounding box. + + Will center the view in the center of the bounding box, and zoom by + the ratio of the size of the bounding box to the size of the Axes3D. + """ + (start_x, start_y, stop_x, stop_y) = bbox + if mode == 'x': + start_y = self.bbox.min[1] + stop_y = self.bbox.max[1] + elif mode == 'y': + start_x = self.bbox.min[0] + stop_x = self.bbox.max[0] + + # Clip to bounding box limits + start_x, stop_x = np.clip(sorted([start_x, stop_x]), + self.bbox.min[0], self.bbox.max[0]) + start_y, stop_y = np.clip(sorted([start_y, stop_y]), + self.bbox.min[1], self.bbox.max[1]) + + # Move the center of the view to the center of the bbox + zoom_center_x = (start_x + stop_x)/2 + zoom_center_y = (start_y + stop_y)/2 + + ax_center_x = (self.bbox.max[0] + self.bbox.min[0])/2 + ax_center_y = (self.bbox.max[1] + self.bbox.min[1])/2 + + self.start_pan(zoom_center_x, zoom_center_y, 2) + self.drag_pan(2, None, ax_center_x, ax_center_y) + self.end_pan() + + # Calculate zoom level + dx = abs(start_x - stop_x) + dy = abs(start_y - stop_y) + scale_u = dx / (self.bbox.max[0] - self.bbox.min[0]) + scale_v = dy / (self.bbox.max[1] - self.bbox.min[1]) + + # Keep aspect ratios equal + scale = max(scale_u, scale_v) + + # Zoom out + if direction == 'out': + scale = 1 / scale + + self._zoom_data_limits(scale, scale, scale) + + def _zoom_data_limits(self, scale_u, scale_v, scale_w): + """ + Zoom in or out of a 3D plot. + + Will scale the data limits by the scale factors. These will be + transformed to the x, y, z data axes based on the current view angles. + A scale factor > 1 zooms out and a scale factor < 1 zooms in. + + For an axes that has had its aspect ratio set to 'equal', 'equalxy', + 'equalyz', or 'equalxz', the relevant axes are constrained to zoom + equally. + + Parameters + ---------- + scale_u : float + Scale factor for the u view axis (view screen horizontal). + scale_v : float + Scale factor for the v view axis (view screen vertical). + scale_w : float + Scale factor for the w view axis (view screen depth). + """ + scale = np.array([scale_u, scale_v, scale_w]) + + # Only perform frame conversion if unequal scale factors + if not np.allclose(scale, scale_u): + # Convert the scale factors from the view frame to the data frame + R = np.array([self._view_u, self._view_v, self._view_w]) + S = scale * np.eye(3) + scale = np.linalg.norm(R.T @ S, axis=1) + + # Set the constrained scale factors to the factor closest to 1 + if self._aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): + ax_idxs = self._equal_aspect_axis_indices(self._aspect) + min_ax_idxs = np.argmin(np.abs(scale[ax_idxs] - 1)) + scale[ax_idxs] = scale[ax_idxs][min_ax_idxs] + + self._scale_axis_limits(scale[0], scale[1], scale[2]) + + def _scale_axis_limits(self, scale_x, scale_y, scale_z): + """ + Keeping the center of the x, y, and z data axes fixed, scale their + limits by scale factors. A scale factor > 1 zooms out and a scale + factor < 1 zooms in. + + Parameters + ---------- + scale_x : float + Scale factor for the x data axis. + scale_y : float + Scale factor for the y data axis. + scale_z : float + Scale factor for the z data axis. + """ + # Get the axis limits and centers + minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() + cx = (maxx + minx)/2 + cy = (maxy + miny)/2 + cz = (maxz + minz)/2 + + # Scale the data range + dx = (maxx - minx)*scale_x + dy = (maxy - miny)*scale_y + dz = (maxz - minz)*scale_z + + # Set the scaled axis limits + self.set_xlim3d(cx - dx/2, cx + dx/2) + self.set_ylim3d(cy - dy/2, cy + dy/2) + self.set_zlim3d(cz - dz/2, cz + dz/2) def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs): """ @@ -1366,9 +1330,6 @@ def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs): def get_zlabel(self): """ Get the z-label text string. - - .. versionadded:: 1.1.0 - This function was added, but not tested. Please report any bugs. """ label = self.zaxis.get_label() return label.get_text() @@ -1390,62 +1351,30 @@ def set_frame_on(self, b): self._frameon = bool(b) self.stale = True - def grid(self, b=True, **kwargs): + def grid(self, visible=True, **kwargs): """ Set / unset 3D grid. .. note:: Currently, this function does not behave the same as - :meth:`matplotlib.axes.Axes.grid`, but it is intended to - eventually support that behavior. - - .. versionadded:: 1.1.0 + `.axes.Axes.grid`, but it is intended to eventually support that + behavior. """ # TODO: Operate on each axes separately if len(kwargs): - b = True - self._draw_grid = b + visible = True + self._draw_grid = visible self.stale = True - def locator_params(self, axis='both', tight=None, **kwargs): - """ - Convenience method for controlling tick locators. - - See :meth:`matplotlib.axes.Axes.locator_params` for full - documentation. Note that this is for Axes3D objects, - therefore, setting *axis* to 'both' will result in the - parameters being set for all three axes. Also, *axis* - can also take a value of 'z' to apply parameters to the - z axis. - - .. versionadded:: 1.1.0 - This function was added, but not tested. Please report any bugs. - """ - _x = axis in ['x', 'both'] - _y = axis in ['y', 'both'] - _z = axis in ['z', 'both'] - if _x: - self.xaxis.get_major_locator().set_params(**kwargs) - if _y: - self.yaxis.get_major_locator().set_params(**kwargs) - if _z: - self.zaxis.get_major_locator().set_params(**kwargs) - self._request_autoscale_view(tight=tight, scalex=_x, scaley=_y, - scalez=_z) - def tick_params(self, axis='both', **kwargs): """ Convenience method for changing the appearance of ticks and tick labels. - See :meth:`matplotlib.axes.Axes.tick_params` for more complete - documentation. - - The only difference is that setting *axis* to 'both' will - mean that the settings are applied to all three axes. Also, - the *axis* parameter also accepts a value of 'z', which - would mean to apply to only the z-axis. + See `.Axes.tick_params` for full documentation. Because this function + applies to 3D Axes, *axis* can also be set to 'z', and setting *axis* + to 'both' autoscales all three axes. Also, because of how Axes3D objects are drawn very differently from regular 2D axes, some of these settings may have @@ -1454,8 +1383,6 @@ def tick_params(self, axis='both', **kwargs): .. note:: Axes3D currently ignores some of these settings. - - .. versionadded:: 1.1.0 """ _api.check_in_list(['x', 'y', 'z', 'both'], axis=axis) if axis in ['x', 'y', 'both']: @@ -1473,27 +1400,15 @@ def tick_params(self, axis='both', **kwargs): def invert_zaxis(self): """ Invert the z-axis. - - .. versionadded:: 1.1.0 - This function was added, but not tested. Please report any bugs. """ bottom, top = self.get_zlim() self.set_zlim(top, bottom, auto=None) - def zaxis_inverted(self): - """ - Returns True if the z-axis is inverted. - - .. versionadded:: 1.1.0 - """ - bottom, top = self.get_zlim() - return top < bottom + zaxis_inverted = _axis_method_wrapper("zaxis", "get_inverted") def get_zbound(self): """ Return the lower and upper z-axis bounds, in increasing order. - - .. versionadded:: 1.1.0 """ bottom, top = self.get_zlim() if bottom < top: @@ -1507,8 +1422,6 @@ def set_zbound(self, lower=None, upper=None): This method will honor axes inversion regardless of parameter order. It will not change the autoscaling setting (`.get_autoscalez_on()`). - - .. versionadded:: 1.1.0 """ if upper is None and np.iterable(lower): lower, upper = lower @@ -1525,9 +1438,11 @@ def set_zbound(self, lower=None, upper=None): def text(self, x, y, z, s, zdir=None, **kwargs): """ - Add text to the plot. kwargs will be passed on to Axes.text, - except for the *zdir* keyword, which sets the direction to be - used as the z direction. + Add text to the plot. + + Keyword arguments will be passed on to `.Axes.text`, except for the + *zdir* keyword, which sets the direction to be used as the z + direction. """ text = super().text(x, y, s, **kwargs) art3d.text_2d_to_3d(text, z, zdir) @@ -1550,7 +1465,7 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): z coordinates of vertices; either one for all points or one for each point. zdir : {'x', 'y', 'z'}, default: 'z' - When plotting 2D data, the direction to use as z ('x', 'y' or 'z'). + When plotting 2D data, the direction to use as z. **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.plot`. """ @@ -1579,13 +1494,12 @@ def plot(self, xs, ys, *args, zdir='z', **kwargs): plot3D = plot - @_api.delete_parameter("3.4", "args", alternative="kwargs") - def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None, + def plot_surface(self, X, Y, Z, *, norm=None, vmin=None, vmax=None, lightsource=None, **kwargs): """ Create a surface plot. - By default it will be colored in shades of a solid color, but it also + By default, it will be colored in shades of a solid color, but it also supports colormapping by supplying the *cmap* argument. .. note:: @@ -1616,8 +1530,6 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None, data is larger, it will be downsampled (by slicing) to these numbers of points. Defaults to 50. - .. versionadded:: 2.0 - rstride, cstride : int Downsampling stride in each direction. These arguments are mutually exclusive with *rcount* and *ccount*. If only one of @@ -1649,19 +1561,15 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None, The lightsource to use when *shade* is True. **kwargs - Other arguments are forwarded to `.Poly3DCollection`. + Other keyword arguments are forwarded to `.Poly3DCollection`. """ had_data = self.has_data() if Z.ndim != 2: raise ValueError("Argument Z must be 2-dimensional.") - if np.any(np.isnan(Z)): - _api.warn_external( - "Z contains NaN values. This may result in rendering " - "artifacts.") - # TODO: Support masked arrays + Z = cbook._to_unmasked_float_array(Z) X, Y, Z = np.broadcast_arrays(X, Y, Z) rows, cols = Z.shape @@ -1676,7 +1584,7 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None, rcount = kwargs.pop('rcount', 50) ccount = kwargs.pop('ccount', 50) - if rcParams['_internal.classic_mode']: + if mpl.rcParams['_internal.classic_mode']: # Strides have priority over counts in classic mode. # So, only compute strides from counts # if counts were explicitly given @@ -1690,24 +1598,18 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None, rstride = int(max(np.ceil(rows / rcount), 1)) cstride = int(max(np.ceil(cols / ccount), 1)) - if 'facecolors' in kwargs: - fcolors = kwargs.pop('facecolors') - else: + fcolors = kwargs.pop('facecolors', None) + + if fcolors is None: color = kwargs.pop('color', None) if color is None: color = self._get_lines.get_next_color() color = np.array(mcolors.to_rgba(color)) - fcolors = None cmap = kwargs.get('cmap', None) shade = kwargs.pop('shade', cmap is None) if shade is None: - _api.warn_deprecated( - "3.1", - message="Passing shade=None to Axes3D.plot_surface() is " - "deprecated since matplotlib 3.1 and will change its " - "semantic or raise an error in matplotlib 3.3. " - "Please use shade=False instead.") + raise ValueError("shade cannot be None.") colset = [] # the sampled facecolor if (rows - 1) % rstride == 0 and \ @@ -1737,17 +1639,36 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None, if fcolors is not None: colset.append(fcolors[rs][cs]) + # In cases where there are NaNs in the data (possibly from masked + # arrays), artifacts can be introduced. Here check whether NaNs exist + # and remove the entries if so + if not isinstance(polys, np.ndarray) or np.isnan(polys).any(): + new_polys = [] + new_colset = [] + + # Depending on fcolors, colset is either an empty list or has as + # many elements as polys. In the former case new_colset results in + # a list with None entries, that is discarded later. + for p, col in itertools.zip_longest(polys, colset): + new_poly = np.array(p)[~np.isnan(p).any(axis=1)] + if len(new_poly): + new_polys.append(new_poly) + new_colset.append(col) + + # Replace previous polys and, if fcolors is not None, colset + polys = new_polys + if fcolors is not None: + colset = new_colset + # note that the striding causes some polygons to have more coordinates # than others - polyc = art3d.Poly3DCollection(polys, *args, **kwargs) if fcolors is not None: - if shade: - colset = self._shade_colors( - colset, self._generate_normals(polys), lightsource) - polyc.set_facecolors(colset) - polyc.set_edgecolors(colset) + polyc = art3d.Poly3DCollection( + polys, edgecolors=colset, facecolors=colset, shade=shade, + lightsource=lightsource, **kwargs) elif cmap: + polyc = art3d.Poly3DCollection(polys, **kwargs) # can't always vectorize, because polys might be jagged if isinstance(polys, np.ndarray): avg_z = polys[..., 2].mean(axis=-1) @@ -1759,99 +1680,16 @@ def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None, if norm is not None: polyc.set_norm(norm) else: - if shade: - colset = self._shade_colors( - color, self._generate_normals(polys), lightsource) - else: - colset = color - polyc.set_facecolors(colset) + polyc = art3d.Poly3DCollection( + polys, facecolors=color, shade=shade, + lightsource=lightsource, **kwargs) self.add_collection(polyc) self.auto_scale_xyz(X, Y, Z, had_data) return polyc - def _generate_normals(self, polygons): - """ - 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. - - Uses three points equally spaced around the polygon. - This normal of course might not make sense for polygons with more than - three points not lying in a plane, but it's a plausible and fast - approximation. - - Parameters - ---------- - polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like - A sequence of polygons to compute normals for, which can have - varying numbers of vertices. If the polygons all have the same - number of vertices and array is passed, then the operation will - be vectorized. - - Returns - ------- - normals : (..., 3) array - A normal vector estimated for the polygon. - """ - if isinstance(polygons, np.ndarray): - # optimization: polygons all have the same number of points, so can - # vectorize - n = polygons.shape[-2] - i1, i2, i3 = 0, n//3, 2*n//3 - v1 = polygons[..., i1, :] - polygons[..., i2, :] - v2 = polygons[..., i2, :] - polygons[..., i3, :] - else: - # The subtraction doesn't vectorize because polygons is jagged. - v1 = np.empty((len(polygons), 3)) - v2 = np.empty((len(polygons), 3)) - for poly_i, ps in enumerate(polygons): - n = len(ps) - i1, i2, i3 = 0, n//3, 2*n//3 - v1[poly_i, :] = ps[i1, :] - ps[i2, :] - v2[poly_i, :] = ps[i2, :] - ps[i3, :] - return np.cross(v1, v2) - - def _shade_colors(self, color, normals, lightsource=None): - """ - Shade *color* using normal vectors given by *normals*. - *color* can also be an array of the same length as *normals*. - """ - if lightsource is None: - # chosen for backwards-compatibility - lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712) - - with np.errstate(invalid="ignore"): - shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True)) - @ lightsource.direction) - mask = ~np.isnan(shade) - - if mask.any(): - # convert dot product to allowed shading fractions - in_norm = mcolors.Normalize(-1, 1) - out_norm = mcolors.Normalize(0.3, 1).inverse - - def norm(x): - return out_norm(in_norm(x)) - - shade[~mask] = 0 - - color = mcolors.to_rgba_array(color) - # shape of color should be (M, 4) (where M is number of faces) - # shape of shade should be (M,) - # colors should have final shape of (M, 4) - alpha = color[:, 3] - colors = norm(shade)[:, np.newaxis] * color - colors[:, 3] = alpha - else: - colors = np.asanyarray(color).copy() - - return colors - - @_api.delete_parameter("3.4", "args", alternative="kwargs") - def plot_wireframe(self, X, Y, Z, *args, **kwargs): + def plot_wireframe(self, X, Y, Z, **kwargs): """ Plot a 3D wireframe. @@ -1874,8 +1712,6 @@ def plot_wireframe(self, X, Y, Z, *args, **kwargs): not sampled in the corresponding direction, producing a 3D line plot rather than a wireframe plot. Defaults to 50. - .. versionadded:: 2.0 - rstride, cstride : int Downsampling stride in each direction. These arguments are mutually exclusive with *rcount* and *ccount*. If only one of @@ -1888,7 +1724,7 @@ def plot_wireframe(self, X, Y, Z, *args, **kwargs): of the new default of ``rcount = ccount = 50``. **kwargs - Other arguments are forwarded to `.Line3DCollection`. + Other keyword arguments are forwarded to `.Line3DCollection`. """ had_data = self.has_data() @@ -1909,7 +1745,7 @@ def plot_wireframe(self, X, Y, Z, *args, **kwargs): rcount = kwargs.pop('rcount', 50) ccount = kwargs.pop('ccount', 50) - if rcParams['_internal.classic_mode']: + if mpl.rcParams['_internal.classic_mode']: # Strides have priority over counts in classic mode. # So, only compute strides from counts # if counts were explicitly given @@ -1965,7 +1801,7 @@ def plot_wireframe(self, X, Y, Z, *args, **kwargs): + [list(zip(xl, yl, zl)) for xl, yl, zl in zip(txlines, tylines, tzlines)]) - linec = art3d.Line3DCollection(lines, *args, **kwargs) + linec = art3d.Line3DCollection(lines, **kwargs) self.add_collection(linec) self.auto_scale_xyz(X, Y, Z, had_data) @@ -1988,7 +1824,7 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, plot_trisurf(X, Y, triangles=triangles, ...) in which case a Triangulation object will be created. See - `.Triangulation` for a explanation of these possibilities. + `.Triangulation` for an explanation of these possibilities. The remaining arguments are:: @@ -2015,15 +1851,13 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, lightsource : `~matplotlib.colors.LightSource` The lightsource to use when *shade* is True. **kwargs - All other arguments are passed on to + All other keyword arguments are passed on to :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` Examples -------- .. plot:: gallery/mplot3d/trisurf3d.py .. plot:: gallery/mplot3d/trisurf3d_2.py - - .. versionadded:: 1.2.0 """ had_data = self.has_data() @@ -2051,9 +1885,8 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, zt = z[triangles] verts = np.stack((xt, yt, zt), axis=-1) - polyc = art3d.Poly3DCollection(verts, *args, **kwargs) - if cmap: + polyc = art3d.Poly3DCollection(verts, *args, **kwargs) # average over the three points of each triangle avg_z = verts[:, :, 2].mean(axis=1) polyc.set_array(avg_z) @@ -2062,12 +1895,9 @@ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, if norm is not None: polyc.set_norm(norm) else: - if shade: - normals = self._generate_normals(verts) - colset = self._shade_colors(color, normals, lightsource) - else: - colset = color - polyc.set_facecolors(colset) + polyc = art3d.Poly3DCollection( + verts, *args, shade=shade, lightsource=lightsource, + facecolors=color, **kwargs) self.add_collection(polyc) self.auto_scale_xyz(tri.x, tri.y, z, had_data) @@ -2090,10 +1920,8 @@ def _3d_extend_contour(self, cset, stride=5): topverts = art3d._paths_to_3d_segments(paths, z - dz) botverts = art3d._paths_to_3d_segments(paths, z + dz) - color = linec.get_color()[0] + color = linec.get_edgecolor()[0] - polyverts = [] - normals = [] nsteps = round(len(topverts[0]) / stride) if nsteps <= 1: if len(topverts[0]) > 1: @@ -2101,10 +1929,11 @@ def _3d_extend_contour(self, cset, stride=5): else: continue + polyverts = [] stepsize = (len(topverts[0]) - 1) / (nsteps - 1) - for i in range(int(round(nsteps)) - 1): - i1 = int(round(i * stepsize)) - i2 = int(round((i + 1) * stepsize)) + for i in range(round(nsteps) - 1): + i1 = round(i * stepsize) + i2 = round((i + 1) * stepsize) polyverts.append([topverts[0][i1], topverts[0][i2], botverts[0][i2], @@ -2112,18 +1941,15 @@ def _3d_extend_contour(self, cset, stride=5): # all polygons have 4 vertices, so vectorize polyverts = np.array(polyverts) - normals = self._generate_normals(polyverts) - - colors = self._shade_colors(color, normals) - colors2 = self._shade_colors(color, normals) polycol = art3d.Poly3DCollection(polyverts, - facecolors=colors, - edgecolors=colors2) + facecolors=color, + edgecolors=color, + shade=True) polycol.set_sort_zpos(z) self.add_collection3d(polycol) for col in colls: - self.collections.remove(col) + col.remove() def add_contour_set( self, cset, extend3d=False, stride=5, zdir='z', offset=None): @@ -2137,13 +1963,34 @@ def add_contour_set( art3d.line_collection_2d_to_3d(linec, z, zdir=zdir) def add_contourf_set(self, cset, zdir='z', offset=None): + self._add_contourf_set(cset, zdir=zdir, offset=offset) + + def _add_contourf_set(self, cset, zdir='z', offset=None): + """ + Returns + ------- + levels : `numpy.ndarray` + Levels at which the filled contours are added. + """ zdir = '-' + zdir - for z, linec in zip(cset.levels, cset.collections): + + midpoints = cset.levels[:-1] + np.diff(cset.levels) / 2 + # Linearly interpolate to get levels for any extensions + if cset._extend_min: + min_level = cset.levels[0] - np.diff(cset.levels[:2]) / 2 + midpoints = np.insert(midpoints, 0, min_level) + if cset._extend_max: + max_level = cset.levels[-1] + np.diff(cset.levels[-2:]) / 2 + midpoints = np.append(midpoints, max_level) + + for z, linec in zip(midpoints, cset.collections): if offset is not None: z = offset art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir) linec.set_sort_zpos(z) + return midpoints + @_preprocess_data() def contour(self, X, Y, Z, *args, extend3d=False, stride=5, zdir='z', offset=None, **kwargs): """ @@ -2151,8 +1998,8 @@ def contour(self, X, Y, Z, *args, Parameters ---------- - X, Y, Z : array-like - Input data. + X, Y, Z : array-like, + Input data. See `.Axes.contour` for supported data shapes. extend3d : bool, default: False Whether to extend contour in 3D. stride : int @@ -2161,7 +2008,10 @@ def contour(self, X, Y, Z, *args, The direction to use. offset : float, optional If specified, plot a projection of the contour lines at this - position in a plane normal to zdir. + position in a plane normal to *zdir*. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.contour`. @@ -2180,14 +2030,12 @@ def contour(self, X, Y, Z, *args, contour3D = contour + @_preprocess_data() def tricontour(self, *args, extend3d=False, stride=5, zdir='z', offset=None, **kwargs): """ Create a 3D contour plot. - .. versionchanged:: 1.3.0 - Added support for custom triangulations - .. note:: This method currently produces incorrect output due to a longstanding bug in 3D PolyCollection rendering. @@ -2195,7 +2043,7 @@ def tricontour(self, *args, Parameters ---------- X, Y, Z : array-like - Input data. + Input data. See `.Axes.tricontour` for supported data shapes. extend3d : bool, default: False Whether to extend contour in 3D. stride : int @@ -2204,13 +2052,15 @@ def tricontour(self, *args, The direction to use. offset : float, optional If specified, plot a projection of the contour lines at this - position in a plane normal to zdir. + position in a plane normal to *zdir*. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`. Returns ------- - matplotlib.tri.tricontour.TriContourSet + matplotlib.tri._tricontour.TriContourSet """ had_data = self.has_data() @@ -2233,6 +2083,17 @@ def tricontour(self, *args, self.auto_scale_xyz(X, Y, Z, had_data) return cset + def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data): + # Autoscale in the zdir based on the levels added, which are + # different from data range if any contour extensions are present + dim_vals = {'x': X, 'y': Y, 'z': Z, zdir: levels} + # Input data and levels have different sizes, but auto_scale_xyz + # expected same-size input, so manually take min/max limits + limits = [(np.nanmin(dim_vals[dim]), np.nanmax(dim_vals[dim])) + for dim in ['x', 'y', 'z']] + self.auto_scale_xyz(*limits, had_data) + + @_preprocess_data() def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): """ Create a 3D filled contour plot. @@ -2240,35 +2101,33 @@ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): Parameters ---------- X, Y, Z : array-like - Input data. + Input data. See `.Axes.contourf` for supported data shapes. zdir : {'x', 'y', 'z'}, default: 'z' The direction to use. offset : float, optional If specified, plot a projection of the contour lines at this - position in a plane normal to zdir. + position in a plane normal to *zdir*. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.contourf`. Returns ------- matplotlib.contour.QuadContourSet - - Notes - ----- - .. versionadded:: 1.1.0 - The *zdir* and *offset* parameters. """ had_data = self.has_data() jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) cset = super().contourf(jX, jY, jZ, *args, **kwargs) - self.add_contourf_set(cset, zdir, offset) + levels = self._add_contourf_set(cset, zdir, offset) - self.auto_scale_xyz(X, Y, Z, had_data) + self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) return cset contourf3D = contourf + @_preprocess_data() def tricontourf(self, *args, zdir='z', offset=None, **kwargs): """ Create a 3D filled contour plot. @@ -2280,26 +2139,21 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs): Parameters ---------- X, Y, Z : array-like - Input data. + Input data. See `.Axes.tricontourf` for supported data shapes. zdir : {'x', 'y', 'z'}, default: 'z' The direction to use. offset : float, optional If specified, plot a projection of the contour lines at this position in a plane normal to zdir. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER *args, **kwargs Other arguments are forwarded to `matplotlib.axes.Axes.tricontourf`. Returns ------- - matplotlib.tri.tricontour.TriContourSet - - Notes - ----- - .. versionadded:: 1.1.0 - The *zdir* and *offset* parameters. - .. versionchanged:: 1.3.0 - Added support for custom triangulations + matplotlib.tri._tricontour.TriContourSet """ had_data = self.has_data() @@ -2317,9 +2171,9 @@ def tricontourf(self, *args, zdir='z', offset=None, **kwargs): tri = Triangulation(jX, jY, tri.triangles, tri.mask) cset = super().tricontourf(tri, jZ, *args, **kwargs) - self.add_contourf_set(cset, zdir, offset) + levels = self._add_contourf_set(cset, zdir, offset) - self.auto_scale_xyz(X, Y, Z, had_data) + self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) return cset def add_collection3d(self, col, zs=0, zdir='z'): @@ -2355,6 +2209,9 @@ def add_collection3d(self, col, zs=0, zdir='z'): collection = super().add_collection(col) return collection + @_preprocess_data(replace_names=["xs", "ys", "zs", "s", + "edgecolors", "c", "facecolor", + "facecolors", "color"]) def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, *args, **kwargs): """ @@ -2363,7 +2220,7 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, Parameters ---------- xs, ys : array-like - The data positions. + The data positions. 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. @@ -2392,8 +2249,10 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, Whether to shade the scatter markers to give the appearance of depth. Each call to ``scatter()`` will perform its depthshading independently. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER **kwargs - All other arguments are passed on to `~.axes.Axes.scatter`. + All other keyword arguments are passed on to `~.axes.Axes.scatter`. Returns ------- @@ -2426,6 +2285,7 @@ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, scatter3D = scatter + @_preprocess_data() def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): """ Add 2D bar(s). @@ -2441,8 +2301,11 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): used for all bars. zdir : {'x', 'y', 'z'}, default: 'z' When plotting 2D data, the direction to use as z ('x', 'y' or 'z'). + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER **kwargs - Other arguments are forwarded to `matplotlib.axes.Axes.bar`. + Other keyword arguments are forwarded to + `matplotlib.axes.Axes.bar`. Returns ------- @@ -2477,12 +2340,13 @@ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): return patches + @_preprocess_data() def bar3d(self, x, y, z, dx, dy, dz, color=None, zsort='average', shade=True, lightsource=None, *args, **kwargs): """ Generate a 3D barplot. - This method creates three dimensional barplot where the width, + This method creates three-dimensional barplot where the width, depth, height, and color of the bars can all be uniquely set. Parameters @@ -2525,6 +2389,9 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, lightsource : `~matplotlib.colors.LightSource` The lightsource to use when *shade* is True. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Any additional keyword arguments are passed onto `~.art3d.Poly3DCollection`. @@ -2532,8 +2399,7 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, Returns ------- collection : `~.art3d.Poly3DCollection` - A collection of three dimensional polygons representing - the bars. + A collection of three-dimensional polygons representing the bars. """ had_data = self.has_data() @@ -2623,15 +2489,11 @@ def bar3d(self, x, y, z, dx, dy, dz, color=None, if len(facecolors) < len(x): facecolors *= (6 * len(x)) - if shade: - normals = self._generate_normals(polys) - sfacecolors = self._shade_colors(facecolors, normals, lightsource) - else: - sfacecolors = facecolors - col = art3d.Poly3DCollection(polys, zsort=zsort, - facecolor=sfacecolors, + facecolors=facecolors, + shade=shade, + lightsource=lightsource, *args, **kwargs) self.add_collection(col) @@ -2646,19 +2508,17 @@ def set_title(self, label, fontdict=None, loc='center', **kwargs): self.title.set_y(0.92 * y) return ret - def quiver(self, *args, + @_preprocess_data() + def quiver(self, X, Y, Z, U, V, W, *, length=1, arrow_length_ratio=.3, pivot='tail', normalize=False, **kwargs): """ - ax.quiver(X, Y, Z, U, V, W, /, length=1, arrow_length_ratio=.3, \ -pivot='tail', normalize=False, **kwargs) - Plot a 3D field of arrows. - The arguments could be array-like or scalars, so long as they - they can be broadcast together. The arguments can also be - masked arrays. If an element in any of argument is masked, then - that corresponding quiver element will not be plotted. + The arguments can be array-like or scalars, so long as they can be + broadcast together. The arguments can also be masked arrays. If an + element in any of argument is masked, then that corresponding quiver + element will not be plotted. Parameters ---------- @@ -2683,9 +2543,12 @@ def quiver(self, *args, Whether all arrows are normalized to have the same length, or keep the lengths defined by *u*, *v*, and *w*. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs Any additional keyword arguments are delegated to - :class:`~matplotlib.collections.LineCollection` + :class:`.Line3DCollection` """ def calc_arrows(UVW, angle=15): @@ -2716,22 +2579,15 @@ def calc_arrows(UVW, angle=15): had_data = self.has_data() - # handle args - argi = 6 - if len(args) < argi: - raise ValueError('Wrong number of arguments. Expected %d got %d' % - (argi, len(args))) - - # first 6 arguments are X, Y, Z, U, V, W - input_args = args[:argi] + input_args = [X, Y, Z, U, V, W] # extract the masks, if any masks = [k.mask for k in input_args if isinstance(k, np.ma.MaskedArray)] # broadcast to match the shape bcast = np.broadcast_arrays(*input_args, *masks) - input_args = bcast[:argi] - masks = bcast[argi:] + input_args = bcast[:6] + masks = bcast[6:] if masks: # combine the masks into one mask = functools.reduce(np.logical_or, masks) @@ -2743,7 +2599,7 @@ def calc_arrows(UVW, angle=15): if any(len(v) == 0 for v in input_args): # No quivers, so just make an empty collection and return early - linec = art3d.Line3DCollection([], *args[argi:], **kwargs) + linec = art3d.Line3DCollection([], **kwargs) self.add_collection(linec) return linec @@ -2757,7 +2613,7 @@ def calc_arrows(UVW, angle=15): shaft_dt -= length / 2 XYZ = np.column_stack(input_args[:3]) - UVW = np.column_stack(input_args[3:argi]).astype(float) + UVW = np.column_stack(input_args[3:]).astype(float) # Normalize rows of UVW norm = np.linalg.norm(UVW, axis=1) @@ -2786,7 +2642,7 @@ def calc_arrows(UVW, angle=15): else: lines = [] - linec = art3d.Line3DCollection(lines, *args[argi:], **kwargs) + linec = art3d.Line3DCollection(lines, **kwargs) self.add_collection(linec) self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data) @@ -2807,8 +2663,6 @@ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, ``filled[0, 0, 0]`` placed with its lower corner at the origin. Occluded faces are not plotted. - .. versionadded:: 2.1 - Parameters ---------- filled : 3D np.array of bool @@ -2834,22 +2688,17 @@ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, can be either a string, or a 1D rgb/rgba array - ``None``, the default, to use a single color for the faces, and the style default for the edges. - - A 3D ndarray of color names, with each item the color for the - corresponding voxel. The size must match the voxels. - - A 4D ndarray of rgb/rgba data, with the components along the - last axis. + - A 3D `~numpy.ndarray` of color names, with each item the color + for the corresponding voxel. The size must match the voxels. + - A 4D `~numpy.ndarray` of rgb/rgba data, with the components + along the last axis. shade : bool, default: True - Whether to shade the facecolors. Shading is always disabled when - *cmap* is specified. - - .. versionadded:: 3.1 + Whether to shade the facecolors. lightsource : `~matplotlib.colors.LightSource` The lightsource to use when *shade* is True. - .. versionadded:: 3.1 - **kwargs Additional keyword arguments to pass onto `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`. @@ -3002,21 +2851,16 @@ def permutation_matrices(n): # shade the faces facecolor = facecolors[coord] edgecolor = edgecolors[coord] - if shade: - normals = self._generate_normals(faces) - facecolor = self._shade_colors(facecolor, normals, lightsource) - if edgecolor is not None: - edgecolor = self._shade_colors( - edgecolor, normals, lightsource - ) poly = art3d.Poly3DCollection( - faces, facecolors=facecolor, edgecolors=edgecolor, **kwargs) + faces, facecolors=facecolor, edgecolors=edgecolor, + shade=shade, lightsource=lightsource, **kwargs) self.add_collection3d(poly) polygons[coord] = poly return polygons + @_preprocess_data(replace_names=["x", "y", "z", "xerr", "yerr", "zerr"]) def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', barsabove=False, errorevery=1, ecolor=None, elinewidth=None, capsize=None, capthick=None, xlolims=False, xuplims=False, @@ -3050,7 +2894,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', The format for the data points / data lines. See `.plot` for details. - Use 'none' (case insensitive) to plot errorbars without any data + Use 'none' (case-insensitive) to plot errorbars without any data markers. ecolor : color, default: None @@ -3082,7 +2926,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', this. *lims*-arguments may be scalars, or array-likes of the same length as the errors. To use limits with inverted axes, `~.Axes.set_xlim` or `~.Axes.set_ylim` must be called before - :meth:`errorbar`. Note the tricky parameter names: setting e.g. + `errorbar`. Note the tricky parameter names: setting e.g. *ylolims* to True means that the y-value is a *lower* limit of the True value, so, only an *upward*-pointing arrow will be drawn! @@ -3112,6 +2956,9 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', Other Parameters ---------------- + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + **kwargs All other keyword arguments for styling errorbar lines are passed `~mpl_toolkits.mplot3d.art3d.Line3DCollection`. @@ -3123,8 +2970,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', had_data = self.has_data() kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D) - # anything that comes in as 'None', drop so the default thing - # happens down stream + # Drop anything that comes in as None to use the default instead. kwargs = {k: v for k, v in kwargs.items() if v is not None} kwargs.setdefault('zorder', 2) @@ -3140,33 +2986,7 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', if not len(x) == len(y) == len(z): raise ValueError("'x', 'y', and 'z' must have the same size") - if isinstance(errorevery, Integral): - errorevery = (0, errorevery) - if isinstance(errorevery, tuple): - if (len(errorevery) == 2 and - isinstance(errorevery[0], Integral) and - isinstance(errorevery[1], Integral)): - errorevery = slice(errorevery[0], None, errorevery[1]) - else: - raise ValueError( - f'errorevery={errorevery!r} is a not a tuple of two ' - f'integers') - - elif isinstance(errorevery, slice): - pass - - elif not isinstance(errorevery, str) and np.iterable(errorevery): - # fancy indexing - try: - x[errorevery] - except (ValueError, IndexError) as err: - raise ValueError( - f"errorevery={errorevery!r} is iterable but not a valid " - f"NumPy fancy index to match " - f"'xerr'/'yerr'/'zerr'") from err - else: - raise ValueError( - f"errorevery={errorevery!r} is not a recognized value") + everymask = self._errorevery_to_mask(x, errorevery) label = kwargs.pop("label", None) kwargs['label'] = '_nolegend_' @@ -3221,16 +3041,13 @@ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', # Make the style dict for caps (the "hats"). eb_cap_style = {**base_style, 'linestyle': 'None'} if capsize is None: - capsize = rcParams["errorbar.capsize"] + capsize = mpl.rcParams["errorbar.capsize"] if capsize > 0: eb_cap_style['markersize'] = 2. * capsize if capthick is not None: eb_cap_style['markeredgewidth'] = capthick eb_cap_style['color'] = ecolor - everymask = np.zeros(len(x), bool) - everymask[errorevery] = True - def _apply_mask(arrays, mask): # Return, for each array in *arrays*, the elements for which *mask* # is True, without using fancy indexing. @@ -3265,19 +3082,19 @@ def _extract_errs(err, data, lomask, himask): # scene is rotated, they are given a standard size based on viewing # them directly in planar form. quiversize = eb_cap_style.get('markersize', - rcParams['lines.markersize']) ** 2 + mpl.rcParams['lines.markersize']) ** 2 quiversize *= self.figure.dpi / 72 quiversize = self.transAxes.inverted().transform([ (0, 0), (quiversize, quiversize)]) quiversize = np.mean(np.diff(quiversize, axis=0)) # quiversize is now in Axes coordinates, and to convert back to data # coordinates, we need to run it through the inverse 3D transform. For - # consistency, this uses a fixed azimuth and elevation. - with cbook._setattr_cm(self, azim=0, elev=0): + # consistency, this uses a fixed elevation, azimuth, and roll. + with cbook._setattr_cm(self, elev=0, azim=0, roll=0): invM = np.linalg.inv(self.get_proj()) - # azim=elev=0 produces the Y-Z plane, so quiversize in 2D 'x' is 'y' in - # 3D, hence the 1 index. - quiversize = np.dot(invM, np.array([quiversize, 0, 0, 0]))[1] + # elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is + # 'y' in 3D, hence the 1 index. + quiversize = np.dot(invM, [quiversize, 0, 0, 0])[1] # Quivers use a fixed 15-degree arrow head, so scale up the length so # that the size corresponds to the base. In other words, this constant # corresponds to the equation tan(15) = (base / 2) / (arrow length). @@ -3367,7 +3184,7 @@ def _digout_minmax(err_arr, coord_label): return errlines, caplines, limmarks - def get_tightbbox(self, renderer, call_axes_locator=True, + def get_tightbbox(self, renderer=None, call_axes_locator=True, bbox_extra_artists=None, *, for_layout_only=False): ret = super().get_tightbbox(renderer, call_axes_locator=call_axes_locator, @@ -3375,20 +3192,15 @@ def get_tightbbox(self, renderer, call_axes_locator=True, for_layout_only=for_layout_only) batch = [ret] if self._axis3don: - for axis in self._get_axis_list(): + for axis in self._axis_map.values(): if axis.get_visible(): - try: - axis_bb = axis.get_tightbbox( - renderer, - for_layout_only=for_layout_only - ) - except TypeError: - # in case downstream library has redefined axis: - axis_bb = axis.get_tightbbox(renderer) - if axis_bb: - batch.append(axis_bb) + axis_bb = martist._get_tightbbox_for_layout_only( + axis, renderer) + if axis_bb: + batch.append(axis_bb) return mtransforms.Bbox.union(batch) + @_preprocess_data() def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', bottom=0, label=None, orientation='z'): """ @@ -3440,6 +3252,9 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', orientation : {'x', 'y', 'z'}, default: 'z' The direction along which stems are drawn. + data : indexable object, optional + DATA_PARAMETER_PLACEHOLDER + Returns ------- `.StemContainer` @@ -3482,7 +3297,7 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', # Determine style for stem lines. linestyle, linemarker, linecolor = _process_plot_format(linefmt) if linestyle is None: - linestyle = rcParams['lines.linestyle'] + linestyle = mpl.rcParams['lines.linestyle'] # Plot everything in required order. baseline, = self.plot(basex, basey, basefmt, zs=bottom, @@ -3504,9 +3319,6 @@ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', stem3D = stem -docstring.interpd.update(Axes3D_kwdoc=artist.kwdoc(Axes3D)) -docstring.dedent_interpd(Axes3D.__init__) - def get_test_data(delta=0.05): """Return a tuple X, Y, Z with a test data set.""" diff --git a/lib/mpl_toolkits/mplot3d/axis3d.py b/lib/mpl_toolkits/mplot3d/axis3d.py index e024104c00f0..3d75aabb65eb 100644 --- a/lib/mpl_toolkits/mplot3d/axis3d.py +++ b/lib/mpl_toolkits/mplot3d/axis3d.py @@ -2,15 +2,27 @@ # Created: 23 Sep 2005 # Parts rewritten by Reinier Heeres +import inspect + import numpy as np -import matplotlib.transforms as mtransforms +import matplotlib as mpl from matplotlib import ( - artist, lines as mlines, axis as maxis, patches as mpatches, rcParams) + _api, artist, lines as mlines, axis as maxis, patches as mpatches, + transforms as mtransforms, colors as mcolors) from . import art3d, proj3d +@_api.deprecated("3.6", alternative="a vendored copy of _move_from_center") def move_from_center(coord, centers, deltas, axmask=(True, True, True)): + """ + For each coordinate where *axmask* is True, move *coord* away from + *centers* by *deltas*. + """ + return _move_from_center(coord, centers, deltas, axmask=axmask) + + +def _move_from_center(coord, centers, deltas, axmask=(True, True, True)): """ For each coordinate where *axmask* is True, move *coord* away from *centers* by *deltas*. @@ -19,8 +31,14 @@ def move_from_center(coord, centers, deltas, axmask=(True, True, True)): return coord + axmask * np.copysign(1, coord - centers) * deltas +@_api.deprecated("3.6", alternative="a vendored copy of _tick_update_position") def tick_update_position(tick, tickxs, tickys, labelpos): """Update tick line and label position and style.""" + _tick_update_position(tick, tickxs, tickys, labelpos) + + +def _tick_update_position(tick, tickxs, tickys, labelpos): + """Update tick line and label position and style.""" tick.label1.set_position(labelpos) tick.label2.set_position(labelpos) @@ -29,47 +47,66 @@ def tick_update_position(tick, tickxs, tickys, labelpos): tick.tick1line.set_linestyle('-') tick.tick1line.set_marker('') tick.tick1line.set_data(tickxs, tickys) - tick.gridline.set_data(0, 0) + tick.gridline.set_data([0], [0]) class Axis(maxis.XAxis): """An Axis class for the 3D plots.""" # These points from the unit cube make up the x, y and z-planes _PLANES = ( - (0, 3, 7, 4), (1, 2, 6, 5), # yz planes - (0, 1, 5, 4), (3, 2, 6, 7), # xz planes - (0, 1, 2, 3), (4, 5, 6, 7), # xy planes + (0, 3, 7, 4), (1, 2, 6, 5), # yz planes + (0, 1, 5, 4), (3, 2, 6, 7), # xz planes + (0, 1, 2, 3), (4, 5, 6, 7), # xy planes ) # Some properties for the axes _AXINFO = { - 'x': {'i': 0, 'tickdir': 1, 'juggled': (1, 0, 2), - 'color': (0.95, 0.95, 0.95, 0.5)}, - 'y': {'i': 1, 'tickdir': 0, 'juggled': (0, 1, 2), - 'color': (0.90, 0.90, 0.90, 0.5)}, - 'z': {'i': 2, 'tickdir': 0, 'juggled': (0, 2, 1), - 'color': (0.925, 0.925, 0.925, 0.5)}, + 'x': {'i': 0, 'tickdir': 1, 'juggled': (1, 0, 2)}, + 'y': {'i': 1, 'tickdir': 0, 'juggled': (0, 1, 2)}, + 'z': {'i': 2, 'tickdir': 0, 'juggled': (0, 2, 1)}, } - def __init__(self, adir, v_intervalx, d_intervalx, axes, *args, - rotate_label=None, **kwargs): - # adir identifies which axes this is - self.adir = adir + def _old_init(self, adir, v_intervalx, d_intervalx, axes, *args, + rotate_label=None, **kwargs): + return locals() + + def _new_init(self, axes, *, rotate_label=None, **kwargs): + return locals() + + def __init__(self, *args, **kwargs): + params = _api.select_matching_signature( + [self._old_init, self._new_init], *args, **kwargs) + if "adir" in params: + _api.warn_deprecated( + "3.6", message=f"The signature of 3D Axis constructors has " + f"changed in %(since)s; the new signature is " + f"{inspect.signature(type(self).__init__)}", pending=True) + if params["adir"] != self.axis_name: + raise ValueError(f"Cannot instantiate {type(self).__name__} " + f"with adir={params['adir']!r}") + axes = params["axes"] + rotate_label = params["rotate_label"] + args = params.get("args", ()) + kwargs = params["kwargs"] + + name = self.axis_name # This is a temporary member variable. # Do not depend on this existing in future releases! - self._axinfo = self._AXINFO[adir].copy() - if rcParams['_internal.classic_mode']: + self._axinfo = self._AXINFO[name].copy() + # Common parts + self._axinfo.update({ + 'label': {'va': 'center', 'ha': 'center', + 'rotation_mode': 'anchor'}, + 'color': mpl.rcParams[f'axes3d.{name}axis.panecolor'], + 'tick': { + 'inward_factor': 0.2, + 'outward_factor': 0.1, + }, + }) + + if mpl.rcParams['_internal.classic_mode']: self._axinfo.update({ - 'label': {'va': 'center', 'ha': 'center'}, - 'tick': { - 'inward_factor': 0.2, - 'outward_factor': 0.1, - 'linewidth': { - True: rcParams['lines.linewidth'], # major - False: rcParams['lines.linewidth'], # minor - } - }, 'axisline': {'linewidth': 0.75, 'color': (0, 0, 0, 1)}, 'grid': { 'color': (0.9, 0.9, 0.9, 1), @@ -77,40 +114,50 @@ def __init__(self, adir, v_intervalx, d_intervalx, axes, *args, 'linestyle': '-', }, }) + self._axinfo['tick'].update({ + 'linewidth': { + True: mpl.rcParams['lines.linewidth'], # major + False: mpl.rcParams['lines.linewidth'], # minor + } + }) else: self._axinfo.update({ - 'label': {'va': 'center', 'ha': 'center'}, - 'tick': { - 'inward_factor': 0.2, - 'outward_factor': 0.1, - 'linewidth': { - True: ( # major - rcParams['xtick.major.width'] if adir in 'xz' else - rcParams['ytick.major.width']), - False: ( # minor - rcParams['xtick.minor.width'] if adir in 'xz' else - rcParams['ytick.minor.width']), - } - }, 'axisline': { - 'linewidth': rcParams['axes.linewidth'], - 'color': rcParams['axes.edgecolor'], + 'linewidth': mpl.rcParams['axes.linewidth'], + 'color': mpl.rcParams['axes.edgecolor'], }, 'grid': { - 'color': rcParams['grid.color'], - 'linewidth': rcParams['grid.linewidth'], - 'linestyle': rcParams['grid.linestyle'], + 'color': mpl.rcParams['grid.color'], + 'linewidth': mpl.rcParams['grid.linewidth'], + 'linestyle': mpl.rcParams['grid.linestyle'], }, }) + self._axinfo['tick'].update({ + 'linewidth': { + True: ( # major + mpl.rcParams['xtick.major.width'] if name in 'xz' + else mpl.rcParams['ytick.major.width']), + False: ( # minor + mpl.rcParams['xtick.minor.width'] if name in 'xz' + else mpl.rcParams['ytick.minor.width']), + } + }) super().__init__(axes, *args, **kwargs) # data and viewing intervals for this direction - self.d_interval = d_intervalx - self.v_interval = v_intervalx + if "d_intervalx" in params: + self.set_data_interval(*params["d_intervalx"]) + if "v_intervalx" in params: + self.set_view_interval(*params["v_intervalx"]) self.set_rotate_label(rotate_label) + self._init3d() # Inline after init3d deprecation elapses. - def init3d(self): + __init__.__signature__ = inspect.signature(_new_init) + adir = _api.deprecated("3.6", pending=True)( + property(lambda self: self.axis_name)) + + def _init3d(self): self.line = mlines.Line2D( xdata=(0, 0), ydata=(0, 0), linewidth=self._axinfo['axisline']['linewidth'], @@ -118,9 +165,7 @@ def init3d(self): antialiased=True) # Store dummy data in Polygon object - self.pane = mpatches.Polygon( - np.array([[0, 0], [0, 1], [1, 0], [0, 0]]), - closed=False, alpha=0.8, facecolor='k', edgecolor='k') + self.pane = mpatches.Polygon([[0, 0], [0, 1]], closed=False) self.set_pane_color(self._axinfo['color']) self.axes._set_artist_props(self.line) @@ -133,6 +178,10 @@ def init3d(self): self.label._transform = self.axes.transData self.offsetText._transform = self.axes.transData + @_api.deprecated("3.6", pending=True) + def init3d(self): # After deprecation elapses, inline _init3d to __init__. + self._init3d() + def get_major_ticks(self, numticks=None): ticks = super().get_major_ticks(numticks) for t in ticks: @@ -149,14 +198,29 @@ def get_minor_ticks(self, numticks=None): obj.set_transform(self.axes.transData) return ticks + @_api.deprecated("3.6") def set_pane_pos(self, xys): + """Set pane position.""" + self._set_pane_pos(xys) + + def _set_pane_pos(self, xys): xys = np.asarray(xys) xys = xys[:, :2] self.pane.xy = xys self.stale = True - def set_pane_color(self, color): - """Set pane color to a RGBA tuple.""" + def set_pane_color(self, color, alpha=None): + """ + Set pane color. + + Parameters + ---------- + color : color + Color for axis pane. + alpha : float, optional + Alpha value for axis pane. If None, base it on *color*. + """ + color = mcolors.to_rgba(color, alpha) self._axinfo['color'] = color self.pane.set_edgecolor(color) self.pane.set_facecolor(color) @@ -183,20 +247,88 @@ def _get_coord_info(self, renderer): self.axes.get_ybound(), self.axes.get_zbound(), ]).T - centers = (maxs + mins) / 2. - deltas = (maxs - mins) / 12. - mins = mins - deltas / 4. - maxs = maxs + deltas / 4. - vals = mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2] - tc = self.axes.tunit_cube(vals, self.axes.M) - avgz = [tc[p1][2] + tc[p2][2] + tc[p3][2] + tc[p4][2] - for p1, p2, p3, p4 in self._PLANES] - highs = np.array([avgz[2*i] < avgz[2*i+1] for i in range(3)]) + # Get the mean value for each bound: + centers = 0.5 * (maxs + mins) + + # Add a small offset between min/max point and the edge of the + # plot: + deltas = (maxs - mins) / 12 + mins -= 0.25 * deltas + maxs += 0.25 * deltas + + # Project the bounds along the current position of the cube: + bounds = mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2] + bounds_proj = self.axes._tunit_cube(bounds, self.axes.M) + + # Determine which one of the parallel planes are higher up: + means_z0 = np.zeros(3) + means_z1 = np.zeros(3) + for i in range(3): + means_z0[i] = np.mean(bounds_proj[self._PLANES[2 * i], 2]) + means_z1[i] = np.mean(bounds_proj[self._PLANES[2 * i + 1], 2]) + highs = means_z0 < means_z1 + + # Special handling for edge-on views + equals = np.abs(means_z0 - means_z1) <= np.finfo(float).eps + if np.sum(equals) == 2: + vertical = np.where(~equals)[0][0] + if vertical == 2: # looking at XY plane + highs = np.array([True, True, highs[2]]) + elif vertical == 1: # looking at XZ plane + highs = np.array([True, highs[1], False]) + elif vertical == 0: # looking at YZ plane + highs = np.array([highs[0], False, False]) + + return mins, maxs, centers, deltas, bounds_proj, highs + + def _get_axis_line_edge_points(self, minmax, maxmin): + """Get the edge points for the black bolded axis line.""" + # When changing vertical axis some of the axes has to be + # moved to the other plane so it looks the same as if the z-axis + # was the vertical axis. + mb = [minmax, maxmin] + mb_rev = mb[::-1] + mm = [[mb, mb_rev, mb_rev], [mb_rev, mb_rev, mb], [mb, mb, mb]] + mm = mm[self.axes._vertical_axis][self._axinfo["i"]] + + juggled = self._axinfo["juggled"] + edge_point_0 = mm[0].copy() + edge_point_0[juggled[0]] = mm[1][juggled[0]] + + edge_point_1 = edge_point_0.copy() + edge_point_1[juggled[1]] = mm[1][juggled[1]] + + return edge_point_0, edge_point_1 + + def _get_tickdir(self): + """ + Get the direction of the tick. - return mins, maxs, centers, deltas, tc, highs + Returns + ------- + tickdir : int + Index which indicates which coordinate the tick line will + align with. + """ + # TODO: Move somewhere else where it's triggered less: + tickdirs_base = [v["tickdir"] for v in self._AXINFO.values()] + info_i = [v["i"] for v in self._AXINFO.values()] + + i = self._axinfo["i"] + j = self.axes._vertical_axis - 2 + # tickdir = [[1, 2, 1], [2, 2, 0], [1, 0, 0]][i] + tickdir = np.roll(info_i, -j)[np.roll(tickdirs_base, j)][i] + return tickdir def draw_pane(self, renderer): + """ + Draw pane. + + Parameters + ---------- + renderer : `~matplotlib.backend_bases.RendererBase` subclass + """ renderer.open_group('pane3d', gid=self.get_gid()) mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) @@ -208,7 +340,7 @@ def draw_pane(self, renderer): else: plane = self._PLANES[2 * index + 1] xys = [tc[p] for p in plane] - self.set_pane_pos(xys) + self._set_pane_pos(xys) self.pane.draw(renderer) renderer.close_group('pane3d') @@ -216,36 +348,30 @@ def draw_pane(self, renderer): @artist.allow_rasterization def draw(self, renderer): self.label._transform = self.axes.transData - renderer.open_group('axis3d', gid=self.get_gid()) + renderer.open_group("axis3d", gid=self.get_gid()) ticks = self._update_ticks() + # Get general axis information: info = self._axinfo - index = info['i'] + index = info["i"] + juggled = info["juggled"] mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) - # Determine grid lines minmax = np.where(highs, maxs, mins) - maxmin = np.where(highs, mins, maxs) + maxmin = np.where(~highs, maxs, mins) - # Draw main axis line - juggled = info['juggled'] - edgep1 = minmax.copy() - edgep1[juggled[0]] = maxmin[juggled[0]] + # Create edge points for the black bolded axis line: + edgep1, edgep2 = self._get_axis_line_edge_points(minmax, maxmin) - edgep2 = edgep1.copy() - edgep2[juggled[1]] = maxmin[juggled[1]] - pep = np.asarray( - proj3d.proj_trans_points([edgep1, edgep2], self.axes.M)) - centpt = proj3d.proj_transform(*centers, self.axes.M) + # Project the edge points along the current position and + # create the line: + pep = proj3d.proj_trans_points([edgep1, edgep2], self.axes.M) + pep = np.asarray(pep) self.line.set_data(pep[0], pep[1]) self.line.draw(renderer) - # Grid points where the planes meet - xyz0 = np.tile(minmax, (len(ticks), 1)) - xyz0[:, index] = [tick.get_loc() for tick in ticks] - # Draw labels # The transAxes transform is used because the Text object # rotates the text relative to the display coordinate system. @@ -269,7 +395,7 @@ def draw(self, renderer): (self.labelpad + default_offset) * deltas_per_point * deltas) axmask = [True, True, True] axmask[index] = False - lxyz = move_from_center(lxyz, centers, labeldeltas, axmask) + lxyz = _move_from_center(lxyz, centers, labeldeltas, axmask) tlx, tly, tlz = proj3d.proj_transform(*lxyz, self.axes.M) self.label.set_position((tlx, tly)) if self.get_rotate_label(self.label.get_text()): @@ -277,6 +403,7 @@ def draw(self, renderer): self.label.set_rotation(angle) self.label.set_va(info['label']['va']) self.label.set_ha(info['label']['ha']) + self.label.set_rotation_mode(info['label']['rotation_mode']) self.label.draw(renderer) # Draw Offset text @@ -290,7 +417,7 @@ def draw(self, renderer): outeredgep = edgep2 outerindex = 1 - pos = move_from_center(outeredgep, centers, labeldeltas, axmask) + pos = _move_from_center(outeredgep, centers, labeldeltas, axmask) olx, oly, olz = proj3d.proj_transform(*pos, self.axes.M) self.offsetText.set_text(self.major.formatter.get_offset()) self.offsetText.set_position((olx, oly)) @@ -300,7 +427,7 @@ def draw(self, renderer): # the alignment point is used as the "fulcrum" for rotation. self.offsetText.set_rotation_mode('anchor') - #---------------------------------------------------------------------- + # ---------------------------------------------------------------------- # Note: the following statement for determining the proper alignment of # the offset text. This was determined entirely by trial-and-error # and should not be in any way considered as "the way". There are @@ -315,6 +442,7 @@ def draw(self, renderer): # Three-letters (e.g., TFT, FTT) are short-hand for the array of bools # from the variable 'highs'. # --------------------------------------------------------------------- + centpt = proj3d.proj_transform(*centers, self.axes.M) if centpt[info['tickdir']] > pep[info['tickdir'], outerindex]: # if FT and if highs has an even number of Trues if (centpt[index] <= pep[index, outerindex] @@ -333,10 +461,7 @@ def draw(self, renderer): if (centpt[index] > pep[index, outerindex] and np.count_nonzero(highs) % 2 == 0): # Usually mean align left, except if it is axis 2 - if index == 2: - align = 'right' - else: - align = 'left' + align = 'right' if index == 2 else 'left' else: # The TT case align = 'right' @@ -346,6 +471,10 @@ def draw(self, renderer): self.offsetText.draw(renderer) if self.axes._draw_grid and len(ticks): + # Grid points where the planes meet + xyz0 = np.tile(minmax, (len(ticks), 1)) + xyz0[:, index] = [tick.get_loc() for tick in ticks] + # Grid lines go from the end of one plane through the plane # intersection (at xyz0) to the end of the other plane. The first # point (0) differs along dimension index-2 and the last (2) along @@ -354,47 +483,45 @@ def draw(self, renderer): lines[:, 0, index - 2] = maxmin[index - 2] lines[:, 2, index - 1] = maxmin[index - 1] self.gridlines.set_segments(lines) - self.gridlines.set_color(info['grid']['color']) - self.gridlines.set_linewidth(info['grid']['linewidth']) - self.gridlines.set_linestyle(info['grid']['linestyle']) + gridinfo = info['grid'] + self.gridlines.set_color(gridinfo['color']) + self.gridlines.set_linewidth(gridinfo['linewidth']) + self.gridlines.set_linestyle(gridinfo['linestyle']) self.gridlines.do_3d_projection() self.gridlines.draw(renderer) - # Draw ticks - tickdir = info['tickdir'] - tickdelta = deltas[tickdir] - if highs[tickdir]: - ticksign = 1 - else: - ticksign = -1 + # Draw ticks: + tickdir = self._get_tickdir() + tickdelta = deltas[tickdir] if highs[tickdir] else -deltas[tickdir] + tick_info = info['tick'] + tick_out = tick_info['outward_factor'] * tickdelta + tick_in = tick_info['inward_factor'] * tickdelta + tick_lw = tick_info['linewidth'] + edgep1_tickdir = edgep1[tickdir] + out_tickdir = edgep1_tickdir + tick_out + in_tickdir = edgep1_tickdir - tick_in + + default_label_offset = 8. # A rough estimate + points = deltas_per_point * deltas for tick in ticks: # Get tick line positions pos = edgep1.copy() pos[index] = tick.get_loc() - pos[tickdir] = ( - edgep1[tickdir] - + info['tick']['outward_factor'] * ticksign * tickdelta) + pos[tickdir] = out_tickdir x1, y1, z1 = proj3d.proj_transform(*pos, self.axes.M) - pos[tickdir] = ( - edgep1[tickdir] - - info['tick']['inward_factor'] * ticksign * tickdelta) + pos[tickdir] = in_tickdir x2, y2, z2 = proj3d.proj_transform(*pos, self.axes.M) # Get position of label - default_offset = 8. # A rough estimate - labeldeltas = ( - (tick.get_pad() + default_offset) * deltas_per_point * deltas) - - axmask = [True, True, True] - axmask[index] = False - pos[tickdir] = edgep1[tickdir] - pos = move_from_center(pos, centers, labeldeltas, axmask) + labeldeltas = (tick.get_pad() + default_label_offset) * points + + pos[tickdir] = edgep1_tickdir + pos = _move_from_center(pos, centers, labeldeltas, axmask) lx, ly, lz = proj3d.proj_transform(*pos, self.axes.M) - tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly)) - tick.tick1line.set_linewidth( - info['tick']['linewidth'][tick._major]) + _tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly)) + tick.tick1line.set_linewidth(tick_lw[tick._major]) tick.draw(renderer) renderer.close_group('axis3d') @@ -402,8 +529,8 @@ def draw(self, renderer): # TODO: Get this to work (more) properly when mplot3d supports the # transforms framework. - def get_tightbbox(self, renderer, *, for_layout_only=False): - # inherited docstring + def get_tightbbox(self, renderer=None, *, for_layout_only=False): + # docstring inherited if not self.get_visible(): return # We have to directly access the internal data structures @@ -437,7 +564,7 @@ def get_tightbbox(self, renderer, *, for_layout_only=False): ticks = ticks_to_draw - bb_1, bb_2 = self._get_tick_bboxes(ticks, renderer) + bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer) other = [] if self.line.get_visible(): @@ -448,27 +575,18 @@ def get_tightbbox(self, renderer, *, for_layout_only=False): return mtransforms.Bbox.union([*bb_1, *bb_2, *other]) - @property - def d_interval(self): - return self.get_data_interval() - - @d_interval.setter - def d_interval(self, minmax): - self.set_data_interval(*minmax) - - @property - def v_interval(self): - return self.get_view_interval() - - @v_interval.setter - def v_interval(self, minmax): - self.set_view_interval(*minmax) - - -# Use classes to look at different data limits + d_interval = _api.deprecated( + "3.6", alternative="get_data_interval", pending=True)( + property(lambda self: self.get_data_interval(), + lambda self, minmax: self.set_data_interval(*minmax))) + v_interval = _api.deprecated( + "3.6", alternative="get_view_interval", pending=True)( + property(lambda self: self.get_view_interval(), + lambda self, minmax: self.set_view_interval(*minmax))) class XAxis(Axis): + axis_name = "x" get_view_interval, set_view_interval = maxis._make_getset_interval( "view", "xy_viewLim", "intervalx") get_data_interval, set_data_interval = maxis._make_getset_interval( @@ -476,6 +594,7 @@ class XAxis(Axis): class YAxis(Axis): + axis_name = "y" get_view_interval, set_view_interval = maxis._make_getset_interval( "view", "xy_viewLim", "intervaly") get_data_interval, set_data_interval = maxis._make_getset_interval( @@ -483,6 +602,7 @@ class YAxis(Axis): class ZAxis(Axis): + axis_name = "z" get_view_interval, set_view_interval = maxis._make_getset_interval( "view", "zz_viewLim", "intervalx") get_data_interval, set_data_interval = maxis._make_getset_interval( diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index cce89c5f3554..646a19781e40 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -6,27 +6,27 @@ import numpy.linalg as linalg -def _line2d_seg_dist(p1, p2, p0): +def _line2d_seg_dist(p, s0, s1): """ - Return the distance(s) from line defined by p1 - p2 to point(s) p0. - - p0[0] = x(s) - p0[1] = y(s) - - intersection point p = p1 + u*(p2-p1) - and intersection point lies within segment if u is between 0 and 1 + Return the distance(s) from point(s) *p* to segment(s) (*s0*, *s1*). + + Parameters + ---------- + p : (ndim,) or (N, ndim) array-like + The points from which the distances are computed. + s0, s1 : (ndim,) or (N, ndim) array-like + The xy(z...) coordinates of the segment endpoints. """ - - x21 = p2[0] - p1[0] - y21 = p2[1] - p1[1] - x01 = np.asarray(p0[0]) - p1[0] - y01 = np.asarray(p0[1]) - p1[1] - - u = (x01*x21 + y01*y21) / (x21**2 + y21**2) - u = np.clip(u, 0, 1) - d = np.hypot(x01 - u*x21, y01 - u*y21) - - return d + s0 = np.asarray(s0) + s01 = s1 - s0 # shape (ndim,) or (N, ndim) + s0p = p - s0 # shape (ndim,) or (N, ndim) + l2 = s01 @ s01 # squared segment length + # Avoid div. by zero for degenerate segments (for them, s01 = (0, 0, ...) + # so the value of l2 doesn't matter; this just replaces 0/0 by 0/1). + l2 = np.where(l2, l2, 1) + # Project onto segment, without going past segment ends. + p1 = s0 + np.multiply.outer(np.clip(s0p @ s01 / l2, 0, 1), s01) + return ((p - p1) ** 2).sum(axis=-1) ** (1/2) def world_transformation(xmin, xmax, @@ -51,55 +51,126 @@ def world_transformation(xmin, xmax, [0, 0, 0, 1]]) -def view_transformation(E, R, V): - n = (E - R) - ## new -# n /= np.linalg.norm(n) -# u = np.cross(V, n) -# u /= np.linalg.norm(u) -# v = np.cross(n, u) -# Mr = np.diag([1.] * 4) -# Mt = np.diag([1.] * 4) -# Mr[:3,:3] = u, v, n -# Mt[:3,-1] = -E - ## end new - - ## old - n = n / np.linalg.norm(n) - u = np.cross(V, n) - u = u / np.linalg.norm(u) - v = np.cross(n, u) - Mr = [[u[0], u[1], u[2], 0], - [v[0], v[1], v[2], 0], - [n[0], n[1], n[2], 0], - [0, 0, 0, 1]] - # - Mt = [[1, 0, 0, -E[0]], - [0, 1, 0, -E[1]], - [0, 0, 1, -E[2]], - [0, 0, 0, 1]] - ## end old - - return np.dot(Mr, Mt) - - -def persp_transformation(zfront, zback): - a = (zfront+zback)/(zfront-zback) - b = -2*(zfront*zback)/(zfront-zback) - return np.array([[1, 0, 0, 0], - [0, 1, 0, 0], - [0, 0, a, b], - [0, 0, -1, 0]]) +def rotation_about_vector(v, angle): + """ + Produce a rotation matrix for an angle in radians about a vector. + """ + vx, vy, vz = v / np.linalg.norm(v) + s = np.sin(angle) + c = np.cos(angle) + t = 2*np.sin(angle/2)**2 # more numerically stable than t = 1-c + + R = np.array([ + [t*vx*vx + c, t*vx*vy - vz*s, t*vx*vz + vy*s], + [t*vy*vx + vz*s, t*vy*vy + c, t*vy*vz - vx*s], + [t*vz*vx - vy*s, t*vz*vy + vx*s, t*vz*vz + c]]) + + return R + + +def _view_axes(E, R, V, roll): + """ + Get the unit viewing axes in data coordinates. + + Parameters + ---------- + E : 3-element numpy array + The coordinates of the eye/camera. + R : 3-element numpy array + The coordinates of the center of the view box. + V : 3-element numpy array + Unit vector in the direction of the vertical axis. + roll : float + The roll angle in radians. + + Returns + ------- + u : 3-element numpy array + Unit vector pointing towards the right of the screen. + v : 3-element numpy array + Unit vector pointing towards the top of the screen. + w : 3-element numpy array + Unit vector pointing out of the screen. + """ + w = (E - R) + w = w/np.linalg.norm(w) + u = np.cross(V, w) + u = u/np.linalg.norm(u) + v = np.cross(w, u) # Will be a unit vector + + # Save some computation for the default roll=0 + if roll != 0: + # A positive rotation of the camera is a negative rotation of the world + Rroll = rotation_about_vector(w, -roll) + u = np.dot(Rroll, u) + v = np.dot(Rroll, v) + return u, v, w + + +def _view_transformation_uvw(u, v, w, E): + """ + Return the view transformation matrix. + + Parameters + ---------- + u : 3-element numpy array + Unit vector pointing towards the right of the screen. + v : 3-element numpy array + Unit vector pointing towards the top of the screen. + w : 3-element numpy array + Unit vector pointing out of the screen. + E : 3-element numpy array + The coordinates of the eye/camera. + """ + Mr = np.eye(4) + Mt = np.eye(4) + Mr[:3, :3] = [u, v, w] + Mt[:3, -1] = -E + M = np.dot(Mr, Mt) + return M + + +def view_transformation(E, R, V, roll): + """ + Return the view transformation matrix. + + Parameters + ---------- + E : 3-element numpy array + The coordinates of the eye/camera. + R : 3-element numpy array + The coordinates of the center of the view box. + V : 3-element numpy array + Unit vector in the direction of the vertical axis. + roll : float + The roll angle in radians. + """ + u, v, w = _view_axes(E, R, V, roll) + M = _view_transformation_uvw(u, v, w, E) + return M + + +def persp_transformation(zfront, zback, focal_length): + e = focal_length + a = 1 # aspect ratio + b = (zfront+zback)/(zfront-zback) + c = -2*(zfront*zback)/(zfront-zback) + proj_matrix = np.array([[e, 0, 0, 0], + [0, e/a, 0, 0], + [0, 0, b, c], + [0, 0, -1, 0]]) + return proj_matrix def ortho_transformation(zfront, zback): # note: w component in the resulting vector will be (zback-zfront), not 1 a = -(zfront + zback) b = -(zfront - zback) - return np.array([[2, 0, 0, 0], - [0, 2, 0, 0], - [0, 0, -2, 0], - [0, 0, a, b]]) + proj_matrix = np.array([[2, 0, 0, 0], + [0, 2, 0, 0], + [0, 0, -2, 0], + [0, 0, a, b]]) + return proj_matrix def _proj_transform_vec(vec, M): @@ -122,6 +193,9 @@ def _proj_transform_vec_clip(vec, M): def inv_transform(xs, ys, zs, M): + """ + Transform the points by the inverse of the projection matrix *M*. + """ iM = linalg.inv(M) vec = _vec_pad_ones(xs, ys, zs) vecr = np.dot(iM, vec) @@ -138,7 +212,7 @@ def _vec_pad_ones(xs, ys, zs): def proj_transform(xs, ys, zs, M): """ - Transform the points by the projection matrix + Transform the points by the projection matrix *M*. """ vec = _vec_pad_ones(xs, ys, zs) return _proj_transform_vec(vec, M) diff --git a/lib/mpl_toolkits/mplot3d/tests/__init__.py b/lib/mpl_toolkits/mplot3d/tests/__init__.py new file mode 100644 index 000000000000..5b6390f4fe26 --- /dev/null +++ b/lib/mpl_toolkits/mplot3d/tests/__init__.py @@ -0,0 +1,10 @@ +from pathlib import Path + + +# Check that the test directories exist +if not (Path(__file__).parent / "baseline_images").exists(): + raise IOError( + 'The baseline image directory does not exist. ' + 'This is most likely because the test data is not installed. ' + 'You may need to install matplotlib from source to get the ' + 'test data.') diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/add_collection3d_zs_array.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/add_collection3d_zs_array.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/add_collection3d_zs_array.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/add_collection3d_zs_array.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/add_collection3d_zs_scalar.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/add_collection3d_zs_scalar.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/add_collection3d_zs_scalar.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/add_collection3d_zs_scalar.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/arc_pathpatch.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/arc_pathpatch.png new file mode 100644 index 000000000000..66e6a9acb986 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/arc_pathpatch.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/aspects.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/aspects.png new file mode 100644 index 000000000000..3bb088e2d131 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/aspects.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/aspects_adjust_box.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/aspects_adjust_box.png new file mode 100644 index 000000000000..7fb448f2c51d Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/aspects_adjust_box.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_cla.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_cla.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_cla.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_cla.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_focal_length.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_focal_length.png new file mode 100644 index 000000000000..1d61e0a0c0f6 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_focal_length.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_isometric.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_isometric.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_isometric.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_isometric.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_labelpad.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_labelpad.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_labelpad.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_labelpad.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_ortho.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_ortho.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_ortho.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_ortho.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_primary_views.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_primary_views.png new file mode 100644 index 000000000000..025156f34d39 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_primary_views.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_rotated.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_rotated.png new file mode 100644 index 000000000000..9e7193d6b326 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/axes3d_rotated.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/bar3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/bar3d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d_notshaded.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/bar3d_notshaded.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d_notshaded.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/bar3d_notshaded.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/bar3d_shaded.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/bar3d_shaded.png new file mode 100644 index 000000000000..39dc9997cb1d Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/bar3d_shaded.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/computed_zorder.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/computed_zorder.png new file mode 100644 index 000000000000..887d409b72c7 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/computed_zorder.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/contour3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/contour3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/contour3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/contour3d.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/contour3d_extend3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/contour3d_extend3d.png new file mode 100644 index 000000000000..061d4add9e47 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/contour3d_extend3d.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/contourf3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/contourf3d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d_fill.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/contourf3d_fill.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/contourf3d_fill.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/contourf3d_fill.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/equal_box_aspect.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/equal_box_aspect.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/equal_box_aspect.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/equal_box_aspect.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/errorbar3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/errorbar3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/errorbar3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/errorbar3d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/errorbar3d_errorevery.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/errorbar3d_errorevery.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/errorbar3d_errorevery.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/errorbar3d_errorevery.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/lines3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/lines3d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/minor_ticks.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/minor_ticks.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/minor_ticks.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/minor_ticks.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/mixedsubplot.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/mixedsubplot.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/mixedsubplot.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/mixedsubplot.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/panecolor_rcparams.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/panecolor_rcparams.png new file mode 100644 index 000000000000..e8e2ac6dcd5a Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/panecolor_rcparams.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/plot_3d_from_2d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/plot_3d_from_2d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/plot_3d_from_2d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/plot_3d_from_2d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_alpha.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/poly3dcollection_alpha.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_alpha.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/poly3dcollection_alpha.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_closed.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/poly3dcollection_closed.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/poly3dcollection_closed.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/poly3dcollection_closed.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_axes_cube.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/proj3d_axes_cube.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_axes_cube.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/proj3d_axes_cube.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_axes_cube_ortho.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/proj3d_axes_cube_ortho.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_axes_cube_ortho.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/proj3d_axes_cube_ortho.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_lines_dists.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/proj3d_lines_dists.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/proj3d_lines_dists.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/proj3d_lines_dists.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/quiver3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/quiver3d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_masked.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/quiver3d_masked.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_masked.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/quiver3d_masked.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_pivot_middle.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/quiver3d_pivot_middle.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_pivot_middle.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/quiver3d_pivot_middle.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_pivot_tail.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/quiver3d_pivot_tail.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/quiver3d_pivot_tail.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/quiver3d_pivot_tail.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/scatter3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/scatter3d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d_color.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/scatter3d_color.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/scatter3d_color.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/scatter3d_color.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/scatter3d_linewidth.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/scatter3d_linewidth.png new file mode 100644 index 000000000000..c4c07dd9e8d6 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/scatter3d_linewidth.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/scatter_spiral.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/scatter_spiral.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/scatter_spiral.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/scatter_spiral.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/stem3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/stem3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/stem3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/stem3d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d_masked.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d_masked.png new file mode 100644 index 000000000000..df7f1ebdf476 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d_masked.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d_masked_strides.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d_masked_strides.png new file mode 100644 index 000000000000..5524fea4537b Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d_masked_strides.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d_shaded.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d_shaded.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/surface3d_shaded.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/surface3d_shaded.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/text3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/text3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/text3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/text3d.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/tricontour.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/tricontour.png new file mode 100644 index 000000000000..4387737e8115 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/tricontour.png differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/trisurf3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/trisurf3d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d_shaded.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/trisurf3d_shaded.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/trisurf3d_shaded.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/trisurf3d_shaded.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-alpha.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-alpha.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-alpha.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-alpha.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-edge-style.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-edge-style.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-edge-style.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-edge-style.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-named-colors.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-named-colors.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-named-colors.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-named-colors.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-rgb-data.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-rgb-data.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-rgb-data.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-rgb-data.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-simple.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-simple.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-simple.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-simple.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-xyz.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-xyz.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/voxels-xyz.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/voxels-xyz.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3d.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/wireframe3d.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3d.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/wireframe3d.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3dzerocstride.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/wireframe3dzerocstride.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3dzerocstride.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/wireframe3dzerocstride.png diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3dzerorstride.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/wireframe3dzerorstride.png similarity index 100% rename from lib/mpl_toolkits/tests/baseline_images/test_mplot3d/wireframe3dzerorstride.png rename to lib/mpl_toolkits/mplot3d/tests/baseline_images/test_axes3d/wireframe3dzerorstride.png diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_legend3d/fancy.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_legend3d/fancy.png new file mode 100644 index 000000000000..d53e297e9df8 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_legend3d/fancy.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_legend3d/legend_bar.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_legend3d/legend_bar.png new file mode 100644 index 000000000000..3502ddb7653f Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_legend3d/legend_bar.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_legend3d/legend_plot.png b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_legend3d/legend_plot.png new file mode 100644 index 000000000000..159430af8d20 Binary files /dev/null and b/lib/mpl_toolkits/mplot3d/tests/baseline_images/test_legend3d/legend_plot.png differ diff --git a/lib/mpl_toolkits/mplot3d/tests/conftest.py b/lib/mpl_toolkits/mplot3d/tests/conftest.py new file mode 100644 index 000000000000..61c2de3e07ba --- /dev/null +++ b/lib/mpl_toolkits/mplot3d/tests/conftest.py @@ -0,0 +1,2 @@ +from matplotlib.testing.conftest import (mpl_test_settings, # noqa + pytest_configure, pytest_unconfigure) diff --git a/lib/mpl_toolkits/mplot3d/tests/test_art3d.py b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py new file mode 100644 index 000000000000..02d35aad0e4b --- /dev/null +++ b/lib/mpl_toolkits/mplot3d/tests/test_art3d.py @@ -0,0 +1,38 @@ +import matplotlib.pyplot as plt + +from matplotlib.backend_bases import MouseEvent + + +def test_scatter_3d_projection_conservation(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + # fix axes3d projection + ax.roll = 0 + ax.elev = 0 + ax.azim = -45 + ax.stale = True + + x = [0, 1, 2, 3, 4] + scatter_collection = ax.scatter(x, x, x) + fig.canvas.draw_idle() + + # Get scatter location on canvas and freeze the data + scatter_offset = scatter_collection.get_offsets() + scatter_location = ax.transData.transform(scatter_offset) + + # Yaw -44 and -46 are enough to produce two set of scatter + # with opposite z-order without moving points too far + for azim in (-44, -46): + ax.azim = azim + ax.stale = True + fig.canvas.draw_idle() + + for i in range(5): + # Create a mouse event used to locate and to get index + # from each dots + event = MouseEvent("button_press_event", fig.canvas, + *scatter_location[i, :]) + contains, ind = scatter_collection.contains(event) + assert contains is True + assert len(ind["ind"]) == 1 + assert ind["ind"][0] == i diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py similarity index 63% rename from lib/mpl_toolkits/tests/test_mplot3d.py rename to lib/mpl_toolkits/mplot3d/tests/test_axes3d.py index 4998386428c8..2ff7d428291c 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/mplot3d/tests/test_axes3d.py @@ -1,19 +1,20 @@ import functools import itertools -import platform import pytest from mpl_toolkits.mplot3d import Axes3D, axes3d, proj3d, art3d import matplotlib as mpl -from matplotlib.backend_bases import MouseButton -from matplotlib.cbook import MatplotlibDeprecationWarning +from matplotlib.backend_bases import (MouseButton, MouseEvent, + NavigationToolbar2) from matplotlib import cm -from matplotlib import colors as mcolors +from matplotlib import colors as mcolors, patches as mpatch from matplotlib.testing.decorators import image_comparison, check_figures_equal from matplotlib.testing.widgets import mock_event from matplotlib.collections import LineCollection, PolyCollection -from matplotlib.patches import Circle +from matplotlib.patches import Circle, PathPatch +from matplotlib.path import Path +from matplotlib.text import Text import matplotlib.pyplot as plt import numpy as np @@ -23,11 +24,80 @@ image_comparison, remove_text=True, style='default') -def test_aspect_equal_error(): +@check_figures_equal(extensions=["png"]) +def test_invisible_axes(fig_test, fig_ref): + ax = fig_test.subplots(subplot_kw=dict(projection='3d')) + ax.set_visible(False) + + +@mpl3d_image_comparison(['aspects.png'], remove_text=False) +def test_aspects(): + aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz') + fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'}) + + # Draw rectangular cuboid with side lengths [1, 1, 5] + r = [0, 1] + scale = np.array([1, 1, 5]) + pts = itertools.combinations(np.array(list(itertools.product(r, r, r))), 2) + for start, end in pts: + if np.sum(np.abs(start - end)) == r[1] - r[0]: + for ax in axs: + ax.plot3D(*zip(start*scale, end*scale)) + for i, ax in enumerate(axs): + ax.set_box_aspect((3, 4, 5)) + ax.set_aspect(aspects[i], adjustable='datalim') + + +@mpl3d_image_comparison(['aspects_adjust_box.png'], remove_text=False) +def test_aspects_adjust_box(): + aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz') + fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'}, + figsize=(11, 3)) + + # Draw rectangular cuboid with side lengths [4, 3, 5] + r = [0, 1] + scale = np.array([4, 3, 5]) + pts = itertools.combinations(np.array(list(itertools.product(r, r, r))), 2) + for start, end in pts: + if np.sum(np.abs(start - end)) == r[1] - r[0]: + for ax in axs: + ax.plot3D(*zip(start*scale, end*scale)) + for i, ax in enumerate(axs): + ax.set_aspect(aspects[i], adjustable='box') + + +def test_axes3d_repr(): fig = plt.figure() ax = fig.add_subplot(projection='3d') - with pytest.raises(NotImplementedError): - ax.set_aspect('equal') + ax.set_label('label') + ax.set_title('title') + ax.set_xlabel('x') + ax.set_ylabel('y') + ax.set_zlabel('z') + assert repr(ax) == ( + "") + + +@mpl3d_image_comparison(['axes3d_primary_views.png']) +def test_axes3d_primary_views(): + # (elev, azim, roll) + views = [(90, -90, 0), # XY + (0, -90, 0), # XZ + (0, 0, 0), # YZ + (-90, 90, 0), # -XY + (0, 90, 0), # -XZ + (0, 180, 0)] # -YZ + # When viewing primary planes, draw the two visible axes so they intersect + # at their low values + fig, axs = plt.subplots(2, 3, subplot_kw={'projection': '3d'}) + for i, ax in enumerate(axs.flat): + ax.set_xlabel('x') + ax.set_ylabel('y') + ax.set_zlabel('z') + ax.set_proj_type('ortho') + ax.view_init(elev=views[i][0], azim=views[i][1], roll=views[i][2]) + plt.tight_layout() @mpl3d_image_comparison(['bar3d.png']) @@ -62,15 +132,15 @@ def test_bar3d_shaded(): x2d, y2d = x2d.ravel(), y2d.ravel() z = x2d + y2d + 1 # Avoid triggering bug with zero-depth boxes. - views = [(-60, 30), (30, 30), (30, -30), (120, -30)] + views = [(30, -60, 0), (30, 30, 30), (-30, 30, -90), (300, -30, 0)] fig = plt.figure(figsize=plt.figaspect(1 / len(views))) axs = fig.subplots( 1, len(views), subplot_kw=dict(projection='3d') ) - for ax, (azim, elev) in zip(axs, views): + for ax, (elev, azim, roll) in zip(axs, views): ax.bar3d(x2d, y2d, x2d * 0, 1, 1, z, shade=True) - ax.view_init(azim=azim, elev=elev) + ax.view_init(elev=elev, azim=azim, roll=roll) fig.canvas.draw() @@ -127,6 +197,17 @@ def test_contour3d(): ax.set_zlim(-100, 100) +@mpl3d_image_comparison(['contour3d_extend3d.png']) +def test_contour3d_extend3d(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + X, Y, Z = axes3d.get_test_data(0.05) + ax.contour(X, Y, Z, zdir='z', offset=-100, cmap=cm.coolwarm, extend3d=True) + ax.set_xlim(-30, 30) + ax.set_ylim(-20, 40) + ax.set_zlim(-80, 80) + + @mpl3d_image_comparison(['contourf3d.png']) def test_contourf3d(): fig = plt.figure() @@ -155,6 +236,34 @@ def test_contourf3d_fill(): ax.set_zlim(-1, 1) +@pytest.mark.parametrize('extend, levels', [['both', [2, 4, 6]], + ['min', [2, 4, 6, 8]], + ['max', [0, 2, 4, 6]]]) +@check_figures_equal(extensions=["png"]) +def test_contourf3d_extend(fig_test, fig_ref, extend, levels): + X, Y = np.meshgrid(np.arange(-2, 2, 0.25), np.arange(-2, 2, 0.25)) + # Z is in the range [0, 8] + Z = X**2 + Y**2 + + # Manually set the over/under colors to be the end of the colormap + cmap = mpl.colormaps['viridis'].copy() + cmap.set_under(cmap(0)) + cmap.set_over(cmap(255)) + # Set vmin/max to be the min/max values plotted on the reference image + kwargs = {'vmin': 1, 'vmax': 7, 'cmap': cmap} + + ax_ref = fig_ref.add_subplot(projection='3d') + ax_ref.contourf(X, Y, Z, levels=[0, 2, 4, 6, 8], **kwargs) + + ax_test = fig_test.add_subplot(projection='3d') + ax_test.contourf(X, Y, Z, levels, extend=extend, **kwargs) + + for ax in [ax_ref, ax_test]: + ax.set_xlim(-2, 2) + ax.set_ylim(-2, 2) + ax.set_zlim(-10, 10) + + @mpl3d_image_comparison(['tricontour.png'], tol=0.02) def test_tricontour(): fig = plt.figure() @@ -170,6 +279,17 @@ def test_tricontour(): ax.tricontourf(x, y, z) +def test_contour3d_1d_input(): + # Check that 1D sequences of different length for {x, y} doesn't error + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + nx, ny = 30, 20 + x = np.linspace(-10, 10, nx) + y = np.linspace(-10, 10, ny) + z = np.random.randint(0, 2, [ny, nx]) + ax.contour(x, y, z, [0.5]) + + @mpl3d_image_comparison(['lines3d.png']) def test_lines3d(): fig = plt.figure() @@ -190,6 +310,23 @@ def test_plot_scalar(fig_test, fig_ref): ax2.plot(1, 1, "o") +def test_invalid_line_data(): + with pytest.raises(RuntimeError, match='x must be'): + art3d.Line3D(0, [], []) + with pytest.raises(RuntimeError, match='y must be'): + art3d.Line3D([], 0, []) + with pytest.raises(RuntimeError, match='z must be'): + art3d.Line3D([], [], 0) + + line = art3d.Line3D([], [], []) + with pytest.raises(RuntimeError, match='x must be'): + line.set_data_3d(0, [], []) + with pytest.raises(RuntimeError, match='y must be'): + line.set_data_3d([], 0, []) + with pytest.raises(RuntimeError, match='z must be'): + line.set_data_3d([], [], 0) + + @mpl3d_image_comparison(['mixedsubplot.png']) def test_mixedsubplots(): def f(t): @@ -256,6 +393,30 @@ def test_scatter3d_color(): color='b', marker='s') +@mpl3d_image_comparison(['scatter3d_linewidth.png']) +def test_scatter3d_linewidth(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + + # Check that array-like linewidth can be set + ax.scatter(np.arange(10), np.arange(10), np.arange(10), + marker='o', linewidth=np.arange(10)) + + +@check_figures_equal(extensions=['png']) +def test_scatter3d_linewidth_modification(fig_ref, fig_test): + # Changing Path3DCollection linewidths with array-like post-creation + # should work correctly. + ax_test = fig_test.add_subplot(projection='3d') + c = ax_test.scatter(np.arange(10), np.arange(10), np.arange(10), + marker='o') + c.set_linewidths(np.arange(10)) + + ax_ref = fig_ref.add_subplot(projection='3d') + ax_ref.scatter(np.arange(10), np.arange(10), np.arange(10), marker='o', + linewidths=np.arange(10)) + + @check_figures_equal(extensions=['png']) def test_scatter3d_modification(fig_ref, fig_test): # Changing Path3DCollection properties post-creation should work correctly. @@ -348,10 +509,10 @@ def test_marker_draw_order_data_reversed(fig_test, fig_ref, azim): color = ['b', 'y'] ax = fig_test.add_subplot(projection='3d') ax.scatter(x, y, z, s=3500, c=color) - ax.view_init(elev=0, azim=azim) + ax.view_init(elev=0, azim=azim, roll=0) ax = fig_ref.add_subplot(projection='3d') ax.scatter(x[::-1], y[::-1], z[::-1], s=3500, c=color[::-1]) - ax.view_init(elev=0, azim=azim) + ax.view_init(elev=0, azim=azim, roll=0) @check_figures_equal(extensions=['png']) @@ -371,11 +532,11 @@ def test_marker_draw_order_view_rotated(fig_test, fig_ref): # axis are not exactly invariant under 180 degree rotation -> deactivate ax.set_axis_off() ax.scatter(x, y, z, s=3500, c=color) - ax.view_init(elev=0, azim=azim) + ax.view_init(elev=0, azim=azim, roll=0) ax = fig_ref.add_subplot(projection='3d') ax.set_axis_off() ax.scatter(x, y, z, s=3500, c=color[::-1]) # color reversed - ax.view_init(elev=0, azim=azim - 180) # view rotated by 180 degrees + ax.view_init(elev=0, azim=azim - 180, roll=0) # view rotated by 180 deg @mpl3d_image_comparison(['plot_3d_from_2d.png'], tol=0.015) @@ -420,6 +581,55 @@ def test_surface3d_shaded(): ax.set_zlim(-1.01, 1.01) +@mpl3d_image_comparison(['surface3d_masked.png']) +def test_surface3d_masked(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11] + y = [1, 2, 3, 4, 5, 6, 7, 8] + + x, y = np.meshgrid(x, y) + matrix = np.array( + [ + [-1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [-1, 1, 2, 3, 4, 4, 4, 3, 2, 1, 1], + [-1, -1., 4, 5, 6, 8, 6, 5, 4, 3, -1.], + [-1, -1., 7, 8, 11, 12, 11, 8, 7, -1., -1.], + [-1, -1., 8, 9, 10, 16, 10, 9, 10, 7, -1.], + [-1, -1., -1., 12, 16, 20, 16, 12, 11, -1., -1.], + [-1, -1., -1., -1., 22, 24, 22, 20, 18, -1., -1.], + [-1, -1., -1., -1., -1., 28, 26, 25, -1., -1., -1.], + ] + ) + z = np.ma.masked_less(matrix, 0) + norm = mcolors.Normalize(vmax=z.max(), vmin=z.min()) + colors = mpl.colormaps["plasma"](norm(z)) + ax.plot_surface(x, y, z, facecolors=colors) + ax.view_init(30, -80, 0) + + +@check_figures_equal(extensions=["png"]) +def test_plot_surface_None_arg(fig_test, fig_ref): + x, y = np.meshgrid(np.arange(5), np.arange(5)) + z = x + y + ax_test = fig_test.add_subplot(projection='3d') + ax_test.plot_surface(x, y, z, facecolors=None) + ax_ref = fig_ref.add_subplot(projection='3d') + ax_ref.plot_surface(x, y, z) + + +@mpl3d_image_comparison(['surface3d_masked_strides.png']) +def test_surface3d_masked_strides(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + + x, y = np.mgrid[-6:6.1:1, -6:6.1:1] + z = np.ma.masked_less(x * y, 2) + + ax.plot_surface(x, y, z, rstride=4, cstride=4) + ax.view_init(60, -45, 0) + + @mpl3d_image_comparison(['text3d.png'], remove_text=False) def test_text3d(): fig = plt.figure() @@ -469,7 +679,7 @@ def test_text3d_modification(fig_ref, fig_test): ax_ref.text(x, y, z, f'({x}, {y}, {z}), dir={zdir}', zdir=zdir) -@mpl3d_image_comparison(['trisurf3d.png'], tol=0.03) +@mpl3d_image_comparison(['trisurf3d.png'], tol=0.061) def test_trisurf3d(): n_angles = 36 n_radii = 8 @@ -626,6 +836,16 @@ def test_patch_collection_modification(fig_test, fig_ref): ax_ref.add_collection3d(c) +def test_poly3dcollection_verts_validation(): + poly = [[0, 0, 1], [0, 1, 1], [0, 1, 0], [0, 0, 0]] + with pytest.raises(ValueError, match=r'list of \(N, 3\) array-like'): + art3d.Poly3DCollection(poly) # should be Poly3DCollection([poly]) + + poly = np.array(poly, dtype=float) + with pytest.raises(ValueError, match=r'list of \(N, 3\) array-like'): + art3d.Poly3DCollection(poly) # should be Poly3DCollection([poly]) + + @mpl3d_image_comparison(['poly3dcollection_closed.png']) def test_poly3dcollection_closed(): fig = plt.figure() @@ -732,14 +952,16 @@ def test_add_collection3d_zs_scalar(): @mpl3d_image_comparison(['axes3d_labelpad.png'], remove_text=False) def test_axes3d_labelpad(): fig = plt.figure() - ax = fig.add_axes(Axes3D(fig, auto_add_to_figure=False)) + ax = fig.add_axes(Axes3D(fig)) # labelpad respects rcParams assert ax.xaxis.labelpad == mpl.rcParams['axes.labelpad'] # labelpad can be set in set_label ax.set_xlabel('X LABEL', labelpad=10) assert ax.xaxis.labelpad == 10 ax.set_ylabel('Y LABEL') - ax.set_zlabel('Z LABEL') + ax.set_zlabel('Z LABEL', labelpad=20) + assert ax.zaxis.labelpad == 20 + assert ax.get_zlabel() == 'Z LABEL' # or manually ax.yaxis.labelpad = 20 ax.zaxis.labelpad = -40 @@ -762,7 +984,7 @@ def test_axes3d_cla(): def test_axes3d_rotated(): fig = plt.figure() ax = fig.add_subplot(1, 1, 1, projection='3d') - ax.view_init(90, 45) # look down, rotated. Should be square + ax.view_init(90, 45, 0) # look down, rotated. Should be square def test_plotsurface_1d_raises(): @@ -782,8 +1004,10 @@ def _test_proj_make_M(): E = np.array([1000, -1000, 2000]) R = np.array([100, 100, 100]) V = np.array([0, 0, 1]) - viewM = proj3d.view_transformation(E, R, V) - perspM = proj3d.persp_transformation(100, -100) + roll = 0 + u, v, w = proj3d._view_axes(E, R, V, roll) + viewM = proj3d._view_transformation_uvw(u, v, w, E) + perspM = proj3d.persp_transformation(100, -100, 1) M = np.dot(perspM, viewM) return M @@ -847,7 +1071,9 @@ def test_proj_axes_cube_ortho(): E = np.array([200, 100, 100]) R = np.array([0, 0, 0]) V = np.array([0, 0, 1]) - viewM = proj3d.view_transformation(E, R, V) + roll = 0 + u, v, w = proj3d._view_axes(E, R, V, roll) + viewM = proj3d._view_transformation_uvw(u, v, w, E) orthoM = proj3d.ortho_transformation(-1, 1) M = np.dot(orthoM, viewM) @@ -904,8 +1130,10 @@ def test_lines_dists(): ys = (100, 150, 30, 200) ax.scatter(xs, ys) - dist = proj3d._line2d_seg_dist(p0, p1, (xs[0], ys[0])) - dist = proj3d._line2d_seg_dist(p0, p1, np.array((xs, ys))) + dist0 = proj3d._line2d_seg_dist((xs[0], ys[0]), p0, p1) + dist = proj3d._line2d_seg_dist(np.array((xs, ys)).T, p0, p1) + assert dist0 == dist[0] + for x, y, d in zip(xs, ys, dist): c = Circle((x, y), d, fill=0) ax.add_patch(c) @@ -914,8 +1142,17 @@ def test_lines_dists(): ax.set_ylim(0, 300) +def test_lines_dists_nowarning(): + # No RuntimeWarning must be emitted for degenerate segments, see GH#22624. + s0 = (10, 30, 50) + p = (20, 150, 180) + proj3d._line2d_seg_dist(p, s0, s0) + proj3d._line2d_seg_dist(np.array(p), s0, s0) + + def test_autoscale(): fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) + assert ax.get_zscale() == 'linear' ax.margins(x=0, y=.1, z=.2) ax.plot([0, 1], [0, 1], [0, 1]) assert ax.get_w_lims() == (0, 1, -.1, 1.1, -.2, 1.2) @@ -923,6 +1160,9 @@ def test_autoscale(): ax.set_autoscalez_on(True) ax.plot([0, 2], [0, 2], [0, 2]) assert ax.get_w_lims() == (0, 1, -.1, 1.1, -.4, 2.4) + ax.autoscale(axis='x') + ax.plot([0, 2], [0, 2], [0, 2]) + assert ax.get_w_lims() == (0, 2, -.1, 1.1, -.4, 2.4) @pytest.mark.parametrize('axis', ('x', 'y', 'z')) @@ -947,6 +1187,22 @@ def test_unautoscale(axis, auto): np.testing.assert_array_equal(get_lim(), (-0.5, 0.5)) +def test_axes3d_focal_length_checks(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + with pytest.raises(ValueError): + ax.set_proj_type('persp', focal_length=0) + with pytest.raises(ValueError): + ax.set_proj_type('ortho', focal_length=1) + + +@mpl3d_image_comparison(['axes3d_focal_length.png'], remove_text=False) +def test_axes3d_focal_length(): + fig, axs = plt.subplots(1, 2, subplot_kw={'projection': '3d'}) + axs[0].set_proj_type('persp', focal_length=np.inf) + axs[1].set_proj_type('persp', focal_length=0.15) + + @mpl3d_image_comparison(['axes3d_ortho.png'], remove_text=False) def test_axes3d_ortho(): fig = plt.figure() @@ -966,7 +1222,7 @@ def test_axes3d_isometric(): for s, e in combinations(np.array(list(product(r, r, r))), 2): if abs(s - e).sum() == r[1] - r[0]: ax.plot3D(*zip(s, e), c='k') - ax.view_init(elev=np.degrees(np.arctan(1. / np.sqrt(2))), azim=-45) + ax.view_init(elev=np.degrees(np.arctan(1. / np.sqrt(2))), azim=-45, roll=0) ax.grid(True) @@ -1119,6 +1375,12 @@ def test_line3d_set_get_data_3d(): np.testing.assert_array_equal((x, y, z), line.get_data_3d()) line.set_data_3d(x2, y2, z2) np.testing.assert_array_equal((x2, y2, z2), line.get_data_3d()) + line.set_xdata(x) + line.set_ydata(y) + line.set_3d_properties(zs=z, zdir='z') + np.testing.assert_array_equal((x, y, z), line.get_data_3d()) + line.set_3d_properties(zs=0, zdir='z') + np.testing.assert_array_equal((x, y, np.zeros_like(z)), line.get_data_3d()) @check_figures_equal(extensions=["png"]) @@ -1155,8 +1417,7 @@ def test_inverted_cla(): def test_ax3d_tickcolour(): fig = plt.figure() - with pytest.warns(MatplotlibDeprecationWarning): - ax = Axes3D(fig) + ax = Axes3D(fig) ax.tick_params(axis='x', colors='red') ax.tick_params(axis='y', colors='red') @@ -1273,7 +1534,7 @@ def test_errorbar3d(): @image_comparison(['stem3d.png'], style='mpl20', - tol=0.0 if platform.machine() == 'x86_64' else 0.003) + tol=0.003) def test_stem3d(): fig, axs = plt.subplots(2, 3, figsize=(8, 6), constrained_layout=True, @@ -1336,6 +1597,9 @@ def test_equal_box_aspect(): ax.axis('off') ax.set_box_aspect((1, 1, 1)) + with pytest.raises(ValueError, match="Argument zoom ="): + ax.set_box_aspect((1, 1, 1), zoom=-1) + def test_colorbar_pos(): num_plots = 2 @@ -1353,6 +1617,55 @@ def test_colorbar_pos(): assert cbar.ax.get_position().extents[1] < 0.2 +def test_inverted_zaxis(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + assert not ax.zaxis_inverted() + assert ax.get_zlim() == (0, 1) + assert ax.get_zbound() == (0, 1) + + # Change bound + ax.set_zbound((0, 2)) + assert not ax.zaxis_inverted() + assert ax.get_zlim() == (0, 2) + assert ax.get_zbound() == (0, 2) + + # Change invert + ax.invert_zaxis() + assert ax.zaxis_inverted() + assert ax.get_zlim() == (2, 0) + assert ax.get_zbound() == (0, 2) + + # Set upper bound + ax.set_zbound(upper=1) + assert ax.zaxis_inverted() + assert ax.get_zlim() == (1, 0) + assert ax.get_zbound() == (0, 1) + + # Set lower bound + ax.set_zbound(lower=2) + assert ax.zaxis_inverted() + assert ax.get_zlim() == (2, 1) + assert ax.get_zbound() == (1, 2) + + +def test_set_zlim(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + assert ax.get_zlim() == (0, 1) + ax.set_zlim(zmax=2) + assert ax.get_zlim() == (0, 2) + ax.set_zlim(zmin=1) + assert ax.get_zlim() == (1, 2) + + with pytest.raises( + TypeError, match="Cannot pass both 'bottom' and 'zmin'"): + ax.set_zlim(bottom=0, zmin=1) + with pytest.raises( + TypeError, match="Cannot pass both 'top' and 'zmax'"): + ax.set_zlim(top=0, zmax=1) + + def test_shared_axes_retick(): fig = plt.figure() ax1 = fig.add_subplot(211, projection="3d") @@ -1403,7 +1716,83 @@ def convert_lim(dmin, dmax): assert z_center != pytest.approx(z_center0) -@pytest.mark.style('default') +@pytest.mark.parametrize("tool,button,key,expected", + [("zoom", MouseButton.LEFT, None, # zoom in + ((0.00, 0.06), (0.01, 0.07), (0.02, 0.08))), + ("zoom", MouseButton.LEFT, 'x', # zoom in + ((-0.01, 0.10), (-0.03, 0.08), (-0.06, 0.06))), + ("zoom", MouseButton.LEFT, 'y', # zoom in + ((-0.07, 0.04), (-0.03, 0.08), (0.00, 0.11))), + ("zoom", MouseButton.RIGHT, None, # zoom out + ((-0.09, 0.15), (-0.07, 0.17), (-0.06, 0.18))), + ("pan", MouseButton.LEFT, None, + ((-0.70, -0.58), (-1.03, -0.91), (-1.27, -1.15))), + ("pan", MouseButton.LEFT, 'x', + ((-0.96, -0.84), (-0.58, -0.46), (-0.06, 0.06))), + ("pan", MouseButton.LEFT, 'y', + ((0.20, 0.32), (-0.51, -0.39), (-1.27, -1.15)))]) +def test_toolbar_zoom_pan(tool, button, key, expected): + # NOTE: The expected zoom values are rough ballparks of moving in the view + # to make sure we are getting the right direction of motion. + # The specific values can and should change if the zoom movement + # scaling factor gets updated. + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + ax.scatter(0, 0, 0) + fig.canvas.draw() + xlim0, ylim0, zlim0 = ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d() + + # Mouse from (0, 0) to (1, 1) + d0 = (0, 0) + d1 = (1, 1) + # 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) + + # Set up the mouse movements + start_event = MouseEvent( + "button_press_event", fig.canvas, *s0, button, key=key) + stop_event = MouseEvent( + "button_release_event", fig.canvas, *s1, button, key=key) + + tb = NavigationToolbar2(fig.canvas) + if tool == "zoom": + tb.zoom() + tb.press_zoom(start_event) + tb.drag_zoom(stop_event) + tb.release_zoom(stop_event) + else: + tb.pan() + tb.press_pan(start_event) + tb.drag_pan(stop_event) + tb.release_pan(stop_event) + + # Should be close, but won't be exact due to screen integer resolution + xlim, ylim, zlim = expected + assert ax.get_xlim3d() == pytest.approx(xlim, abs=0.01) + assert ax.get_ylim3d() == pytest.approx(ylim, abs=0.01) + assert ax.get_zlim3d() == pytest.approx(zlim, abs=0.01) + + # Ensure that back, forward, and home buttons work + tb.back() + assert ax.get_xlim3d() == pytest.approx(xlim0) + assert ax.get_ylim3d() == pytest.approx(ylim0) + assert ax.get_zlim3d() == pytest.approx(zlim0) + + tb.forward() + assert ax.get_xlim3d() == pytest.approx(xlim, abs=0.01) + assert ax.get_ylim3d() == pytest.approx(ylim, abs=0.01) + assert ax.get_zlim3d() == pytest.approx(zlim, abs=0.01) + + tb.home() + assert ax.get_xlim3d() == pytest.approx(xlim0) + assert ax.get_ylim3d() == pytest.approx(ylim0) + assert ax.get_zlim3d() == pytest.approx(zlim0) + + +@mpl.style.context('default') @check_figures_equal(extensions=["png"]) def test_scalarmap_update(fig_test, fig_ref): @@ -1433,6 +1822,187 @@ def test_subfigure_simple(): ax = sf[1].add_subplot(1, 1, 1, projection='3d', label='other') +# Update style when regenerating the test image +@image_comparison(baseline_images=['computed_zorder'], remove_text=True, + extensions=['png'], style=('classic', '_classic_test_patch')) +def test_computed_zorder(): + fig = plt.figure() + ax1 = fig.add_subplot(221, projection='3d') + ax2 = fig.add_subplot(222, projection='3d') + ax2.computed_zorder = False + + # create a horizontal plane + corners = ((0, 0, 0), (0, 5, 0), (5, 5, 0), (5, 0, 0)) + for ax in (ax1, ax2): + tri = art3d.Poly3DCollection([corners], + facecolors='white', + edgecolors='black', + zorder=1) + ax.add_collection3d(tri) + + # plot a vector + ax.plot((2, 2), (2, 2), (0, 4), c='red', zorder=2) + + # plot some points + ax.scatter((3, 3), (1, 3), (1, 3), c='red', zorder=10) + + ax.set_xlim((0, 5.0)) + ax.set_ylim((0, 5.0)) + ax.set_zlim((0, 2.5)) + + ax3 = fig.add_subplot(223, projection='3d') + ax4 = fig.add_subplot(224, projection='3d') + ax4.computed_zorder = False + + dim = 10 + X, Y = np.meshgrid((-dim, dim), (-dim, dim)) + Z = np.zeros((2, 2)) + + angle = 0.5 + X2, Y2 = np.meshgrid((-dim, dim), (0, dim)) + Z2 = Y2 * angle + X3, Y3 = np.meshgrid((-dim, dim), (-dim, 0)) + Z3 = Y3 * angle + + r = 7 + M = 1000 + th = np.linspace(0, 2 * np.pi, M) + x, y, z = r * np.cos(th), r * np.sin(th), angle * r * np.sin(th) + for ax in (ax3, ax4): + ax.plot_surface(X2, Y3, Z3, + color='blue', + alpha=0.5, + linewidth=0, + zorder=-1) + ax.plot(x[y < 0], y[y < 0], z[y < 0], + lw=5, + linestyle='--', + color='green', + zorder=0) + + ax.plot_surface(X, Y, Z, + color='red', + alpha=0.5, + linewidth=0, + zorder=1) + + ax.plot(r * np.sin(th), r * np.cos(th), np.zeros(M), + lw=5, + linestyle='--', + color='black', + zorder=2) + + ax.plot_surface(X2, Y2, Z2, + color='blue', + alpha=0.5, + linewidth=0, + zorder=3) + + ax.plot(x[y > 0], y[y > 0], z[y > 0], lw=5, + linestyle='--', + color='green', + zorder=4) + ax.view_init(elev=20, azim=-20, roll=0) + ax.axis('off') + + +def test_format_coord(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + x = np.arange(10) + ax.plot(x, np.sin(x)) + fig.canvas.draw() + assert ax.format_coord(0, 0) == 'x=1.8066, y=1.0367, z=−0.0553' + # Modify parameters + ax.view_init(roll=30, vertical_axis="y") + fig.canvas.draw() + assert ax.format_coord(0, 0) == 'x=9.1651, y=−0.9215, z=−0.0359' + # Reset parameters + ax.view_init() + fig.canvas.draw() + assert ax.format_coord(0, 0) == 'x=1.8066, y=1.0367, z=−0.0553' + + +def test_get_axis_position(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + x = np.arange(10) + ax.plot(x, np.sin(x)) + fig.canvas.draw() + assert ax.get_axis_position() == (False, True, False) + + +def test_margins(): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + ax.margins(0.2) + assert ax.margins() == (0.2, 0.2, 0.2) + ax.margins(0.1, 0.2, 0.3) + assert ax.margins() == (0.1, 0.2, 0.3) + ax.margins(x=0) + assert ax.margins() == (0, 0.2, 0.3) + ax.margins(y=0.1) + assert ax.margins() == (0, 0.1, 0.3) + ax.margins(z=0) + assert ax.margins() == (0, 0.1, 0) + + +@pytest.mark.parametrize('err, args, kwargs, match', ( + (ValueError, (-1,), {}, r'margin must be greater than -0\.5'), + (ValueError, (1, -1, 1), {}, r'margin must be greater than -0\.5'), + (ValueError, (1, 1, -1), {}, r'margin must be greater than -0\.5'), + (ValueError, tuple(), {'x': -1}, r'margin must be greater than -0\.5'), + (ValueError, tuple(), {'y': -1}, r'margin must be greater than -0\.5'), + (ValueError, tuple(), {'z': -1}, r'margin must be greater than -0\.5'), + (TypeError, (1, ), {'x': 1}, + 'Cannot pass both positional and keyword'), + (TypeError, (1, ), {'x': 1, 'y': 1, 'z': 1}, + 'Cannot pass both positional and keyword'), + (TypeError, (1, ), {'x': 1, 'y': 1}, + 'Cannot pass both positional and keyword'), + (TypeError, (1, 1), {}, 'Must pass a single positional argument for'), +)) +def test_margins_errors(err, args, kwargs, match): + with pytest.raises(err, match=match): + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + ax.margins(*args, **kwargs) + + +@check_figures_equal(extensions=["png"]) +def test_text_3d(fig_test, fig_ref): + ax = fig_ref.add_subplot(projection="3d") + txt = Text(0.5, 0.5, r'Foo bar $\int$') + art3d.text_2d_to_3d(txt, z=1) + ax.add_artist(txt) + assert txt.get_position_3d() == (0.5, 0.5, 1) + + ax = fig_test.add_subplot(projection="3d") + t3d = art3d.Text3D(0.5, 0.5, 1, r'Foo bar $\int$') + ax.add_artist(t3d) + assert t3d.get_position_3d() == (0.5, 0.5, 1) + + +def test_draw_single_lines_from_Nx1(): + # Smoke test for GH#23459 + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + ax.plot([[0], [1]], [[0], [1]], [[0], [1]]) + + +@check_figures_equal(extensions=["png"]) +def test_pathpatch_3d(fig_test, fig_ref): + ax = fig_ref.add_subplot(projection="3d") + path = Path.unit_rectangle() + patch = PathPatch(path) + art3d.pathpatch_2d_to_3d(patch, z=(0, 0.5, 0.7, 1, 0), zdir='y') + ax.add_artist(patch) + + ax = fig_test.add_subplot(projection="3d") + pp3d = art3d.PathPatch3D(path, zs=(0, 0.5, 0.7, 1, 0), zdir='y') + ax.add_artist(pp3d) + + @image_comparison(baseline_images=['scatter_spiral.png'], remove_text=True, style='default') @@ -1444,3 +2014,163 @@ def test_scatter_spiral(): # force at least 1 draw! fig.canvas.draw() + + +def test_Poly3DCollection_get_facecolor(): + # Smoke test to see that get_facecolor does not raise + # See GH#4067 + y, x = np.ogrid[1:10:100j, 1:10:100j] + z2 = np.cos(x) ** 3 - np.sin(y) ** 2 + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + r = ax.plot_surface(x, y, z2, cmap='hot') + r.get_facecolor() + + +def test_Poly3DCollection_get_edgecolor(): + # Smoke test to see that get_edgecolor does not raise + # See GH#4067 + y, x = np.ogrid[1:10:100j, 1:10:100j] + z2 = np.cos(x) ** 3 - np.sin(y) ** 2 + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + r = ax.plot_surface(x, y, z2, cmap='hot') + r.get_edgecolor() + + +@pytest.mark.parametrize( + "vertical_axis, proj_expected, axis_lines_expected, tickdirs_expected", + [ + ( + "z", + [ + [0.0, 1.142857, 0.0, -0.571429], + [0.0, 0.0, 0.857143, -0.428571], + [0.0, 0.0, 0.0, -10.0], + [-1.142857, 0.0, 0.0, 10.571429], + ], + [ + ([0.05617978, 0.06329114], [-0.04213483, -0.04746835]), + ([-0.06329114, 0.06329114], [-0.04746835, -0.04746835]), + ([-0.06329114, -0.06329114], [-0.04746835, 0.04746835]), + ], + [1, 0, 0], + ), + ( + "y", + [ + [1.142857, 0.0, 0.0, -0.571429], + [0.0, 0.857143, 0.0, -0.428571], + [0.0, 0.0, 0.0, -10.0], + [0.0, 0.0, -1.142857, 10.571429], + ], + [ + ([-0.06329114, 0.06329114], [0.04746835, 0.04746835]), + ([0.06329114, 0.06329114], [-0.04746835, 0.04746835]), + ([-0.05617978, -0.06329114], [0.04213483, 0.04746835]), + ], + [2, 2, 0], + ), + ( + "x", + [ + [0.0, 0.0, 1.142857, -0.571429], + [0.857143, 0.0, 0.0, -0.428571], + [0.0, 0.0, 0.0, -10.0], + [0.0, -1.142857, 0.0, 10.571429], + ], + [ + ([-0.06329114, -0.06329114], [0.04746835, -0.04746835]), + ([0.06329114, 0.05617978], [0.04746835, 0.04213483]), + ([0.06329114, -0.06329114], [0.04746835, 0.04746835]), + ], + [1, 2, 1], + ), + ], +) +def test_view_init_vertical_axis( + vertical_axis, proj_expected, axis_lines_expected, tickdirs_expected +): + """ + Test the actual projection, axis lines and ticks matches expected values. + + Parameters + ---------- + vertical_axis : str + Axis to align vertically. + proj_expected : ndarray + Expected values from ax.get_proj(). + axis_lines_expected : tuple of arrays + Edgepoints of the axis line. Expected values retrieved according + to ``ax.get_[xyz]axis().line.get_data()``. + tickdirs_expected : list of int + indexes indicating which axis to create a tick line along. + """ + rtol = 2e-06 + ax = plt.subplot(1, 1, 1, projection="3d") + ax.view_init(elev=0, azim=0, roll=0, vertical_axis=vertical_axis) + ax.figure.canvas.draw() + + # Assert the projection matrix: + proj_actual = ax.get_proj() + np.testing.assert_allclose(proj_expected, proj_actual, rtol=rtol) + + for i, axis in enumerate([ax.get_xaxis(), ax.get_yaxis(), ax.get_zaxis()]): + # Assert black lines are correctly aligned: + axis_line_expected = axis_lines_expected[i] + axis_line_actual = axis.line.get_data() + np.testing.assert_allclose(axis_line_expected, axis_line_actual, + rtol=rtol) + + # Assert ticks are correctly aligned: + tickdir_expected = tickdirs_expected[i] + tickdir_actual = axis._get_tickdir() + np.testing.assert_array_equal(tickdir_expected, tickdir_actual) + + +@image_comparison(baseline_images=['arc_pathpatch.png'], + remove_text=True, + style='default') +def test_arc_pathpatch(): + ax = plt.subplot(1, 1, 1, projection="3d") + a = mpatch.Arc((0.5, 0.5), width=0.5, height=0.9, + angle=20, theta1=10, theta2=130) + ax.add_patch(a) + art3d.pathpatch_2d_to_3d(a, z=0, zdir='z') + + +@image_comparison(baseline_images=['panecolor_rcparams.png'], + remove_text=True, + style='mpl20') +def test_panecolor_rcparams(): + with plt.rc_context({'axes3d.xaxis.panecolor': 'r', + 'axes3d.yaxis.panecolor': 'g', + 'axes3d.zaxis.panecolor': 'b'}): + fig = plt.figure(figsize=(1, 1)) + fig.add_subplot(projection='3d') + + +@check_figures_equal(extensions=["png"]) +def test_mutating_input_arrays_y_and_z(fig_test, fig_ref): + """ + Test to see if the `z` axis does not get mutated + after a call to `Axes3D.plot` + + test cases came from GH#8990 + """ + ax1 = fig_test.add_subplot(111, projection='3d') + x = [1, 2, 3] + y = [0.0, 0.0, 0.0] + z = [0.0, 0.0, 0.0] + ax1.plot(x, y, z, 'o-') + + # mutate y,z to get a nontrivial line + y[:] = [1, 2, 3] + z[:] = [1, 2, 3] + + # draw the same plot without mutating x and y + ax2 = fig_ref.add_subplot(111, projection='3d') + x = [1, 2, 3] + y = [0.0, 0.0, 0.0] + z = [0.0, 0.0, 0.0] + ax2.plot(x, y, z, 'o-') diff --git a/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py b/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py new file mode 100644 index 000000000000..bdd46754fe5d --- /dev/null +++ b/lib/mpl_toolkits/mplot3d/tests/test_legend3d.py @@ -0,0 +1,109 @@ + +import numpy as np + +from matplotlib.colors import same_color +from matplotlib.testing.decorators import image_comparison +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import art3d + + +# Update style when regenerating the test image +@image_comparison(['legend_plot.png'], remove_text=True, + style=('classic', '_classic_test_patch')) +def test_legend_plot(): + fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) + x = np.arange(10) + ax.plot(x, 5 - x, 'o', zdir='y', label='z=1') + ax.plot(x, x - 5, 'o', zdir='y', label='z=-1') + ax.legend() + + +# Update style when regenerating the test image +@image_comparison(['legend_bar.png'], remove_text=True, + style=('classic', '_classic_test_patch')) +def test_legend_bar(): + fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) + x = np.arange(10) + b1 = ax.bar(x, x, zdir='y', align='edge', color='m') + b2 = ax.bar(x, x[::-1], zdir='x', align='edge', color='g') + ax.legend([b1[0], b2[0]], ['up', 'down']) + + +# Update style when regenerating the test image +@image_comparison(['fancy.png'], remove_text=True, + style=('classic', '_classic_test_patch')) +def test_fancy(): + fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) + ax.plot(np.arange(10), np.full(10, 5), np.full(10, 5), 'o--', label='line') + ax.scatter(np.arange(10), np.arange(10, 0, -1), label='scatter') + ax.errorbar(np.full(10, 5), np.arange(10), np.full(10, 10), + xerr=0.5, zerr=0.5, label='errorbar') + ax.legend(loc='lower left', ncols=2, title='My legend', numpoints=1) + + +def test_linecollection_scaled_dashes(): + lines1 = [[(0, .5), (.5, 1)], [(.3, .6), (.2, .2)]] + lines2 = [[[0.7, .2], [.8, .4]], [[.5, .7], [.6, .1]]] + lines3 = [[[0.6, .2], [.8, .4]], [[.5, .7], [.1, .1]]] + lc1 = art3d.Line3DCollection(lines1, linestyles="--", lw=3) + lc2 = art3d.Line3DCollection(lines2, linestyles="-.") + lc3 = art3d.Line3DCollection(lines3, linestyles=":", lw=.5) + + fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) + ax.add_collection(lc1) + ax.add_collection(lc2) + ax.add_collection(lc3) + + leg = ax.legend([lc1, lc2, lc3], ['line1', 'line2', 'line 3']) + h1, h2, h3 = leg.legend_handles + + for oh, lh in zip((lc1, lc2, lc3), (h1, h2, h3)): + assert oh.get_linestyles()[0] == lh._dash_pattern + + +def test_handlerline3d(): + # Test marker consistency for monolithic Line3D legend handler. + fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) + ax.scatter([0, 1], [0, 1], marker="v") + handles = [art3d.Line3D([0], [0], [0], marker="v")] + leg = ax.legend(handles, ["Aardvark"], numpoints=1) + assert handles[0].get_marker() == leg.legend_handles[0].get_marker() + + +def test_contour_legend_elements(): + from matplotlib.collections import LineCollection + x, y = np.mgrid[1:10, 1:10] + h = x * y + colors = ['blue', '#00FF00', 'red'] + + fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) + cs = ax.contour(x, y, h, levels=[10, 30, 50], colors=colors, extend='both') + + artists, labels = cs.legend_elements() + assert labels == ['$x = 10.0$', '$x = 30.0$', '$x = 50.0$'] + assert all(isinstance(a, LineCollection) for a in artists) + assert all(same_color(a.get_color(), c) + for a, c in zip(artists, colors)) + + +def test_contourf_legend_elements(): + from matplotlib.patches import Rectangle + x, y = np.mgrid[1:10, 1:10] + h = x * y + + fig, ax = plt.subplots(subplot_kw=dict(projection='3d')) + cs = ax.contourf(x, y, h, levels=[10, 30, 50], + colors=['#FFFF00', '#FF00FF', '#00FFFF'], + extend='both') + cs.cmap.set_over('red') + cs.cmap.set_under('blue') + cs.changed() + artists, labels = cs.legend_elements() + assert labels == ['$x \\leq -1e+250s$', + '$10.0 < x \\leq 30.0$', + '$30.0 < x \\leq 50.0$', + '$x > 1e+250s$'] + expected_colors = ('blue', '#FFFF00', '#FF00FF', 'red') + assert all(isinstance(a, Rectangle) for a in artists) + assert all(same_color(a.get_facecolor(), c) + for a, c in zip(artists, expected_colors)) diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid/imagegrid_cbar_mode.png b/lib/mpl_toolkits/tests/baseline_images/test_axes_grid/imagegrid_cbar_mode.png deleted file mode 100644 index a42548f9f6cd..000000000000 Binary files a/lib/mpl_toolkits/tests/baseline_images/test_axes_grid/imagegrid_cbar_mode.png and /dev/null differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_axislines/ParasiteAxesAuxTrans_meshplot.png b/lib/mpl_toolkits/tests/baseline_images/test_axisartist_axislines/ParasiteAxesAuxTrans_meshplot.png deleted file mode 100644 index e8116fe1228f..000000000000 Binary files a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_axislines/ParasiteAxesAuxTrans_meshplot.png and /dev/null differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_clip_path/clip_path.png b/lib/mpl_toolkits/tests/baseline_images/test_axisartist_clip_path/clip_path.png deleted file mode 100644 index 1f296b6d06d5..000000000000 Binary files a/lib/mpl_toolkits/tests/baseline_images/test_axisartist_clip_path/clip_path.png and /dev/null differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_rotated.png b/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_rotated.png deleted file mode 100644 index 0c79fd32e42c..000000000000 Binary files a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/axes3d_rotated.png and /dev/null differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d_shaded.png b/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d_shaded.png deleted file mode 100644 index c22f2a5671d3..000000000000 Binary files a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d_shaded.png and /dev/null differ diff --git a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/tricontour.png b/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/tricontour.png deleted file mode 100644 index 7d8eb501601e..000000000000 Binary files a/lib/mpl_toolkits/tests/baseline_images/test_mplot3d/tricontour.png and /dev/null differ diff --git a/lib/mpl_toolkits/tests/conftest.py b/lib/mpl_toolkits/tests/conftest.py deleted file mode 100644 index 81829c903c58..000000000000 --- a/lib/mpl_toolkits/tests/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -from matplotlib.testing.conftest import (mpl_test_settings, - mpl_image_comparison_parameters, - pytest_configure, pytest_unconfigure) diff --git a/lib/mpl_toolkits/tests/test_axes_grid.py b/lib/mpl_toolkits/tests/test_axes_grid.py deleted file mode 100644 index f789c31b7c67..000000000000 --- a/lib/mpl_toolkits/tests/test_axes_grid.py +++ /dev/null @@ -1,57 +0,0 @@ -import numpy as np - -import matplotlib as mpl -from matplotlib.testing.decorators import image_comparison -import matplotlib.pyplot as plt -from mpl_toolkits.axes_grid1 import ImageGrid - - -# The original version of this test relied on mpl_toolkits's slightly different -# colorbar implementation; moving to matplotlib's own colorbar implementation -# caused the small image comparison error. -@image_comparison(['imagegrid_cbar_mode.png'], - remove_text=True, style='mpl20', tol=0.3) -def test_imagegrid_cbar_mode_edge(): - # Remove this line when this test image is regenerated. - plt.rcParams['pcolormesh.snap'] = False - - X, Y = np.meshgrid(np.linspace(0, 6, 30), np.linspace(0, 6, 30)) - arr = np.sin(X) * np.cos(Y) + 1j*(np.sin(3*Y) * np.cos(Y/2.)) - - fig = plt.figure(figsize=(18, 9)) - - positions = (241, 242, 243, 244, 245, 246, 247, 248) - directions = ['row']*4 + ['column']*4 - cbar_locations = ['left', 'right', 'top', 'bottom']*2 - - for position, direction, location in zip( - positions, directions, cbar_locations): - grid = ImageGrid(fig, position, - nrows_ncols=(2, 2), - direction=direction, - cbar_location=location, - cbar_size='20%', - cbar_mode='edge') - ax1, ax2, ax3, ax4, = grid - - ax1.imshow(arr.real, cmap='nipy_spectral') - ax2.imshow(arr.imag, cmap='hot') - ax3.imshow(np.abs(arr), cmap='jet') - ax4.imshow(np.arctan2(arr.imag, arr.real), cmap='hsv') - - # In each row/column, the "first" colorbars must be overwritten by the - # "second" ones. To achieve this, clear out the axes first. - for ax in grid: - ax.cax.cla() - cb = ax.cax.colorbar( - ax.images[0], - ticks=mpl.ticker.MaxNLocator(5)) # old default locator. - - -def test_imagegrid(): - fig = plt.figure() - grid = ImageGrid(fig, 111, nrows_ncols=(1, 1)) - ax = grid[0] - im = ax.imshow([[1, 2]], norm=mpl.colors.LogNorm()) - cb = ax.cax.colorbar(im) - assert isinstance(cb.locator, mpl.colorbar._ColorbarLogLocator) diff --git a/lib/mpl_toolkits/tests/test_axisartist_clip_path.py b/lib/mpl_toolkits/tests/test_axisartist_clip_path.py deleted file mode 100644 index a81c12dcf8e5..000000000000 --- a/lib/mpl_toolkits/tests/test_axisartist_clip_path.py +++ /dev/null @@ -1,32 +0,0 @@ -import numpy as np -import matplotlib.pyplot as plt -from matplotlib.testing.decorators import image_comparison -from matplotlib.transforms import Bbox - -from mpl_toolkits.axisartist.clip_path import clip_line_to_rect - - -@image_comparison(['clip_path.png'], style='default') -def test_clip_path(): - x = np.array([-3, -2, -1, 0., 1, 2, 3, 2, 1, 0, -1, -2, -3, 5]) - y = np.arange(len(x)) - - fig, ax = plt.subplots() - ax.plot(x, y, lw=1) - - bbox = Bbox.from_extents(-2, 3, 2, 12.5) - rect = plt.Rectangle(bbox.p0, bbox.width, bbox.height, - facecolor='none', edgecolor='k', ls='--') - ax.add_patch(rect) - - clipped_lines, ticks = clip_line_to_rect(x, y, bbox) - for lx, ly in clipped_lines: - ax.plot(lx, ly, lw=1, color='C1') - for px, py in zip(lx, ly): - assert bbox.contains(px, py) - - ccc = iter(['C3o', 'C2x', 'C3o', 'C2x']) - for ttt in ticks: - cc = next(ccc) - for (xx, yy), aa in ttt: - ax.plot([xx], [yy], cc) diff --git a/lib/mpl_toolkits/tests/test_axisartist_grid_finder.py b/lib/mpl_toolkits/tests/test_axisartist_grid_finder.py deleted file mode 100644 index 3a0913c003f2..000000000000 --- a/lib/mpl_toolkits/tests/test_axisartist_grid_finder.py +++ /dev/null @@ -1,13 +0,0 @@ -from mpl_toolkits.axisartist.grid_finder import ( - FormatterPrettyPrint, - MaxNLocator) - - -def test_pretty_print_format(): - locator = MaxNLocator() - locs, nloc, factor = locator(0, 100) - - fmt = FormatterPrettyPrint() - - assert fmt("left", None, locs) == \ - [r'$\mathdefault{%d}$' % (l, ) for l in locs] diff --git a/setup.cfg.template b/mplsetup.cfg.template similarity index 50% rename from setup.cfg.template rename to mplsetup.cfg.template index 6b40f29fc217..30985b2e313d 100644 --- a/setup.cfg.template +++ b/mplsetup.cfg.template @@ -1,40 +1,35 @@ -# Rename this file to setup.cfg to modify Matplotlib's build options. - -[metadata] -license_files = LICENSE/* - -[egg_info] +# Rename this file to mplsetup.cfg to modify Matplotlib's build options. [libs] # By default, Matplotlib builds with LTO, which may be slow if you re-compile # often, and don't need the space saving/speedup. +# #enable_lto = True +# # By default, Matplotlib downloads and builds its own copies of FreeType and of # Qhull. You may set the following to True to instead link against a system # FreeType/Qhull. As an exception, Matplotlib defaults to the system version # of FreeType on AIX. +# #system_freetype = False #system_qhull = False [packages] -# There are a number of data subpackages from Matplotlib that are -# considered optional. All except 'tests' data (meaning the baseline -# image files) are installed by default, but that can be changed here. -#tests = False - -[gui_support] -# Matplotlib supports multiple GUI toolkits, known as backends. -# The MacOSX backend requires the Cocoa headers included with XCode. -# You can select whether to build it by uncommenting the following line. -# It is never built on Linux or Windows, regardless of the config value. +# Some of Matplotlib's components are optional: the MacOSX backend (installed +# by default on MacOSX; requires the Cocoa headers included with XCode), and +# the test data (i.e., the baseline image files; not installed by default). +# You can control whether they are installed by uncommenting the following +# lines. Note that the MacOSX backend is never built on Linux or Windows, +# regardless of the config value. # +#tests = False #macosx = True [rc_options] # User-configurable options # -# Default backend, one of: Agg, Cairo, GTK3Agg, GTK3Cairo, MacOSX, Pdf, Ps, -# Qt4Agg, Qt5Agg, SVG, TkAgg, WX, WXAgg. +# Default backend, one of: Agg, Cairo, GTK3Agg, GTK3Cairo, GTK4Agg, GTK4Cairo, +# MacOSX, Pdf, Ps, QtAgg, QtCairo, SVG, TkAgg, WX, WXAgg. # # The Agg, Ps, Pdf and SVG backends do not require external dependencies. Do # not choose MacOSX if you have disabled the relevant extension modules. The diff --git a/plot_types/3D/README.rst b/plot_types/3D/README.rst new file mode 100644 index 000000000000..e7157d4ba628 --- /dev/null +++ b/plot_types/3D/README.rst @@ -0,0 +1,6 @@ +.. _3D_plots: + +3D +-- + +3D plots using the `mpl_toolkits.mplot3d` library. diff --git a/plot_types/3D/scatter3d_simple.py b/plot_types/3D/scatter3d_simple.py new file mode 100644 index 000000000000..023a46448ccf --- /dev/null +++ b/plot_types/3D/scatter3d_simple.py @@ -0,0 +1,29 @@ +""" +============== +3D scatterplot +============== + +See `~mpl_toolkits.mplot3d.axes3d.Axes3D.scatter`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# Make data +np.random.seed(19680801) +n = 100 +rng = np.random.default_rng() +xs = rng.uniform(23, 32, n) +ys = rng.uniform(0, 100, n) +zs = rng.uniform(-50, -25, n) + +# Plot +fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) +ax.scatter(xs, ys, zs) + +ax.set(xticklabels=[], + yticklabels=[], + zticklabels=[]) + +plt.show() diff --git a/plot_types/3D/surface3d_simple.py b/plot_types/3D/surface3d_simple.py new file mode 100644 index 000000000000..b1aff7d23b12 --- /dev/null +++ b/plot_types/3D/surface3d_simple.py @@ -0,0 +1,29 @@ +""" +===================== +3D surface +===================== + +See `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_surface`. +""" +import matplotlib.pyplot as plt +from matplotlib import cm +import numpy as np + +plt.style.use('_mpl-gallery') + +# Make data +X = np.arange(-5, 5, 0.25) +Y = np.arange(-5, 5, 0.25) +X, Y = np.meshgrid(X, Y) +R = np.sqrt(X**2 + Y**2) +Z = np.sin(R) + +# Plot the surface +fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) +ax.plot_surface(X, Y, Z, vmin=Z.min() * 2, cmap=cm.Blues) + +ax.set(xticklabels=[], + yticklabels=[], + zticklabels=[]) + +plt.show() diff --git a/plot_types/3D/trisurf3d_simple.py b/plot_types/3D/trisurf3d_simple.py new file mode 100644 index 000000000000..92832c1b5b3a --- /dev/null +++ b/plot_types/3D/trisurf3d_simple.py @@ -0,0 +1,34 @@ +""" +====================== +Triangular 3D surfaces +====================== + +See `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_trisurf`. +""" +import matplotlib.pyplot as plt +from matplotlib import cm +import numpy as np + +plt.style.use('_mpl-gallery') + +n_radii = 8 +n_angles = 36 + +# Make radii and angles spaces +radii = np.linspace(0.125, 1.0, n_radii) +angles = np.linspace(0, 2*np.pi, n_angles, endpoint=False)[..., np.newaxis] + +# Convert polar (radii, angles) coords to cartesian (x, y) coords. +x = np.append(0, (radii*np.cos(angles)).flatten()) +y = np.append(0, (radii*np.sin(angles)).flatten()) +z = np.sin(-x*y) + +# Plot +fig, ax = plt.subplots(subplot_kw={'projection': '3d'}) +ax.plot_trisurf(x, y, z, vmin=z.min() * 2, cmap=cm.Blues) + +ax.set(xticklabels=[], + yticklabels=[], + zticklabels=[]) + +plt.show() diff --git a/plot_types/3D/voxels_simple.py b/plot_types/3D/voxels_simple.py new file mode 100644 index 000000000000..c3473e108969 --- /dev/null +++ b/plot_types/3D/voxels_simple.py @@ -0,0 +1,31 @@ +""" +========================== +3D voxel / volumetric plot +========================== + +See `~mpl_toolkits.mplot3d.axes3d.Axes3D.voxels`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# Prepare some coordinates +x, y, z = np.indices((8, 8, 8)) + +# Draw cuboids in the top left and bottom right corners +cube1 = (x < 3) & (y < 3) & (z < 3) +cube2 = (x >= 5) & (y >= 5) & (z >= 5) + +# Combine the objects into a single boolean array +voxelarray = cube1 | cube2 + +# Plot +fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) +ax.voxels(voxelarray, edgecolor='k') + +ax.set(xticklabels=[], + yticklabels=[], + zticklabels=[]) + +plt.show() diff --git a/plot_types/3D/wire3d_simple.py b/plot_types/3D/wire3d_simple.py new file mode 100644 index 000000000000..c0eaf40210e8 --- /dev/null +++ b/plot_types/3D/wire3d_simple.py @@ -0,0 +1,24 @@ +""" +================= +3D wireframe plot +================= + +See `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_wireframe`. +""" +from mpl_toolkits.mplot3d import axes3d +import matplotlib.pyplot as plt + +plt.style.use('_mpl-gallery') + +# Make data +X, Y, Z = axes3d.get_test_data(0.05) + +# Plot +fig, ax = plt.subplots(subplot_kw={"projection": "3d"}) +ax.plot_wireframe(X, Y, Z, rstride=10, cstride=10) + +ax.set(xticklabels=[], + yticklabels=[], + zticklabels=[]) + +plt.show() diff --git a/plot_types/README.rst b/plot_types/README.rst new file mode 100644 index 000000000000..ef5b08fbedbe --- /dev/null +++ b/plot_types/README.rst @@ -0,0 +1,12 @@ +.. _plot_types: + +.. redirect-from:: /tutorials/basic/sample_plots + +Plot types +========== + +Overview of many common plotting commands in Matplotlib. + +Note that we have stripped all labels, but they are present by default. +See the `gallery <../gallery/index.html>`_ for many more examples and +the `tutorials page <../tutorials/index.html>`_ for longer examples. diff --git a/plot_types/arrays/README.rst b/plot_types/arrays/README.rst new file mode 100644 index 000000000000..43844ee9490c --- /dev/null +++ b/plot_types/arrays/README.rst @@ -0,0 +1,6 @@ +.. _array_plots: + +Plots of arrays and fields +-------------------------- + +Plotting for arrays of data ``Z(x, y)`` and fields ``U(x, y), V(x, y)``. diff --git a/plot_types/arrays/barbs.py b/plot_types/arrays/barbs.py new file mode 100644 index 000000000000..63e492869039 --- /dev/null +++ b/plot_types/arrays/barbs.py @@ -0,0 +1,33 @@ +""" +================= +barbs(X, Y, U, V) +================= + +See `~matplotlib.axes.Axes.barbs`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data: +X, Y = np.meshgrid([1, 2, 3, 4], [1, 2, 3, 4]) +angle = np.pi / 180 * np.array([[15., 30, 35, 45], + [25., 40, 55, 60], + [35., 50, 65, 75], + [45., 60, 75, 90]]) +amplitude = np.array([[5, 10, 25, 50], + [10, 15, 30, 60], + [15, 26, 50, 70], + [20, 45, 80, 100]]) +U = amplitude * np.sin(angle) +V = amplitude * np.cos(angle) + +# plot: +fig, ax = plt.subplots() + +ax.barbs(X, Y, U, V, barbcolor='C0', flagcolor='C0', length=7, linewidth=1.5) + +ax.set(xlim=(0, 4.5), ylim=(0, 4.5)) + +plt.show() diff --git a/plot_types/arrays/contour.py b/plot_types/arrays/contour.py new file mode 100644 index 000000000000..fe79c18d2b58 --- /dev/null +++ b/plot_types/arrays/contour.py @@ -0,0 +1,23 @@ +""" +================ +contour(X, Y, Z) +================ + +See `~matplotlib.axes.Axes.contour`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data +X, Y = np.meshgrid(np.linspace(-3, 3, 256), np.linspace(-3, 3, 256)) +Z = (1 - X/2 + X**5 + Y**3) * np.exp(-X**2 - Y**2) +levels = np.linspace(np.min(Z), np.max(Z), 7) + +# plot +fig, ax = plt.subplots() + +ax.contour(X, Y, Z, levels=levels) + +plt.show() diff --git a/plot_types/arrays/contourf.py b/plot_types/arrays/contourf.py new file mode 100644 index 000000000000..bde2f984fc0f --- /dev/null +++ b/plot_types/arrays/contourf.py @@ -0,0 +1,23 @@ +""" +================= +contourf(X, Y, Z) +================= + +See `~matplotlib.axes.Axes.contourf`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data +X, Y = np.meshgrid(np.linspace(-3, 3, 256), np.linspace(-3, 3, 256)) +Z = (1 - X/2 + X**5 + Y**3) * np.exp(-X**2 - Y**2) +levels = np.linspace(Z.min(), Z.max(), 7) + +# plot +fig, ax = plt.subplots() + +ax.contourf(X, Y, Z, levels=levels) + +plt.show() diff --git a/plot_types/arrays/imshow.py b/plot_types/arrays/imshow.py new file mode 100644 index 000000000000..be647d1f2924 --- /dev/null +++ b/plot_types/arrays/imshow.py @@ -0,0 +1,23 @@ +""" +========= +imshow(Z) +========= + +See `~matplotlib.axes.Axes.imshow`. +""" + +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data +X, Y = np.meshgrid(np.linspace(-3, 3, 16), np.linspace(-3, 3, 16)) +Z = (1 - X/2 + X**5 + Y**3) * np.exp(-X**2 - Y**2) + +# plot +fig, ax = plt.subplots() + +ax.imshow(Z) + +plt.show() diff --git a/plot_types/arrays/pcolormesh.py b/plot_types/arrays/pcolormesh.py new file mode 100644 index 000000000000..b490dcb99d3f --- /dev/null +++ b/plot_types/arrays/pcolormesh.py @@ -0,0 +1,25 @@ +""" +=================== +pcolormesh(X, Y, Z) +=================== + +`~.axes.Axes.pcolormesh` is more flexible than `~.axes.Axes.imshow` in that +the x and y vectors need not be equally spaced (indeed they can be skewed). + +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data with uneven sampling in x +x = [-3, -2, -1.6, -1.2, -.8, -.5, -.2, .1, .3, .5, .8, 1.1, 1.5, 1.9, 2.3, 3] +X, Y = np.meshgrid(x, np.linspace(-3, 3, 128)) +Z = (1 - X/2 + X**5 + Y**3) * np.exp(-X**2 - Y**2) + +# plot +fig, ax = plt.subplots() + +ax.pcolormesh(X, Y, Z, vmin=-0.5, vmax=1.0) + +plt.show() diff --git a/plot_types/arrays/quiver.py b/plot_types/arrays/quiver.py new file mode 100644 index 000000000000..5d6dc808c518 --- /dev/null +++ b/plot_types/arrays/quiver.py @@ -0,0 +1,28 @@ +""" +================== +quiver(X, Y, U, V) +================== + +See `~matplotlib.axes.Axes.quiver`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data +x = np.linspace(-4, 4, 6) +y = np.linspace(-4, 4, 6) +X, Y = np.meshgrid(x, y) +U = X + Y +V = Y - X + +# plot +fig, ax = plt.subplots() + +ax.quiver(X, Y, U, V, color="C0", angles='xy', + scale_units='xy', scale=5, width=.015) + +ax.set(xlim=(-5, 5), ylim=(-5, 5)) + +plt.show() diff --git a/plot_types/arrays/streamplot.py b/plot_types/arrays/streamplot.py new file mode 100644 index 000000000000..3f1e2ef4e1cc --- /dev/null +++ b/plot_types/arrays/streamplot.py @@ -0,0 +1,25 @@ +""" +====================== +streamplot(X, Y, U, V) +====================== + +See `~matplotlib.axes.Axes.streamplot`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make a stream function: +X, Y = np.meshgrid(np.linspace(-3, 3, 256), np.linspace(-3, 3, 256)) +Z = (1 - X/2 + X**5 + Y**3) * np.exp(-X**2 - Y**2) +# make U and V out of the streamfunction: +V = np.diff(Z[1:, :], axis=1) +U = -np.diff(Z[:, 1:], axis=0) + +# plot: +fig, ax = plt.subplots() + +ax.streamplot(X[1:, 1:], Y[1:, 1:], U, V) + +plt.show() diff --git a/plot_types/basic/README.rst b/plot_types/basic/README.rst new file mode 100644 index 000000000000..9ce35d083177 --- /dev/null +++ b/plot_types/basic/README.rst @@ -0,0 +1,6 @@ +.. _basic_plots: + +Basic +----- + +Basic plot types, usually y versus x. diff --git a/plot_types/basic/bar.py b/plot_types/basic/bar.py new file mode 100644 index 000000000000..e23d27baa06c --- /dev/null +++ b/plot_types/basic/bar.py @@ -0,0 +1,25 @@ +""" +============== +bar(x, height) +============== + +See `~matplotlib.axes.Axes.bar`. +""" +import matplotlib.pyplot as plt +import numpy as np +plt.style.use('_mpl-gallery') + +# make data: +np.random.seed(3) +x = 0.5 + np.arange(8) +y = np.random.uniform(2, 7, len(x)) + +# plot +fig, ax = plt.subplots() + +ax.bar(x, y, width=1, edgecolor="white", linewidth=0.7) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/basic/fill_between.py b/plot_types/basic/fill_between.py new file mode 100644 index 000000000000..a454c3c30772 --- /dev/null +++ b/plot_types/basic/fill_between.py @@ -0,0 +1,29 @@ +""" +======================= +fill_between(x, y1, y2) +======================= + +See `~matplotlib.axes.Axes.fill_between`. +""" + +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data +np.random.seed(1) +x = np.linspace(0, 8, 16) +y1 = 3 + 4*x/8 + np.random.uniform(0.0, 0.5, len(x)) +y2 = 1 + 2*x/8 + np.random.uniform(0.0, 0.5, len(x)) + +# plot +fig, ax = plt.subplots() + +ax.fill_between(x, y1, y2, alpha=.5, linewidth=0) +ax.plot(x, (y1 + y2)/2, linewidth=2) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/basic/plot.py b/plot_types/basic/plot.py new file mode 100644 index 000000000000..3808137e52fd --- /dev/null +++ b/plot_types/basic/plot.py @@ -0,0 +1,26 @@ +""" +========== +plot(x, y) +========== + +See `~matplotlib.axes.Axes.plot`. +""" + +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data +x = np.linspace(0, 10, 100) +y = 4 + 2 * np.sin(2 * x) + +# plot +fig, ax = plt.subplots() + +ax.plot(x, y, linewidth=2.0) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/basic/scatter_plot.py b/plot_types/basic/scatter_plot.py new file mode 100644 index 000000000000..792016c0e79c --- /dev/null +++ b/plot_types/basic/scatter_plot.py @@ -0,0 +1,29 @@ +""" +============= +scatter(x, y) +============= + +See `~matplotlib.axes.Axes.scatter`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make the data +np.random.seed(3) +x = 4 + np.random.normal(0, 2, 24) +y = 4 + np.random.normal(0, 2, len(x)) +# size and color: +sizes = np.random.uniform(15, 80, len(x)) +colors = np.random.uniform(15, 80, len(x)) + +# plot +fig, ax = plt.subplots() + +ax.scatter(x, y, s=sizes, c=colors, vmin=0, vmax=100) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/basic/stackplot.py b/plot_types/basic/stackplot.py new file mode 100644 index 000000000000..5b23b52775b3 --- /dev/null +++ b/plot_types/basic/stackplot.py @@ -0,0 +1,27 @@ +""" +=============== +stackplot(x, y) +=============== +See `~matplotlib.axes.Axes.stackplot` +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data +x = np.arange(0, 10, 2) +ay = [1, 1.25, 2, 2.75, 3] +by = [1, 1, 1, 1, 1] +cy = [2, 1, 2, 1, 2] +y = np.vstack([ay, by, cy]) + +# plot +fig, ax = plt.subplots() + +ax.stackplot(x, y) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/basic/stem.py b/plot_types/basic/stem.py new file mode 100644 index 000000000000..8e7b29283c01 --- /dev/null +++ b/plot_types/basic/stem.py @@ -0,0 +1,26 @@ +""" +========== +stem(x, y) +========== + +See `~matplotlib.axes.Axes.stem`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data +np.random.seed(3) +x = 0.5 + np.arange(8) +y = np.random.uniform(2, 7, len(x)) + +# plot +fig, ax = plt.subplots() + +ax.stem(x, y) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/basic/step.py b/plot_types/basic/step.py new file mode 100644 index 000000000000..558550a5c498 --- /dev/null +++ b/plot_types/basic/step.py @@ -0,0 +1,26 @@ +""" +========== +step(x, y) +========== + +See `~matplotlib.axes.Axes.step`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data +np.random.seed(3) +x = 0.5 + np.arange(8) +y = np.random.uniform(2, 7, len(x)) + +# plot +fig, ax = plt.subplots() + +ax.step(x, y, linewidth=2.5) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/stats/README.rst b/plot_types/stats/README.rst new file mode 100644 index 000000000000..b2ca855aafbc --- /dev/null +++ b/plot_types/stats/README.rst @@ -0,0 +1,6 @@ +.. _stats_plots: + +Statistics plots +---------------- + +Plots for statistical analysis. diff --git a/plot_types/stats/boxplot_plot.py b/plot_types/stats/boxplot_plot.py new file mode 100644 index 000000000000..cdad3c52320f --- /dev/null +++ b/plot_types/stats/boxplot_plot.py @@ -0,0 +1,30 @@ +""" +========== +boxplot(X) +========== + +See `~matplotlib.axes.Axes.boxplot`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data: +np.random.seed(10) +D = np.random.normal((3, 5, 4), (1.25, 1.00, 1.25), (100, 3)) + +# plot +fig, ax = plt.subplots() +VP = ax.boxplot(D, positions=[2, 4, 6], widths=1.5, patch_artist=True, + showmeans=False, showfliers=False, + medianprops={"color": "white", "linewidth": 0.5}, + boxprops={"facecolor": "C0", "edgecolor": "white", + "linewidth": 0.5}, + whiskerprops={"color": "C0", "linewidth": 1.5}, + capprops={"color": "C0", "linewidth": 1.5}) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/stats/errorbar_plot.py b/plot_types/stats/errorbar_plot.py new file mode 100644 index 000000000000..0e226e11b315 --- /dev/null +++ b/plot_types/stats/errorbar_plot.py @@ -0,0 +1,27 @@ +""" +========================== +errorbar(x, y, yerr, xerr) +========================== + +See `~matplotlib.axes.Axes.errorbar`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data: +np.random.seed(1) +x = [2, 4, 6] +y = [3.6, 5, 4.2] +yerr = [0.9, 1.2, 0.5] + +# plot: +fig, ax = plt.subplots() + +ax.errorbar(x, y, yerr, fmt='o', linewidth=2, capsize=6) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/stats/eventplot.py b/plot_types/stats/eventplot.py new file mode 100644 index 000000000000..da8c33c28425 --- /dev/null +++ b/plot_types/stats/eventplot.py @@ -0,0 +1,26 @@ +""" +============ +eventplot(D) +============ + +See `~matplotlib.axes.Axes.eventplot`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data: +np.random.seed(1) +x = [2, 4, 6] +D = np.random.gamma(4, size=(3, 50)) + +# plot: +fig, ax = plt.subplots() + +ax.eventplot(D, orientation="vertical", lineoffsets=x, linewidth=0.75) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/stats/hexbin.py b/plot_types/stats/hexbin.py new file mode 100644 index 000000000000..91e771308afd --- /dev/null +++ b/plot_types/stats/hexbin.py @@ -0,0 +1,25 @@ +""" +=============== +hexbin(x, y, C) +=============== + +See `~matplotlib.axes.Axes.hexbin`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data: correlated + noise +np.random.seed(1) +x = np.random.randn(5000) +y = 1.2 * x + np.random.randn(5000) / 3 + +# plot: +fig, ax = plt.subplots() + +ax.hexbin(x, y, gridsize=20) + +ax.set(xlim=(-2, 2), ylim=(-3, 3)) + +plt.show() diff --git a/plot_types/stats/hist2d.py b/plot_types/stats/hist2d.py new file mode 100644 index 000000000000..3e43f7ee8ace --- /dev/null +++ b/plot_types/stats/hist2d.py @@ -0,0 +1,25 @@ +""" +============ +hist2d(x, y) +============ + +See `~matplotlib.axes.Axes.hist2d`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data: correlated + noise +np.random.seed(1) +x = np.random.randn(5000) +y = 1.2 * x + np.random.randn(5000) / 3 + +# plot: +fig, ax = plt.subplots() + +ax.hist2d(x, y, bins=(np.arange(-3, 3, 0.1), np.arange(-3, 3, 0.1))) + +ax.set(xlim=(-2, 2), ylim=(-3, 3)) + +plt.show() diff --git a/plot_types/stats/hist_plot.py b/plot_types/stats/hist_plot.py new file mode 100644 index 000000000000..6c86a0aca216 --- /dev/null +++ b/plot_types/stats/hist_plot.py @@ -0,0 +1,25 @@ +""" +======= +hist(x) +======= + +See `~matplotlib.axes.Axes.hist`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data +np.random.seed(1) +x = 4 + np.random.normal(0, 1.5, 200) + +# plot: +fig, ax = plt.subplots() + +ax.hist(x, bins=8, linewidth=0.5, edgecolor="white") + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 56), yticks=np.linspace(0, 56, 9)) + +plt.show() diff --git a/plot_types/stats/pie.py b/plot_types/stats/pie.py new file mode 100644 index 000000000000..80484a0eb932 --- /dev/null +++ b/plot_types/stats/pie.py @@ -0,0 +1,26 @@ +""" +====== +pie(x) +====== + +See `~matplotlib.axes.Axes.pie`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + + +# make data +x = [1, 2, 3, 4] +colors = plt.get_cmap('Blues')(np.linspace(0.2, 0.7, len(x))) + +# plot +fig, ax = plt.subplots() +ax.pie(x, colors=colors, radius=3, center=(4, 4), + wedgeprops={"linewidth": 1, "edgecolor": "white"}, frame=True) + +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/stats/violin.py b/plot_types/stats/violin.py new file mode 100644 index 000000000000..c8a987a690dd --- /dev/null +++ b/plot_types/stats/violin.py @@ -0,0 +1,28 @@ +""" +============= +violinplot(D) +============= + +See `~matplotlib.axes.Axes.violinplot`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery') + +# make data: +np.random.seed(10) +D = np.random.normal((3, 5, 4), (0.75, 1.00, 0.75), (200, 3)) + +# plot: +fig, ax = plt.subplots() + +vp = ax.violinplot(D, [2, 4, 6], widths=2, + showmeans=False, showmedians=False, showextrema=False) +# styling: +for body in vp['bodies']: + body.set_alpha(0.9) +ax.set(xlim=(0, 8), xticks=np.arange(1, 8), + ylim=(0, 8), yticks=np.arange(1, 8)) + +plt.show() diff --git a/plot_types/unstructured/README.rst b/plot_types/unstructured/README.rst new file mode 100644 index 000000000000..89b7844775d2 --- /dev/null +++ b/plot_types/unstructured/README.rst @@ -0,0 +1,9 @@ +.. _unstructured_plots: + +Unstructured coordinates +------------------------- + +Sometimes we collect data ``z`` at coordinates ``(x,y)`` and want to visualize +as a contour. Instead of gridding the data and then using +`~.axes.Axes.contour`, we can use a triangulation algorithm and fill the +triangles. diff --git a/plot_types/unstructured/tricontour.py b/plot_types/unstructured/tricontour.py new file mode 100644 index 000000000000..83b0a212fd83 --- /dev/null +++ b/plot_types/unstructured/tricontour.py @@ -0,0 +1,28 @@ +""" +=================== +tricontour(x, y, z) +=================== + +See `~matplotlib.axes.Axes.tricontour`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data: +np.random.seed(1) +x = np.random.uniform(-3, 3, 256) +y = np.random.uniform(-3, 3, 256) +z = (1 - x/2 + x**5 + y**3) * np.exp(-x**2 - y**2) +levels = np.linspace(z.min(), z.max(), 7) + +# plot: +fig, ax = plt.subplots() + +ax.plot(x, y, 'o', markersize=2, color='lightgrey') +ax.tricontour(x, y, z, levels=levels) + +ax.set(xlim=(-3, 3), ylim=(-3, 3)) + +plt.show() diff --git a/plot_types/unstructured/tricontourf.py b/plot_types/unstructured/tricontourf.py new file mode 100644 index 000000000000..da909c02f0b2 --- /dev/null +++ b/plot_types/unstructured/tricontourf.py @@ -0,0 +1,28 @@ +""" +==================== +tricontourf(x, y, z) +==================== + +See `~matplotlib.axes.Axes.tricontourf`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data: +np.random.seed(1) +x = np.random.uniform(-3, 3, 256) +y = np.random.uniform(-3, 3, 256) +z = (1 - x/2 + x**5 + y**3) * np.exp(-x**2 - y**2) +levels = np.linspace(z.min(), z.max(), 7) + +# plot: +fig, ax = plt.subplots() + +ax.plot(x, y, 'o', markersize=2, color='grey') +ax.tricontourf(x, y, z, levels=levels) + +ax.set(xlim=(-3, 3), ylim=(-3, 3)) + +plt.show() diff --git a/plot_types/unstructured/tripcolor.py b/plot_types/unstructured/tripcolor.py new file mode 100644 index 000000000000..e2619a68444e --- /dev/null +++ b/plot_types/unstructured/tripcolor.py @@ -0,0 +1,27 @@ +""" +================== +tripcolor(x, y, z) +================== + +See `~matplotlib.axes.Axes.tripcolor`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data: +np.random.seed(1) +x = np.random.uniform(-3, 3, 256) +y = np.random.uniform(-3, 3, 256) +z = (1 - x/2 + x**5 + y**3) * np.exp(-x**2 - y**2) + +# plot: +fig, ax = plt.subplots() + +ax.plot(x, y, 'o', markersize=2, color='grey') +ax.tripcolor(x, y, z) + +ax.set(xlim=(-3, 3), ylim=(-3, 3)) + +plt.show() diff --git a/plot_types/unstructured/triplot.py b/plot_types/unstructured/triplot.py new file mode 100644 index 000000000000..78cf8e32a318 --- /dev/null +++ b/plot_types/unstructured/triplot.py @@ -0,0 +1,26 @@ +""" +============= +triplot(x, y) +============= + +See `~matplotlib.axes.Axes.triplot`. +""" +import matplotlib.pyplot as plt +import numpy as np + +plt.style.use('_mpl-gallery-nogrid') + +# make data: +np.random.seed(1) +x = np.random.uniform(-3, 3, 256) +y = np.random.uniform(-3, 3, 256) +z = (1 - x/2 + x**5 + y**3) * np.exp(-x**2 - y**2) + +# plot: +fig, ax = plt.subplots() + +ax.triplot(x, y) + +ax.set(xlim=(-3, 3), ylim=(-3, 3)) + +plt.show() diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000000..907b05a39ba4 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +build-backend = "setuptools.build_meta" +requires = [ + "certifi>=2020.06.20", + "oldest-supported-numpy", + "pybind11>=2.6", + "setuptools_scm>=7", +] diff --git a/pytest.ini b/pytest.ini index 905d3a5e5a52..f4a8057e0fcc 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,4 +1,9 @@ -# Additional configuration is in matplotlib/testing/conftest.py. +# Because tests can be run from an installed copy, most of our Pytest +# configuration is in the `pytest_configure` function in +# `lib/matplotlib/testing/conftest.py`. This configuration file exists only to +# set a minimum pytest version and to prevent pytest from wasting time trying +# to check examples and documentation files that are not really tests. + [pytest] minversion = 3.6 diff --git a/requirements/doc/doc-requirements.txt b/requirements/doc/doc-requirements.txt index 193c888b2f95..392cdcd8b638 100644 --- a/requirements/doc/doc-requirements.txt +++ b/requirements/doc/doc-requirements.txt @@ -7,12 +7,15 @@ # Install the documentation requirements with: # pip install -r requirements/doc/doc-requirements.txt # -sphinx>=1.8.1,!=2.0.0 +sphinx>=3.0.0,!=6.1.2 colorspacious ipython ipywidgets -numpydoc>=0.8 +numpydoc>=1.0 +packaging>=20 +pydata-sphinx-theme~=0.12.0 +mpl-sphinx-theme~=3.7.0 sphinxcontrib-svg2pdfconverter>=1.1.0 -sphinx-gallery>=0.7 +sphinx-gallery>=0.10 sphinx-copybutton -scipy +sphinx-design diff --git a/requirements/testing/all.txt b/requirements/testing/all.txt index fb7beef90f16..299cb0817dcb 100644 --- a/requirements/testing/all.txt +++ b/requirements/testing/all.txt @@ -1,11 +1,12 @@ # pip requirements for all the CI builds certifi -coverage +coverage!=6.3 +psutil pytest!=4.6.0,!=5.4.0 pytest-cov pytest-rerunfailures pytest-timeout pytest-xdist -python-dateutil +pytest-xvfb tornado diff --git a/requirements/testing/extra.txt b/requirements/testing/extra.txt index 611dce4cdfd8..8d314a141218 100644 --- a/requirements/testing/extra.txt +++ b/requirements/testing/extra.txt @@ -1,8 +1,11 @@ -# Extra pip requirements for the Python 3.7+ builds +# Extra pip requirements for the Python 3.8+ builds +--prefer-binary ipykernel nbconvert[execute]!=6.0.0,!=6.0.1 nbformat!=5.0.0,!=5.0.1 pandas!=0.25.0 pikepdf pytz +pywin32; sys.platform == 'win32' +xarray diff --git a/requirements/testing/flake8.txt b/requirements/testing/flake8.txt index f98708973072..a4d006b8551e 100644 --- a/requirements/testing/flake8.txt +++ b/requirements/testing/flake8.txt @@ -5,3 +5,5 @@ flake8>=3.8 pydocstyle>=5.1.0 # 1.4.0 adds docstring-convention=all flake8-docstrings>=1.4.0 +# fix bug where flake8 aborts checking on syntax error +flake8-force diff --git a/requirements/testing/minver.txt b/requirements/testing/minver.txt index 6ef24615bde2..2eb2f958af96 100644 --- a/requirements/testing/minver.txt +++ b/requirements/testing/minver.txt @@ -1,8 +1,12 @@ # Extra pip requirements for the minimum-version CI run +contourpy==1.0.1 cycler==0.10 kiwisolver==1.0.1 -numpy==1.16.0 -pillow==6.2.0 -pyparsing==2.2.1 +importlib-resources==3.2.0 +numpy==1.20.0 +packaging==20.0 +pillow==6.2.1 +pyparsing==2.3.1 python-dateutil==2.7 +fonttools==4.22.0 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000000..9d4cf0e7b72c --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +# NOTE: Matplotlib-specific configuration options have been moved to +# mplsetup.cfg.template. + +[metadata] +license_files = LICENSE/* diff --git a/setup.py b/setup.py index b51ae806c2fb..490850ad6203 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ """ -The matplotlib build options can be modified with a setup.cfg file. See -setup.cfg.template for more information. +The Matplotlib build options can be modified with a mplsetup.cfg file. See +mplsetup.cfg.template for more information. """ # NOTE: This file must remain Python 2 compatible for the foreseeable future, @@ -8,8 +8,8 @@ # and/or pip. import sys -py_min_version = (3, 7) # minimal supported python version -since_mpl_version = (3, 4) # py_min_version is required since this mpl version +py_min_version = (3, 8) # minimal supported python version +since_mpl_version = (3, 6) # py_min_version is required since this mpl version if sys.version_info < py_min_version: error = """ @@ -29,30 +29,17 @@ import shutil import subprocess -from setuptools import setup, find_packages, Extension -from setuptools.command.build_ext import build_ext as BuildExtCommand -from setuptools.command.test import test as TestCommand +from setuptools import setup, find_packages, Distribution, Extension +import setuptools.command.build_ext +import setuptools.command.build_py +import setuptools.command.sdist -# The setuptools version of sdist adds a setup.cfg file to the tree. -# We don't want that, so we simply remove it, and it will fall back to -# vanilla distutils. -try: - from setuptools.command import sdist -except ImportError: - pass -else: - del sdist.sdist.make_release_tree - -from distutils.errors import CompileError -from distutils.dist import Distribution +# sys.path modified to find setupext.py during pyproject.toml builds. +sys.path.append(str(Path(__file__).resolve().parent)) import setupext from setupext import print_raw, print_status -# Get the version from versioneer -import versioneer -__version__ = versioneer.get_version() - # These are the packages in the order we want to display them. mpl_packages = [ @@ -74,19 +61,22 @@ def has_flag(self, flagname): f.write('int main (int argc, char **argv) { return 0; }') try: self.compile([f.name], extra_postargs=[flagname]) - except CompileError: + except Exception as exc: + # https://github.com/pypa/setuptools/issues/2698 + if type(exc).__name__ != "CompileError": + raise return False return True -class NoopTestCommand(TestCommand): - def __init__(self, dist): - print("Matplotlib does not support running tests with " - "'python setup.py test'. Please run 'pytest'.") - - -class BuildExtraLibraries(BuildExtCommand): +class BuildExtraLibraries(setuptools.command.build_ext.build_ext): def finalize_options(self): + # If coverage is enabled then need to keep the .o and .gcno files in a + # non-temporary directory otherwise coverage info not collected. + cppflags = os.getenv('CPPFLAGS') + if cppflags and '--coverage' in cppflags: + self.build_temp = 'build' + self.distribution.ext_modules[:] = [ ext for package in good_packages @@ -152,7 +142,7 @@ def prepare_flags(name, enable_lto): stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True) - except Exception as e: + except Exception: pass else: version = result.stdout.lower() @@ -179,12 +169,6 @@ def prepare_flags(name, enable_lto): return env def build_extensions(self): - # Remove the -Wstrict-prototypes option, it's not valid for C++. Fixed - # in Py3.7 as bpo-5755. - try: - self.compiler.compiler_so.remove('-Wstrict-prototypes') - except (ValueError, AttributeError): - pass if (self.compiler.compiler_type == 'msvc' and os.environ.get('MPL_DISABLE_FH4')): # Disable FH4 Exception Handling implementation so that we don't @@ -199,10 +183,50 @@ def build_extensions(self): package.do_custom_build(env) return super().build_extensions() + def build_extension(self, ext): + # When C coverage is enabled, the path to the object file is saved. + # Since we re-use source files in multiple extensions, libgcov will + # complain at runtime that it is trying to save coverage for the same + # object file at different timestamps (since each source is compiled + # again for each extension). Thus, we need to use unique temporary + # build directories to store object files for each extension. + orig_build_temp = self.build_temp + self.build_temp = os.path.join(self.build_temp, ext.name) + try: + super().build_extension(ext) + finally: + self.build_temp = orig_build_temp + + +def update_matplotlibrc(path): + # If packagers want to change the default backend, insert a `#backend: ...` + # line. Otherwise, use the default `##backend: Agg` which has no effect + # even after decommenting, which allows _auto_backend_sentinel to be filled + # in at import time. + template_lines = path.read_text(encoding="utf-8").splitlines(True) + backend_line_idx, = [ # Also asserts that there is a single such line. + idx for idx, line in enumerate(template_lines) + if "#backend:" in line] + template_lines[backend_line_idx] = ( + "#backend: {}\n".format(setupext.options["backend"]) + if setupext.options["backend"] + else "##backend: Agg\n") + path.write_text("".join(template_lines), encoding="utf-8") + + +class BuildPy(setuptools.command.build_py.build_py): + def run(self): + super().run() + if not getattr(self, 'editable_mode', False): + update_matplotlibrc( + Path(self.build_lib, "matplotlib/mpl-data/matplotlibrc")) + -cmdclass = versioneer.get_cmdclass() -cmdclass['test'] = NoopTestCommand -cmdclass['build_ext'] = BuildExtraLibraries +class Sdist(setuptools.command.sdist.sdist): + def make_release_tree(self, base_dir, files): + super().make_release_tree(base_dir, files) + update_matplotlibrc( + Path(base_dir, "lib/matplotlib/mpl-data/matplotlibrc")) package_data = {} # Will be filled below by the various components. @@ -215,7 +239,7 @@ def build_extensions(self): # Go through all of the packages and figure out which ones we are # going to build/install. print_raw() - print_raw("Edit setup.cfg to change the build options; " + print_raw("Edit mplsetup.cfg to change the build options; " "suppress output with --quiet.") print_raw() print_raw("BUILDING MATPLOTLIB") @@ -243,26 +267,13 @@ def build_extensions(self): package_data.setdefault(key, []) package_data[key] = list(set(val + package_data[key])) - # Write the default matplotlibrc file - with open('matplotlibrc.template') as fd: - template_lines = fd.read().splitlines(True) - backend_line_idx, = [ # Also asserts that there is a single such line. - idx for idx, line in enumerate(template_lines) - if line.startswith('#backend:')] - if setupext.options['backend']: - template_lines[backend_line_idx] = ( - 'backend: {}'.format(setupext.options['backend'])) - with open('lib/matplotlib/mpl-data/matplotlibrc', 'w') as fd: - fd.write(''.join(template_lines)) - -setup( # Finally, pass this all along to distutils to do the heavy lifting. +setup( # Finally, pass this all along to setuptools to do the heavy lifting. name="matplotlib", - version=__version__, description="Python plotting package", author="John D. Hunter, Michael Droettboom", author_email="matplotlib-users@python.org", url="https://matplotlib.org", - download_url="https://matplotlib.org/users/installing.html", + download_url="https://matplotlib.org/stable/users/installing/index.html", project_urls={ 'Documentation': 'https://matplotlib.org', 'Source Code': 'https://github.com/matplotlib/matplotlib', @@ -270,8 +281,8 @@ def build_extensions(self): 'Forum': 'https://discourse.matplotlib.org/', 'Donate': 'https://numfocus.org/donate-to-matplotlib' }, - long_description=Path("README.rst").read_text(encoding="utf-8"), - long_description_content_type="text/x-rst", + long_description=Path("README.md").read_text(encoding="utf-8"), + long_description_content_type="text/markdown", license="PSF", platforms="any", classifiers=[ @@ -282,9 +293,10 @@ def build_extensions(self): 'License :: OSI Approved :: Python Software Foundation License', 'Programming Language :: Python', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Topic :: Scientific/Engineering :: Visualization', ], @@ -298,18 +310,45 @@ def build_extensions(self): package_data=package_data, python_requires='>={}'.format('.'.join(str(n) for n in py_min_version)), - setup_requires=[ - "certifi>=2020.06.20", - "numpy>=1.16", - ], + # When updating the list of dependencies, add an api_changes/development + # entry and also update the following places: + # - lib/matplotlib/__init__.py (matplotlib._check_versions()) + # - requirements/testing/minver.txt + # - doc/devel/dependencies.rst + # - .github/workflows/tests.yml + # - environment.yml install_requires=[ + "contourpy>=1.0.1", "cycler>=0.10", + "fonttools>=4.22.0", "kiwisolver>=1.0.1", - "numpy>=1.16", + "numpy>=1.20", + "packaging>=20.0", "pillow>=6.2.0", - "pyparsing>=2.2.1", + "pyparsing>=2.3.1", "python-dateutil>=2.7", - ], - - cmdclass=cmdclass, + ] + ( + # Installing from a git checkout that is not producing a wheel. + ["setuptools_scm>=7"] if ( + Path(__file__).with_name(".git").exists() and + os.environ.get("CIBUILDWHEEL", "0") != "1" + ) else [] + ), + extras_require={ + ':python_version<"3.10"': [ + "importlib-resources>=3.2.0", + ], + }, + use_scm_version={ + "version_scheme": "release-branch-semver", + "local_scheme": "node-and-date", + "write_to": "lib/matplotlib/_version.py", + "parentdir_prefix_version": "matplotlib-", + "fallback_version": "0.0+UNKNOWN", + }, + cmdclass={ + "build_ext": BuildExtraLibraries, + "build_py": BuildPy, + "sdist": Sdist, + }, ) diff --git a/setupext.py b/setupext.py index d8d0b6b39369..a898d642d631 100644 --- a/setupext.py +++ b/setupext.py @@ -1,6 +1,4 @@ import configparser -from distutils import ccompiler, sysconfig -from distutils.core import Extension import functools import hashlib from io import BytesIO @@ -12,19 +10,23 @@ import shutil import subprocess import sys +import sysconfig import tarfile +from tempfile import TemporaryDirectory import textwrap import urllib.request -import versioneer + +from pybind11.setup_helpers import Pybind11Extension +from setuptools import Distribution, Extension _log = logging.getLogger(__name__) def _get_xdg_cache_dir(): """ - Return the XDG cache directory. + Return the `XDG cache directory`__. - See https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + __ https://specifications.freedesktop.org/basedir-spec/latest/ """ cache_dir = os.environ.get('XDG_CACHE_HOME') if not cache_dir: @@ -117,6 +119,13 @@ def get_and_extract_tarball(urls, sha, dirname): """ toplevel = Path("build", dirname) if not toplevel.exists(): # Download it or load it from cache. + try: + import certifi # noqa + except ImportError as e: + raise ImportError( + f"`certifi` is unavailable ({e}) so unable to download any of " + f"the following: {urls}.") from None + Path("build").mkdir(exist_ok=True) for url in urls: try: @@ -167,28 +176,42 @@ def get_and_extract_tarball(urls, sha, dirname): '955e17244e9b38adb0c98df66abb50467312e6bb70eac07e49ce6bd1a20e809a', '2.10.1': '3a60d391fd579440561bf0e7f31af2222bc610ad6ce4d9d7bd2165bca8669110', + '2.11.1': + 'f8db94d307e9c54961b39a1cc799a67d46681480696ed72ecf78d4473770f09b' } -# This is the version of FreeType to use when building a local -# version. It must match the value in -# lib/matplotlib.__init__.py and also needs to be changed below in the -# embedded windows build script (grep for "REMINDER" in this file) -LOCAL_FREETYPE_VERSION = '2.6.1' +# This is the version of FreeType to use when building a local version. It +# must match the value in lib/matplotlib.__init__.py, and the cache path in +# `.circleci/config.yml`. +TESTING_VERSION_OF_FREETYPE = '2.6.1' +if sys.platform.startswith('win') and platform.machine() == 'ARM64': + # older versions of freetype are not supported for win/arm64 + # Matplotlib tests will not pass + LOCAL_FREETYPE_VERSION = '2.11.1' +else: + LOCAL_FREETYPE_VERSION = TESTING_VERSION_OF_FREETYPE + LOCAL_FREETYPE_HASH = _freetype_hashes.get(LOCAL_FREETYPE_VERSION, 'unknown') +# Also update the cache path in `.circleci/config.yml`. LOCAL_QHULL_VERSION = '2020.2' +LOCAL_QHULL_HASH = ( + 'b5c2d7eb833278881b952c8a52d20179eab87766b00b865000469a45c1838b7e') -# matplotlib build options, which can be altered using setup.cfg -setup_cfg = os.environ.get('MPLSETUPCFG') or 'setup.cfg' +# Matplotlib build options, which can be altered using mplsetup.cfg +mplsetup_cfg = os.environ.get('MPLSETUPCFG') or 'mplsetup.cfg' config = configparser.ConfigParser() -if os.path.exists(setup_cfg): - config.read(setup_cfg) +if os.path.exists(mplsetup_cfg): + config.read(mplsetup_cfg) options = { 'backend': config.get('rc_options', 'backend', fallback=None), 'system_freetype': config.getboolean( - 'libs', 'system_freetype', fallback=sys.platform.startswith('aix')), - 'system_qhull': config.getboolean('libs', 'system_qhull', - fallback=False), + 'libs', 'system_freetype', + fallback=sys.platform.startswith(('aix', 'os400')) + ), + 'system_qhull': config.getboolean( + 'libs', 'system_qhull', fallback=sys.platform.startswith('os400') + ), } @@ -201,7 +224,7 @@ def print_raw(*args, **kwargs): pass # Suppress our own output. def print_status(package, status): initial_indent = "%12s: " % package indent = ' ' * 18 - print_raw(textwrap.fill(str(status), width=80, + print_raw(textwrap.fill(status, width=80, initial_indent=initial_indent, subsequent_indent=indent)) @@ -319,16 +342,15 @@ def do_custom_build(self, env): class OptionalPackage(SetupPackage): - config_category = "packages" default_config = True def check(self): """ - Check whether ``setup.cfg`` requests this package to be installed. + Check whether ``mplsetup.cfg`` requests this package to be installed. May be overridden by subclasses for additional checks. """ - if config.getboolean(self.config_category, self.name, + if config.getboolean("packages", self.name, fallback=self.default_config): return "installing" else: # Configuration opt-out by user @@ -358,9 +380,6 @@ def _pkg_data_helper(pkg, subdir): class Matplotlib(SetupPackage): name = "matplotlib" - def check(self): - return versioneer.get_version() - def get_package_data(self): return { 'matplotlib': [ @@ -375,7 +394,6 @@ def get_extensions(self): # agg ext = Extension( "matplotlib.backends._backend_agg", [ - "src/mplutils.cpp", "src/py_converters.cpp", "src/_backend_agg.cpp", "src/_backend_agg_wrapper.cpp", @@ -392,22 +410,11 @@ def get_extensions(self): "win32": ["ole32", "shell32", "user32"], }.get(sys.platform, []))) yield ext - # contour - ext = Extension( - "matplotlib._contour", [ - "src/_contour.cpp", - "src/_contour_wrapper.cpp", - "src/py_converters.cpp", - ]) - add_numpy_flags(ext) - add_libagg_flags(ext) - yield ext # ft2font ext = Extension( "matplotlib.ft2font", [ "src/ft2font.cpp", "src/ft2font_wrapper.cpp", - "src/mplutils.cpp", "src/py_converters.cpp", ]) FreeType.add_flags(ext) @@ -417,8 +424,6 @@ def get_extensions(self): # image ext = Extension( "matplotlib._image", [ - "src/_image.cpp", - "src/mplutils.cpp", "src/_image_wrapper.cpp", "src/py_converters.cpp", ]) @@ -436,7 +441,7 @@ def get_extensions(self): yield ext # qhull ext = Extension( - "matplotlib._qhull", ["src/qhull_wrap.c"], + "matplotlib._qhull", ["src/_qhull_wrapper.cpp"], define_macros=[("MPL_DEVNULL", os.devnull)]) add_numpy_flags(ext) Qhull.add_flags(ext) @@ -448,20 +453,19 @@ def get_extensions(self): ], include_dirs=["src"], # psapi library needed for finding Tcl/Tk at run time. - libraries=({"linux": ["dl"], "win32": ["psapi"], - "cygwin": ["psapi"]}.get(sys.platform, [])), + libraries={"linux": ["dl"], "win32": ["comctl32", "psapi"], + "cygwin": ["comctl32", "psapi"]}.get(sys.platform, []), extra_link_args={"win32": ["-mwindows"]}.get(sys.platform, [])) add_numpy_flags(ext) add_libagg_flags(ext) yield ext # tri - ext = Extension( + ext = Pybind11Extension( "matplotlib._tri", [ "src/tri/_tri.cpp", "src/tri/_tri_wrapper.cpp", - "src/mplutils.cpp", - ]) - add_numpy_flags(ext) + ], + cxx_std=11) yield ext # ttconv ext = Extension( @@ -486,10 +490,17 @@ def get_package_data(self): *_pkg_data_helper('matplotlib', 'tests/baseline_images'), *_pkg_data_helper('matplotlib', 'tests/tinypages'), 'tests/cmr10.pfb', + 'tests/Courier10PitchBT-Bold.pfb', 'tests/mpltest.ttf', + 'tests/test_*.ipynb', ], 'mpl_toolkits': [ - *_pkg_data_helper('mpl_toolkits', 'tests/baseline_images'), + *_pkg_data_helper('mpl_toolkits', + 'axes_grid1/tests/baseline_images'), + *_pkg_data_helper('mpl_toolkits', + 'axisartist/tests/baseline_images'), + *_pkg_data_helper('mpl_toolkits', + 'mplot3d/tests/baseline_images'), ] } @@ -532,9 +543,27 @@ def add_libagg_flags_and_sources(ext): os.path.join("extern", "agg24-svn", "src", x) for x in agg_sources) -# First compile checkdep_freetype2.c, which aborts the compilation either -# with "foo.h: No such file or directory" if the header is not found, or an -# appropriate error message if the header indicates a too-old version. +def get_ccompiler(): + """ + Return a new CCompiler instance. + + CCompiler used to be constructible via `distutils.ccompiler.new_compiler`, + but this API was removed as part of the distutils deprecation. Instead, + we trick setuptools into instantiating it by creating a dummy Distribution + with a list of extension modules that claims to be truthy, but is actually + empty, and then running the Distribution's build_ext command. (If using + a plain empty ext_modules, build_ext would early-return without doing + anything.) + """ + + class L(list): + def __bool__(self): + return True + + build_ext = Distribution({"ext_modules": L()}).get_command_obj("build_ext") + build_ext.finalize_options() + build_ext.run() + return build_ext.compiler class FreeType(SetupPackage): @@ -542,6 +571,9 @@ class FreeType(SetupPackage): @classmethod def add_flags(cls, ext): + # checkdep_freetype2.c immediately aborts the compilation either with + # "foo.h: No such file or directory" if the header is not found, or an + # appropriate error message if the header indicates a too-old version. ext.sources.insert(0, 'src/checkdep_freetype2.c') if options.get('system_freetype'): pkg_config_setup_extension( @@ -577,7 +609,9 @@ def do_custom_build(self, env): (f'https://downloads.sourceforge.net/project/freetype' f'/freetype2/{LOCAL_FREETYPE_VERSION}/{tarball}'), (f'https://download.savannah.gnu.org/releases/freetype' - f'/{tarball}') + f'/{tarball}'), + (f'https://download.savannah.gnu.org/releases/freetype' + f'/freetype-old/{tarball}') ], sha=LOCAL_FREETYPE_HASH, dirname=f'freetype-{LOCAL_FREETYPE_VERSION}', @@ -592,12 +626,32 @@ def do_custom_build(self, env): print(f"Building freetype in {src_path}") if sys.platform != 'win32': # compilation on non-windows - env = {**env, "CFLAGS": "{} -fPIC".format(env.get("CFLAGS", ""))} - subprocess.check_call( - ["./configure", "--with-zlib=no", "--with-bzip2=no", - "--with-png=no", "--with-harfbuzz=no", "--enable-static", - "--disable-shared"], - env=env, cwd=src_path) + env = { + **{ + var: value + for var, value in sysconfig.get_config_vars().items() + if var in {"CC", "CFLAGS", "CXX", "CXXFLAGS", "LD", + "LDFLAGS"} + }, + **env, + } + configure_ac = Path(src_path, "builds/unix/configure.ac") + if ((src_path / "autogen.sh").exists() + and not configure_ac.exists()): + print(f"{configure_ac} does not exist. " + f"Using sh autogen.sh to generate.") + subprocess.check_call( + ["sh", "./autogen.sh"], env=env, cwd=src_path) + env["CFLAGS"] = env.get("CFLAGS", "") + " -fPIC" + configure = [ + "./configure", "--with-zlib=no", "--with-bzip2=no", + "--with-png=no", "--with-harfbuzz=no", "--enable-static", + "--disable-shared" + ] + host = sysconfig.get_config_var('HOST_GNU_TYPE') + if host is not None: # May be unset on PyPy. + configure.append(f"--host={host}") + subprocess.check_call(configure, env=env, cwd=src_path) if 'GNUMAKE' in env: make = env['GNUMAKE'] elif 'MAKE' in env: @@ -615,22 +669,31 @@ def do_custom_build(self, env): subprocess.check_call([make], env=env, cwd=src_path) else: # compilation on windows shutil.rmtree(src_path / "objs", ignore_errors=True) - msbuild_platform = ( - 'x64' if platform.architecture()[0] == '64bit' else 'Win32') - base_path = Path("build/freetype-2.6.1/builds/windows") - vc = 'vc2010' - sln_path = ( - base_path / vc / "freetype.sln" + is_x64 = platform.architecture()[0] == '64bit' + if platform.machine() == 'ARM64': + msbuild_platform = 'ARM64' + elif is_x64: + msbuild_platform = 'x64' + else: + msbuild_platform = 'Win32' + base_path = Path( + f"build/freetype-{LOCAL_FREETYPE_VERSION}/builds/windows" ) + vc = 'vc2010' + sln_path = base_path / vc / "freetype.sln" # https://developercommunity.visualstudio.com/comments/190992/view.html - (sln_path.parent / "Directory.Build.props").write_text(""" - - - - $([Microsoft.Build.Utilities.ToolLocationHelper]::GetLatestSDKTargetPlatformVersion('Windows', '10.0')) - - -""") + (sln_path.parent / "Directory.Build.props").write_text( + "" + "" + "" + # WindowsTargetPlatformVersion must be given on a single line. + "$(" + "[Microsoft.Build.Utilities.ToolLocationHelper]" + "::GetLatestSDKTargetPlatformVersion('Windows', '10.0')" + ")" + "" + "", + encoding="utf-8") # It is not a trivial task to determine PlatformToolset to plug it # into msbuild command, and Directory.Build.props will not override # the value in the project file. @@ -645,16 +708,47 @@ def do_custom_build(self, env): f.truncate() f.write(vcxproj) - cc = ccompiler.new_compiler() - cc.initialize() # Get msbuild in the %PATH% of cc.spawn. - cc.spawn(["msbuild", str(sln_path), + cc = get_ccompiler() + cc.initialize() + # On setuptools versions that use "local" distutils, + # ``cc.spawn(["msbuild", ...])`` no longer manages to locate the + # right executable, even though they are correctly on the PATH, + # because only the env kwarg to Popen() is updated, and not + # os.environ["PATH"]. Instead, use shutil.which to walk the PATH + # and get absolute executable paths. + with TemporaryDirectory() as tmpdir: + dest = Path(tmpdir, "path") + cc.spawn([ + sys.executable, "-c", + "import pathlib, shutil, sys\n" + "dest = pathlib.Path(sys.argv[1])\n" + "dest.write_text(shutil.which('msbuild'))\n", + str(dest), + ]) + msbuild_path = dest.read_text() + # Freetype 2.10.0+ support static builds. + msbuild_config = ( + "Release Static" + if [*map(int, LOCAL_FREETYPE_VERSION.split("."))] >= [2, 10] + else "Release" + ) + + cc.spawn([msbuild_path, str(sln_path), "/t:Clean;Build", - f"/p:Configuration=Release;Platform={msbuild_platform}"]) + f"/p:Configuration={msbuild_config};" + f"Platform={msbuild_platform}"]) # Move to the corresponding Unix build path. (src_path / "objs" / ".libs").mkdir() # Be robust against change of FreeType version. - lib_path, = (src_path / "objs" / vc / msbuild_platform).glob( - "freetype*.lib") + lib_paths = Path(src_path / "objs").rglob('freetype*.lib') + # Select FreeType library for required platform + lib_path, = [ + p for p in lib_paths + if msbuild_platform in p.resolve().as_uri() + ] + print( + f"Copying {lib_path} to {src_path}/objs/.libs/libfreetype.lib" + ) shutil.copy2(lib_path, src_path / "objs/.libs/libfreetype.lib") @@ -675,7 +769,7 @@ def do_custom_build(self, env): toplevel = get_and_extract_tarball( urls=["http://www.qhull.org/download/qhull-2020-src-8.0.2.tgz"], - sha="b5c2d7eb833278881b952c8a52d20179eab87766b00b865000469a45c1838b7e", + sha=LOCAL_QHULL_HASH, dirname=f"qhull-{LOCAL_QHULL_VERSION}", ) shutil.copyfile(toplevel / "COPYING.txt", "LICENSE/LICENSE_QHULL") @@ -683,13 +777,13 @@ def do_custom_build(self, env): for ext in self._extensions_to_update: qhull_path = Path(f'build/qhull-{LOCAL_QHULL_VERSION}/src') ext.include_dirs.insert(0, str(qhull_path)) - ext.sources.extend(map(str, sorted(qhull_path.glob('libqhull_r/*.c')))) + ext.sources.extend( + map(str, sorted(qhull_path.glob('libqhull_r/*.c')))) if sysconfig.get_config_var("LIBM") == "-lm": ext.libraries.extend("m") class BackendMacOSX(OptionalPackage): - config_category = 'gui_support' name = 'macosx' def check(self): @@ -698,11 +792,11 @@ def check(self): return super().check() def get_extensions(self): - sources = [ - 'src/_macosx.m' - ] - ext = Extension('matplotlib.backends._macosx', sources) - ext.extra_compile_args.extend(['-Werror=unguarded-availability']) + ext = Extension( + 'matplotlib.backends._macosx', [ + 'src/_macosx.m' + ]) + ext.extra_compile_args.extend(['-Werror']) ext.extra_link_args.extend(['-framework', 'Cocoa']) if platform.python_implementation().lower() == 'pypy': ext.extra_compile_args.append('-DPYPY=1') diff --git a/src/_backend_agg.cpp b/src/_backend_agg.cpp index 0a9e7ab7e160..335e40971948 100644 --- a/src/_backend_agg.cpp +++ b/src/_backend_agg.cpp @@ -11,7 +11,7 @@ void BufferRegion::to_string_argb(uint8_t *buf) unsigned char tmp; size_t i, j; - memcpy(buf, data, height * stride); + memcpy(buf, data, (size_t) height * stride); for (i = 0; i < (size_t)height; ++i) { pix = buf + i * stride; @@ -45,7 +45,7 @@ RendererAgg::RendererAgg(unsigned int width, unsigned int height, double dpi) rendererBase(), rendererAA(), rendererBin(), - theRasterizer(8192), + theRasterizer(32768), lastclippath(NULL), _fill_color(agg::rgba(1, 1, 1, 0)) { @@ -152,10 +152,10 @@ bool RendererAgg::render_clippath(py::PathIterator &clippath, rendererBaseAlphaMask.clear(agg::gray8(0, 0)); transformed_path_t transformed_clippath(clippath, trans); - nan_removed_t nan_removed_clippath(transformed_clippath, true, clippath.has_curves()); + nan_removed_t nan_removed_clippath(transformed_clippath, true, clippath.has_codes()); snapped_t snapped_clippath(nan_removed_clippath, snap_mode, clippath.total_vertices(), 0.0); simplify_t simplified_clippath(snapped_clippath, - clippath.should_simplify() && !clippath.has_curves(), + clippath.should_simplify() && !clippath.has_codes(), clippath.simplify_threshold()); curve_t curved_clippath(simplified_clippath); theRasterizer.add_path(curved_clippath); diff --git a/src/_backend_agg.h b/src/_backend_agg.h index 8436f6840f24..31a58db9f8f1 100644 --- a/src/_backend_agg.h +++ b/src/_backend_agg.h @@ -43,7 +43,7 @@ /**********************************************************************/ -// a helper class to pass agg::buffer objects around. +// a helper class to pass agg::buffer objects around. class BufferRegion { @@ -176,8 +176,7 @@ class RendererAgg ColorArray &edgecolors, LineWidthArray &linewidths, DashesVector &linestyles, - AntialiasedArray &antialiaseds, - e_offset_position offset_position); + AntialiasedArray &antialiaseds); template void draw_quad_mesh(GCAgg &gc, @@ -277,9 +276,8 @@ class RendererAgg LineWidthArray &linewidths, DashesVector &linestyles, AntialiasedArray &antialiaseds, - e_offset_position offset_position, bool check_snap, - bool has_curves); + bool has_codes); template void _draw_gouraud_triangle(PointArray &points, @@ -472,7 +470,7 @@ RendererAgg::draw_path(GCAgg &gc, PathIterator &path, agg::trans_affine &trans, trans *= agg::trans_affine_scaling(1.0, -1.0); trans *= agg::trans_affine_translation(0.0, (double)height); - bool clip = !face.first && !gc.has_hatchpath() && !path.has_curves(); + bool clip = !face.first && !gc.has_hatchpath(); bool simplify = path.should_simplify() && clip; double snapping_linewidth = points_to_pixels(gc.linewidth); if (gc.color.a == 0.0) { @@ -480,7 +478,7 @@ RendererAgg::draw_path(GCAgg &gc, PathIterator &path, agg::trans_affine &trans, } transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, path.has_curves()); + nan_removed_t nan_removed(tpath, true, path.has_codes()); clipped_t clipped(nan_removed, clip, width, height); snapped_t snapped(clipped, gc.snap_mode, path.total_vertices(), snapping_linewidth); simplify_t simplified(snapped, simplify, path.simplify_threshold()); @@ -514,7 +512,7 @@ inline void RendererAgg::draw_markers(GCAgg &gc, trans *= agg::trans_affine_translation(0.5, (double)height + 0.5); transformed_path_t marker_path_transformed(marker_path, marker_trans); - nan_removed_t marker_path_nan_removed(marker_path_transformed, true, marker_path.has_curves()); + nan_removed_t marker_path_nan_removed(marker_path_transformed, true, marker_path.has_codes()); snap_t marker_path_snapped(marker_path_nan_removed, gc.snap_mode, marker_path.total_vertices(), @@ -911,9 +909,8 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, LineWidthArray &linewidths, DashesVector &linestyles, AntialiasedArray &antialiaseds, - e_offset_position offset_position, bool check_snap, - bool has_curves) + bool has_codes) { typedef agg::conv_transform transformed_path_t; typedef PathNanRemover nan_removed_t; @@ -969,11 +966,7 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, double xo = offsets(i % Noffsets, 0); double yo = offsets(i % Noffsets, 1); offset_trans.transform(&xo, &yo); - if (offset_position == OFFSET_POSITION_DATA) { - trans = agg::trans_affine_translation(xo, yo) * trans; - } else { - trans *= agg::trans_affine_translation(xo, yo); - } + trans *= agg::trans_affine_translation(xo, yo); } // These transformations must be done post-offsets @@ -999,17 +992,17 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, } } - bool do_clip = !face.first && !gc.has_hatchpath() && !has_curves; + bool do_clip = !face.first && !gc.has_hatchpath(); if (check_snap) { gc.isaa = antialiaseds(i % Naa); transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, has_curves); + nan_removed_t nan_removed(tpath, true, has_codes); clipped_t clipped(nan_removed, do_clip, width, height); snapped_t snapped( clipped, gc.snap_mode, path.total_vertices(), points_to_pixels(gc.linewidth)); - if (has_curves) { + if (has_codes) { snapped_curve_t curve(snapped); _draw_path(curve, has_clippath, face, gc); } else { @@ -1019,9 +1012,9 @@ inline void RendererAgg::_draw_path_collection_generic(GCAgg &gc, gc.isaa = antialiaseds(i % Naa); transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, has_curves); + nan_removed_t nan_removed(tpath, true, has_codes); clipped_t clipped(nan_removed, do_clip, width, height); - if (has_curves) { + if (has_codes) { curve_t curve(clipped); _draw_path(curve, has_clippath, face, gc); } else { @@ -1047,8 +1040,7 @@ inline void RendererAgg::draw_path_collection(GCAgg &gc, ColorArray &edgecolors, LineWidthArray &linewidths, DashesVector &linestyles, - AntialiasedArray &antialiaseds, - e_offset_position offset_position) + AntialiasedArray &antialiaseds) { _draw_path_collection_generic(gc, master_transform, @@ -1064,7 +1056,6 @@ inline void RendererAgg::draw_path_collection(GCAgg &gc, linewidths, linestyles, antialiaseds, - offset_position, true, true); } @@ -1133,7 +1124,7 @@ class QuadMeshGenerator inline size_t num_paths() const { - return m_meshWidth * m_meshHeight; + return (size_t) m_meshWidth * m_meshHeight; } inline path_iterator operator()(size_t i) const @@ -1175,7 +1166,6 @@ inline void RendererAgg::draw_quad_mesh(GCAgg &gc, linewidths, linestyles, antialiaseds, - OFFSET_POSITION_FIGURE, true, // check_snap false); } diff --git a/src/_backend_agg_basic_types.h b/src/_backend_agg_basic_types.h index 9f65253d9f50..3ee86312ef2b 100644 --- a/src/_backend_agg_basic_types.h +++ b/src/_backend_agg_basic_types.h @@ -69,11 +69,6 @@ class Dashes typedef std::vector DashesVector; -enum e_offset_position { - OFFSET_POSITION_FIGURE, - OFFSET_POSITION_DATA -}; - class GCAgg { public: diff --git a/src/_backend_agg_wrapper.cpp b/src/_backend_agg_wrapper.cpp index 49a271b96b6a..15053b08fb70 100644 --- a/src/_backend_agg_wrapper.cpp +++ b/src/_backend_agg_wrapper.cpp @@ -12,6 +12,8 @@ typedef struct Py_ssize_t suboffsets[3]; } PyRendererAgg; +static PyTypeObject PyRendererAggType; + typedef struct { PyObject_HEAD @@ -21,6 +23,8 @@ typedef struct Py_ssize_t suboffsets[3]; } PyBufferRegion; +static PyTypeObject PyBufferRegionType; + /********************************************************************** * BufferRegion @@ -40,15 +44,21 @@ static void PyBufferRegion_dealloc(PyBufferRegion *self) Py_TYPE(self)->tp_free((PyObject *)self); } -static PyObject *PyBufferRegion_to_string(PyBufferRegion *self, PyObject *args, PyObject *kwds) +static PyObject *PyBufferRegion_to_string(PyBufferRegion *self, PyObject *args) { + char const* msg = + "BufferRegion.to_string is deprecated since Matplotlib 3.7 and will " + "be removed two minor releases later; use np.asarray(region) instead."; + if (PyErr_WarnEx(PyExc_DeprecationWarning, msg, 1)) { + return NULL; + } return PyBytes_FromStringAndSize((const char *)self->x->get_data(), - self->x->get_height() * self->x->get_stride()); + (Py_ssize_t) self->x->get_height() * self->x->get_stride()); } /* TODO: This doesn't seem to be used internally. Remove? */ -static PyObject *PyBufferRegion_set_x(PyBufferRegion *self, PyObject *args, PyObject *kwds) +static PyObject *PyBufferRegion_set_x(PyBufferRegion *self, PyObject *args) { int x; if (!PyArg_ParseTuple(args, "i:set_x", &x)) { @@ -59,7 +69,7 @@ static PyObject *PyBufferRegion_set_x(PyBufferRegion *self, PyObject *args, PyOb Py_RETURN_NONE; } -static PyObject *PyBufferRegion_set_y(PyBufferRegion *self, PyObject *args, PyObject *kwds) +static PyObject *PyBufferRegion_set_y(PyBufferRegion *self, PyObject *args) { int y; if (!PyArg_ParseTuple(args, "i:set_y", &y)) { @@ -70,19 +80,28 @@ static PyObject *PyBufferRegion_set_y(PyBufferRegion *self, PyObject *args, PyOb Py_RETURN_NONE; } -static PyObject *PyBufferRegion_get_extents(PyBufferRegion *self, PyObject *args, PyObject *kwds) +static PyObject *PyBufferRegion_get_extents(PyBufferRegion *self, PyObject *args) { agg::rect_i rect = self->x->get_rect(); return Py_BuildValue("IIII", rect.x1, rect.y1, rect.x2, rect.y2); } -static PyObject *PyBufferRegion_to_string_argb(PyBufferRegion *self, PyObject *args, PyObject *kwds) +static PyObject *PyBufferRegion_to_string_argb(PyBufferRegion *self, PyObject *args) { + char const* msg = + "BufferRegion.to_string_argb is deprecated since Matplotlib 3.7 and " + "will be removed two minor releases later; use " + "np.take(region, [2, 1, 0, 3], axis=2) instead."; + if (PyErr_WarnEx(PyExc_DeprecationWarning, msg, 1)) { + return NULL; + } PyObject *bufobj; uint8_t *buf; - - bufobj = PyBytes_FromStringAndSize(NULL, self->x->get_height() * self->x->get_stride()); + Py_ssize_t height, stride; + height = self->x->get_height(); + stride = self->x->get_stride(); + bufobj = PyBytes_FromStringAndSize(NULL, height * stride); buf = (uint8_t *)PyBytes_AS_STRING(bufobj); CALL_CPP_CLEANUP("to_string_argb", (self->x->to_string_argb(buf)), Py_DECREF(bufobj)); @@ -114,9 +133,7 @@ int PyBufferRegion_get_buffer(PyBufferRegion *self, Py_buffer *buf, int flags) return 1; } -static PyTypeObject PyBufferRegionType; - -static PyTypeObject *PyBufferRegion_init_type(PyObject *m, PyTypeObject *type) +static PyTypeObject *PyBufferRegion_init_type() { static PyMethodDef methods[] = { { "to_string", (PyCFunction)PyBufferRegion_to_string, METH_NOARGS, NULL }, @@ -128,26 +145,17 @@ static PyTypeObject *PyBufferRegion_init_type(PyObject *m, PyTypeObject *type) }; static PyBufferProcs buffer_procs; - memset(&buffer_procs, 0, sizeof(PyBufferProcs)); buffer_procs.bf_getbuffer = (getbufferproc)PyBufferRegion_get_buffer; - memset(type, 0, sizeof(PyTypeObject)); - type->tp_name = "matplotlib.backends._backend_agg.BufferRegion"; - type->tp_basicsize = sizeof(PyBufferRegion); - type->tp_dealloc = (destructor)PyBufferRegion_dealloc; - type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - type->tp_methods = methods; - type->tp_new = PyBufferRegion_new; - type->tp_as_buffer = &buffer_procs; - - if (PyType_Ready(type) < 0) { - return NULL; - } - - /* Don't need to add to module, since you can't create buffer - regions directly from Python */ + PyBufferRegionType.tp_name = "matplotlib.backends._backend_agg.BufferRegion"; + PyBufferRegionType.tp_basicsize = sizeof(PyBufferRegion); + PyBufferRegionType.tp_dealloc = (destructor)PyBufferRegion_dealloc; + PyBufferRegionType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + PyBufferRegionType.tp_methods = methods; + PyBufferRegionType.tp_new = PyBufferRegion_new; + PyBufferRegionType.tp_as_buffer = &buffer_procs; - return type; + return &PyBufferRegionType; } /********************************************************************** @@ -198,7 +206,7 @@ static void PyRendererAgg_dealloc(PyRendererAgg *self) Py_TYPE(self)->tp_free((PyObject *)self); } -static PyObject *PyRendererAgg_draw_path(PyRendererAgg *self, PyObject *args, PyObject *kwds) +static PyObject *PyRendererAgg_draw_path(PyRendererAgg *self, PyObject *args) { GCAgg gc; py::PathIterator path; @@ -227,7 +235,7 @@ static PyObject *PyRendererAgg_draw_path(PyRendererAgg *self, PyObject *args, Py Py_RETURN_NONE; } -static PyObject *PyRendererAgg_draw_text_image(PyRendererAgg *self, PyObject *args, PyObject *kwds) +static PyObject *PyRendererAgg_draw_text_image(PyRendererAgg *self, PyObject *args) { numpy::array_view image; double x; @@ -252,7 +260,7 @@ static PyObject *PyRendererAgg_draw_text_image(PyRendererAgg *self, PyObject *ar Py_RETURN_NONE; } -PyObject *PyRendererAgg_draw_markers(PyRendererAgg *self, PyObject *args, PyObject *kwds) +PyObject *PyRendererAgg_draw_markers(PyRendererAgg *self, PyObject *args) { GCAgg gc; py::PathIterator marker_path; @@ -288,7 +296,7 @@ PyObject *PyRendererAgg_draw_markers(PyRendererAgg *self, PyObject *args, PyObje Py_RETURN_NONE; } -static PyObject *PyRendererAgg_draw_image(PyRendererAgg *self, PyObject *args, PyObject *kwds) +static PyObject *PyRendererAgg_draw_image(PyRendererAgg *self, PyObject *args) { GCAgg gc; double x; @@ -316,11 +324,11 @@ static PyObject *PyRendererAgg_draw_image(PyRendererAgg *self, PyObject *args, P } static PyObject * -PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args, PyObject *kwds) +PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args) { GCAgg gc; agg::trans_affine master_transform; - PyObject *pathobj; + py::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; agg::trans_affine offset_trans; @@ -330,15 +338,16 @@ PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args, PyObject DashesVector dashes; numpy::array_view antialiaseds; PyObject *ignored; - e_offset_position offset_position; + PyObject *offset_position; // offset position is no longer used if (!PyArg_ParseTuple(args, - "O&O&OO&O&O&O&O&O&O&O&OO&:draw_path_collection", + "O&O&O&O&O&O&O&O&O&O&O&OO:draw_path_collection", &convert_gcagg, &gc, &convert_trans_affine, &master_transform, - &pathobj, + &convert_pathgen, + &paths, &convert_transforms, &transforms, &convert_points, @@ -356,38 +365,27 @@ PyRendererAgg_draw_path_collection(PyRendererAgg *self, PyObject *args, PyObject &antialiaseds.converter, &antialiaseds, &ignored, - &convert_offset_position, &offset_position)) { return NULL; } - try - { - py::PathGenerator path(pathobj); - - CALL_CPP("draw_path_collection", - (self->x->draw_path_collection(gc, - master_transform, - path, - transforms, - offsets, - offset_trans, - facecolors, - edgecolors, - linewidths, - dashes, - antialiaseds, - offset_position))); - } - catch (const py::exception &) - { - return NULL; - } + CALL_CPP("draw_path_collection", + (self->x->draw_path_collection(gc, + master_transform, + paths, + transforms, + offsets, + offset_trans, + facecolors, + edgecolors, + linewidths, + dashes, + antialiaseds))); Py_RETURN_NONE; } -static PyObject *PyRendererAgg_draw_quad_mesh(PyRendererAgg *self, PyObject *args, PyObject *kwds) +static PyObject *PyRendererAgg_draw_quad_mesh(PyRendererAgg *self, PyObject *args) { GCAgg gc; agg::trans_affine master_transform; @@ -439,7 +437,7 @@ static PyObject *PyRendererAgg_draw_quad_mesh(PyRendererAgg *self, PyObject *arg } static PyObject * -PyRendererAgg_draw_gouraud_triangle(PyRendererAgg *self, PyObject *args, PyObject *kwds) +PyRendererAgg_draw_gouraud_triangle(PyRendererAgg *self, PyObject *args) { GCAgg gc; numpy::array_view points; @@ -480,7 +478,7 @@ PyRendererAgg_draw_gouraud_triangle(PyRendererAgg *self, PyObject *args, PyObjec } static PyObject * -PyRendererAgg_draw_gouraud_triangles(PyRendererAgg *self, PyObject *args, PyObject *kwds) +PyRendererAgg_draw_gouraud_triangles(PyRendererAgg *self, PyObject *args) { GCAgg gc; numpy::array_view points; @@ -550,14 +548,14 @@ int PyRendererAgg_get_buffer(PyRendererAgg *self, Py_buffer *buf, int flags) return 1; } -static PyObject *PyRendererAgg_clear(PyRendererAgg *self, PyObject *args, PyObject *kwds) +static PyObject *PyRendererAgg_clear(PyRendererAgg *self, PyObject *args) { CALL_CPP("clear", self->x->clear()); Py_RETURN_NONE; } -static PyObject *PyRendererAgg_copy_from_bbox(PyRendererAgg *self, PyObject *args, PyObject *kwds) +static PyObject *PyRendererAgg_copy_from_bbox(PyRendererAgg *self, PyObject *args) { agg::rect_d bbox; BufferRegion *reg; @@ -575,7 +573,7 @@ static PyObject *PyRendererAgg_copy_from_bbox(PyRendererAgg *self, PyObject *arg return regobj; } -static PyObject *PyRendererAgg_restore_region(PyRendererAgg *self, PyObject *args, PyObject *kwds) +static PyObject *PyRendererAgg_restore_region(PyRendererAgg *self, PyObject *args) { PyBufferRegion *regobj; int xx1 = 0, yy1 = 0, xx2 = 0, yy2 = 0, x = 0, y = 0; @@ -602,9 +600,7 @@ static PyObject *PyRendererAgg_restore_region(PyRendererAgg *self, PyObject *arg Py_RETURN_NONE; } -PyTypeObject PyRendererAggType; - -static PyTypeObject *PyRendererAgg_init_type(PyObject *m, PyTypeObject *type) +static PyTypeObject *PyRendererAgg_init_type() { static PyMethodDef methods[] = { {"draw_path", (PyCFunction)PyRendererAgg_draw_path, METH_VARARGS, NULL}, @@ -624,64 +620,36 @@ static PyTypeObject *PyRendererAgg_init_type(PyObject *m, PyTypeObject *type) }; static PyBufferProcs buffer_procs; - memset(&buffer_procs, 0, sizeof(PyBufferProcs)); buffer_procs.bf_getbuffer = (getbufferproc)PyRendererAgg_get_buffer; - memset(type, 0, sizeof(PyTypeObject)); - type->tp_name = "matplotlib.backends._backend_agg.RendererAgg"; - type->tp_basicsize = sizeof(PyRendererAgg); - type->tp_dealloc = (destructor)PyRendererAgg_dealloc; - type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - type->tp_methods = methods; - type->tp_init = (initproc)PyRendererAgg_init; - type->tp_new = PyRendererAgg_new; - type->tp_as_buffer = &buffer_procs; - - if (PyType_Ready(type) < 0) { - return NULL; - } - - if (PyModule_AddObject(m, "RendererAgg", (PyObject *)type)) { - return NULL; - } + PyRendererAggType.tp_name = "matplotlib.backends._backend_agg.RendererAgg"; + PyRendererAggType.tp_basicsize = sizeof(PyRendererAgg); + PyRendererAggType.tp_dealloc = (destructor)PyRendererAgg_dealloc; + PyRendererAggType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + PyRendererAggType.tp_methods = methods; + PyRendererAggType.tp_init = (initproc)PyRendererAgg_init; + PyRendererAggType.tp_new = PyRendererAgg_new; + PyRendererAggType.tp_as_buffer = &buffer_procs; - return type; + return &PyRendererAggType; } -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_backend_agg", - NULL, - 0, - NULL, - NULL, - NULL, - NULL, - NULL -}; +static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "_backend_agg" }; #pragma GCC visibility push(default) PyMODINIT_FUNC PyInit__backend_agg(void) { - PyObject *m; - - m = PyModule_Create(&moduledef); - - if (m == NULL) { - return NULL; - } - import_array(); - - if (!PyRendererAgg_init_type(m, &PyRendererAggType)) { - return NULL; - } - - if (!PyBufferRegion_init_type(m, &PyBufferRegionType)) { + PyObject *m; + if (!(m = PyModule_Create(&moduledef)) + || prepare_and_add_type(PyRendererAgg_init_type(), m) + // BufferRegion is not constructible from Python, thus not added to the module. + || PyType_Ready(PyBufferRegion_init_type()) + ) { + Py_XDECREF(m); return NULL; } - return m; } diff --git a/src/_c_internal_utils.c b/src/_c_internal_utils.c index 4a61fe5b6ee7..f340f0397203 100644 --- a/src/_c_internal_utils.c +++ b/src/_c_internal_utils.c @@ -68,7 +68,13 @@ mpl_GetCurrentProcessExplicitAppUserModelID(PyObject* module) wchar_t* appid = NULL; HRESULT hr = GetCurrentProcessExplicitAppUserModelID(&appid); if (FAILED(hr)) { +#if defined(PYPY_VERSION_NUM) && PYPY_VERSION_NUM < 0x07030600 + /* Remove when we require PyPy 7.3.6 */ + PyErr_SetFromWindowsErr(hr); + return NULL; +#else return PyErr_SetFromWindowsErr(hr); +#endif } PyObject* py_appid = PyUnicode_FromWideChar(appid, -1); CoTaskMemFree(appid); @@ -89,7 +95,13 @@ mpl_SetCurrentProcessExplicitAppUserModelID(PyObject* module, PyObject* arg) HRESULT hr = SetCurrentProcessExplicitAppUserModelID(appid); PyMem_Free(appid); if (FAILED(hr)) { +#if defined(PYPY_VERSION_NUM) && PYPY_VERSION_NUM < 0x07030600 + /* Remove when we require PyPy 7.3.6 */ + PyErr_SetFromWindowsErr(hr); + return NULL; +#else return PyErr_SetFromWindowsErr(hr); +#endif } Py_RETURN_NONE; #else @@ -124,36 +136,85 @@ mpl_SetForegroundWindow(PyObject* module, PyObject *arg) #endif } +static PyObject* +mpl_SetProcessDpiAwareness_max(PyObject* module) +{ +#ifdef _WIN32 +#ifdef _DPI_AWARENESS_CONTEXTS_ + // These functions and options were added in later Windows 10 updates, so + // must be loaded dynamically. + typedef BOOL (WINAPI *IsValidDpiAwarenessContext_t)(DPI_AWARENESS_CONTEXT); + typedef BOOL (WINAPI *SetProcessDpiAwarenessContext_t)(DPI_AWARENESS_CONTEXT); + + HMODULE user32 = LoadLibrary("user32.dll"); + IsValidDpiAwarenessContext_t IsValidDpiAwarenessContextPtr = + (IsValidDpiAwarenessContext_t)GetProcAddress( + user32, "IsValidDpiAwarenessContext"); + SetProcessDpiAwarenessContext_t SetProcessDpiAwarenessContextPtr = + (SetProcessDpiAwarenessContext_t)GetProcAddress( + user32, "SetProcessDpiAwarenessContext"); + DPI_AWARENESS_CONTEXT ctxs[3] = { + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, // Win10 Creators Update + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE, // Win10 + DPI_AWARENESS_CONTEXT_SYSTEM_AWARE}; // Win10 + if (IsValidDpiAwarenessContextPtr != NULL + && SetProcessDpiAwarenessContextPtr != NULL) { + for (int i = 0; i < sizeof(ctxs) / sizeof(DPI_AWARENESS_CONTEXT); ++i) { + if (IsValidDpiAwarenessContextPtr(ctxs[i])) { + SetProcessDpiAwarenessContextPtr(ctxs[i]); + break; + } + } + } else { + // Added in Windows Vista. + SetProcessDPIAware(); + } + FreeLibrary(user32); +#else + // Added in Windows Vista. + SetProcessDPIAware(); +#endif +#endif + Py_RETURN_NONE; +} + static PyMethodDef functions[] = { {"display_is_valid", (PyCFunction)mpl_display_is_valid, METH_NOARGS, "display_is_valid()\n--\n\n" "Check whether the current X11 or Wayland display is valid.\n\n" "On Linux, returns True if either $DISPLAY is set and XOpenDisplay(NULL)\n" "succeeds, or $WAYLAND_DISPLAY is set and wl_display_connect(NULL)\n" - "succeeds. On other platforms, always returns True."}, + "succeeds.\n\n" + "On other platforms, always returns True."}, {"Win32_GetCurrentProcessExplicitAppUserModelID", (PyCFunction)mpl_GetCurrentProcessExplicitAppUserModelID, METH_NOARGS, "Win32_GetCurrentProcessExplicitAppUserModelID()\n--\n\n" - "Wrapper for Windows's GetCurrentProcessExplicitAppUserModelID. On \n" - "non-Windows platforms, always returns None."}, + "Wrapper for Windows's GetCurrentProcessExplicitAppUserModelID.\n\n" + "On non-Windows platforms, always returns None."}, {"Win32_SetCurrentProcessExplicitAppUserModelID", (PyCFunction)mpl_SetCurrentProcessExplicitAppUserModelID, METH_O, "Win32_SetCurrentProcessExplicitAppUserModelID(appid, /)\n--\n\n" - "Wrapper for Windows's SetCurrentProcessExplicitAppUserModelID. On \n" - "non-Windows platforms, a no-op."}, + "Wrapper for Windows's SetCurrentProcessExplicitAppUserModelID.\n\n" + "On non-Windows platforms, does nothing."}, {"Win32_GetForegroundWindow", (PyCFunction)mpl_GetForegroundWindow, METH_NOARGS, "Win32_GetForegroundWindow()\n--\n\n" - "Wrapper for Windows' GetForegroundWindow. On non-Windows platforms, \n" - "always returns None."}, + "Wrapper for Windows' GetForegroundWindow.\n\n" + "On non-Windows platforms, always returns None."}, {"Win32_SetForegroundWindow", (PyCFunction)mpl_SetForegroundWindow, METH_O, "Win32_SetForegroundWindow(hwnd, /)\n--\n\n" - "Wrapper for Windows' SetForegroundWindow. On non-Windows platforms, \n" - "a no-op."}, + "Wrapper for Windows' SetForegroundWindow.\n\n" + "On non-Windows platforms, does nothing."}, + {"Win32_SetProcessDpiAwareness_max", + (PyCFunction)mpl_SetProcessDpiAwareness_max, METH_NOARGS, + "Win32_SetProcessDpiAwareness_max()\n--\n\n" + "Set Windows' process DPI awareness to best option available.\n\n" + "On non-Windows platforms, does nothing."}, {NULL, NULL}}; // sentinel. static PyModuleDef util_module = { - PyModuleDef_HEAD_INIT, "_c_internal_utils", "", 0, functions, NULL, NULL, NULL, NULL}; + PyModuleDef_HEAD_INIT, "_c_internal_utils", NULL, 0, functions +}; #pragma GCC visibility push(default) PyMODINIT_FUNC PyInit__c_internal_utils(void) diff --git a/src/_contour.cpp b/src/_contour.cpp deleted file mode 100644 index ac655d73de27..000000000000 --- a/src/_contour.cpp +++ /dev/null @@ -1,1784 +0,0 @@ -// This file contains liberal use of asserts to assist code development and -// debugging. Standard matplotlib builds disable asserts so they cause no -// performance reduction. To enable the asserts, you need to undefine the -// NDEBUG macro, which is achieved by adding the following -// undef_macros=['NDEBUG'] -// to the appropriate make_extension call in setupext.py, and then rebuilding. -#define NO_IMPORT_ARRAY - -#include "mplutils.h" -#include "_contour.h" -#include - - -// 'kind' codes. -#define MOVETO 1 -#define LINETO 2 -#define CLOSEPOLY 79 - -// Point indices from current quad index. -#define POINT_SW (quad) -#define POINT_SE (quad+1) -#define POINT_NW (quad+_nx) -#define POINT_NE (quad+_nx+1) - -// CacheItem masks, only accessed directly to set. To read, use accessors -// detailed below. 1 and 2 refer to level indices (lower and upper). -#define MASK_Z_LEVEL 0x0003 // Combines the following two. -#define MASK_Z_LEVEL_1 0x0001 // z > lower_level. -#define MASK_Z_LEVEL_2 0x0002 // z > upper_level. -#define MASK_VISITED_1 0x0004 // Algorithm has visited this quad. -#define MASK_VISITED_2 0x0008 -#define MASK_SADDLE_1 0x0010 // quad is a saddle quad. -#define MASK_SADDLE_2 0x0020 -#define MASK_SADDLE_LEFT_1 0x0040 // Contours turn left at saddle quad. -#define MASK_SADDLE_LEFT_2 0x0080 -#define MASK_SADDLE_START_SW_1 0x0100 // Next visit starts on S or W edge. -#define MASK_SADDLE_START_SW_2 0x0200 -#define MASK_BOUNDARY_S 0x0400 // S edge of quad is a boundary. -#define MASK_BOUNDARY_W 0x0800 // W edge of quad is a boundary. -// EXISTS_QUAD bit is always used, but the 4 EXISTS_CORNER are only used if -// _corner_mask is true. Only one of EXISTS_QUAD or EXISTS_??_CORNER is ever -// set per quad, hence not using unique bits for each; care is needed when -// testing for these flags as they overlap. -#define MASK_EXISTS_QUAD 0x1000 // All of quad exists (is not masked). -#define MASK_EXISTS_SW_CORNER 0x2000 // SW corner exists, NE corner is masked. -#define MASK_EXISTS_SE_CORNER 0x3000 -#define MASK_EXISTS_NW_CORNER 0x4000 -#define MASK_EXISTS_NE_CORNER 0x5000 -#define MASK_EXISTS 0x7000 // Combines all 5 EXISTS masks. - -// The following are only needed for filled contours. -#define MASK_VISITED_S 0x10000 // Algorithm has visited S boundary. -#define MASK_VISITED_W 0x20000 // Algorithm has visited W boundary. -#define MASK_VISITED_CORNER 0x40000 // Algorithm has visited corner edge. - - -// Accessors for various CacheItem masks. li is shorthand for level_index. -#define Z_LEVEL(quad) (_cache[quad] & MASK_Z_LEVEL) -#define Z_NE Z_LEVEL(POINT_NE) -#define Z_NW Z_LEVEL(POINT_NW) -#define Z_SE Z_LEVEL(POINT_SE) -#define Z_SW Z_LEVEL(POINT_SW) -#define VISITED(quad,li) ((_cache[quad] & (li==1 ? MASK_VISITED_1 : MASK_VISITED_2)) != 0) -#define VISITED_S(quad) ((_cache[quad] & MASK_VISITED_S) != 0) -#define VISITED_W(quad) ((_cache[quad] & MASK_VISITED_W) != 0) -#define VISITED_CORNER(quad) ((_cache[quad] & MASK_VISITED_CORNER) != 0) -#define SADDLE(quad,li) ((_cache[quad] & (li==1 ? MASK_SADDLE_1 : MASK_SADDLE_2)) != 0) -#define SADDLE_LEFT(quad,li) ((_cache[quad] & (li==1 ? MASK_SADDLE_LEFT_1 : MASK_SADDLE_LEFT_2)) != 0) -#define SADDLE_START_SW(quad,li) ((_cache[quad] & (li==1 ? MASK_SADDLE_START_SW_1 : MASK_SADDLE_START_SW_2)) != 0) -#define BOUNDARY_S(quad) ((_cache[quad] & MASK_BOUNDARY_S) != 0) -#define BOUNDARY_W(quad) ((_cache[quad] & MASK_BOUNDARY_W) != 0) -#define BOUNDARY_N(quad) BOUNDARY_S(quad+_nx) -#define BOUNDARY_E(quad) BOUNDARY_W(quad+1) -#define EXISTS_QUAD(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_QUAD) -#define EXISTS_NONE(quad) ((_cache[quad] & MASK_EXISTS) == 0) -// The following are only used if _corner_mask is true. -#define EXISTS_SW_CORNER(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_SW_CORNER) -#define EXISTS_SE_CORNER(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_SE_CORNER) -#define EXISTS_NW_CORNER(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_NW_CORNER) -#define EXISTS_NE_CORNER(quad) ((_cache[quad] & MASK_EXISTS) == MASK_EXISTS_NE_CORNER) -#define EXISTS_ANY_CORNER(quad) (!EXISTS_NONE(quad) && !EXISTS_QUAD(quad)) -#define EXISTS_W_EDGE(quad) (EXISTS_QUAD(quad) || EXISTS_SW_CORNER(quad) || EXISTS_NW_CORNER(quad)) -#define EXISTS_E_EDGE(quad) (EXISTS_QUAD(quad) || EXISTS_SE_CORNER(quad) || EXISTS_NE_CORNER(quad)) -#define EXISTS_S_EDGE(quad) (EXISTS_QUAD(quad) || EXISTS_SW_CORNER(quad) || EXISTS_SE_CORNER(quad)) -#define EXISTS_N_EDGE(quad) (EXISTS_QUAD(quad) || EXISTS_NW_CORNER(quad) || EXISTS_NE_CORNER(quad)) -// Note that EXISTS_NE_CORNER(quad) is equivalent to BOUNDARY_SW(quad), etc. - - - -QuadEdge::QuadEdge() - : quad(-1), edge(Edge_None) -{} - -QuadEdge::QuadEdge(long quad_, Edge edge_) - : quad(quad_), edge(edge_) -{} - -bool QuadEdge::operator<(const QuadEdge& other) const -{ - if (quad != other.quad) - return quad < other.quad; - else - return edge < other.edge; -} - -bool QuadEdge::operator==(const QuadEdge& other) const -{ - return quad == other.quad && edge == other.edge; -} - -bool QuadEdge::operator!=(const QuadEdge& other) const -{ - return !operator==(other); -} - -std::ostream& operator<<(std::ostream& os, const QuadEdge& quad_edge) -{ - return os << quad_edge.quad << ' ' << quad_edge.edge; -} - - - -XY::XY() -{} - -XY::XY(const double& x_, const double& y_) - : x(x_), y(y_) -{} - -bool XY::operator==(const XY& other) const -{ - return x == other.x && y == other.y; -} - -bool XY::operator!=(const XY& other) const -{ - return x != other.x || y != other.y; -} - -XY XY::operator*(const double& multiplier) const -{ - return XY(x*multiplier, y*multiplier); -} - -const XY& XY::operator+=(const XY& other) -{ - x += other.x; - y += other.y; - return *this; -} - -const XY& XY::operator-=(const XY& other) -{ - x -= other.x; - y -= other.y; - return *this; -} - -XY XY::operator+(const XY& other) const -{ - return XY(x + other.x, y + other.y); -} - -XY XY::operator-(const XY& other) const -{ - return XY(x - other.x, y - other.y); -} - -std::ostream& operator<<(std::ostream& os, const XY& xy) -{ - return os << '(' << xy.x << ' ' << xy.y << ')'; -} - - - -ContourLine::ContourLine(bool is_hole) - : std::vector(), - _is_hole(is_hole), - _parent(0) -{} - -void ContourLine::add_child(ContourLine* child) -{ - assert(!_is_hole && "Cannot add_child to a hole"); - assert(child != 0 && "Null child ContourLine"); - _children.push_back(child); -} - -void ContourLine::clear_parent() -{ - assert(is_hole() && "Cannot clear parent of non-hole"); - assert(_parent != 0 && "Null parent ContourLine"); - _parent = 0; -} - -const ContourLine::Children& ContourLine::get_children() const -{ - assert(!_is_hole && "Cannot get_children of a hole"); - return _children; -} - -const ContourLine* ContourLine::get_parent() const -{ - assert(_is_hole && "Cannot get_parent of a non-hole"); - return _parent; -} - -ContourLine* ContourLine::get_parent() -{ - assert(_is_hole && "Cannot get_parent of a non-hole"); - return _parent; -} - -bool ContourLine::is_hole() const -{ - return _is_hole; -} - -void ContourLine::push_back(const XY& point) -{ - if (empty() || point != back()) - std::vector::push_back(point); -} - -void ContourLine::set_parent(ContourLine* parent) -{ - assert(_is_hole && "Cannot set parent of a non-hole"); - assert(parent != 0 && "Null parent ContourLine"); - _parent = parent; -} - -void ContourLine::write() const -{ - std::cout << "ContourLine " << this << " of " << size() << " points:"; - for (const_iterator it = begin(); it != end(); ++it) - std::cout << ' ' << *it; - if (is_hole()) - std::cout << " hole, parent=" << get_parent(); - else { - std::cout << " not hole"; - if (!_children.empty()) { - std::cout << ", children="; - for (Children::const_iterator it = _children.begin(); - it != _children.end(); ++it) - std::cout << *it << ' '; - } - } - std::cout << std::endl; -} - - - -Contour::Contour() -{} - -Contour::~Contour() -{ - delete_contour_lines(); -} - -void Contour::delete_contour_lines() -{ - for (iterator line_it = begin(); line_it != end(); ++line_it) { - delete *line_it; - *line_it = 0; - } - std::vector::clear(); -} - -void Contour::write() const -{ - std::cout << "Contour of " << size() << " lines." << std::endl; - for (const_iterator it = begin(); it != end(); ++it) - (*it)->write(); -} - - - -ParentCache::ParentCache(long nx, long x_chunk_points, long y_chunk_points) - : _nx(nx), - _x_chunk_points(x_chunk_points), - _y_chunk_points(y_chunk_points), - _lines(0), // Initialised when first needed. - _istart(0), - _jstart(0) -{ - assert(_x_chunk_points > 0 && _y_chunk_points > 0 && - "Chunk sizes must be positive"); -} - -ContourLine* ParentCache::get_parent(long quad) -{ - long index = quad_to_index(quad); - ContourLine* parent = _lines[index]; - while (parent == 0) { - index -= _x_chunk_points; - assert(index >= 0 && "Failed to find parent in chunk ParentCache"); - parent = _lines[index]; - } - assert(parent != 0 && "Failed to find parent in chunk ParentCache"); - return parent; -} - -long ParentCache::quad_to_index(long quad) const -{ - long i = quad % _nx; - long j = quad / _nx; - long index = (i-_istart) + (j-_jstart)*_x_chunk_points; - - assert(i >= _istart && i < _istart + _x_chunk_points && - "i-index outside chunk"); - assert(j >= _jstart && j < _jstart + _y_chunk_points && - "j-index outside chunk"); - assert(index >= 0 && index < static_cast(_lines.size()) && - "ParentCache index outside chunk"); - - return index; -} - -void ParentCache::set_chunk_starts(long istart, long jstart) -{ - assert(istart >= 0 && jstart >= 0 && - "Chunk start indices cannot be negative"); - _istart = istart; - _jstart = jstart; - if (_lines.empty()) - _lines.resize(_x_chunk_points*_y_chunk_points, 0); - else - std::fill(_lines.begin(), _lines.end(), (ContourLine*)0); -} - -void ParentCache::set_parent(long quad, ContourLine& contour_line) -{ - assert(!_lines.empty() && - "Accessing ParentCache before it has been initialised"); - long index = quad_to_index(quad); - if (_lines[index] == 0) - _lines[index] = (contour_line.is_hole() ? contour_line.get_parent() - : &contour_line); -} - - - -QuadContourGenerator::QuadContourGenerator(const CoordinateArray& x, - const CoordinateArray& y, - const CoordinateArray& z, - const MaskArray& mask, - bool corner_mask, - long chunk_size) - : _x(x), - _y(y), - _z(z), - _nx(static_cast(_x.dim(1))), - _ny(static_cast(_x.dim(0))), - _n(_nx*_ny), - _corner_mask(corner_mask), - _chunk_size(chunk_size > 0 ? std::min(chunk_size, std::max(_nx, _ny)-1) - : std::max(_nx, _ny)-1), - _nxchunk(calc_chunk_count(_nx)), - _nychunk(calc_chunk_count(_ny)), - _chunk_count(_nxchunk*_nychunk), - _cache(new CacheItem[_n]), - _parent_cache(_nx, - chunk_size > 0 ? chunk_size+1 : _nx, - chunk_size > 0 ? chunk_size+1 : _ny) -{ - assert(!_x.empty() && !_y.empty() && !_z.empty() && "Empty array"); - assert(_y.dim(0) == _x.dim(0) && _y.dim(1) == _x.dim(1) && - "Different-sized y and x arrays"); - assert(_z.dim(0) == _x.dim(0) && _z.dim(1) == _x.dim(1) && - "Different-sized z and x arrays"); - assert((mask.empty() || - (mask.dim(0) == _x.dim(0) && mask.dim(1) == _x.dim(1))) && - "Different-sized mask and x arrays"); - - init_cache_grid(mask); -} - -QuadContourGenerator::~QuadContourGenerator() -{ - delete [] _cache; -} - -void QuadContourGenerator::append_contour_line_to_vertices( - ContourLine& contour_line, - PyObject* vertices_list) const -{ - assert(vertices_list != 0 && "Null python vertices_list"); - - // Convert ContourLine to python equivalent, and clear it. - npy_intp dims[2] = {static_cast(contour_line.size()), 2}; - numpy::array_view line(dims); - npy_intp i = 0; - for (ContourLine::const_iterator point = contour_line.begin(); - point != contour_line.end(); ++point, ++i) { - line(i, 0) = point->x; - line(i, 1) = point->y; - } - if (PyList_Append(vertices_list, line.pyobj_steal())) { - Py_XDECREF(vertices_list); - throw std::runtime_error("Unable to add contour line to vertices_list"); - } - - contour_line.clear(); -} - -void QuadContourGenerator::append_contour_to_vertices_and_codes( - Contour& contour, - PyObject* vertices_list, - PyObject* codes_list) const -{ - assert(vertices_list != 0 && "Null python vertices_list"); - assert(codes_list != 0 && "Null python codes_list"); - - // Convert Contour to python equivalent, and clear it. - for (Contour::iterator line_it = contour.begin(); line_it != contour.end(); - ++line_it) { - ContourLine& line = **line_it; - if (line.is_hole()) { - // If hole has already been converted to python its parent will be - // set to 0 and it can be deleted. - if (line.get_parent() != 0) { - delete *line_it; - *line_it = 0; - } - } - else { - // Non-holes are converted to python together with their child - // holes so that they are rendered correctly. - ContourLine::const_iterator point; - ContourLine::Children::const_iterator children_it; - - const ContourLine::Children& children = line.get_children(); - npy_intp npoints = static_cast(line.size() + 1); - for (children_it = children.begin(); children_it != children.end(); - ++children_it) - npoints += static_cast((*children_it)->size() + 1); - - npy_intp vertices_dims[2] = {npoints, 2}; - numpy::array_view vertices(vertices_dims); - double* vertices_ptr = vertices.data(); - - npy_intp codes_dims[1] = {npoints}; - numpy::array_view codes(codes_dims); - unsigned char* codes_ptr = codes.data(); - - for (point = line.begin(); point != line.end(); ++point) { - *vertices_ptr++ = point->x; - *vertices_ptr++ = point->y; - *codes_ptr++ = (point == line.begin() ? MOVETO : LINETO); - } - point = line.begin(); - *vertices_ptr++ = point->x; - *vertices_ptr++ = point->y; - *codes_ptr++ = CLOSEPOLY; - - for (children_it = children.begin(); children_it != children.end(); - ++children_it) { - ContourLine& child = **children_it; - for (point = child.begin(); point != child.end(); ++point) { - *vertices_ptr++ = point->x; - *vertices_ptr++ = point->y; - *codes_ptr++ = (point == child.begin() ? MOVETO : LINETO); - } - point = child.begin(); - *vertices_ptr++ = point->x; - *vertices_ptr++ = point->y; - *codes_ptr++ = CLOSEPOLY; - - child.clear_parent(); // To indicate it can be deleted. - } - - if (PyList_Append(vertices_list, vertices.pyobj_steal()) || - PyList_Append(codes_list, codes.pyobj_steal())) { - Py_XDECREF(vertices_list); - Py_XDECREF(codes_list); - contour.delete_contour_lines(); - throw std::runtime_error("Unable to add contour line to vertices and codes lists"); - } - - delete *line_it; - *line_it = 0; - } - } - - // Delete remaining contour lines. - contour.delete_contour_lines(); -} - -long QuadContourGenerator::calc_chunk_count(long point_count) const -{ - assert(point_count > 0 && "point count must be positive"); - assert(_chunk_size > 0 && "Chunk size must be positive"); - - if (_chunk_size > 0) { - long count = (point_count-1) / _chunk_size; - if (count*_chunk_size < point_count-1) - ++count; - - assert(count >= 1 && "Invalid chunk count"); - return count; - } - else - return 1; -} - -PyObject* QuadContourGenerator::create_contour(const double& level) -{ - init_cache_levels(level, level); - - PyObject* vertices_list = PyList_New(0); - if (vertices_list == 0) - throw std::runtime_error("Failed to create Python list"); - - // Lines that start and end on boundaries. - long ichunk, jchunk, istart, iend, jstart, jend; - for (long ijchunk = 0; ijchunk < _chunk_count; ++ijchunk) { - get_chunk_limits(ijchunk, ichunk, jchunk, istart, iend, jstart, jend); - - for (long j = jstart; j < jend; ++j) { - long quad_end = iend + j*_nx; - for (long quad = istart + j*_nx; quad < quad_end; ++quad) { - if (EXISTS_NONE(quad) || VISITED(quad,1)) continue; - - if (BOUNDARY_S(quad) && Z_SW >= 1 && Z_SE < 1 && - start_line(vertices_list, quad, Edge_S, level)) continue; - - if (BOUNDARY_W(quad) && Z_NW >= 1 && Z_SW < 1 && - start_line(vertices_list, quad, Edge_W, level)) continue; - - if (BOUNDARY_N(quad) && Z_NE >= 1 && Z_NW < 1 && - start_line(vertices_list, quad, Edge_N, level)) continue; - - if (BOUNDARY_E(quad) && Z_SE >= 1 && Z_NE < 1 && - start_line(vertices_list, quad, Edge_E, level)) continue; - - if (_corner_mask) { - // Equates to NE boundary. - if (EXISTS_SW_CORNER(quad) && Z_SE >= 1 && Z_NW < 1 && - start_line(vertices_list, quad, Edge_NE, level)) continue; - - // Equates to NW boundary. - if (EXISTS_SE_CORNER(quad) && Z_NE >= 1 && Z_SW < 1 && - start_line(vertices_list, quad, Edge_NW, level)) continue; - - // Equates to SE boundary. - if (EXISTS_NW_CORNER(quad) && Z_SW >= 1 && Z_NE < 1 && - start_line(vertices_list, quad, Edge_SE, level)) continue; - - // Equates to SW boundary. - if (EXISTS_NE_CORNER(quad) && Z_NW >= 1 && Z_SE < 1 && - start_line(vertices_list, quad, Edge_SW, level)) continue; - } - } - } - } - - // Internal loops. - ContourLine contour_line(false); // Reused for each contour line. - for (long ijchunk = 0; ijchunk < _chunk_count; ++ijchunk) { - get_chunk_limits(ijchunk, ichunk, jchunk, istart, iend, jstart, jend); - - for (long j = jstart; j < jend; ++j) { - long quad_end = iend + j*_nx; - for (long quad = istart + j*_nx; quad < quad_end; ++quad) { - if (EXISTS_NONE(quad) || VISITED(quad,1)) - continue; - - Edge start_edge = get_start_edge(quad, 1); - if (start_edge == Edge_None) - continue; - - QuadEdge quad_edge(quad, start_edge); - QuadEdge start_quad_edge(quad_edge); - - // To obtain output identical to that produced by legacy code, - // sometimes need to ignore the first point and add it on the - // end instead. - bool ignore_first = (start_edge == Edge_N); - follow_interior(contour_line, quad_edge, 1, level, - !ignore_first, &start_quad_edge, 1, false); - if (ignore_first && !contour_line.empty()) - contour_line.push_back(contour_line.front()); - append_contour_line_to_vertices(contour_line, vertices_list); - - // Repeat if saddle point but not visited. - if (SADDLE(quad,1) && !VISITED(quad,1)) - --quad; - } - } - } - - return vertices_list; -} - -PyObject* QuadContourGenerator::create_filled_contour(const double& lower_level, - const double& upper_level) -{ - init_cache_levels(lower_level, upper_level); - - Contour contour; - - PyObject* vertices = PyList_New(0); - if (vertices == 0) - throw std::runtime_error("Failed to create Python list"); - - PyObject* codes = PyList_New(0); - if (codes == 0) { - Py_XDECREF(vertices); - throw std::runtime_error("Failed to create Python list"); - } - - long ichunk, jchunk, istart, iend, jstart, jend; - for (long ijchunk = 0; ijchunk < _chunk_count; ++ijchunk) { - get_chunk_limits(ijchunk, ichunk, jchunk, istart, iend, jstart, jend); - _parent_cache.set_chunk_starts(istart, jstart); - - for (long j = jstart; j < jend; ++j) { - long quad_end = iend + j*_nx; - for (long quad = istart + j*_nx; quad < quad_end; ++quad) { - if (!EXISTS_NONE(quad)) - single_quad_filled(contour, quad, lower_level, upper_level); - } - } - - // Clear VISITED_W and VISITED_S flags that are reused by later chunks. - if (jchunk < _nychunk-1) { - long quad_end = iend + jend*_nx; - for (long quad = istart + jend*_nx; quad < quad_end; ++quad) - _cache[quad] &= ~MASK_VISITED_S; - } - - if (ichunk < _nxchunk-1) { - long quad_end = iend + jend*_nx; - for (long quad = iend + jstart*_nx; quad < quad_end; quad += _nx) - _cache[quad] &= ~MASK_VISITED_W; - } - - // Create python objects to return for this chunk. - append_contour_to_vertices_and_codes(contour, vertices, codes); - } - - PyObject* tuple = PyTuple_New(2); - if (tuple == 0) { - Py_XDECREF(vertices); - Py_XDECREF(codes); - throw std::runtime_error("Failed to create Python tuple"); - } - - // No error checking here as filling in a brand new pre-allocated tuple. - PyTuple_SET_ITEM(tuple, 0, vertices); - PyTuple_SET_ITEM(tuple, 1, codes); - - return tuple; -} - -XY QuadContourGenerator::edge_interp(const QuadEdge& quad_edge, - const double& level) -{ - assert(quad_edge.quad >= 0 && quad_edge.quad < _n && - "Quad index out of bounds"); - assert(quad_edge.edge != Edge_None && "Invalid edge"); - return interp(get_edge_point_index(quad_edge, true), - get_edge_point_index(quad_edge, false), - level); -} - -unsigned int QuadContourGenerator::follow_boundary( - ContourLine& contour_line, - QuadEdge& quad_edge, - const double& lower_level, - const double& upper_level, - unsigned int level_index, - const QuadEdge& start_quad_edge) -{ - assert(quad_edge.quad >= 0 && quad_edge.quad < _n && - "Quad index out of bounds"); - assert(quad_edge.edge != Edge_None && "Invalid edge"); - assert(is_edge_a_boundary(quad_edge) && "Not a boundary edge"); - assert((level_index == 1 || level_index == 2) && - "level index must be 1 or 2"); - assert(start_quad_edge.quad >= 0 && start_quad_edge.quad < _n && - "Start quad index out of bounds"); - assert(start_quad_edge.edge != Edge_None && "Invalid start edge"); - - // Only called for filled contours, so always updates _parent_cache. - unsigned int end_level = 0; - bool first_edge = true; - bool stop = false; - long& quad = quad_edge.quad; - - while (true) { - // Levels of start and end points of quad_edge. - unsigned int start_level = - (first_edge ? Z_LEVEL(get_edge_point_index(quad_edge, true)) - : end_level); - long end_point = get_edge_point_index(quad_edge, false); - end_level = Z_LEVEL(end_point); - - if (level_index == 1) { - if (start_level <= level_index && end_level == 2) { - // Increasing z, switching levels from 1 to 2. - level_index = 2; - stop = true; - } - else if (start_level >= 1 && end_level == 0) { - // Decreasing z, keeping same level. - stop = true; - } - } - else { // level_index == 2 - if (start_level <= level_index && end_level == 2) { - // Increasing z, keeping same level. - stop = true; - } - else if (start_level >= 1 && end_level == 0) { - // Decreasing z, switching levels from 2 to 1. - level_index = 1; - stop = true; - } - } - - if (!first_edge && !stop && quad_edge == start_quad_edge) - // Return if reached start point of contour line. Do this before - // checking/setting VISITED flags as will already have been - // visited. - break; - - switch (quad_edge.edge) { - case Edge_E: - assert(!VISITED_W(quad+1) && "Already visited"); - _cache[quad+1] |= MASK_VISITED_W; - break; - case Edge_N: - assert(!VISITED_S(quad+_nx) && "Already visited"); - _cache[quad+_nx] |= MASK_VISITED_S; - break; - case Edge_W: - assert(!VISITED_W(quad) && "Already visited"); - _cache[quad] |= MASK_VISITED_W; - break; - case Edge_S: - assert(!VISITED_S(quad) && "Already visited"); - _cache[quad] |= MASK_VISITED_S; - break; - case Edge_NE: - case Edge_NW: - case Edge_SW: - case Edge_SE: - assert(!VISITED_CORNER(quad) && "Already visited"); - _cache[quad] |= MASK_VISITED_CORNER; - break; - default: - assert(0 && "Invalid Edge"); - break; - } - - if (stop) { - // Exiting boundary to enter interior. - contour_line.push_back(edge_interp(quad_edge, - level_index == 1 ? lower_level - : upper_level)); - break; - } - - move_to_next_boundary_edge(quad_edge); - - // Just moved to new quad edge, so label parent of start of quad edge. - switch (quad_edge.edge) { - case Edge_W: - case Edge_SW: - case Edge_S: - case Edge_SE: - if (!EXISTS_SE_CORNER(quad)) - _parent_cache.set_parent(quad, contour_line); - break; - case Edge_E: - case Edge_NE: - case Edge_N: - case Edge_NW: - if (!EXISTS_SW_CORNER(quad)) - _parent_cache.set_parent(quad + 1, contour_line); - break; - default: - assert(0 && "Invalid edge"); - break; - } - - // Add point to contour. - contour_line.push_back(get_point_xy(end_point)); - - if (first_edge) - first_edge = false; - } - - return level_index; -} - -void QuadContourGenerator::follow_interior(ContourLine& contour_line, - QuadEdge& quad_edge, - unsigned int level_index, - const double& level, - bool want_initial_point, - const QuadEdge* start_quad_edge, - unsigned int start_level_index, - bool set_parents) -{ - assert(quad_edge.quad >= 0 && quad_edge.quad < _n && - "Quad index out of bounds."); - assert(quad_edge.edge != Edge_None && "Invalid edge"); - assert((level_index == 1 || level_index == 2) && - "level index must be 1 or 2"); - assert((start_quad_edge == 0 || - (start_quad_edge->quad >= 0 && start_quad_edge->quad < _n)) && - "Start quad index out of bounds."); - assert((start_quad_edge == 0 || start_quad_edge->edge != Edge_None) && - "Invalid start edge"); - assert((start_level_index == 1 || start_level_index == 2) && - "start level index must be 1 or 2"); - - long& quad = quad_edge.quad; - Edge& edge = quad_edge.edge; - - if (want_initial_point) - contour_line.push_back(edge_interp(quad_edge, level)); - - CacheItem visited_mask = (level_index == 1 ? MASK_VISITED_1 : MASK_VISITED_2); - CacheItem saddle_mask = (level_index == 1 ? MASK_SADDLE_1 : MASK_SADDLE_2); - Dir dir = Dir_Straight; - - while (true) { - assert(!EXISTS_NONE(quad) && "Quad does not exist"); - assert(!(_cache[quad] & visited_mask) && "Quad already visited"); - - // Determine direction to move to next quad. If the quad is already - // labelled as a saddle quad then the direction is easily read from - // the cache. Otherwise the direction is determined differently - // depending on whether the quad is a corner quad or not. - - if (_cache[quad] & saddle_mask) { - // Already identified as a saddle quad, so direction is easy. - dir = (SADDLE_LEFT(quad,level_index) ? Dir_Left : Dir_Right); - _cache[quad] |= visited_mask; - } - else if (EXISTS_ANY_CORNER(quad)) { - // Need z-level of point opposite the entry edge, as that - // determines whether contour turns left or right. - long point_opposite = -1; - switch (edge) { - case Edge_E: - point_opposite = (EXISTS_SE_CORNER(quad) ? POINT_SW - : POINT_NW); - break; - case Edge_N: - point_opposite = (EXISTS_NW_CORNER(quad) ? POINT_SW - : POINT_SE); - break; - case Edge_W: - point_opposite = (EXISTS_SW_CORNER(quad) ? POINT_SE - : POINT_NE); - break; - case Edge_S: - point_opposite = (EXISTS_SW_CORNER(quad) ? POINT_NW - : POINT_NE); - break; - case Edge_NE: point_opposite = POINT_SW; break; - case Edge_NW: point_opposite = POINT_SE; break; - case Edge_SW: point_opposite = POINT_NE; break; - case Edge_SE: point_opposite = POINT_NW; break; - default: assert(0 && "Invalid edge"); break; - } - assert(point_opposite != -1 && "Failed to find opposite point"); - - // Lower-level polygons (level_index == 1) always have higher - // values to the left of the contour. Upper-level contours - // (level_index == 2) are reversed, which is what the fancy XOR - // does below. - if ((Z_LEVEL(point_opposite) >= level_index) ^ (level_index == 2)) - dir = Dir_Right; - else - dir = Dir_Left; - _cache[quad] |= visited_mask; - } - else { - // Calculate configuration of this quad. - long point_left = -1, point_right = -1; - switch (edge) { - case Edge_E: point_left = POINT_SW; point_right = POINT_NW; break; - case Edge_N: point_left = POINT_SE; point_right = POINT_SW; break; - case Edge_W: point_left = POINT_NE; point_right = POINT_SE; break; - case Edge_S: point_left = POINT_NW; point_right = POINT_NE; break; - default: assert(0 && "Invalid edge"); break; - } - - unsigned int config = (Z_LEVEL(point_left) >= level_index) << 1 | - (Z_LEVEL(point_right) >= level_index); - - // Upper level (level_index == 2) polygons are reversed compared to - // lower level ones, i.e. higher values on the right rather than - // the left. - if (level_index == 2) - config = 3 - config; - - // Calculate turn direction to move to next quad along contour line. - if (config == 1) { - // New saddle quad, set up cache bits for it. - double zmid = 0.25*(get_point_z(POINT_SW) + - get_point_z(POINT_SE) + - get_point_z(POINT_NW) + - get_point_z(POINT_NE)); - _cache[quad] |= (level_index == 1 ? MASK_SADDLE_1 : MASK_SADDLE_2); - if ((zmid > level) ^ (level_index == 2)) { - dir = Dir_Right; - } - else { - dir = Dir_Left; - _cache[quad] |= (level_index == 1 ? MASK_SADDLE_LEFT_1 - : MASK_SADDLE_LEFT_2); - } - if (edge == Edge_N || edge == Edge_E) { - // Next visit to this quad must start on S or W. - _cache[quad] |= (level_index == 1 ? MASK_SADDLE_START_SW_1 - : MASK_SADDLE_START_SW_2); - } - } - else { - // Normal (non-saddle) quad. - dir = (config == 0 ? Dir_Left - : (config == 3 ? Dir_Right : Dir_Straight)); - _cache[quad] |= visited_mask; - } - } - - // Use dir to determine exit edge. - edge = get_exit_edge(quad_edge, dir); - - if (set_parents) { - if (edge == Edge_E) - _parent_cache.set_parent(quad+1, contour_line); - else if (edge == Edge_W) - _parent_cache.set_parent(quad, contour_line); - } - - // Add new point to contour line. - contour_line.push_back(edge_interp(quad_edge, level)); - - // Stop if reached boundary. - if (is_edge_a_boundary(quad_edge)) - break; - - move_to_next_quad(quad_edge); - assert(quad_edge.quad >= 0 && quad_edge.quad < _n && - "Quad index out of bounds"); - - // Return if reached start point of contour line. - if (start_quad_edge != 0 && - quad_edge == *start_quad_edge && - level_index == start_level_index) - break; - } -} - -void QuadContourGenerator::get_chunk_limits(long ijchunk, - long& ichunk, - long& jchunk, - long& istart, - long& iend, - long& jstart, - long& jend) -{ - assert(ijchunk >= 0 && ijchunk < _chunk_count && "ijchunk out of bounds"); - ichunk = ijchunk % _nxchunk; - jchunk = ijchunk / _nxchunk; - istart = ichunk*_chunk_size; - iend = (ichunk == _nxchunk-1 ? _nx : (ichunk+1)*_chunk_size); - jstart = jchunk*_chunk_size; - jend = (jchunk == _nychunk-1 ? _ny : (jchunk+1)*_chunk_size); -} - -Edge QuadContourGenerator::get_corner_start_edge(long quad, - unsigned int level_index) const -{ - assert(quad >= 0 && quad < _n && "Quad index out of bounds"); - assert((level_index == 1 || level_index == 2) && - "level index must be 1 or 2"); - assert(EXISTS_ANY_CORNER(quad) && "Quad is not a corner"); - - // Diagram for NE corner. Rotate for other corners. - // - // edge12 - // point1 +---------+ point2 - // \ | - // \ | edge23 - // edge31 \ | - // \ | - // + point3 - // - long point1, point2, point3; - Edge edge12, edge23, edge31; - switch (_cache[quad] & MASK_EXISTS) { - case MASK_EXISTS_SW_CORNER: - point1 = POINT_SE; point2 = POINT_SW; point3 = POINT_NW; - edge12 = Edge_S; edge23 = Edge_W; edge31 = Edge_NE; - break; - case MASK_EXISTS_SE_CORNER: - point1 = POINT_NE; point2 = POINT_SE; point3 = POINT_SW; - edge12 = Edge_E; edge23 = Edge_S; edge31 = Edge_NW; - break; - case MASK_EXISTS_NW_CORNER: - point1 = POINT_SW; point2 = POINT_NW; point3 = POINT_NE; - edge12 = Edge_W; edge23 = Edge_N; edge31 = Edge_SE; - break; - case MASK_EXISTS_NE_CORNER: - point1 = POINT_NW; point2 = POINT_NE; point3 = POINT_SE; - edge12 = Edge_N; edge23 = Edge_E; edge31 = Edge_SW; - break; - default: - assert(0 && "Invalid EXISTS for quad"); - return Edge_None; - } - - unsigned int config = (Z_LEVEL(point1) >= level_index) << 2 | - (Z_LEVEL(point2) >= level_index) << 1 | - (Z_LEVEL(point3) >= level_index); - - // Upper level (level_index == 2) polygons are reversed compared to lower - // level ones, i.e. higher values on the right rather than the left. - if (level_index == 2) - config = 7 - config; - - switch (config) { - case 0: return Edge_None; - case 1: return edge23; - case 2: return edge12; - case 3: return edge12; - case 4: return edge31; - case 5: return edge23; - case 6: return edge31; - case 7: return Edge_None; - default: assert(0 && "Invalid config"); return Edge_None; - } -} - -long QuadContourGenerator::get_edge_point_index(const QuadEdge& quad_edge, - bool start) const -{ - assert(quad_edge.quad >= 0 && quad_edge.quad < _n && - "Quad index out of bounds"); - assert(quad_edge.edge != Edge_None && "Invalid edge"); - - // Edges are ordered anticlockwise around their quad, as indicated by - // directions of arrows in diagrams below. - // Full quad NW corner (others similar) - // - // POINT_NW Edge_N POINT_NE POINT_NW Edge_N POINT_NE - // +----<-----+ +----<-----+ - // | | | / - // | | | quad / - // Edge_W V quad ^ Edge_E Edge_W V ^ - // | | | / Edge_SE - // | | | / - // +---->-----+ + - // POINT_SW Edge_S POINT_SE POINT_SW - // - const long& quad = quad_edge.quad; - switch (quad_edge.edge) { - case Edge_E: return (start ? POINT_SE : POINT_NE); - case Edge_N: return (start ? POINT_NE : POINT_NW); - case Edge_W: return (start ? POINT_NW : POINT_SW); - case Edge_S: return (start ? POINT_SW : POINT_SE); - case Edge_NE: return (start ? POINT_SE : POINT_NW); - case Edge_NW: return (start ? POINT_NE : POINT_SW); - case Edge_SW: return (start ? POINT_NW : POINT_SE); - case Edge_SE: return (start ? POINT_SW : POINT_NE); - default: assert(0 && "Invalid edge"); return 0; - } -} - -Edge QuadContourGenerator::get_exit_edge(const QuadEdge& quad_edge, - Dir dir) const -{ - assert(quad_edge.quad >= 0 && quad_edge.quad < _n && - "Quad index out of bounds"); - assert(quad_edge.edge != Edge_None && "Invalid edge"); - - const long& quad = quad_edge.quad; - const Edge& edge = quad_edge.edge; - if (EXISTS_ANY_CORNER(quad)) { - // Corner directions are always left or right. A corner is a triangle, - // entered via one edge so the other two edges are the left and right - // ones. - switch (edge) { - case Edge_E: - return (EXISTS_SE_CORNER(quad) - ? (dir == Dir_Left ? Edge_S : Edge_NW) - : (dir == Dir_Right ? Edge_N : Edge_SW)); - case Edge_N: - return (EXISTS_NW_CORNER(quad) - ? (dir == Dir_Right ? Edge_W : Edge_SE) - : (dir == Dir_Left ? Edge_E : Edge_SW)); - case Edge_W: - return (EXISTS_SW_CORNER(quad) - ? (dir == Dir_Right ? Edge_S : Edge_NE) - : (dir == Dir_Left ? Edge_N : Edge_SE)); - case Edge_S: - return (EXISTS_SW_CORNER(quad) - ? (dir == Dir_Left ? Edge_W : Edge_NE) - : (dir == Dir_Right ? Edge_E : Edge_NW)); - case Edge_NE: return (dir == Dir_Left ? Edge_S : Edge_W); - case Edge_NW: return (dir == Dir_Left ? Edge_E : Edge_S); - case Edge_SW: return (dir == Dir_Left ? Edge_N : Edge_E); - case Edge_SE: return (dir == Dir_Left ? Edge_W : Edge_N); - default: assert(0 && "Invalid edge"); return Edge_None; - } - } - else { - // A full quad has four edges, entered via one edge so that other three - // edges correspond to left, straight and right directions. - switch (edge) { - case Edge_E: - return (dir == Dir_Left ? Edge_S : - (dir == Dir_Right ? Edge_N : Edge_W)); - case Edge_N: - return (dir == Dir_Left ? Edge_E : - (dir == Dir_Right ? Edge_W : Edge_S)); - case Edge_W: - return (dir == Dir_Left ? Edge_N : - (dir == Dir_Right ? Edge_S : Edge_E)); - case Edge_S: - return (dir == Dir_Left ? Edge_W : - (dir == Dir_Right ? Edge_E : Edge_N)); - default: assert(0 && "Invalid edge"); return Edge_None; - } - } -} - -XY QuadContourGenerator::get_point_xy(long point) const -{ - assert(point >= 0 && point < _n && "Point index out of bounds."); - return XY(_x.data()[static_cast(point)], - _y.data()[static_cast(point)]); -} - -const double& QuadContourGenerator::get_point_z(long point) const -{ - assert(point >= 0 && point < _n && "Point index out of bounds."); - return _z.data()[static_cast(point)]; -} - -Edge QuadContourGenerator::get_quad_start_edge(long quad, - unsigned int level_index) const -{ - assert(quad >= 0 && quad < _n && "Quad index out of bounds"); - assert((level_index == 1 || level_index == 2) && - "level index must be 1 or 2"); - assert(EXISTS_QUAD(quad) && "Quad does not exist"); - - unsigned int config = (Z_NW >= level_index) << 3 | - (Z_NE >= level_index) << 2 | - (Z_SW >= level_index) << 1 | - (Z_SE >= level_index); - - // Upper level (level_index == 2) polygons are reversed compared to lower - // level ones, i.e. higher values on the right rather than the left. - if (level_index == 2) - config = 15 - config; - - switch (config) { - case 0: return Edge_None; - case 1: return Edge_E; - case 2: return Edge_S; - case 3: return Edge_E; - case 4: return Edge_N; - case 5: return Edge_N; - case 6: - // If already identified as a saddle quad then the start edge is - // read from the cache. Otherwise return either valid start edge - // and the subsequent call to follow_interior() will correctly set - // up saddle bits in cache. - if (!SADDLE(quad,level_index) || SADDLE_START_SW(quad,level_index)) - return Edge_S; - else - return Edge_N; - case 7: return Edge_N; - case 8: return Edge_W; - case 9: - // See comment for 6 above. - if (!SADDLE(quad,level_index) || SADDLE_START_SW(quad,level_index)) - return Edge_W; - else - return Edge_E; - case 10: return Edge_S; - case 11: return Edge_E; - case 12: return Edge_W; - case 13: return Edge_W; - case 14: return Edge_S; - case 15: return Edge_None; - default: assert(0 && "Invalid config"); return Edge_None; - } -} - -Edge QuadContourGenerator::get_start_edge(long quad, - unsigned int level_index) const -{ - if (EXISTS_ANY_CORNER(quad)) - return get_corner_start_edge(quad, level_index); - else - return get_quad_start_edge(quad, level_index); -} - -void QuadContourGenerator::init_cache_grid(const MaskArray& mask) -{ - long i, j, quad; - - if (mask.empty()) { - // No mask, easy to calculate quad existence and boundaries together. - quad = 0; - for (j = 0; j < _ny; ++j) { - for (i = 0; i < _nx; ++i, ++quad) { - _cache[quad] = 0; - - if (i < _nx-1 && j < _ny-1) - _cache[quad] |= MASK_EXISTS_QUAD; - - if ((i % _chunk_size == 0 || i == _nx-1) && j < _ny-1) - _cache[quad] |= MASK_BOUNDARY_W; - - if ((j % _chunk_size == 0 || j == _ny-1) && i < _nx-1) - _cache[quad] |= MASK_BOUNDARY_S; - } - } - } - else { - // Casting avoids problem when sizeof(bool) != sizeof(npy_bool). - const npy_bool* mask_ptr = - reinterpret_cast(mask.data()); - - // Have mask so use two stages. - // Stage 1, determine if quads/corners exist. - quad = 0; - for (j = 0; j < _ny; ++j) { - for (i = 0; i < _nx; ++i, ++quad) { - _cache[quad] = 0; - - if (i < _nx-1 && j < _ny-1) { - unsigned int config = mask_ptr[POINT_NW] << 3 | - mask_ptr[POINT_NE] << 2 | - mask_ptr[POINT_SW] << 1 | - mask_ptr[POINT_SE]; - - if (_corner_mask) { - switch (config) { - case 0: _cache[quad] = MASK_EXISTS_QUAD; break; - case 1: _cache[quad] = MASK_EXISTS_NW_CORNER; break; - case 2: _cache[quad] = MASK_EXISTS_NE_CORNER; break; - case 4: _cache[quad] = MASK_EXISTS_SW_CORNER; break; - case 8: _cache[quad] = MASK_EXISTS_SE_CORNER; break; - default: - // Do nothing, quad is masked out. - break; - } - } - else if (config == 0) - _cache[quad] = MASK_EXISTS_QUAD; - } - } - } - - // Stage 2, calculate W and S boundaries. For each quad use boundary - // data already calculated for quads to W and S, so must iterate - // through quads in correct order (increasing i and j indices). - // Cannot use boundary data for quads to E and N as have not yet - // calculated it. - quad = 0; - for (j = 0; j < _ny; ++j) { - for (i = 0; i < _nx; ++i, ++quad) { - if (_corner_mask) { - bool W_exists_none = (i == 0 || EXISTS_NONE(quad-1)); - bool S_exists_none = (j == 0 || EXISTS_NONE(quad-_nx)); - bool W_exists_E_edge = (i > 0 && EXISTS_E_EDGE(quad-1)); - bool S_exists_N_edge = (j > 0 && EXISTS_N_EDGE(quad-_nx)); - - if ((EXISTS_W_EDGE(quad) && W_exists_none) || - (EXISTS_NONE(quad) && W_exists_E_edge) || - (i % _chunk_size == 0 && EXISTS_W_EDGE(quad) && - W_exists_E_edge)) - _cache[quad] |= MASK_BOUNDARY_W; - - if ((EXISTS_S_EDGE(quad) && S_exists_none) || - (EXISTS_NONE(quad) && S_exists_N_edge) || - (j % _chunk_size == 0 && EXISTS_S_EDGE(quad) && - S_exists_N_edge)) - _cache[quad] |= MASK_BOUNDARY_S; - } - else { - bool W_exists_quad = (i > 0 && EXISTS_QUAD(quad-1)); - bool S_exists_quad = (j > 0 && EXISTS_QUAD(quad-_nx)); - - if ((EXISTS_QUAD(quad) != W_exists_quad) || - (i % _chunk_size == 0 && EXISTS_QUAD(quad) && - W_exists_quad)) - _cache[quad] |= MASK_BOUNDARY_W; - - if ((EXISTS_QUAD(quad) != S_exists_quad) || - (j % _chunk_size == 0 && EXISTS_QUAD(quad) && - S_exists_quad)) - _cache[quad] |= MASK_BOUNDARY_S; - } - } - } - } -} - -void QuadContourGenerator::init_cache_levels(const double& lower_level, - const double& upper_level) -{ - assert(upper_level >= lower_level && - "upper and lower levels are wrong way round"); - - bool two_levels = (lower_level != upper_level); - CacheItem keep_mask = - (_corner_mask ? MASK_EXISTS | MASK_BOUNDARY_S | MASK_BOUNDARY_W - : MASK_EXISTS_QUAD | MASK_BOUNDARY_S | MASK_BOUNDARY_W); - - if (two_levels) { - const double* z_ptr = _z.data(); - for (long quad = 0; quad < _n; ++quad, ++z_ptr) { - _cache[quad] &= keep_mask; - if (*z_ptr > upper_level) - _cache[quad] |= MASK_Z_LEVEL_2; - else if (*z_ptr > lower_level) - _cache[quad] |= MASK_Z_LEVEL_1; - } - } - else { - const double* z_ptr = _z.data(); - for (long quad = 0; quad < _n; ++quad, ++z_ptr) { - _cache[quad] &= keep_mask; - if (*z_ptr > lower_level) - _cache[quad] |= MASK_Z_LEVEL_1; - } - } -} - -XY QuadContourGenerator::interp( - long point1, long point2, const double& level) const -{ - assert(point1 >= 0 && point1 < _n && "Point index 1 out of bounds."); - assert(point2 >= 0 && point2 < _n && "Point index 2 out of bounds."); - assert(point1 != point2 && "Identical points"); - double fraction = (get_point_z(point2) - level) / - (get_point_z(point2) - get_point_z(point1)); - return get_point_xy(point1)*fraction + get_point_xy(point2)*(1.0 - fraction); -} - -bool QuadContourGenerator::is_edge_a_boundary(const QuadEdge& quad_edge) const -{ - assert(quad_edge.quad >= 0 && quad_edge.quad < _n && - "Quad index out of bounds"); - assert(quad_edge.edge != Edge_None && "Invalid edge"); - - switch (quad_edge.edge) { - case Edge_E: return BOUNDARY_E(quad_edge.quad); - case Edge_N: return BOUNDARY_N(quad_edge.quad); - case Edge_W: return BOUNDARY_W(quad_edge.quad); - case Edge_S: return BOUNDARY_S(quad_edge.quad); - case Edge_NE: return EXISTS_SW_CORNER(quad_edge.quad); - case Edge_NW: return EXISTS_SE_CORNER(quad_edge.quad); - case Edge_SW: return EXISTS_NE_CORNER(quad_edge.quad); - case Edge_SE: return EXISTS_NW_CORNER(quad_edge.quad); - default: assert(0 && "Invalid edge"); return true; - } -} - -void QuadContourGenerator::move_to_next_boundary_edge(QuadEdge& quad_edge) const -{ - assert(is_edge_a_boundary(quad_edge) && "QuadEdge is not a boundary"); - - long& quad = quad_edge.quad; - Edge& edge = quad_edge.edge; - - quad = get_edge_point_index(quad_edge, false); - - // quad is now such that POINT_SW is the end point of the quad_edge passed - // to this function. - - // To find the next boundary edge, first attempt to turn left 135 degrees - // and if that edge is a boundary then move to it. If not, attempt to turn - // left 90 degrees, then left 45 degrees, then straight on, etc, until can - // move. - // First determine which edge to attempt first. - int index = 0; - switch (edge) { - case Edge_E: index = 0; break; - case Edge_SE: index = 1; break; - case Edge_S: index = 2; break; - case Edge_SW: index = 3; break; - case Edge_W: index = 4; break; - case Edge_NW: index = 5; break; - case Edge_N: index = 6; break; - case Edge_NE: index = 7; break; - default: assert(0 && "Invalid edge"); break; - } - - // If _corner_mask not set, only need to consider odd index in loop below. - if (!_corner_mask) - ++index; - - // Try each edge in turn until a boundary is found. - int start_index = index; - do - { - switch (index) { - case 0: - if (EXISTS_SE_CORNER(quad-_nx-1)) { // Equivalent to BOUNDARY_NW - quad -= _nx+1; - edge = Edge_NW; - return; - } - break; - case 1: - if (BOUNDARY_N(quad-_nx-1)) { - quad -= _nx+1; - edge = Edge_N; - return; - } - break; - case 2: - if (EXISTS_SW_CORNER(quad-1)) { // Equivalent to BOUNDARY_NE - quad -= 1; - edge = Edge_NE; - return; - } - break; - case 3: - if (BOUNDARY_E(quad-1)) { - quad -= 1; - edge = Edge_E; - return; - } - break; - case 4: - if (EXISTS_NW_CORNER(quad)) { // Equivalent to BOUNDARY_SE - edge = Edge_SE; - return; - } - break; - case 5: - if (BOUNDARY_S(quad)) { - edge = Edge_S; - return; - } - break; - case 6: - if (EXISTS_NE_CORNER(quad-_nx)) { // Equivalent to BOUNDARY_SW - quad -= _nx; - edge = Edge_SW; - return; - } - break; - case 7: - if (BOUNDARY_W(quad-_nx)) { - quad -= _nx; - edge = Edge_W; - return; - } - break; - default: assert(0 && "Invalid index"); break; - } - - if (_corner_mask) - index = (index + 1) % 8; - else - index = (index + 2) % 8; - } while (index != start_index); - - assert(0 && "Failed to find next boundary edge"); -} - -void QuadContourGenerator::move_to_next_quad(QuadEdge& quad_edge) const -{ - assert(quad_edge.quad >= 0 && quad_edge.quad < _n && - "Quad index out of bounds"); - assert(quad_edge.edge != Edge_None && "Invalid edge"); - - // Move from quad_edge.quad to the neighbouring quad in the direction - // specified by quad_edge.edge. - switch (quad_edge.edge) { - case Edge_E: quad_edge.quad += 1; quad_edge.edge = Edge_W; break; - case Edge_N: quad_edge.quad += _nx; quad_edge.edge = Edge_S; break; - case Edge_W: quad_edge.quad -= 1; quad_edge.edge = Edge_E; break; - case Edge_S: quad_edge.quad -= _nx; quad_edge.edge = Edge_N; break; - default: assert(0 && "Invalid edge"); break; - } -} - -void QuadContourGenerator::single_quad_filled(Contour& contour, - long quad, - const double& lower_level, - const double& upper_level) -{ - assert(quad >= 0 && quad < _n && "Quad index out of bounds"); - - // Order of checking is important here as can have different ContourLines - // from both lower and upper levels in the same quad. First check the S - // edge, then move up the quad to the N edge checking as required. - - // Possible starts from S boundary. - if (BOUNDARY_S(quad) && EXISTS_S_EDGE(quad)) { - - // Lower-level start from S boundary into interior. - if (!VISITED_S(quad) && Z_SW >= 1 && Z_SE == 0) - contour.push_back(start_filled(quad, Edge_S, 1, NotHole, Interior, - lower_level, upper_level)); - - // Upper-level start from S boundary into interior. - if (!VISITED_S(quad) && Z_SW < 2 && Z_SE == 2) - contour.push_back(start_filled(quad, Edge_S, 2, NotHole, Interior, - lower_level, upper_level)); - - // Lower-level start following S boundary from W to E. - if (!VISITED_S(quad) && Z_SW <= 1 && Z_SE == 1) - contour.push_back(start_filled(quad, Edge_S, 1, NotHole, Boundary, - lower_level, upper_level)); - - // Upper-level start following S boundary from W to E. - if (!VISITED_S(quad) && Z_SW == 2 && Z_SE == 1) - contour.push_back(start_filled(quad, Edge_S, 2, NotHole, Boundary, - lower_level, upper_level)); - } - - // Possible starts from W boundary. - if (BOUNDARY_W(quad) && EXISTS_W_EDGE(quad)) { - - // Lower-level start from W boundary into interior. - if (!VISITED_W(quad) && Z_NW >= 1 && Z_SW == 0) - contour.push_back(start_filled(quad, Edge_W, 1, NotHole, Interior, - lower_level, upper_level)); - - // Upper-level start from W boundary into interior. - if (!VISITED_W(quad) && Z_NW < 2 && Z_SW == 2) - contour.push_back(start_filled(quad, Edge_W, 2, NotHole, Interior, - lower_level, upper_level)); - - // Lower-level start following W boundary from N to S. - if (!VISITED_W(quad) && Z_NW <= 1 && Z_SW == 1) - contour.push_back(start_filled(quad, Edge_W, 1, NotHole, Boundary, - lower_level, upper_level)); - - // Upper-level start following W boundary from N to S. - if (!VISITED_W(quad) && Z_NW == 2 && Z_SW == 1) - contour.push_back(start_filled(quad, Edge_W, 2, NotHole, Boundary, - lower_level, upper_level)); - } - - // Possible starts from NE boundary. - if (EXISTS_SW_CORNER(quad)) { // i.e. BOUNDARY_NE - - // Lower-level start following NE boundary from SE to NW, hole. - if (!VISITED_CORNER(quad) && Z_NW == 1 && Z_SE == 1) - contour.push_back(start_filled(quad, Edge_NE, 1, Hole, Boundary, - lower_level, upper_level)); - } - // Possible starts from SE boundary. - else if (EXISTS_NW_CORNER(quad)) { // i.e. BOUNDARY_SE - - // Lower-level start from N to SE. - if (!VISITED(quad,1) && Z_NW == 0 && Z_SW == 0 && Z_NE >= 1) - contour.push_back(start_filled(quad, Edge_N, 1, NotHole, Interior, - lower_level, upper_level)); - - // Upper-level start from SE to N, hole. - if (!VISITED(quad,2) && Z_NW < 2 && Z_SW < 2 && Z_NE == 2) - contour.push_back(start_filled(quad, Edge_SE, 2, Hole, Interior, - lower_level, upper_level)); - - // Upper-level start from N to SE. - if (!VISITED(quad,2) && Z_NW == 2 && Z_SW == 2 && Z_NE < 2) - contour.push_back(start_filled(quad, Edge_N, 2, NotHole, Interior, - lower_level, upper_level)); - - // Lower-level start from SE to N, hole. - if (!VISITED(quad,1) && Z_NW >= 1 && Z_SW >= 1 && Z_NE == 0) - contour.push_back(start_filled(quad, Edge_SE, 1, Hole, Interior, - lower_level, upper_level)); - } - // Possible starts from NW boundary. - else if (EXISTS_SE_CORNER(quad)) { // i.e. BOUNDARY_NW - - // Lower-level start from NW to E. - if (!VISITED(quad,1) && Z_SW == 0 && Z_SE == 0 && Z_NE >= 1) - contour.push_back(start_filled(quad, Edge_NW, 1, NotHole, Interior, - lower_level, upper_level)); - - // Upper-level start from E to NW, hole. - if (!VISITED(quad,2) && Z_SW < 2 && Z_SE < 2 && Z_NE == 2) - contour.push_back(start_filled(quad, Edge_E, 2, Hole, Interior, - lower_level, upper_level)); - - // Upper-level start from NW to E. - if (!VISITED(quad,2) && Z_SW == 2 && Z_SE == 2 && Z_NE < 2) - contour.push_back(start_filled(quad, Edge_NW, 2, NotHole, Interior, - lower_level, upper_level)); - - // Lower-level start from E to NW, hole. - if (!VISITED(quad,1) && Z_SW >= 1 && Z_SE >= 1 && Z_NE == 0) - contour.push_back(start_filled(quad, Edge_E, 1, Hole, Interior, - lower_level, upper_level)); - } - // Possible starts from SW boundary. - else if (EXISTS_NE_CORNER(quad)) { // i.e. BOUNDARY_SW - - // Lower-level start from SW boundary into interior. - if (!VISITED_CORNER(quad) && Z_NW >= 1 && Z_SE == 0) - contour.push_back(start_filled(quad, Edge_SW, 1, NotHole, Interior, - lower_level, upper_level)); - - // Upper-level start from SW boundary into interior. - if (!VISITED_CORNER(quad) && Z_NW < 2 && Z_SE == 2) - contour.push_back(start_filled(quad, Edge_SW, 2, NotHole, Interior, - lower_level, upper_level)); - - // Lower-level start following SW boundary from NW to SE. - if (!VISITED_CORNER(quad) && Z_NW <= 1 && Z_SE == 1) - contour.push_back(start_filled(quad, Edge_SW, 1, NotHole, Boundary, - lower_level, upper_level)); - - // Upper-level start following SW boundary from NW to SE. - if (!VISITED_CORNER(quad) && Z_NW == 2 && Z_SE == 1) - contour.push_back(start_filled(quad, Edge_SW, 2, NotHole, Boundary, - lower_level, upper_level)); - } - - // A full (unmasked) quad can only have a start on the NE corner, i.e. from - // N to E (lower level) or E to N (upper level). Any other start will have - // already been created in a call to this function for a prior quad so we - // don't need to test for it again here. - // - // The situation is complicated by the possibility that the quad is a - // saddle quad, in which case a contour line starting on the N could leave - // by either the W or the E. We only need to consider those leaving E. - // - // A NE corner can also have a N to E or E to N start. - if (EXISTS_QUAD(quad) || EXISTS_NE_CORNER(quad)) { - - // Lower-level start from N to E. - if (!VISITED(quad,1) && Z_NW == 0 && Z_SE == 0 && Z_NE >= 1 && - (!SADDLE(quad,1) || SADDLE_LEFT(quad,1))) - contour.push_back(start_filled(quad, Edge_N, 1, NotHole, Interior, - lower_level, upper_level)); - - // Upper-level start from E to N, hole. - if (!VISITED(quad,2) && Z_NW < 2 && Z_SE < 2 && Z_NE == 2 && - (!SADDLE(quad,2) || !SADDLE_LEFT(quad,2))) - contour.push_back(start_filled(quad, Edge_E, 2, Hole, Interior, - lower_level, upper_level)); - - // Upper-level start from N to E. - if (!VISITED(quad,2) && Z_NW == 2 && Z_SE == 2 && Z_NE < 2 && - (!SADDLE(quad,2) || SADDLE_LEFT(quad,2))) - contour.push_back(start_filled(quad, Edge_N, 2, NotHole, Interior, - lower_level, upper_level)); - - // Lower-level start from E to N, hole. - if (!VISITED(quad,1) && Z_NW >= 1 && Z_SE >= 1 && Z_NE == 0 && - (!SADDLE(quad,1) || !SADDLE_LEFT(quad,1))) - contour.push_back(start_filled(quad, Edge_E, 1, Hole, Interior, - lower_level, upper_level)); - - // All possible contours passing through the interior of this quad - // should have already been created, so assert this. - assert((VISITED(quad,1) || get_start_edge(quad, 1) == Edge_None) && - "Found start of contour that should have already been created"); - assert((VISITED(quad,2) || get_start_edge(quad, 2) == Edge_None) && - "Found start of contour that should have already been created"); - } - - // Lower-level start following N boundary from E to W, hole. - // This is required for an internal masked region which is a hole in a - // surrounding contour line. - if (BOUNDARY_N(quad) && EXISTS_N_EDGE(quad) && - !VISITED_S(quad+_nx) && Z_NW == 1 && Z_NE == 1) - contour.push_back(start_filled(quad, Edge_N, 1, Hole, Boundary, - lower_level, upper_level)); -} - -ContourLine* QuadContourGenerator::start_filled( - long quad, - Edge edge, - unsigned int start_level_index, - HoleOrNot hole_or_not, - BoundaryOrInterior boundary_or_interior, - const double& lower_level, - const double& upper_level) -{ - assert(quad >= 0 && quad < _n && "Quad index out of bounds"); - assert(edge != Edge_None && "Invalid edge"); - assert((start_level_index == 1 || start_level_index == 2) && - "start level index must be 1 or 2"); - - ContourLine* contour_line = new ContourLine(hole_or_not == Hole); - if (hole_or_not == Hole) { - // Find and set parent ContourLine. - ContourLine* parent = _parent_cache.get_parent(quad + 1); - assert(parent != 0 && "Failed to find parent ContourLine"); - contour_line->set_parent(parent); - parent->add_child(contour_line); - } - - QuadEdge quad_edge(quad, edge); - const QuadEdge start_quad_edge(quad_edge); - unsigned int level_index = start_level_index; - - // If starts on interior, can only finish on interior. - // If starts on boundary, can only finish on boundary. - - while (true) { - if (boundary_or_interior == Interior) { - double level = (level_index == 1 ? lower_level : upper_level); - follow_interior(*contour_line, quad_edge, level_index, level, - false, &start_quad_edge, start_level_index, true); - } - else { - level_index = follow_boundary( - *contour_line, quad_edge, lower_level, - upper_level, level_index, start_quad_edge); - } - - if (quad_edge == start_quad_edge && (boundary_or_interior == Boundary || - level_index == start_level_index)) - break; - - if (boundary_or_interior == Boundary) - boundary_or_interior = Interior; - else - boundary_or_interior = Boundary; - } - - return contour_line; -} - -bool QuadContourGenerator::start_line( - PyObject* vertices_list, long quad, Edge edge, const double& level) -{ - assert(vertices_list != 0 && "Null python vertices list"); - assert(is_edge_a_boundary(QuadEdge(quad, edge)) && - "QuadEdge is not a boundary"); - - QuadEdge quad_edge(quad, edge); - ContourLine contour_line(false); - follow_interior(contour_line, quad_edge, 1, level, true, 0, 1, false); - append_contour_line_to_vertices(contour_line, vertices_list); - return VISITED(quad,1); -} - -void QuadContourGenerator::write_cache(bool grid_only) const -{ - std::cout << "-----------------------------------------------" << std::endl; - for (long quad = 0; quad < _n; ++quad) - write_cache_quad(quad, grid_only); - std::cout << "-----------------------------------------------" << std::endl; -} - -void QuadContourGenerator::write_cache_quad(long quad, bool grid_only) const -{ - long j = quad / _nx; - long i = quad - j*_nx; - std::cout << quad << ": i=" << i << " j=" << j - << " EXISTS=" << EXISTS_QUAD(quad); - if (_corner_mask) - std::cout << " CORNER=" << EXISTS_SW_CORNER(quad) << EXISTS_SE_CORNER(quad) - << EXISTS_NW_CORNER(quad) << EXISTS_NE_CORNER(quad); - std::cout << " BNDY=" << (BOUNDARY_S(quad)>0) << (BOUNDARY_W(quad)>0); - if (!grid_only) { - std::cout << " Z=" << Z_LEVEL(quad) - << " SAD=" << SADDLE(quad,1) << SADDLE(quad,2) - << " LEFT=" << SADDLE_LEFT(quad,1) << SADDLE_LEFT(quad,2) - << " NW=" << SADDLE_START_SW(quad,1) << SADDLE_START_SW(quad,2) - << " VIS=" << VISITED(quad,1) << VISITED(quad,2) - << VISITED_S(quad) << VISITED_W(quad) - << VISITED_CORNER(quad); - } - std::cout << std::endl; -} diff --git a/src/_contour.h b/src/_contour.h deleted file mode 100644 index 6232b3abd2a7..000000000000 --- a/src/_contour.h +++ /dev/null @@ -1,530 +0,0 @@ -/* - * QuadContourGenerator - * -------------------- - * A QuadContourGenerator generates contours for scalar fields defined on - * quadrilateral grids. A single QuadContourGenerator object can create both - * line contours (at single levels) and filled contours (between pairs of - * levels) for the same field. - * - * A field to be contoured has nx, ny points in the x- and y-directions - * respectively. The quad grid is defined by x and y arrays of shape(ny, nx), - * and the field itself is the z array also of shape(ny, nx). There is an - * optional boolean mask; if it exists then it also has shape(ny, nx). The - * mask applies to grid points rather than quads. - * - * How quads are masked based on the point mask is determined by the boolean - * 'corner_mask' flag. If false then any quad that has one or more of its four - * corner points masked is itself masked. If true the behaviour is the same - * except that any quad which has exactly one of its four corner points masked - * has only the triangular corner (half of the quad) adjacent to that point - * masked; the opposite triangular corner has three unmasked points and is not - * masked. - * - * By default the entire domain of nx*ny points is contoured together which can - * result in some very long polygons. The alternative is to break up the - * domain into subdomains or 'chunks' of smaller size, each of which is - * independently contoured. The size of these chunks is controlled by the - * 'nchunk' (or 'chunk_size') parameter. Chunking not only results in shorter - * polygons but also requires slightly less RAM. It can result in rendering - * artifacts though, depending on backend, antialiased flag and alpha value. - * - * Notation - * -------- - * i and j are array indices in the x- and y-directions respectively. Although - * a single element of an array z can be accessed using z[j][i] or z(j,i), it - * is often convenient to use the single quad index z[quad], where - * quad = i + j*nx - * and hence - * i = quad % nx - * j = quad / nx - * - * Rather than referring to x- and y-directions, compass directions are used - * instead such that W, E, S, N refer to the -x, +x, -y, +y directions - * respectively. To move one quad to the E you would therefore add 1 to the - * quad index, to move one quad to the N you would add nx to the quad index. - * - * Cache - * ----- - * Lots of information that is reused during contouring is stored as single - * bits in a mesh-sized cache, indexed by quad. Each quad's cache entry stores - * information about the quad itself such as if it is masked, and about the - * point at the SW corner of the quad, and about the W and S edges. Hence - * information about each point and each edge is only stored once in the cache. - * - * Cache information is divided into two types: that which is constant over the - * lifetime of the QuadContourGenerator, and that which changes for each - * contouring operation. The former is all grid-specific information such - * as quad and corner masks, and which edges are boundaries, either between - * masked and non-masked regions or between adjacent chunks. The latter - * includes whether points lie above or below the current contour levels, plus - * some flags to indicate how the contouring is progressing. - * - * Line Contours - * ------------- - * A line contour connects points with the same z-value. Each point of such a - * contour occurs on an edge of the grid, at a point linearly interpolated to - * the contour z-level from the z-values at the end points of the edge. The - * direction of a line contour is such that higher values are to the left of - * the contour, so any edge that the contour passes through will have a left- - * hand end point with z > contour level and a right-hand end point with - * z <= contour level. - * - * Line contours are of two types. Firstly there are open line strips that - * start on a boundary, traverse the interior of the domain and end on a - * boundary. Secondly there are closed line loops that occur completely within - * the interior of the domain and do not touch a boundary. - * - * The QuadContourGenerator makes two sweeps through the grid to generate line - * contours for a particular level. In the first sweep it looks only for start - * points that occur on boundaries, and when it finds one it follows the - * contour through the interior until it finishes on another boundary edge. - * Each quad that is visited by the algorithm has a 'visited' flag set in the - * cache to indicate that the quad does not need to be visited again. In the - * second sweep all non-visited quads are checked to see if they contain part - * of an interior closed loop, and again each time one is found it is followed - * through the domain interior until it returns back to its start quad and is - * therefore completed. - * - * The situation is complicated by saddle quads that have two opposite corners - * with z >= contour level and the other two corners with z < contour level. - * These therefore contain two segments of a line contour, and the visited - * flags take account of this by only being set on the second visit. On the - * first visit a number of saddle flags are set in the cache to indicate which - * one of the two segments has been completed so far. - * - * Filled Contours - * --------------- - * Filled contours are produced between two contour levels and are always - * closed polygons. They can occur completely within the interior of the - * domain without touching a boundary, following either the lower or upper - * contour levels. Those on the lower level are exactly like interior line - * contours with higher values on the left. Those on the upper level are - * reversed such that higher values are on the right. - * - * Filled contours can also involve a boundary in which case they consist of - * one or more sections along a boundary and one or more sections through the - * interior. Interior sections can be on either level, and again those on the - * upper level have higher values on the right. Boundary sections can remain - * on either contour level or switch between the two. - * - * Once the start of a filled contour is found, the algorithm is similar to - * that for line contours in that it follows the contour to its end, which - * because filled contours are always closed polygons will be by returning - * back to the start. However, because two levels must be considered, each - * level has its own set of saddle and visited flags and indeed some extra - * visited flags for boundary edges. - * - * The major complication for filled contours is that some polygons can be - * holes (with points ordered clockwise) within other polygons (with points - * ordered anticlockwise). When it comes to rendering filled contours each - * non-hole polygon must be rendered along with its zero or more contained - * holes or the rendering will not be correct. The filled contour finding - * algorithm could progress pretty much as the line contour algorithm does, - * taking each polygon as it is found, but then at the end there would have to - * be an extra step to identify the parent non-hole polygon for each hole. - * This is not a particularly onerous task but it does not scale well and can - * easily dominate the execution time of the contour finding for even modest - * problems. It is much better to identity each hole's parent non-hole during - * the sweep algorithm. - * - * This requirement dictates the order that filled contours are identified. As - * the algorithm sweeps up through the grid, every time a polygon passes - * through a quad a ParentCache object is updated with the new possible parent. - * When a new hole polygon is started, the ParentCache is used to find the - * first possible parent in the same quad or to the S of it. Great care is - * needed each time a new quad is checked to see if a new polygon should be - * started, as a single quad can have multiple polygon starts, e.g. a quad - * could be a saddle quad for both lower and upper contour levels, meaning it - * has four contour line segments passing through it which could all be from - * different polygons. The S-most polygon must be started first, then the next - * S-most and so on until the N-most polygon is started in that quad. - */ -#ifndef MPL_CONTOUR_H -#define MPL_CONTOUR_H - -#include "numpy_cpp.h" -#include -#include -#include -#include - - -// Edge of a quad including diagonal edges of masked quads if _corner_mask true. -typedef enum -{ - // Listing values here so easier to check for debug purposes. - Edge_None = -1, - Edge_E = 0, - Edge_N = 1, - Edge_W = 2, - Edge_S = 3, - // The following are only used if _corner_mask is true. - Edge_NE = 4, - Edge_NW = 5, - Edge_SW = 6, - Edge_SE = 7 -} Edge; - -// Combination of a quad and an edge of that quad. -// An invalid quad edge has quad of -1. -struct QuadEdge -{ - QuadEdge(); - QuadEdge(long quad_, Edge edge_); - bool operator<(const QuadEdge& other) const; - bool operator==(const QuadEdge& other) const; - bool operator!=(const QuadEdge& other) const; - friend std::ostream& operator<<(std::ostream& os, - const QuadEdge& quad_edge); - - long quad; - Edge edge; -}; - -// 2D point with x,y coordinates. -struct XY -{ - XY(); - XY(const double& x_, const double& y_); - bool operator==(const XY& other) const; - bool operator!=(const XY& other) const; - XY operator*(const double& multiplier) const; - const XY& operator+=(const XY& other); - const XY& operator-=(const XY& other); - XY operator+(const XY& other) const; - XY operator-(const XY& other) const; - friend std::ostream& operator<<(std::ostream& os, const XY& xy); - - double x, y; -}; - -// A single line of a contour, which may be a closed line loop or an open line -// strip. Identical adjacent points are avoided using push_back(). -// A ContourLine is either a hole (points ordered clockwise) or it is not -// (points ordered anticlockwise). Each hole has a parent ContourLine that is -// not a hole; each non-hole contains zero or more child holes. A non-hole and -// its child holes must be rendered together to obtain the correct results. -class ContourLine : public std::vector -{ -public: - typedef std::list Children; - - ContourLine(bool is_hole); - void add_child(ContourLine* child); - void clear_parent(); - const Children& get_children() const; - const ContourLine* get_parent() const; - ContourLine* get_parent(); - bool is_hole() const; - void push_back(const XY& point); - void set_parent(ContourLine* parent); - void write() const; - -private: - bool _is_hole; - ContourLine* _parent; // Only set if is_hole, not owned. - Children _children; // Only set if !is_hole, not owned. -}; - - -// A Contour is a collection of zero or more ContourLines. -class Contour : public std::vector -{ -public: - Contour(); - virtual ~Contour(); - void delete_contour_lines(); - void write() const; -}; - - -// Single chunk of ContourLine parents, indexed by quad. As a chunk's filled -// contours are created, the ParentCache is updated each time a ContourLine -// passes through each quad. When a new ContourLine is created, if it is a -// hole its parent ContourLine is read from the ParentCache by looking at the -// start quad, then each quad to the S in turn until a non-zero ContourLine is -// found. -class ParentCache -{ -public: - ParentCache(long nx, long x_chunk_points, long y_chunk_points); - ContourLine* get_parent(long quad); - void set_chunk_starts(long istart, long jstart); - void set_parent(long quad, ContourLine& contour_line); - -private: - long quad_to_index(long quad) const; - - long _nx; - long _x_chunk_points, _y_chunk_points; // Number of points not quads. - std::vector _lines; // Not owned. - long _istart, _jstart; -}; - - -// See overview of algorithm at top of file. -class QuadContourGenerator -{ -public: - typedef numpy::array_view CoordinateArray; - typedef numpy::array_view MaskArray; - - // Constructor with optional mask. - // x, y, z: double arrays of shape (ny,nx). - // mask: boolean array, ether empty (if no mask), or of shape (ny,nx). - // corner_mask: flag for different masking behaviour. - // chunk_size: 0 for no chunking, or +ve integer for size of chunks that - // the domain is subdivided into. - QuadContourGenerator(const CoordinateArray& x, - const CoordinateArray& y, - const CoordinateArray& z, - const MaskArray& mask, - bool corner_mask, - long chunk_size); - - // Destructor. - ~QuadContourGenerator(); - - // Create and return polygons for a line (i.e. non-filled) contour at the - // specified level. - PyObject* create_contour(const double& level); - - // Create and return polygons for a filled contour between the two - // specified levels. - PyObject* create_filled_contour(const double& lower_level, - const double& upper_level); - -private: - // Typedef for following either a boundary of the domain or the interior; - // clearer than using a boolean. - typedef enum - { - Boundary, - Interior - } BoundaryOrInterior; - - // Typedef for direction of movement from one quad to the next. - typedef enum - { - Dir_Right = -1, - Dir_Straight = 0, - Dir_Left = +1 - } Dir; - - // Typedef for a polygon being a hole or not; clearer than using a boolean. - typedef enum - { - NotHole, - Hole - } HoleOrNot; - - // Append a C++ ContourLine to the end of a python list. Used for line - // contours where each ContourLine is converted to a separate numpy array - // of (x,y) points. - // Clears the ContourLine too. - void append_contour_line_to_vertices(ContourLine& contour_line, - PyObject* vertices_list) const; - - // Append a C++ Contour to the end of two python lists. Used for filled - // contours where each non-hole ContourLine and its child holes are - // represented by a numpy array of (x,y) points and a second numpy array of - // 'kinds' or 'codes' that indicates where the points array is split into - // individual polygons. - // Clears the Contour too, freeing each ContourLine as soon as possible - // for minimum RAM usage. - void append_contour_to_vertices_and_codes(Contour& contour, - PyObject* vertices_list, - PyObject* codes_list) const; - - // Return number of chunks that fit in the specified point_count. - long calc_chunk_count(long point_count) const; - - // Return the point on the specified QuadEdge that intersects the specified - // level. - XY edge_interp(const QuadEdge& quad_edge, const double& level); - - // Follow a contour along a boundary, appending points to the ContourLine - // as it progresses. Only called for filled contours. Stops when the - // contour leaves the boundary to move into the interior of the domain, or - // when the start_quad_edge is reached in which case the ContourLine is a - // completed closed loop. Always adds the end point of each boundary edge - // to the ContourLine, regardless of whether moving to another boundary - // edge or leaving the boundary into the interior. Never adds the start - // point of the first boundary edge to the ContourLine. - // contour_line: ContourLine to append points to. - // quad_edge: on entry the QuadEdge to start from, on exit the QuadEdge - // that is stopped on. - // lower_level: lower contour z-value. - // upper_level: upper contour z-value. - // level_index: level index started on (1 = lower, 2 = upper level). - // start_quad_edge: QuadEdge that the ContourLine started from, which is - // used to check if the ContourLine is finished. - // Returns the end level_index. - unsigned int follow_boundary(ContourLine& contour_line, - QuadEdge& quad_edge, - const double& lower_level, - const double& upper_level, - unsigned int level_index, - const QuadEdge& start_quad_edge); - - // Follow a contour across the interior of the domain, appending points to - // the ContourLine as it progresses. Called for both line and filled - // contours. Stops when the contour reaches a boundary or, if the - // start_quad_edge is specified, when quad_edge == start_quad_edge and - // level_index == start_level_index. Always adds the end point of each - // quad traversed to the ContourLine; only adds the start point of the - // first quad if want_initial_point flag is true. - // contour_line: ContourLine to append points to. - // quad_edge: on entry the QuadEdge to start from, on exit the QuadEdge - // that is stopped on. - // level_index: level index started on (1 = lower, 2 = upper level). - // level: contour z-value. - // want_initial_point: whether want to append the initial point to the - // ContourLine or not. - // start_quad_edge: the QuadEdge that the ContourLine started from to - // check if the ContourLine is finished, or 0 if no check should occur. - // start_level_index: the level_index that the ContourLine started from. - // set_parents: whether should set ParentCache as it progresses or not. - // This is true for filled contours, false for line contours. - void follow_interior(ContourLine& contour_line, - QuadEdge& quad_edge, - unsigned int level_index, - const double& level, - bool want_initial_point, - const QuadEdge* start_quad_edge, - unsigned int start_level_index, - bool set_parents); - - // Return the index limits of a particular chunk. - void get_chunk_limits(long ijchunk, - long& ichunk, - long& jchunk, - long& istart, - long& iend, - long& jstart, - long& jend); - - // Check if a contour starts within the specified corner quad on the - // specified level_index, and if so return the start edge. Otherwise - // return Edge_None. - Edge get_corner_start_edge(long quad, unsigned int level_index) const; - - // Return index of point at start or end of specified QuadEdge, assuming - // anticlockwise ordering around non-masked quads. - long get_edge_point_index(const QuadEdge& quad_edge, bool start) const; - - // Return the edge to exit a quad from, given the specified entry quad_edge - // and direction to move in. - Edge get_exit_edge(const QuadEdge& quad_edge, Dir dir) const; - - // Return the (x,y) coordinates of the specified point index. - XY get_point_xy(long point) const; - - // Return the z-value of the specified point index. - const double& get_point_z(long point) const; - - // Check if a contour starts within the specified non-corner quad on the - // specified level_index, and if so return the start edge. Otherwise - // return Edge_None. - Edge get_quad_start_edge(long quad, unsigned int level_index) const; - - // Check if a contour starts within the specified quad, whether it is a - // corner or a full quad, and if so return the start edge. Otherwise - // return Edge_None. - Edge get_start_edge(long quad, unsigned int level_index) const; - - // Initialise the cache to contain grid information that is constant - // across the lifetime of this object, i.e. does not vary between calls to - // create_contour() and create_filled_contour(). - void init_cache_grid(const MaskArray& mask); - - // Initialise the cache with information that is specific to contouring the - // specified two levels. The levels are the same for contour lines, - // different for filled contours. - void init_cache_levels(const double& lower_level, - const double& upper_level); - - // Return the (x,y) point at which the level intersects the line connecting - // the two specified point indices. - XY interp(long point1, long point2, const double& level) const; - - // Return true if the specified QuadEdge is a boundary, i.e. is either an - // edge between a masked and non-masked quad/corner or is a chunk boundary. - bool is_edge_a_boundary(const QuadEdge& quad_edge) const; - - // Follow a boundary from one QuadEdge to the next in an anticlockwise - // manner around the non-masked region. - void move_to_next_boundary_edge(QuadEdge& quad_edge) const; - - // Move from the quad specified by quad_edge.quad to the neighbouring quad - // by crossing the edge specified by quad_edge.edge. - void move_to_next_quad(QuadEdge& quad_edge) const; - - // Check for filled contours starting within the specified quad and - // complete any that are found, appending them to the specified Contour. - void single_quad_filled(Contour& contour, - long quad, - const double& lower_level, - const double& upper_level); - - // Start and complete a filled contour line. - // quad: index of quad to start ContourLine in. - // edge: edge of quad to start ContourLine from. - // start_level_index: the level_index that the ContourLine starts from. - // hole_or_not: whether the ContourLine is a hole or not. - // boundary_or_interior: whether the ContourLine starts on a boundary or - // the interior. - // lower_level: lower contour z-value. - // upper_level: upper contour z-value. - // Returns newly created ContourLine. - ContourLine* start_filled(long quad, - Edge edge, - unsigned int start_level_index, - HoleOrNot hole_or_not, - BoundaryOrInterior boundary_or_interior, - const double& lower_level, - const double& upper_level); - - // Start and complete a line contour that both starts and end on a - // boundary, traversing the interior of the domain. - // vertices_list: Python list that the ContourLine should be appended to. - // quad: index of quad to start ContourLine in. - // edge: boundary edge to start ContourLine from. - // level: contour z-value. - // Returns true if the start quad does not need to be visited again, i.e. - // VISITED(quad,1). - bool start_line(PyObject* vertices_list, - long quad, - Edge edge, - const double& level); - - // Debug function that writes the cache status to stdout. - void write_cache(bool grid_only = false) const; - - // Debug function that writes that cache status for a single quad to - // stdout. - void write_cache_quad(long quad, bool grid_only) const; - - - - // Note that mask is not stored as once it has been used to initialise the - // cache it is no longer needed. - CoordinateArray _x, _y, _z; - long _nx, _ny; // Number of points in each direction. - long _n; // Total number of points (and hence quads). - - bool _corner_mask; - long _chunk_size; // Number of quads per chunk (not points). - // Always > 0, unlike python nchunk which is 0 - // for no chunking. - - long _nxchunk, _nychunk; // Number of chunks in each direction. - long _chunk_count; // Total number of chunks. - - typedef uint32_t CacheItem; - CacheItem* _cache; - - ParentCache _parent_cache; // On W quad sides. -}; - -#endif // _CONTOUR_H diff --git a/src/_contour_wrapper.cpp b/src/_contour_wrapper.cpp deleted file mode 100644 index da891eb70048..000000000000 --- a/src/_contour_wrapper.cpp +++ /dev/null @@ -1,188 +0,0 @@ -#include "_contour.h" -#include "mplutils.h" -#include "py_converters.h" -#include "py_exceptions.h" - -/* QuadContourGenerator */ - -typedef struct -{ - PyObject_HEAD - QuadContourGenerator* ptr; -} PyQuadContourGenerator; - -static PyTypeObject PyQuadContourGeneratorType; - -static PyObject* PyQuadContourGenerator_new(PyTypeObject* type, PyObject* args, PyObject* kwds) -{ - PyQuadContourGenerator* self; - self = (PyQuadContourGenerator*)type->tp_alloc(type, 0); - self->ptr = NULL; - return (PyObject*)self; -} - -const char* PyQuadContourGenerator_init__doc__ = - "QuadContourGenerator(x, y, z, mask, corner_mask, chunk_size)\n" - "--\n\n" - "Create a new C++ QuadContourGenerator object\n"; - -static int PyQuadContourGenerator_init(PyQuadContourGenerator* self, PyObject* args, PyObject* kwds) -{ - QuadContourGenerator::CoordinateArray x, y, z; - QuadContourGenerator::MaskArray mask; - bool corner_mask; - long chunk_size; - - if (!PyArg_ParseTuple(args, "O&O&O&O&O&l", - &x.converter_contiguous, &x, - &y.converter_contiguous, &y, - &z.converter_contiguous, &z, - &mask.converter_contiguous, &mask, - &convert_bool, &corner_mask, - &chunk_size)) { - return -1; - } - - if (x.empty() || y.empty() || z.empty() || - y.dim(0) != x.dim(0) || z.dim(0) != x.dim(0) || - y.dim(1) != x.dim(1) || z.dim(1) != x.dim(1)) { - PyErr_SetString(PyExc_ValueError, - "x, y and z must all be 2D arrays with the same dimensions"); - return -1; - } - - if (z.dim(0) < 2 || z.dim(1) < 2) { - PyErr_SetString(PyExc_ValueError, - "x, y and z must all be at least 2x2 arrays"); - return -1; - } - - // Mask array is optional, if set must be same size as other arrays. - if (!mask.empty() && (mask.dim(0) != x.dim(0) || mask.dim(1) != x.dim(1))) { - PyErr_SetString(PyExc_ValueError, - "If mask is set it must be a 2D array with the same dimensions as x."); - return -1; - } - - CALL_CPP_INIT("QuadContourGenerator", - (self->ptr = new QuadContourGenerator( - x, y, z, mask, corner_mask, chunk_size))); - return 0; -} - -static void PyQuadContourGenerator_dealloc(PyQuadContourGenerator* self) -{ - delete self->ptr; - Py_TYPE(self)->tp_free((PyObject *)self); -} - -const char* PyQuadContourGenerator_create_contour__doc__ = - "create_contour(level)\n" - "--\n\n" - "Create and return a non-filled contour."; - -static PyObject* PyQuadContourGenerator_create_contour(PyQuadContourGenerator* self, PyObject* args, PyObject* kwds) -{ - double level; - if (!PyArg_ParseTuple(args, "d:create_contour", &level)) { - return NULL; - } - - PyObject* result; - CALL_CPP("create_contour", (result = self->ptr->create_contour(level))); - return result; -} - -const char* PyQuadContourGenerator_create_filled_contour__doc__ = - "create_filled_contour(lower_level, upper_level)\n" - "--\n\n" - "Create and return a filled contour"; - -static PyObject* PyQuadContourGenerator_create_filled_contour(PyQuadContourGenerator* self, PyObject* args, PyObject* kwds) -{ - double lower_level, upper_level; - if (!PyArg_ParseTuple(args, "dd:create_filled_contour", - &lower_level, &upper_level)) { - return NULL; - } - - if (lower_level >= upper_level) - { - PyErr_SetString(PyExc_ValueError, - "filled contour levels must be increasing"); - return NULL; - } - - PyObject* result; - CALL_CPP("create_filled_contour", - (result = self->ptr->create_filled_contour(lower_level, - upper_level))); - return result; -} - -static PyTypeObject* PyQuadContourGenerator_init_type(PyObject* m, PyTypeObject* type) -{ - static PyMethodDef methods[] = { - {"create_contour", (PyCFunction)PyQuadContourGenerator_create_contour, METH_VARARGS, PyQuadContourGenerator_create_contour__doc__}, - {"create_filled_contour", (PyCFunction)PyQuadContourGenerator_create_filled_contour, METH_VARARGS, PyQuadContourGenerator_create_filled_contour__doc__}, - {NULL} - }; - - memset(type, 0, sizeof(PyTypeObject)); - type->tp_name = "matplotlib.QuadContourGenerator"; - type->tp_doc = PyQuadContourGenerator_init__doc__; - type->tp_basicsize = sizeof(PyQuadContourGenerator); - type->tp_dealloc = (destructor)PyQuadContourGenerator_dealloc; - type->tp_flags = Py_TPFLAGS_DEFAULT; - type->tp_methods = methods; - type->tp_new = PyQuadContourGenerator_new; - type->tp_init = (initproc)PyQuadContourGenerator_init; - - if (PyType_Ready(type) < 0) { - return NULL; - } - - if (PyModule_AddObject(m, "QuadContourGenerator", (PyObject*)type)) { - return NULL; - } - - return type; -} - - -/* Module */ - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_contour", - NULL, - 0, - NULL, - NULL, - NULL, - NULL, - NULL -}; - -#pragma GCC visibility push(default) - -PyMODINIT_FUNC PyInit__contour(void) -{ - PyObject *m; - - m = PyModule_Create(&moduledef); - - if (m == NULL) { - return NULL; - } - - if (!PyQuadContourGenerator_init_type(m, &PyQuadContourGeneratorType)) { - return NULL; - } - - import_array(); - - return m; -} - -#pragma GCC visibility pop diff --git a/src/_image.cpp b/src/_image.cpp deleted file mode 100644 index 28e509a4a445..000000000000 --- a/src/_image.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -#define NO_IMPORT_ARRAY - -#include - -// utilities for irregular grids -void _bin_indices_middle( - unsigned int *irows, int nrows, const float *ys1, unsigned long ny, float dy, float y_min) -{ - int i, j, j_last; - unsigned int *rowstart = irows; - const float *ys2 = ys1 + 1; - const float *yl = ys1 + ny; - float yo = y_min + dy / 2.0f; - float ym = 0.5f * (*ys1 + *ys2); - // y/rows - j = 0; - j_last = j; - for (i = 0; i < nrows; i++, yo += dy, rowstart++) { - while (ys2 != yl && yo > ym) { - ys1 = ys2; - ys2 = ys1 + 1; - ym = 0.5f * (*ys1 + *ys2); - j++; - } - *rowstart = j - j_last; - j_last = j; - } -} - -void _bin_indices_middle_linear(float *arows, - unsigned int *irows, - int nrows, - const float *y, - unsigned long ny, - float dy, - float y_min) -{ - int i; - int ii = 0; - int iilast = (int)ny - 1; - float sc = 1 / dy; - int iy0 = (int)floor(sc * (y[ii] - y_min)); - int iy1 = (int)floor(sc * (y[ii + 1] - y_min)); - float invgap = 1.0f / (iy1 - iy0); - for (i = 0; i < nrows && i <= iy0; i++) { - irows[i] = 0; - arows[i] = 1.0; - } - for (; i < nrows; i++) { - while (i > iy1 && ii < iilast) { - ii++; - iy0 = iy1; - iy1 = (int)floor(sc * (y[ii + 1] - y_min)); - invgap = 1.0f / (iy1 - iy0); - } - if (i >= iy0 && i <= iy1) { - irows[i] = ii; - arows[i] = (iy1 - i) * invgap; - } else - break; - } - for (; i < nrows; i++) { - irows[i] = iilast - 1; - arows[i] = 0.0; - } -} - -void _bin_indices(int *irows, int nrows, const double *y, unsigned long ny, double sc, double offs) -{ - int i; - if (sc * (y[ny - 1] - y[0]) > 0) { - int ii = 0; - int iilast = (int)ny - 1; - int iy0 = (int)floor(sc * (y[ii] - offs)); - int iy1 = (int)floor(sc * (y[ii + 1] - offs)); - for (i = 0; i < nrows && i < iy0; i++) { - irows[i] = -1; - } - for (; i < nrows; i++) { - while (i > iy1 && ii < iilast) { - ii++; - iy0 = iy1; - iy1 = (int)floor(sc * (y[ii + 1] - offs)); - } - if (i >= iy0 && i <= iy1) - irows[i] = ii; - else - break; - } - for (; i < nrows; i++) { - irows[i] = -1; - } - } else { - int iilast = (int)ny - 1; - int ii = iilast; - int iy0 = (int)floor(sc * (y[ii] - offs)); - int iy1 = (int)floor(sc * (y[ii - 1] - offs)); - for (i = 0; i < nrows && i < iy0; i++) { - irows[i] = -1; - } - for (; i < nrows; i++) { - while (i > iy1 && ii > 1) { - ii--; - iy0 = iy1; - iy1 = (int)floor(sc * (y[ii - 1] - offs)); - } - if (i >= iy0 && i <= iy1) - irows[i] = ii - 1; - else - break; - } - for (; i < nrows; i++) { - irows[i] = -1; - } - } -} diff --git a/src/_image.h b/src/_image.h deleted file mode 100644 index 37a080fff1d4..000000000000 --- a/src/_image.h +++ /dev/null @@ -1,198 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -/* image.h - * - */ - -#ifndef MPL_IMAGE_H -#define MPL_IMAGE_H - -#include - - -// utilities for irregular grids -void _bin_indices_middle( - unsigned int *irows, int nrows, const float *ys1, unsigned long ny, float dy, float y_min); -void _bin_indices_middle_linear(float *arows, - unsigned int *irows, - int nrows, - const float *y, - unsigned long ny, - float dy, - float y_min); -void _bin_indices(int *irows, int nrows, const double *y, unsigned long ny, double sc, double offs); - -template -void pcolor(CoordinateArray &x, - CoordinateArray &y, - ColorArray &d, - unsigned int rows, - unsigned int cols, - float bounds[4], - int interpolation, - OutputArray &out) -{ - if (rows >= 32768 || cols >= 32768) { - throw std::runtime_error("rows and cols must both be less than 32768"); - } - - float x_min = bounds[0]; - float x_max = bounds[1]; - float y_min = bounds[2]; - float y_max = bounds[3]; - float width = x_max - x_min; - float height = y_max - y_min; - float dx = width / ((float)cols); - float dy = height / ((float)rows); - - // Check we have something to output to - if (rows == 0 || cols == 0) { - throw std::runtime_error("Cannot scale to zero size"); - } - - if (d.dim(2) != 4) { - throw std::runtime_error("data must be in RGBA format"); - } - - // Check dimensions match - unsigned long nx = x.dim(0); - unsigned long ny = y.dim(0); - if (nx != (unsigned long)d.dim(1) || ny != (unsigned long)d.dim(0)) { - throw std::runtime_error("data and axis dimensions do not match"); - } - - // Allocate memory for pointer arrays - std::vector rowstarts(rows); - std::vector colstarts(cols); - - // Calculate the pointer arrays to map input x to output x - unsigned int i, j; - unsigned int *colstart = &colstarts[0]; - unsigned int *rowstart = &rowstarts[0]; - const float *xs1 = x.data(); - const float *ys1 = y.data(); - - // Copy data to output buffer - const unsigned char *start; - const unsigned char *inposition; - size_t inrowsize = nx * 4; - size_t rowsize = cols * 4; - unsigned char *position = (unsigned char *)out.data(); - unsigned char *oldposition = NULL; - start = d.data(); - - if (interpolation == NEAREST) { - _bin_indices_middle(colstart, cols, xs1, nx, dx, x_min); - _bin_indices_middle(rowstart, rows, ys1, ny, dy, y_min); - for (i = 0; i < rows; i++, rowstart++) { - if (i > 0 && *rowstart == 0) { - memcpy(position, oldposition, rowsize * sizeof(unsigned char)); - oldposition = position; - position += rowsize; - } else { - oldposition = position; - start += *rowstart * inrowsize; - inposition = start; - for (j = 0, colstart = &colstarts[0]; j < cols; j++, position += 4, colstart++) { - inposition += *colstart * 4; - memcpy(position, inposition, 4 * sizeof(unsigned char)); - } - } - } - } else if (interpolation == BILINEAR) { - std::vector acols(cols); - std::vector arows(rows); - - _bin_indices_middle_linear(&acols[0], colstart, cols, xs1, nx, dx, x_min); - _bin_indices_middle_linear(&arows[0], rowstart, rows, ys1, ny, dy, y_min); - double a00, a01, a10, a11, alpha, beta; - - // Copy data to output buffer - for (i = 0; i < rows; i++) { - for (j = 0; j < cols; j++) { - alpha = arows[i]; - beta = acols[j]; - - a00 = alpha * beta; - a01 = alpha * (1.0 - beta); - a10 = (1.0 - alpha) * beta; - a11 = 1.0 - a00 - a01 - a10; - - for (size_t k = 0; k < 4; ++k) { - position[k] = - d(rowstart[i], colstart[j], k) * a00 + - d(rowstart[i], colstart[j] + 1, k) * a01 + - d(rowstart[i] + 1, colstart[j], k) * a10 + - d(rowstart[i] + 1, colstart[j] + 1, k) * a11; - } - position += 4; - } - } - } -} - -template -void pcolor2(CoordinateArray &x, - CoordinateArray &y, - ColorArray &d, - unsigned int rows, - unsigned int cols, - float bounds[4], - Color &bg, - OutputArray &out) -{ - double x_left = bounds[0]; - double x_right = bounds[1]; - double y_bot = bounds[2]; - double y_top = bounds[3]; - - // Check we have something to output to - if (rows == 0 || cols == 0) { - throw std::runtime_error("rows or cols is zero; there are no pixels"); - } - - if (d.dim(2) != 4) { - throw std::runtime_error("data must be in RGBA format"); - } - - // Check dimensions match - unsigned long nx = x.dim(0); - unsigned long ny = y.dim(0); - if (nx != (unsigned long)d.dim(1) + 1 || ny != (unsigned long)d.dim(0) + 1) { - throw std::runtime_error("data and axis bin boundary dimensions are incompatible"); - } - - if (bg.dim(0) != 4) { - throw std::runtime_error("bg must be in RGBA format"); - } - - std::vector irows(rows); - std::vector jcols(cols); - - // Calculate the pointer arrays to map input x to output x - size_t i, j; - const double *x0 = x.data(); - const double *y0 = y.data(); - double sx = cols / (x_right - x_left); - double sy = rows / (y_top - y_bot); - _bin_indices(&jcols[0], cols, x0, nx, sx, x_left); - _bin_indices(&irows[0], rows, y0, ny, sy, y_bot); - - // Copy data to output buffer - unsigned char *position = (unsigned char *)out.data(); - - for (i = 0; i < rows; i++) { - for (j = 0; j < cols; j++) { - if (irows[i] == -1 || jcols[j] == -1) { - memcpy(position, (const unsigned char *)bg.data(), 4 * sizeof(unsigned char)); - } else { - for (size_t k = 0; k < 4; ++k) { - position[k] = d(irows[i], jcols[j], k); - } - } - position += 4; - } - } -} - -#endif diff --git a/src/_image_resample.h b/src/_image_resample.h index a508afa33ee4..99cedd9b2c93 100644 --- a/src/_image_resample.h +++ b/src/_image_resample.h @@ -25,7 +25,7 @@ //---------------------------------------------------------------------------- // Anti-Grain Geometry - Version 2.4 -// Copyright (C) 2002-2005 Maxim Shemanarev (http://www.antigrain.com) +// Copyright (C) 2002-2005 Maxim Shemanarev (http://antigrain.com/) // // Permission to copy, use, modify, sell and distribute this software // is granted provided this copyright notice appears in all copies. @@ -35,7 +35,7 @@ //---------------------------------------------------------------------------- // Contact: mcseem@antigrain.com // mcseemagg@yahoo.com -// http://www.antigrain.com +// http://antigrain.com/ //---------------------------------------------------------------------------- // // Adaptation for high precision colors has been sponsored by diff --git a/src/_image_wrapper.cpp b/src/_image_wrapper.cpp index 73d093aa3b18..9eba0249d3e9 100644 --- a/src/_image_wrapper.cpp +++ b/src/_image_wrapper.cpp @@ -1,6 +1,5 @@ #include "mplutils.h" #include "_image_resample.h" -#include "_image.h" #include "numpy_cpp.h" #include "py_converters.h" @@ -10,7 +9,8 @@ * */ const char* image_resample__doc__ = -"resample(input_array, output_array, matrix, interpolation=NEAREST, alpha=1.0, norm=False, radius=1)\n\n" +"resample(input_array, output_array, matrix, interpolation=NEAREST, alpha=1.0, norm=False, radius=1)\n" +"--\n\n" "Resample input_array, blending it in-place into output_array, using an\n" "affine transformation.\n\n" @@ -290,111 +290,13 @@ image_resample(PyObject *self, PyObject* args, PyObject *kwargs) return NULL; } - -const char *image_pcolor__doc__ = - "pcolor(x, y, data, rows, cols, bounds)\n" - "\n" - "Generate a pseudo-color image from data on a non-uniform grid using\n" - "nearest neighbour or linear interpolation.\n" - "bounds = (x_min, x_max, y_min, y_max)\n" - "interpolation = NEAREST or BILINEAR \n"; - -static PyObject *image_pcolor(PyObject *self, PyObject *args, PyObject *kwds) -{ - numpy::array_view x; - numpy::array_view y; - numpy::array_view d; - npy_intp rows, cols; - float bounds[4]; - int interpolation; - - if (!PyArg_ParseTuple(args, - "O&O&O&nn(ffff)i:pcolor", - &x.converter, - &x, - &y.converter, - &y, - &d.converter_contiguous, - &d, - &rows, - &cols, - &bounds[0], - &bounds[1], - &bounds[2], - &bounds[3], - &interpolation)) { - return NULL; - } - - npy_intp dim[3] = {rows, cols, 4}; - numpy::array_view output(dim); - - CALL_CPP("pcolor", (pcolor(x, y, d, rows, cols, bounds, interpolation, output))); - - return output.pyobj(); -} - -const char *image_pcolor2__doc__ = - "pcolor2(x, y, data, rows, cols, bounds, bg)\n" - "\n" - "Generate a pseudo-color image from data on a non-uniform grid\n" - "specified by its cell boundaries.\n" - "bounds = (x_left, x_right, y_bot, y_top)\n" - "bg = ndarray of 4 uint8 representing background rgba\n"; - -static PyObject *image_pcolor2(PyObject *self, PyObject *args, PyObject *kwds) -{ - numpy::array_view x; - numpy::array_view y; - numpy::array_view d; - npy_intp rows, cols; - float bounds[4]; - numpy::array_view bg; - - if (!PyArg_ParseTuple(args, - "O&O&O&nn(ffff)O&:pcolor2", - &x.converter_contiguous, - &x, - &y.converter_contiguous, - &y, - &d.converter_contiguous, - &d, - &rows, - &cols, - &bounds[0], - &bounds[1], - &bounds[2], - &bounds[3], - &bg.converter, - &bg)) { - return NULL; - } - - npy_intp dim[3] = {rows, cols, 4}; - numpy::array_view output(dim); - - CALL_CPP("pcolor2", (pcolor2(x, y, d, rows, cols, bounds, bg, output))); - - return output.pyobj(); -} - static PyMethodDef module_functions[] = { {"resample", (PyCFunction)image_resample, METH_VARARGS|METH_KEYWORDS, image_resample__doc__}, - {"pcolor", (PyCFunction)image_pcolor, METH_VARARGS, image_pcolor__doc__}, - {"pcolor2", (PyCFunction)image_pcolor2, METH_VARARGS, image_pcolor2__doc__}, {NULL} }; static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_image", - NULL, - 0, - module_functions, - NULL, - NULL, - NULL, - NULL + PyModuleDef_HEAD_INIT, "_image", NULL, 0, module_functions, }; #pragma GCC visibility push(default) @@ -403,6 +305,8 @@ PyMODINIT_FUNC PyInit__image(void) { PyObject *m; + import_array(); + m = PyModule_Create(&moduledef); if (m == NULL) { @@ -427,11 +331,10 @@ PyMODINIT_FUNC PyInit__image(void) PyModule_AddIntConstant(m, "LANCZOS", LANCZOS) || PyModule_AddIntConstant(m, "BLACKMAN", BLACKMAN) || PyModule_AddIntConstant(m, "_n_interpolation", _n_interpolation)) { + Py_DECREF(m); return NULL; } - import_array(); - return m; } diff --git a/src/_macosx.m b/src/_macosx.m index 190157811edc..dcb236a861f3 100755 --- a/src/_macosx.m +++ b/src/_macosx.m @@ -3,6 +3,7 @@ #include #include #include +#include "mplutils.h" #ifndef PYPY /* Remove this once Python is fixed: https://bugs.python.org/issue23237 */ @@ -11,43 +12,10 @@ /* Proper way to check for the OS X version we are compiling for, from * https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/cross_development/Using/using.html - */ -#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 1070 -#define COMPILING_FOR_10_7 -#endif -#if __MAC_OS_X_VERSION_MIN_REQUIRED >= 101000 -#define COMPILING_FOR_10_10 -#endif - -#if __MAC_OS_X_VERSION_MIN_REQUIRED < 101200 -/* A lot of symbols were renamed in Sierra and cause deprecation warnings - so define macros for the new names if we are compiling on an older SDK */ -#define NSEventMaskAny NSAnyEventMask -#define NSEventTypeApplicationDefined NSApplicationDefined -#define NSEventModifierFlagCommand NSCommandKeyMask -#define NSEventModifierFlagControl NSControlKeyMask -#define NSEventModifierFlagOption NSAlternateKeyMask -#define NSEventModifierFlagShift NSShiftKeyMask -#define NSEventTypeKeyUp NSKeyUp -#define NSEventTypeKeyDown NSKeyDown -#define NSEventTypeMouseMoved NSMouseMoved -#define NSEventTypeLeftMouseDown NSLeftMouseDown -#define NSEventTypeRightMouseDown NSRightMouseDown -#define NSEventTypeOtherMouseDown NSOtherMouseDown -#define NSEventTypeLeftMouseDragged NSLeftMouseDragged -#define NSEventTypeRightMouseDragged NSRightMouseDragged -#define NSEventTypeOtherMouseDragged NSOtherMouseDragged -#define NSEventTypeLeftMouseUp NSLeftMouseUp -#define NSEventTypeRightMouseUp NSRightMouseUp -#define NSEventTypeOtherMouseUp NSOtherMouseUp -#define NSWindowStyleMaskClosable NSClosableWindowMask -#define NSWindowStyleMaskMiniaturizable NSMiniaturizableWindowMask -#define NSWindowStyleMaskResizable NSResizableWindowMask -#define NSWindowStyleMaskTitled NSTitledWindowMask -#endif + * Renamed symbols cause deprecation warnings, so define macros for the new + * names if we are compiling on an older SDK */ #if __MAC_OS_X_VERSION_MIN_REQUIRED < 101400 -/* A few more deprecations in Mojave */ #define NSButtonTypeMomentaryLight NSMomentaryLightButton #define NSButtonTypePushOnPushOff NSPushOnPushOffButton #define NSBezelStyleShadowlessSquare NSShadowlessSquareBezelStyle @@ -64,6 +32,20 @@ Needed to know when to stop the NSApp */ static long FigureWindowCount = 0; +/* Keep track of modifier key states for flagsChanged + to keep track of press vs release */ +static bool lastCommand = false; +static bool lastControl = false; +static bool lastShift = false; +static bool lastOption = false; +static bool lastCapsLock = false; +/* Keep track of whether this specific key modifier was pressed or not */ +static bool keyChangeCommand = false; +static bool keyChangeControl = false; +static bool keyChangeShift = false; +static bool keyChangeOption = false; +static bool keyChangeCapsLock = false; + /* -------------------------- Helper function ---------------------------- */ static void @@ -96,7 +78,8 @@ static void _sigint_callback(CFSocketRef s, CFRunLoopStop(runloop); } -static CGEventRef _eventtap_callback(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) +static CGEventRef _eventtap_callback( + CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) { CFRunLoopRef runloop = refcon; CFRunLoopStop(runloop); @@ -121,8 +104,8 @@ static int wait_for_stdin(void) #ifdef PYOSINPUTHOOK_REPETITIVE if (!CFReadStreamHasBytesAvailable(stream)) /* This is possible because of how PyOS_InputHook is called from Python */ - { #endif + { int error; int channel[2]; CFSocketRef sigint_socket = NULL; @@ -135,8 +118,7 @@ static int wait_for_stdin(void) &clientContext); CFReadStreamScheduleWithRunLoop(stream, runloop, kCFRunLoopDefaultMode); error = socketpair(AF_UNIX, SOCK_STREAM, 0, channel); - if (error==0) - { + if (!error) { CFSocketContext context; context.version = 0; context.info = &interrupted; @@ -150,15 +132,11 @@ static int wait_for_stdin(void) kCFSocketReadCallBack, _sigint_callback, &context); - if (sigint_socket) - { - CFRunLoopSourceRef source; - source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, - sigint_socket, - 0); + if (sigint_socket) { + CFRunLoopSourceRef source = CFSocketCreateRunLoopSource( + kCFAllocatorDefault, sigint_socket, 0); CFRelease(sigint_socket); - if (source) - { + if (source) { CFRunLoopAddSource(runloop, source, kCFRunLoopDefaultMode); CFRelease(source); sigint_fd = channel[0]; @@ -174,25 +152,22 @@ static int wait_for_stdin(void) untilDate: [NSDate distantPast] inMode: NSDefaultRunLoopMode dequeue: YES]; - if (!event) break; + if (!event) { break; } [NSApp sendEvent: event]; } CFRunLoopRun(); - if (interrupted || CFReadStreamHasBytesAvailable(stream)) break; + if (interrupted || CFReadStreamHasBytesAvailable(stream)) { break; } } - if (py_sigint_handler) PyOS_setsig(SIGINT, py_sigint_handler); - CFReadStreamUnscheduleFromRunLoop(stream, - runloop, - kCFRunLoopCommonModes); - if (sigint_socket) CFSocketInvalidate(sigint_socket); - if (error==0) { + if (py_sigint_handler) { PyOS_setsig(SIGINT, py_sigint_handler); } + CFReadStreamUnscheduleFromRunLoop( + stream, runloop, kCFRunLoopCommonModes); + if (sigint_socket) { CFSocketInvalidate(sigint_socket); } + if (!error) { close(channel[0]); close(channel[1]); } -#ifdef PYOSINPUTHOOK_REPETITIVE } -#endif CFReadStreamClose(stream); CFRelease(stream); if (interrupted) { @@ -221,14 +196,6 @@ - (BOOL)closeButtonPressed; - (void)dealloc; @end -@interface ToolWindow : NSWindow -{ -} -- (ToolWindow*)initWithContentRect:(NSRect)rect master:(NSWindow*)window; -- (void)masterCloses:(NSNotification*)notification; -- (void)close; -@end - @interface View : NSView { PyObject* canvas; NSRect rubberband; @@ -237,6 +204,8 @@ @interface View : NSView } - (void)dealloc; - (void)drawRect:(NSRect)rect; +- (void)updateDevicePixelRatio:(double)scale; +- (void)windowDidChangeBackingProperties:(NSNotification*)notification; - (void)windowDidResize:(NSNotification*)notification; - (View*)initWithFrame:(NSRect)rect; - (void)setCanvas: (PyObject*)newCanvas; @@ -262,17 +231,55 @@ - (void)keyDown:(NSEvent*)event; - (void)keyUp:(NSEvent*)event; - (void)scrollWheel:(NSEvent *)event; - (BOOL)acceptsFirstResponder; -//- (void)flagsChanged:(NSEvent*)event; +- (void)flagsChanged:(NSEvent*)event; @end /* ---------------------------- Python classes ---------------------------- */ +// Acquire the GIL, call a method with no args, discarding the result and +// printing any exception. +static void gil_call_method(PyObject* obj, const char* name) +{ + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject* result = PyObject_CallMethod(obj, name, NULL); + if (result) { + Py_DECREF(result); + } else { + PyErr_Print(); + } + PyGILState_Release(gstate); +} + +void process_event(char const* cls_name, char const* fmt, ...) +{ + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject* module = NULL, * cls = NULL, + * args = NULL, * kwargs = NULL, + * event = NULL, * result = NULL; + va_list argp; + va_start(argp, fmt); + if (!(module = PyImport_ImportModule("matplotlib.backend_bases")) + || !(cls = PyObject_GetAttrString(module, cls_name)) + || !(args = PyTuple_New(0)) + || !(kwargs = Py_VaBuildValue(fmt, argp)) + || !(event = PyObject_Call(cls, args, kwargs)) + || !(result = PyObject_CallMethod(event, "_process", ""))) { + PyErr_Print(); + } + va_end(argp); + Py_XDECREF(module); + Py_XDECREF(cls); + Py_XDECREF(args); + Py_XDECREF(kwargs); + Py_XDECREF(event); + Py_XDECREF(result); + PyGILState_Release(gstate); +} + static bool backend_inited = false; static void lazy_init(void) { - if (backend_inited) { - return; - } + if (backend_inited) { return; } backend_inited = true; NSApp = [NSApplication sharedApplication]; @@ -309,17 +316,54 @@ static CGFloat _get_device_scale(CGContextRef cr) return pixelSize.width; } +int mpl_check_modifier( + NSUInteger modifiers, NSEventModifierFlags flag, + PyObject* list, char const* name) +{ + int status = 0; + if (modifiers & flag) { + PyObject* py_name = NULL; + if (!(py_name = PyUnicode_FromString(name)) + || PyList_Append(list, py_name)) { + status = -1; // failure + } + Py_XDECREF(py_name); + } + return status; +} + +PyObject* mpl_modifiers(NSEvent* event) +{ + PyGILState_STATE gstate = PyGILState_Ensure(); + PyObject* list = NULL; + if (!(list = PyList_New(0))) { + goto exit; + } + NSUInteger modifiers = [event modifierFlags]; + if (mpl_check_modifier(modifiers, NSEventModifierFlagControl, list, "ctrl") + || mpl_check_modifier(modifiers, NSEventModifierFlagOption, list, "alt") + || mpl_check_modifier(modifiers, NSEventModifierFlagShift, list, "shift") + || mpl_check_modifier(modifiers, NSEventModifierFlagCommand, list, "cmd")) { + Py_CLEAR(list); // On failure, return NULL with an exception set. + } +exit: + PyGILState_Release(gstate); + return list; +} + typedef struct { PyObject_HEAD View* view; } FigureCanvas; +static PyTypeObject FigureCanvasType; + static PyObject* FigureCanvas_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { lazy_init(); FigureCanvas *self = (FigureCanvas*)type->tp_alloc(type, 0); - if (!self) return NULL; + if (!self) { return NULL; } self->view = [View alloc]; return (PyObject*)self; } @@ -327,16 +371,27 @@ static CGFloat _get_device_scale(CGContextRef cr) static int FigureCanvas_init(FigureCanvas *self, PyObject *args, PyObject *kwds) { - int width; - int height; - if(!self->view) - { + if (!self->view) { PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); return -1; } - - if(!PyArg_ParseTuple(args, "ii", &width, &height)) return -1; - + PyObject *builtins = NULL, + *super_obj = NULL, + *super_init = NULL, + *init_res = NULL, + *wh = NULL; + // super(FigureCanvasMac, self).__init__(*args, **kwargs) + if (!(builtins = PyImport_AddModule("builtins")) // borrowed. + || !(super_obj = PyObject_CallMethod(builtins, "super", "OO", &FigureCanvasType, self)) + || !(super_init = PyObject_GetAttrString(super_obj, "__init__")) + || !(init_res = PyObject_Call(super_init, args, kwds))) { + goto exit; + } + int width, height; + if (!(wh = PyObject_CallMethod((PyObject*)self, "get_width_height", "")) + || !PyArg_ParseTuple(wh, "ii", &width, &height)) { + goto exit; + } NSRect rect = NSMakeRect(0.0, 0.0, width, height); self->view = [self->view initWithFrame: rect]; self->view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable; @@ -348,17 +403,20 @@ static CGFloat _get_device_scale(CGContextRef cr) owner: self->view userInfo: nil]]; [self->view setCanvas: (PyObject*)self]; - return 0; + +exit: + Py_XDECREF(super_obj); + Py_XDECREF(super_init); + Py_XDECREF(init_res); + Py_XDECREF(wh); + return PyErr_Occurred() ? -1 : 0; } static void FigureCanvas_dealloc(FigureCanvas* self) { - if (self->view) - { - [self->view setCanvas: NULL]; - [self->view release]; - } + [self->view setCanvas: NULL]; + [self->view release]; Py_TYPE(self)->tp_free((PyObject*)self); } @@ -370,41 +428,49 @@ static CGFloat _get_device_scale(CGContextRef cr) } static PyObject* -FigureCanvas_draw(FigureCanvas* self) +FigureCanvas_update(FigureCanvas* self) { - View* view = self->view; - - if(view) /* The figure may have been closed already */ - { - [view display]; - } - + [self->view setNeedsDisplay: YES]; Py_RETURN_NONE; } static PyObject* -FigureCanvas_draw_idle(FigureCanvas* self) +FigureCanvas_flush_events(FigureCanvas* self) { - View* view = self->view; - if(!view) - { - PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); - return NULL; + // We run the app, matching any events that are waiting in the queue + // to process, breaking out of the loop when no events remain and + // displaying the canvas if needed. + NSEvent *event; + while (true) { + event = [NSApp nextEventMatchingMask: NSEventMaskAny + untilDate: [NSDate distantPast] + inMode: NSDefaultRunLoopMode + dequeue: YES]; + if (!event) { + break; + } + [NSApp sendEvent:event]; } - [view setNeedsDisplay: YES]; + [self->view displayIfNeeded]; Py_RETURN_NONE; } static PyObject* -FigureCanvas_flush_events(FigureCanvas* self) +FigureCanvas_set_cursor(PyObject* unused, PyObject* args) { - View* view = self->view; - if(!view) - { - PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); - return NULL; + int i; + if (!PyArg_ParseTuple(args, "i", &i)) { return NULL; } + switch (i) { + case 1: [[NSCursor arrowCursor] set]; break; + case 2: [[NSCursor pointingHandCursor] set]; break; + case 3: [[NSCursor crosshairCursor] set]; break; + case 4: [[NSCursor openHandCursor] set]; break; + /* OSX handles busy state itself so no need to set a cursor here */ + case 5: break; + case 6: [[NSCursor resizeLeftRightCursor] set]; break; + case 7: [[NSCursor resizeUpDownCursor] set]; break; + default: return NULL; } - [view displayIfNeeded]; Py_RETURN_NONE; } @@ -412,41 +478,20 @@ static CGFloat _get_device_scale(CGContextRef cr) FigureCanvas_set_rubberband(FigureCanvas* self, PyObject *args) { View* view = self->view; - int x0, y0, x1, y1; - NSRect rubberband; - if(!view) - { + if (!view) { PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); return NULL; } - if(!PyArg_ParseTuple(args, "iiii", &x0, &y0, &x1, &y1)) return NULL; - + int x0, y0, x1, y1; + if (!PyArg_ParseTuple(args, "iiii", &x0, &y0, &x1, &y1)) { + return NULL; + } x0 /= view->device_scale; x1 /= view->device_scale; y0 /= view->device_scale; y1 /= view->device_scale; - - if (x1 > x0) - { - rubberband.origin.x = x0; - rubberband.size.width = x1 - x0; - } - else - { - rubberband.origin.x = x1; - rubberband.size.width = x0 - x1; - } - if (y1 > y0) - { - rubberband.origin.y = y0; - rubberband.size.height = y1 - y0; - } - else - { - rubberband.origin.y = y1; - rubberband.size.height = y0 - y1; - } - + NSRect rubberband = NSMakeRect(x0 < x1 ? x0 : x1, y0 < y1 ? y0 : y1, + abs(x1 - x0), abs(y1 - y0)); [view setRubberband: rubberband]; Py_RETURN_NONE; } @@ -454,13 +499,7 @@ static CGFloat _get_device_scale(CGContextRef cr) static PyObject* FigureCanvas_remove_rubberband(FigureCanvas* self) { - View* view = self->view; - if(!view) - { - PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); - return NULL; - } - [view removeRubberband]; + [self->view removeRubberband]; Py_RETURN_NONE; } @@ -470,8 +509,9 @@ static CGFloat _get_device_scale(CGContextRef cr) float timeout = 0.0; static char* kwlist[] = {"timeout", NULL}; - if(!PyArg_ParseTupleAndKeywords(args, keywords, "f", kwlist, &timeout)) + if (!PyArg_ParseTupleAndKeywords(args, keywords, "f", kwlist, &timeout)) { return NULL; + } int error; int interrupted = 0; @@ -482,8 +522,7 @@ static CGFloat _get_device_scale(CGContextRef cr) CFRunLoopRef runloop = CFRunLoopGetCurrent(); error = pipe(channel); - if (error==0) - { + if (!error) { CFSocketContext context = {0, NULL, NULL, NULL, NULL}; fcntl(channel[1], F_SETFL, O_WRONLY | O_NONBLOCK); @@ -493,15 +532,11 @@ static CGFloat _get_device_scale(CGContextRef cr) kCFSocketReadCallBack, _sigint_callback, &context); - if (sigint_socket) - { - CFRunLoopSourceRef source; - source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, - sigint_socket, - 0); + if (sigint_socket) { + CFRunLoopSourceRef source = CFSocketCreateRunLoopSource( + kCFAllocatorDefault, sigint_socket, 0); CFRelease(sigint_socket); - if (source) - { + if (source) { CFRunLoopAddSource(runloop, source, kCFRunLoopDefaultMode); CFRelease(source); sigint_fd = channel[1]; @@ -520,15 +555,14 @@ static CGFloat _get_device_scale(CGContextRef cr) untilDate: date inMode: NSDefaultRunLoopMode dequeue: YES]; - if (!event || [event type]==NSEventTypeApplicationDefined) break; + if (!event || [event type]==NSEventTypeApplicationDefined) { break; } [NSApp sendEvent: event]; } - if (py_sigint_handler) PyOS_setsig(SIGINT, py_sigint_handler); - - if (sigint_socket) CFSocketInvalidate(sigint_socket); - if (error==0) close(channel[1]); - if (interrupted) raise(SIGINT); + if (py_sigint_handler) { PyOS_setsig(SIGINT, py_sigint_handler); } + if (sigint_socket) { CFSocketInvalidate(sigint_socket); } + if (!error) { close(channel[1]); } + if (interrupted) { raise(SIGINT); } Py_RETURN_NONE; } @@ -549,87 +583,47 @@ static CGFloat _get_device_scale(CGContextRef cr) Py_RETURN_NONE; } -static PyMethodDef FigureCanvas_methods[] = { - {"draw", - (PyCFunction)FigureCanvas_draw, - METH_NOARGS, - NULL, // docstring inherited. - }, - {"draw_idle", - (PyCFunction)FigureCanvas_draw_idle, - METH_NOARGS, - NULL, // docstring inherited. - }, - {"flush_events", - (PyCFunction)FigureCanvas_flush_events, - METH_NOARGS, - "Flush the GUI events for the figure." - }, - {"set_rubberband", - (PyCFunction)FigureCanvas_set_rubberband, - METH_VARARGS, - "Specifies a new rubberband rectangle and invalidates it." - }, - {"remove_rubberband", - (PyCFunction)FigureCanvas_remove_rubberband, - METH_NOARGS, - "Removes the current rubberband rectangle." - }, - {"start_event_loop", - (PyCFunction)FigureCanvas_start_event_loop, - METH_KEYWORDS | METH_VARARGS, - "Runs the event loop until the timeout or until stop_event_loop is called.\n", - }, - {"stop_event_loop", - (PyCFunction)FigureCanvas_stop_event_loop, - METH_NOARGS, - "Stops the event loop that was started by start_event_loop.\n", - }, - {NULL} /* Sentinel */ -}; - -static char FigureCanvas_doc[] = -"A FigureCanvas object wraps a Cocoa NSView object.\n"; - static PyTypeObject FigureCanvasType = { PyVarObject_HEAD_INIT(NULL, 0) - "_macosx.FigureCanvas", /*tp_name*/ - sizeof(FigureCanvas), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor)FigureCanvas_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - (reprfunc)FigureCanvas_repr, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - FigureCanvas_doc, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - FigureCanvas_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)FigureCanvas_init, /* tp_init */ - 0, /* tp_alloc */ - FigureCanvas_new, /* tp_new */ + .tp_name = "_macosx.FigureCanvas", + .tp_basicsize = sizeof(FigureCanvas), + .tp_dealloc = (destructor)FigureCanvas_dealloc, + .tp_repr = (reprfunc)FigureCanvas_repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_init = (initproc)FigureCanvas_init, + .tp_new = (newfunc)FigureCanvas_new, + .tp_doc = "A FigureCanvas object wraps a Cocoa NSView object.", + .tp_methods = (PyMethodDef[]){ + {"update", + (PyCFunction)FigureCanvas_update, + METH_NOARGS, + NULL}, // docstring inherited + {"flush_events", + (PyCFunction)FigureCanvas_flush_events, + METH_NOARGS, + NULL}, // docstring inherited + {"set_cursor", + (PyCFunction)FigureCanvas_set_cursor, + METH_VARARGS, + "Set the active cursor."}, + {"set_rubberband", + (PyCFunction)FigureCanvas_set_rubberband, + METH_VARARGS, + "Specify a new rubberband rectangle and invalidate it."}, + {"remove_rubberband", + (PyCFunction)FigureCanvas_remove_rubberband, + METH_NOARGS, + "Remove the current rubberband rectangle."}, + {"start_event_loop", + (PyCFunction)FigureCanvas_start_event_loop, + METH_KEYWORDS | METH_VARARGS, + NULL}, // docstring inherited + {"stop_event_loop", + (PyCFunction)FigureCanvas_stop_event_loop, + METH_NOARGS, + NULL}, // docstring inherited + {} // sentinel + }, }; typedef struct { @@ -642,10 +636,9 @@ static CGFloat _get_device_scale(CGContextRef cr) { lazy_init(); Window* window = [Window alloc]; - if (!window) return NULL; + if (!window) { return NULL; } FigureManager *self = (FigureManager*)type->tp_alloc(type, 0); - if (!self) - { + if (!self) { [window release]; return NULL; } @@ -657,42 +650,26 @@ static CGFloat _get_device_scale(CGContextRef cr) static int FigureManager_init(FigureManager *self, PyObject *args, PyObject *kwds) { - NSRect rect; - Window* window; - View* view; - PyObject* size; - int width, height; - PyObject* obj; - FigureCanvas* canvas; - - if(!self->window) - { - PyErr_SetString(PyExc_RuntimeError, "NSWindow* is NULL"); + PyObject* canvas; + if (!PyArg_ParseTuple(args, "O", &canvas)) { return -1; } - if(!PyArg_ParseTuple(args, "O", &obj)) return -1; - - canvas = (FigureCanvas*)obj; - view = canvas->view; - if (!view) /* Something really weird going on */ - { + View* view = ((FigureCanvas*)canvas)->view; + if (!view) { /* Something really weird going on */ PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); return -1; } - size = PyObject_CallMethod(obj, "get_width_height", ""); - if(!size) return -1; - if(!PyArg_ParseTuple(size, "ii", &width, &height)) - { Py_DECREF(size); - return -1; + PyObject* size = PyObject_CallMethod(canvas, "get_width_height", ""); + int width, height; + if (!size || !PyArg_ParseTuple(size, "ii", &width, &height)) { + Py_XDECREF(size); + return -1; } Py_DECREF(size); - rect.origin.x = 100; - rect.origin.y = 350; - rect.size.height = height; - rect.size.width = width; + NSRect rect = NSMakeRect( /* x */ 100, /* y */ 350, width, height); self->window = [self->window initWithContentRect: rect styleMask: NSWindowStyleMaskTitled @@ -702,10 +679,11 @@ static CGFloat _get_device_scale(CGContextRef cr) backing: NSBackingStoreBuffered defer: YES withManager: (PyObject*)self]; - window = self->window; + Window* window = self->window; [window setDelegate: view]; [window makeFirstResponder: view]; [[window contentView] addSubview: view]; + [view updateDevicePixelRatio: [window backingScaleFactor]]; return 0; } @@ -720,34 +698,67 @@ static CGFloat _get_device_scale(CGContextRef cr) static void FigureManager_dealloc(FigureManager* self) { - Window* window = self->window; - if(window) - { - [window close]; - } + [self->window close]; Py_TYPE(self)->tp_free((PyObject*)self); } static PyObject* -FigureManager_show(FigureManager* self) +FigureManager__show(FigureManager* self) { - Window* window = self->window; - if(window) - { - [window makeKeyAndOrderFront: nil]; - [window orderFrontRegardless]; - } + [self->window makeKeyAndOrderFront: nil]; + Py_RETURN_NONE; +} + +static PyObject* +FigureManager__raise(FigureManager* self) +{ + [self->window orderFrontRegardless]; Py_RETURN_NONE; } static PyObject* FigureManager_destroy(FigureManager* self) { - Window* window = self->window; - if(window) - { - [window close]; - self->window = NULL; + [self->window close]; + self->window = NULL; + Py_RETURN_NONE; +} + +static PyObject* +FigureManager_set_icon(PyObject* null, PyObject* args) { + PyObject* icon_path; + if (!PyArg_ParseTuple(args, "O&", &PyUnicode_FSDecoder, &icon_path)) { + return NULL; + } + const char* icon_path_ptr = PyUnicode_AsUTF8(icon_path); + if (!icon_path_ptr) { + Py_DECREF(icon_path); + return NULL; + } + @autoreleasepool { + NSString* ns_icon_path = [NSString stringWithUTF8String: icon_path_ptr]; + Py_DECREF(icon_path); + if (!ns_icon_path) { + PyErr_SetString(PyExc_RuntimeError, "Could not convert to NSString*"); + return NULL; + } + NSImage* image = [[[NSImage alloc] initByReferencingFile: ns_icon_path] autorelease]; + if (!image) { + PyErr_SetString(PyExc_RuntimeError, "Could not create NSImage*"); + return NULL; + } + if (!image.valid) { + PyErr_SetString(PyExc_RuntimeError, "Image is not valid"); + return NULL; + } + @try { + NSApplication* app = [NSApplication sharedApplication]; + app.applicationIconImage = image; + } + @catch (NSException* exception) { + PyErr_SetString(PyExc_RuntimeError, exception.reason.UTF8String); + return NULL; + } } Py_RETURN_NONE; } @@ -760,32 +771,16 @@ static CGFloat _get_device_scale(CGContextRef cr) if (!PyArg_ParseTuple(args, "s", &title)) { return NULL; } - Window* window = self->window; - if(window) - { - NSString* ns_title = [[[NSString alloc] - initWithCString: title - encoding: NSUTF8StringEncoding] autorelease]; - [window setTitle: ns_title]; - } + [self->window setTitle: [NSString stringWithUTF8String: title]]; Py_RETURN_NONE; } static PyObject* FigureManager_get_window_title(FigureManager* self) { - Window* window = self->window; - PyObject* result = NULL; - if(window) - { - NSString* title = [window title]; - if (title) { - const char* cTitle = [title UTF8String]; - result = PyUnicode_FromString(cTitle); - } - } - if (result) { - return result; + NSString* title = [self->window title]; + if (title) { + return PyUnicode_FromString([title UTF8String]); } else { Py_RETURN_NONE; } @@ -799,85 +794,61 @@ static CGFloat _get_device_scale(CGContextRef cr) return NULL; } Window* window = self->window; - if(window) - { + if (window) { + CGFloat device_pixel_ratio = [window backingScaleFactor]; + width /= device_pixel_ratio; + height /= device_pixel_ratio; // 36 comes from hard-coded size of toolbar later in code [window setContentSize: NSMakeSize(width, height + 36.)]; } Py_RETURN_NONE; } -static PyMethodDef FigureManager_methods[] = { - {"show", - (PyCFunction)FigureManager_show, - METH_NOARGS, - "Shows the window associated with the figure manager." - }, - {"destroy", - (PyCFunction)FigureManager_destroy, - METH_NOARGS, - "Closes the window associated with the figure manager." - }, - {"set_window_title", - (PyCFunction)FigureManager_set_window_title, - METH_VARARGS, - "Sets the title of the window associated with the figure manager." - }, - {"get_window_title", - (PyCFunction)FigureManager_get_window_title, - METH_NOARGS, - "Returns the title of the window associated with the figure manager." - }, - {"resize", - (PyCFunction)FigureManager_resize, - METH_VARARGS, - "Resize the window (in pixels)." - }, - {NULL} /* Sentinel */ -}; - -static char FigureManager_doc[] = -"A FigureManager object wraps a Cocoa NSWindow object.\n"; +static PyObject* +FigureManager_full_screen_toggle(FigureManager* self) +{ + [self->window toggleFullScreen: nil]; + Py_RETURN_NONE; +} static PyTypeObject FigureManagerType = { PyVarObject_HEAD_INIT(NULL, 0) - "_macosx.FigureManager", /*tp_name*/ - sizeof(FigureManager), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor)FigureManager_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - (reprfunc)FigureManager_repr, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - FigureManager_doc, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - FigureManager_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)FigureManager_init, /* tp_init */ - 0, /* tp_alloc */ - FigureManager_new, /* tp_new */ + .tp_name = "_macosx.FigureManager", + .tp_basicsize = sizeof(FigureManager), + .tp_dealloc = (destructor)FigureManager_dealloc, + .tp_repr = (reprfunc)FigureManager_repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_init = (initproc)FigureManager_init, + .tp_new = (newfunc)FigureManager_new, + .tp_doc = "A FigureManager object wraps a Cocoa NSWindow object.", + .tp_methods = (PyMethodDef[]){ // All docstrings are inherited. + {"_show", + (PyCFunction)FigureManager__show, + METH_NOARGS}, + {"_raise", + (PyCFunction)FigureManager__raise, + METH_NOARGS}, + {"destroy", + (PyCFunction)FigureManager_destroy, + METH_NOARGS}, + {"set_icon", + (PyCFunction)FigureManager_set_icon, + METH_STATIC | METH_VARARGS, + "Set application icon"}, + {"set_window_title", + (PyCFunction)FigureManager_set_window_title, + METH_VARARGS}, + {"get_window_title", + (PyCFunction)FigureManager_get_window_title, + METH_NOARGS}, + {"resize", + (PyCFunction)FigureManager_resize, + METH_VARARGS}, + {"full_screen_toggle", + (PyCFunction)FigureManager_full_screen_toggle, + METH_NOARGS}, + {} // sentinel + }, }; @interface NavigationToolbar2Handler : NSObject @@ -886,7 +857,7 @@ @interface NavigationToolbar2Handler : NSObject NSButton* zoombutton; } - (NavigationToolbar2Handler*)initWithToolbar:(PyObject*)toolbar; -- (void)installCallbacks:(SEL[7])actions forButtons: (NSButton*[7])buttons; +- (void)installCallbacks:(SEL[7])actions forButtons:(NSButton*[7])buttons; - (void)home:(id)sender; - (void)back:(id)sender; - (void)forward:(id)sender; @@ -906,231 +877,88 @@ - (void)save_figure:(id)sender; @implementation NavigationToolbar2Handler - (NavigationToolbar2Handler*)initWithToolbar:(PyObject*)theToolbar -{ [self init]; +{ + [self init]; toolbar = theToolbar; return self; } -- (void)installCallbacks:(SEL[7])actions forButtons: (NSButton*[7])buttons +- (void)installCallbacks:(SEL[7])actions forButtons:(NSButton*[7])buttons { int i; - for (i = 0; i < 7; i++) - { + for (i = 0; i < 7; i++) { SEL action = actions[i]; NSButton* button = buttons[i]; [button setTarget: self]; [button setAction: action]; - if (action==@selector(pan:)) panbutton = button; - if (action==@selector(zoom:)) zoombutton = button; + if (action == @selector(pan:)) { panbutton = button; } + if (action == @selector(zoom:)) { zoombutton = button; } } } --(void)home:(id)sender -{ PyObject* result; - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(toolbar, "home", ""); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); -} - --(void)back:(id)sender -{ PyObject* result; - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(toolbar, "back", ""); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); -} - --(void)forward:(id)sender -{ PyObject* result; - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(toolbar, "forward", ""); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); -} +-(void)home:(id)sender { gil_call_method(toolbar, "home"); } +-(void)back:(id)sender { gil_call_method(toolbar, "back"); } +-(void)forward:(id)sender { gil_call_method(toolbar, "forward"); } -(void)pan:(id)sender -{ PyObject* result; - PyGILState_STATE gstate; - if ([sender state]) - { - if (zoombutton) [zoombutton setState: NO]; - } - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(toolbar, "pan", ""); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); +{ + if ([sender state]) { [zoombutton setState:NO]; } + gil_call_method(toolbar, "pan"); } -(void)zoom:(id)sender -{ PyObject* result; - PyGILState_STATE gstate; - if ([sender state]) - { - if (panbutton) [panbutton setState: NO]; - } - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(toolbar, "zoom", ""); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); +{ + if ([sender state]) { [panbutton setState:NO]; } + gil_call_method(toolbar, "zoom"); } --(void)configure_subplots:(id)sender -{ PyObject* canvas; - View* view; - PyObject* size; - NSRect rect; - int width, height; - - rect.origin.x = 100; - rect.origin.y = 350; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* master = PyObject_GetAttrString(toolbar, "canvas"); - if (master==nil) - { - PyErr_Print(); - PyGILState_Release(gstate); - return; - } - canvas = PyObject_CallMethod(toolbar, "prepare_configure_subplots", ""); - if(!canvas) - { - PyErr_Print(); - Py_DECREF(master); - PyGILState_Release(gstate); - return; - } +-(void)configure_subplots:(id)sender { gil_call_method(toolbar, "configure_subplots"); } +-(void)save_figure:(id)sender { gil_call_method(toolbar, "save_figure"); } +@end - view = ((FigureCanvas*)canvas)->view; - if (!view) /* Something really weird going on */ - { - PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); - PyErr_Print(); - Py_DECREF(canvas); - Py_DECREF(master); - PyGILState_Release(gstate); - return; +static PyObject* +NavigationToolbar2_new(PyTypeObject *type, PyObject *args, PyObject *kwds) +{ + lazy_init(); + NavigationToolbar2Handler* handler = [NavigationToolbar2Handler alloc]; + if (!handler) { return NULL; } + NavigationToolbar2 *self = (NavigationToolbar2*)type->tp_alloc(type, 0); + if (!self) { + [handler release]; + return NULL; } - - size = PyObject_CallMethod(canvas, "get_width_height", ""); - Py_DECREF(canvas); - if(!size) - { - PyErr_Print(); - Py_DECREF(master); - PyGILState_Release(gstate); - return; - } - - int ok = PyArg_ParseTuple(size, "ii", &width, &height); - Py_DECREF(size); - if (!ok) - { - PyErr_Print(); - Py_DECREF(master); - PyGILState_Release(gstate); - return; - } - - NSWindow* mw = [((FigureCanvas*)master)->view window]; - Py_DECREF(master); - PyGILState_Release(gstate); - - rect.size.width = width; - rect.size.height = height; - - ToolWindow* window = [ [ToolWindow alloc] initWithContentRect: rect - master: mw]; - [window setContentView: view]; - [view release]; - [window makeKeyAndOrderFront: nil]; -} - --(void)save_figure:(id)sender -{ PyObject* result; - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(toolbar, "save_figure", ""); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); -} -@end - -static PyObject* -NavigationToolbar2_new(PyTypeObject *type, PyObject *args, PyObject *kwds) -{ - lazy_init(); - NavigationToolbar2Handler* handler = [NavigationToolbar2Handler alloc]; - if (!handler) return NULL; - NavigationToolbar2 *self = (NavigationToolbar2*)type->tp_alloc(type, 0); - if (!self) - { - [handler release]; - return NULL; - } - self->handler = handler; - return (PyObject*)self; -} + self->handler = handler; + return (PyObject*)self; +} static int NavigationToolbar2_init(NavigationToolbar2 *self, PyObject *args, PyObject *kwds) { - PyObject* obj; FigureCanvas* canvas; - View* view; - - int i; - NSRect rect; - NSSize size; - NSSize scale; + const char* images[7]; + const char* tooltips[7]; const float gap = 2; const int height = 36; const int imagesize = 24; - self->height = height; - - obj = PyObject_GetAttrString((PyObject*)self, "canvas"); - if (obj==NULL) - { - PyErr_SetString(PyExc_AttributeError, "Attempt to install toolbar for NULL canvas"); - return -1; - } - Py_DECREF(obj); /* Don't increase the reference count */ - if (!PyObject_IsInstance(obj, (PyObject*) &FigureCanvasType)) - { - PyErr_SetString(PyExc_TypeError, "Attempt to install toolbar for object that is not a FigureCanvas"); + if (!PyArg_ParseTuple(args, "O!(sssssss)(sssssss)", + &FigureCanvasType, &canvas, + &images[0], &images[1], &images[2], &images[3], + &images[4], &images[5], &images[6], + &tooltips[0], &tooltips[1], &tooltips[2], &tooltips[3], + &tooltips[4], &tooltips[5], &tooltips[6])) { return -1; } - canvas = (FigureCanvas*)obj; - view = canvas->view; - if(!view) - { + + View* view = canvas->view; + if (!view) { PyErr_SetString(PyExc_RuntimeError, "NSView* is NULL"); return -1; } + self->height = height; + NSRect bounds = [view bounds]; NSWindow* window = [view window]; @@ -1140,16 +968,6 @@ -(void)save_figure:(id)sender bounds.size.height += height; [window setContentSize: bounds.size]; - const char* images[7]; - const char* tooltips[7]; - if (!PyArg_ParseTuple(args, "(sssssss)(sssssss)", - &images[0], &images[1], &images[2], &images[3], - &images[4], &images[5], &images[6], - &tooltips[0], &tooltips[1], &tooltips[2], &tooltips[3], - &tooltips[4], &tooltips[5], &tooltips[6])) { - return -1; - } - NSButton* buttons[7]; SEL actions[7] = {@selector(home:), @selector(back:), @@ -1166,30 +984,29 @@ -(void)save_figure:(id)sender NSButtonTypeMomentaryLight, NSButtonTypeMomentaryLight}; - rect.origin.x = 0; - rect.origin.y = 0; - rect.size.width = imagesize; - rect.size.height = imagesize; -#ifdef COMPILING_FOR_10_7 + NSRect rect; + NSSize size; + NSSize scale; + + rect = NSMakeRect(0, 0, imagesize, imagesize); rect = [window convertRectToBacking: rect]; -#endif size = rect.size; - scale.width = imagesize / size.width; - scale.height = imagesize / size.height; + scale = NSMakeSize(imagesize / size.width, imagesize / size.height); rect.size.width = 32; rect.size.height = 32; rect.origin.x = gap; rect.origin.y = 0.5*(height - rect.size.height); - for (i = 0; i < 7; i++) { - NSString* filename = [NSString stringWithCString: images[i] - encoding: NSUTF8StringEncoding]; - NSString* tooltip = [NSString stringWithCString: tooltips[i] - encoding: NSUTF8StringEncoding]; + for (int i = 0; i < 7; i++) { + NSString* filename = [NSString stringWithUTF8String: images[i]]; + NSString* tooltip = [NSString stringWithUTF8String: tooltips[i]]; NSImage* image = [[NSImage alloc] initWithContentsOfFile: filename]; buttons[i] = [[NSButton alloc] initWithFrame: rect]; [image setSize: size]; + // Specify that it is a template image so the content tint + // color gets updated with the system theme (dark/light) + [image setTemplate: YES]; [buttons[i] setBezelStyle: NSBezelStyleShadowlessSquare]; [buttons[i] setButtonType: buttontypes[i]]; [buttons[i] setImage: image]; @@ -1206,22 +1023,20 @@ -(void)save_figure:(id)sender [self->handler installCallbacks: actions forButtons: buttons]; NSFont* font = [NSFont systemFontOfSize: 0.0]; - rect.size.width = 300; - rect.size.height = 0; - rect.origin.x += height; - NSTextView* messagebox = [[NSTextView alloc] initWithFrame: rect]; - if (@available(macOS 10.11, *)) { - messagebox.textContainer.maximumNumberOfLines = 2; - messagebox.textContainer.lineBreakMode = NSLineBreakByTruncatingTail; - } + // rect.origin.x is now at the far right edge of the buttons + // we want the messagebox to take up the rest of the toolbar area + // Make it a zero-width box if we don't have enough room + rect.size.width = fmax(bounds.size.width - rect.origin.x, 0); + rect.origin.x = bounds.size.width - rect.size.width; + NSTextView* messagebox = [[[NSTextView alloc] initWithFrame: rect] autorelease]; + messagebox.textContainer.maximumNumberOfLines = 2; + messagebox.textContainer.lineBreakMode = NSLineBreakByTruncatingTail; + messagebox.alignment = NSTextAlignmentRight; [messagebox setFont: font]; [messagebox setDrawsBackground: NO]; [messagebox setSelectable: NO]; /* if selectable, the messagebox can become first responder, * which is not supposed to happen */ - rect = [messagebox frame]; - rect.origin.y = 0.5 * (height - rect.size.height); - [messagebox setFrameOrigin: rect.origin]; [[window contentView] addSubview: messagebox]; [messagebox release]; [[window contentView] display]; @@ -1243,88 +1058,57 @@ -(void)save_figure:(id)sender return PyUnicode_FromFormat("NavigationToolbar2 object %p", (void*)self); } -static char NavigationToolbar2_doc[] = -"NavigationToolbar2\n"; - static PyObject* NavigationToolbar2_set_message(NavigationToolbar2 *self, PyObject* args) { const char* message; - if(!PyArg_ParseTuple(args, "y", &message)) return NULL; + if (!PyArg_ParseTuple(args, "s", &message)) { return NULL; } NSTextView* messagebox = self->messagebox; - if (messagebox) - { + if (messagebox) { NSString* text = [NSString stringWithUTF8String: message]; [messagebox setString: text]; - // Adjust width with the window size + // Adjust width and height with the window size and content NSRect rectWindow = [messagebox.superview frame]; NSRect rect = [messagebox frame]; + // Entire region to the right of the buttons rect.size.width = rectWindow.size.width - rect.origin.x; [messagebox setFrame: rect]; - - // Adjust height with the content size + // We want to control the vertical position of + // the rect by the content size to center it vertically [messagebox.layoutManager ensureLayoutForTextContainer: messagebox.textContainer]; - NSRect contentSize = [messagebox.layoutManager usedRectForTextContainer: messagebox.textContainer]; - rect = [messagebox frame]; - rect.origin.y = 0.5 * (self->height - contentSize.size.height); + NSRect contentRect = [messagebox.layoutManager usedRectForTextContainer: messagebox.textContainer]; + rect.origin.y = 0.5 * (self->height - contentRect.size.height); + rect.size.height = contentRect.size.height; [messagebox setFrame: rect]; + // Disable cursorRects so that the cursor doesn't get updated by events + // in NSApp (like resizing TextViews), we want to handle the cursor + // changes from within MPL with set_cursor() ourselves + [[messagebox.superview window] disableCursorRects]; } Py_RETURN_NONE; } -static PyMethodDef NavigationToolbar2_methods[] = { - {"set_message", - (PyCFunction)NavigationToolbar2_set_message, - METH_VARARGS, - "Set the message to be displayed on the toolbar." - }, - {NULL} /* Sentinel */ -}; - static PyTypeObject NavigationToolbar2Type = { PyVarObject_HEAD_INIT(NULL, 0) - "_macosx.NavigationToolbar2", /*tp_name*/ - sizeof(NavigationToolbar2), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor)NavigationToolbar2_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - (reprfunc)NavigationToolbar2_repr, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - NavigationToolbar2_doc, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - NavigationToolbar2_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)NavigationToolbar2_init, /* tp_init */ - 0, /* tp_alloc */ - NavigationToolbar2_new, /* tp_new */ + .tp_name = "_macosx.NavigationToolbar2", + .tp_basicsize = sizeof(NavigationToolbar2), + .tp_dealloc = (destructor)NavigationToolbar2_dealloc, + .tp_repr = (reprfunc)NavigationToolbar2_repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_init = (initproc)NavigationToolbar2_init, + .tp_new = (newfunc)NavigationToolbar2_new, + .tp_doc = "NavigationToolbar2", + .tp_methods = (PyMethodDef[]){ // All docstrings are inherited. + {"set_message", + (PyCFunction)NavigationToolbar2_set_message, + METH_VARARGS}, + {} // sentinel + }, }; static PyObject* @@ -1332,55 +1116,24 @@ -(void)save_figure:(id)sender { int result; const char* title; + const char* directory; const char* default_filename; - if (!PyArg_ParseTuple(args, "ss", &title, &default_filename)) { + if (!PyArg_ParseTuple(args, "sss", &title, &directory, &default_filename)) { return NULL; } NSSavePanel* panel = [NSSavePanel savePanel]; - [panel setTitle: [NSString stringWithCString: title - encoding: NSASCIIStringEncoding]]; - NSString* ns_default_filename = - [[NSString alloc] - initWithCString: default_filename - encoding: NSUTF8StringEncoding]; - [panel setNameFieldStringValue: ns_default_filename]; + [panel setTitle: [NSString stringWithUTF8String: title]]; + [panel setDirectoryURL: [NSURL fileURLWithPath: [NSString stringWithUTF8String: directory] + isDirectory: YES]]; + [panel setNameFieldStringValue: [NSString stringWithUTF8String: default_filename]]; result = [panel runModal]; - [ns_default_filename release]; -#ifdef COMPILING_FOR_10_10 - if (result == NSModalResponseOK) -#else - if (result == NSOKButton) -#endif - { - NSURL* url = [panel URL]; - NSString* filename = [url path]; + if (result == NSModalResponseOK) { + NSString *filename = [[panel URL] path]; if (!filename) { PyErr_SetString(PyExc_RuntimeError, "Failed to obtain filename"); return 0; } - unsigned int n = [filename length]; - unichar* buffer = malloc(n*sizeof(unichar)); - [filename getCharacters: buffer]; - PyObject* string = PyUnicode_FromKindAndData(PyUnicode_2BYTE_KIND, buffer, n); - free(buffer); - return string; - } - Py_RETURN_NONE; -} - -static PyObject* -set_cursor(PyObject* unused, PyObject* args) -{ - int i; - if(!PyArg_ParseTuple(args, "i", &i)) return NULL; - switch (i) - { case 0: [[NSCursor pointingHandCursor] set]; break; - case 1: [[NSCursor arrowCursor] set]; break; - case 2: [[NSCursor crosshairCursor] set]; break; - case 3: [[NSCursor openHandCursor] set]; break; - /* OSX handles busy state itself so no need to set a cursor here */ - case 4: break; - default: return NULL; + return PyUnicode_FromString([filename UTF8String]); } Py_RETURN_NONE; } @@ -1390,8 +1143,7 @@ @implementation WindowServerConnectionManager + (WindowServerConnectionManager *)sharedManager { - if (sharedWindowServerConnectionManager == nil) - { + if (sharedWindowServerConnectionManager == nil) { sharedWindowServerConnectionManager = [[super allocWithZone:NULL] init]; } return sharedWindowServerConnectionManager; @@ -1433,8 +1185,8 @@ - (void)launch:(NSNotification*)notification CFMachPortRef port; CFRunLoopSourceRef source; NSDictionary* dictionary = [notification userInfo]; - if (! [[dictionary valueForKey:@"NSApplicationName"] - isEqualToString:@"Python"]) + if (![[dictionary valueForKey:@"NSApplicationName"] + localizedCaseInsensitiveContainsString:@"python"]) return; NSNumber* psnLow = [dictionary valueForKey: @"NSApplicationProcessSerialNumberLow"]; NSNumber* psnHigh = [dictionary valueForKey: @"NSApplicationProcessSerialNumberHigh"]; @@ -1480,15 +1232,7 @@ - (NSRect)constrainFrameRect:(NSRect)rect toScreen:(NSScreen*)screen - (BOOL)closeButtonPressed { - PyObject* result; - PyGILState_STATE gstate; - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(manager, "close", ""); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); + gil_call_method(manager, "_close_button_pressed"); return YES; } @@ -1516,36 +1260,6 @@ - (void)dealloc } @end -@implementation ToolWindow -- (ToolWindow*)initWithContentRect:(NSRect)rect master:(NSWindow*)window -{ - [self initWithContentRect: rect - styleMask: NSWindowStyleMaskTitled - | NSWindowStyleMaskClosable - | NSWindowStyleMaskResizable - | NSWindowStyleMaskMiniaturizable - backing: NSBackingStoreBuffered - defer: YES]; - [self setTitle: @"Subplot Configuration Tool"]; - [[NSNotificationCenter defaultCenter] addObserver: self - selector: @selector(masterCloses:) - name: NSWindowWillCloseNotification - object: window]; - return self; -} - -- (void)masterCloses:(NSNotification*)notification -{ - [self close]; -} - -- (void)close -{ - [[NSNotificationCenter defaultCenter] removeObserver: self]; - [super close]; -} -@end - @implementation View - (BOOL)isFlipped { @@ -1563,7 +1277,7 @@ - (View*)initWithFrame:(NSRect)rect - (void)dealloc { FigureCanvas* fc = (FigureCanvas*)canvas; - if (fc) fc->view = NULL; + if (fc) { fc->view = NULL; } [super dealloc]; } @@ -1599,7 +1313,7 @@ static int _copy_agg_buffer(CGContextRef cr, PyObject *renderer) const size_t bitsPerPixel = bitsPerComponent * nComponents; const size_t bytesPerRow = nComponents * bytesPerComponent * ncols; - CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); + CGColorSpaceRef colorspace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); if (!colorspace) { _buffer_release(buffer, NULL, 0); return 1; @@ -1652,16 +1366,7 @@ -(void)drawRect:(NSRect)rect CGContextRef cr = [[NSGraphicsContext currentContext] CGContext]; - double new_device_scale = _get_device_scale(cr); - - if (device_scale != new_device_scale) { - device_scale = new_device_scale; - if (!PyObject_CallMethod(canvas, "_set_device_scale", "d", device_scale, NULL)) { - PyErr_Print(); - goto exit; - } - } - if (!(renderer = PyObject_CallMethod(canvas, "_draw", "", NULL)) + if (!(renderer = PyObject_CallMethod(canvas, "get_renderer", "")) || !(renderer_buffer = PyObject_GetAttrString(renderer, "_renderer"))) { PyErr_Print(); goto exit; @@ -1671,7 +1376,18 @@ -(void)drawRect:(NSRect)rect goto exit; } if (!NSIsEmptyRect(rubberband)) { - NSFrameRect(rubberband); + // We use bezier paths so we can stroke the outside with a dash + // pattern alternating white/black with two separate paths offset + // in phase. + NSBezierPath *white_path = [NSBezierPath bezierPathWithRect: rubberband]; + NSBezierPath *black_path = [NSBezierPath bezierPathWithRect: rubberband]; + CGFloat dash_pattern[2] = {3, 3}; + [white_path setLineDash: dash_pattern count: 2 phase: 0]; + [black_path setLineDash: dash_pattern count: 2 phase: 3]; + [[NSColor whiteColor] setStroke]; + [white_path stroke]; + [[NSColor blackColor] setStroke]; + [black_path stroke]; } exit: @@ -1681,6 +1397,38 @@ -(void)drawRect:(NSRect)rect PyGILState_Release(gstate); } +- (void)updateDevicePixelRatio:(double)scale +{ + PyObject* change = NULL; + PyGILState_STATE gstate = PyGILState_Ensure(); + + device_scale = scale; + if (!(change = PyObject_CallMethod(canvas, "_set_device_pixel_ratio", "d", device_scale))) { + PyErr_Print(); + goto exit; + } + if (PyObject_IsTrue(change)) { + // Notify that there was a resize_event that took place + process_event( + "ResizeEvent", "{s:s, s:O}", + "name", "resize_event", "canvas", canvas); + gil_call_method(canvas, "draw_idle"); + [self setNeedsDisplay: YES]; + } + + exit: + Py_XDECREF(change); + + PyGILState_Release(gstate); +} + +- (void)windowDidChangeBackingProperties:(NSNotification *)notification +{ + Window* window = [notification object]; + + [self updateDevicePixelRatio: [window backingScaleFactor]]; +} + - (void)windowDidResize: (NSNotification*)notification { int width, height; @@ -1695,7 +1443,7 @@ - (void)windowDidResize: (NSNotification*)notification PyGILState_STATE gstate = PyGILState_Ensure(); PyObject* result = PyObject_CallMethod( canvas, "resize", "ii", width, height); - if(result) + if (result) Py_DECREF(result); else PyErr_Print(); @@ -1705,16 +1453,9 @@ - (void)windowDidResize: (NSNotification*)notification - (void)windowWillClose:(NSNotification*)notification { - PyGILState_STATE gstate; - PyObject* result; - - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "close_event", ""); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); + process_event( + "CloseEvent", "{s:s, s:O}", + "name", "close_event", "canvas", canvas); } - (BOOL)windowShouldClose:(NSNotification*)notification @@ -1730,57 +1471,45 @@ - (BOOL)windowShouldClose:(NSNotification*)notification data1: 0 data2: 0]; [NSApp postEvent: event atStart: true]; - if ([window respondsToSelector: @selector(closeButtonPressed)]) - { BOOL closed = [((Window*) window) closeButtonPressed]; - /* If closed, the window has already been closed via the manager. */ - if (closed) return NO; + if ([window respondsToSelector: @selector(closeButtonPressed)]) { + BOOL closed = [((Window*) window) closeButtonPressed]; + /* If closed, the window has already been closed via the manager. */ + if (closed) { return NO; } } return YES; } - (void)mouseEntered:(NSEvent *)event { - PyGILState_STATE gstate; - PyObject* result; - int x, y; NSPoint location = [event locationInWindow]; location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; - - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "enter_notify_event", "O(ii)", - Py_None, x, y); - - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); + process_event( + "LocationEvent", "{s:s, s:O, s:i, s:i, s:N}", + "name", "figure_enter_event", "canvas", canvas, "x", x, "y", y, + "modifiers", mpl_modifiers(event)); } - (void)mouseExited:(NSEvent *)event { - PyGILState_STATE gstate; - PyObject* result; - - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "leave_notify_event", ""); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - PyGILState_Release(gstate); + int x, y; + NSPoint location = [event locationInWindow]; + location = [self convertPoint: location fromView: nil]; + x = location.x * device_scale; + y = location.y * device_scale; + process_event( + "LocationEvent", "{s:s, s:O, s:i, s:i, s:N}", + "name", "figure_leave_event", "canvas", canvas, "x", x, "y", y, + "modifiers", mpl_modifiers(event)); } - (void)mouseDown:(NSEvent *)event { int x, y; - int num; + int button; int dblclick = 0; - PyObject* result; - PyGILState_STATE gstate; NSPoint location = [event locationInWindow]; location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; @@ -1790,63 +1519,53 @@ - (void)mouseDown:(NSEvent *)event { unsigned int modifier = [event modifierFlags]; if (modifier & NSEventModifierFlagControl) /* emulate a right-button click */ - num = 3; + button = 3; else if (modifier & NSEventModifierFlagOption) /* emulate a middle-button click */ - num = 2; + button = 2; else { - num = 1; + button = 1; if ([NSCursor currentCursor]==[NSCursor openHandCursor]) [[NSCursor closedHandCursor] set]; } break; } - case NSEventTypeOtherMouseDown: num = 2; break; - case NSEventTypeRightMouseDown: num = 3; break; + case NSEventTypeOtherMouseDown: button = 2; break; + case NSEventTypeRightMouseDown: button = 3; break; default: return; /* Unknown mouse event */ } if ([event clickCount] == 2) { dblclick = 1; } - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "button_press_event", "iiii", x, y, num, dblclick); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:i, s:N}", + "name", "button_press_event", "canvas", canvas, "x", x, "y", y, + "button", button, "dblclick", dblclick, "modifiers", mpl_modifiers(event)); } - (void)mouseUp:(NSEvent *)event { - int num; + int button; int x, y; - PyObject* result; - PyGILState_STATE gstate; NSPoint location = [event locationInWindow]; location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; switch ([event type]) { case NSEventTypeLeftMouseUp: - num = 1; + button = 1; if ([NSCursor currentCursor]==[NSCursor closedHandCursor]) [[NSCursor openHandCursor] set]; break; - case NSEventTypeOtherMouseUp: num = 2; break; - case NSEventTypeRightMouseUp: num = 3; break; + case NSEventTypeOtherMouseUp: button = 2; break; + case NSEventTypeRightMouseUp: button = 3; break; default: return; /* Unknown mouse event */ } - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "button_release_event", "iii", x, y, num); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:N}", + "name", "button_release_event", "canvas", canvas, "x", x, "y", y, + "button", button, "modifiers", mpl_modifiers(event)); } - (void)mouseMoved:(NSEvent *)event @@ -1856,14 +1575,10 @@ - (void)mouseMoved:(NSEvent *)event location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallMethod(canvas, "motion_notify_event", "ii", x, y); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i, s:N}", + "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y, + "modifiers", mpl_modifiers(event)); } - (void)mouseDragged:(NSEvent *)event @@ -1873,281 +1588,171 @@ - (void)mouseDragged:(NSEvent *)event location = [self convertPoint: location fromView: nil]; x = location.x * device_scale; y = location.y * device_scale; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallMethod(canvas, "motion_notify_event", "ii", x, y); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); -} - -- (void)rightMouseDown:(NSEvent *)event -{ - int x, y; - int num = 3; - int dblclick = 0; - PyObject* result; - PyGILState_STATE gstate; - NSPoint location = [event locationInWindow]; - location = [self convertPoint: location fromView: nil]; - x = location.x * device_scale; - y = location.y * device_scale; - gstate = PyGILState_Ensure(); - if ([event clickCount] == 2) { - dblclick = 1; - } - result = PyObject_CallMethod(canvas, "button_press_event", "iiii", x, y, num, dblclick); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); -} - -- (void)rightMouseUp:(NSEvent *)event -{ - int x, y; - int num = 3; - PyObject* result; - PyGILState_STATE gstate; - NSPoint location = [event locationInWindow]; - location = [self convertPoint: location fromView: nil]; - x = location.x * device_scale; - y = location.y * device_scale; - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "button_release_event", "iii", x, y, num); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); -} - -- (void)rightMouseDragged:(NSEvent *)event -{ - int x, y; - NSPoint location = [event locationInWindow]; - location = [self convertPoint: location fromView: nil]; - x = location.x * device_scale; - y = location.y * device_scale; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallMethod(canvas, "motion_notify_event", "ii", x, y); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i, s:N}", + "name", "motion_notify_event", "canvas", canvas, "x", x, "y", y, + "modifiers", mpl_modifiers(event)); } -- (void)otherMouseDown:(NSEvent *)event -{ - int x, y; - int num = 2; - int dblclick = 0; - PyObject* result; - PyGILState_STATE gstate; - NSPoint location = [event locationInWindow]; - location = [self convertPoint: location fromView: nil]; - x = location.x * device_scale; - y = location.y * device_scale; - gstate = PyGILState_Ensure(); - if ([event clickCount] == 2) { - dblclick = 1; - } - result = PyObject_CallMethod(canvas, "button_press_event", "iiii", x, y, num, dblclick); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); -} - -- (void)otherMouseUp:(NSEvent *)event -{ - int x, y; - int num = 2; - PyObject* result; - PyGILState_STATE gstate; - NSPoint location = [event locationInWindow]; - location = [self convertPoint: location fromView: nil]; - x = location.x * device_scale; - y = location.y * device_scale; - gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "button_release_event", "iii", x, y, num); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); -} - -- (void)otherMouseDragged:(NSEvent *)event -{ - int x, y; - NSPoint location = [event locationInWindow]; - location = [self convertPoint: location fromView: nil]; - x = location.x * device_scale; - y = location.y * device_scale; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallMethod(canvas, "motion_notify_event", "ii", x, y); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); -} +- (void)rightMouseDown:(NSEvent *)event { [self mouseDown: event]; } +- (void)rightMouseUp:(NSEvent *)event { [self mouseUp: event]; } +- (void)rightMouseDragged:(NSEvent *)event { [self mouseDragged: event]; } +- (void)otherMouseDown:(NSEvent *)event { [self mouseDown: event]; } +- (void)otherMouseUp:(NSEvent *)event { [self mouseUp: event]; } +- (void)otherMouseDragged:(NSEvent *)event { [self mouseDragged: event]; } - (void)setRubberband:(NSRect)rect { - if (!NSIsEmptyRect(rubberband)) [self setNeedsDisplayInRect: rubberband]; + // The space we want to redraw is a union of the previous rubberband + // with the new rubberband and then expanded (negative inset) by one + // in each direction to account for the stroke linewidth. + [self setNeedsDisplayInRect: NSInsetRect(NSUnionRect(rect, rubberband), -1, -1)]; rubberband = rect; - [self setNeedsDisplayInRect: rubberband]; } - (void)removeRubberband { - if (NSIsEmptyRect(rubberband)) return; + if (NSIsEmptyRect(rubberband)) { return; } [self setNeedsDisplayInRect: rubberband]; rubberband = NSZeroRect; } - - - (const char*)convertKeyEvent:(NSEvent*)event { - NSDictionary* specialkeymappings = [NSDictionary dictionaryWithObjectsAndKeys: - @"left", [NSNumber numberWithUnsignedLong:NSLeftArrowFunctionKey], - @"right", [NSNumber numberWithUnsignedLong:NSRightArrowFunctionKey], - @"up", [NSNumber numberWithUnsignedLong:NSUpArrowFunctionKey], - @"down", [NSNumber numberWithUnsignedLong:NSDownArrowFunctionKey], - @"f1", [NSNumber numberWithUnsignedLong:NSF1FunctionKey], - @"f2", [NSNumber numberWithUnsignedLong:NSF2FunctionKey], - @"f3", [NSNumber numberWithUnsignedLong:NSF3FunctionKey], - @"f4", [NSNumber numberWithUnsignedLong:NSF4FunctionKey], - @"f5", [NSNumber numberWithUnsignedLong:NSF5FunctionKey], - @"f6", [NSNumber numberWithUnsignedLong:NSF6FunctionKey], - @"f7", [NSNumber numberWithUnsignedLong:NSF7FunctionKey], - @"f8", [NSNumber numberWithUnsignedLong:NSF8FunctionKey], - @"f9", [NSNumber numberWithUnsignedLong:NSF9FunctionKey], - @"f10", [NSNumber numberWithUnsignedLong:NSF10FunctionKey], - @"f11", [NSNumber numberWithUnsignedLong:NSF11FunctionKey], - @"f12", [NSNumber numberWithUnsignedLong:NSF12FunctionKey], - @"f13", [NSNumber numberWithUnsignedLong:NSF13FunctionKey], - @"f14", [NSNumber numberWithUnsignedLong:NSF14FunctionKey], - @"f15", [NSNumber numberWithUnsignedLong:NSF15FunctionKey], - @"f16", [NSNumber numberWithUnsignedLong:NSF16FunctionKey], - @"f17", [NSNumber numberWithUnsignedLong:NSF17FunctionKey], - @"f18", [NSNumber numberWithUnsignedLong:NSF18FunctionKey], - @"f19", [NSNumber numberWithUnsignedLong:NSF19FunctionKey], - @"scroll_lock", [NSNumber numberWithUnsignedLong:NSScrollLockFunctionKey], - @"break", [NSNumber numberWithUnsignedLong:NSBreakFunctionKey], - @"insert", [NSNumber numberWithUnsignedLong:NSInsertFunctionKey], - @"delete", [NSNumber numberWithUnsignedLong:NSDeleteFunctionKey], - @"home", [NSNumber numberWithUnsignedLong:NSHomeFunctionKey], - @"end", [NSNumber numberWithUnsignedLong:NSEndFunctionKey], - @"pagedown", [NSNumber numberWithUnsignedLong:NSPageDownFunctionKey], - @"pageup", [NSNumber numberWithUnsignedLong:NSPageUpFunctionKey], - @"backspace", [NSNumber numberWithUnsignedLong:NSDeleteCharacter], - @"enter", [NSNumber numberWithUnsignedLong:NSEnterCharacter], - @"tab", [NSNumber numberWithUnsignedLong:NSTabCharacter], - @"enter", [NSNumber numberWithUnsignedLong:NSCarriageReturnCharacter], - @"backtab", [NSNumber numberWithUnsignedLong:NSBackTabCharacter], - @"escape", [NSNumber numberWithUnsignedLong:27], - nil - ]; - NSMutableString* returnkey = [NSMutableString string]; - if ([event modifierFlags] & NSEventModifierFlagControl) - [returnkey appendString:@"ctrl+" ]; - if ([event modifierFlags] & NSEventModifierFlagOption) + if (keyChangeControl) { + // When control is the key that was pressed, return the full word + [returnkey appendString:@"control+"]; + } else if (([event modifierFlags] & NSEventModifierFlagControl)) { + // If control is already pressed, return the shortened version + [returnkey appendString:@"ctrl+"]; + } + if (([event modifierFlags] & NSEventModifierFlagOption) || keyChangeOption) { [returnkey appendString:@"alt+" ]; - if ([event modifierFlags] & NSEventModifierFlagCommand) + } + if (([event modifierFlags] & NSEventModifierFlagCommand) || keyChangeCommand) { [returnkey appendString:@"cmd+" ]; - - unichar uc = [[event charactersIgnoringModifiers] characterAtIndex:0]; - NSString* specialchar = [specialkeymappings objectForKey:[NSNumber numberWithUnsignedLong:uc]]; - if (specialchar){ - if ([event modifierFlags] & NSEventModifierFlagShift) - [returnkey appendString:@"shift+" ]; - [returnkey appendString:specialchar]; } - else - [returnkey appendString:[event charactersIgnoringModifiers]]; + // Don't print caps_lock unless it was the key that got pressed + if (keyChangeCapsLock) { + [returnkey appendString:@"caps_lock+" ]; + } + + // flagsChanged event can't handle charactersIgnoringModifiers + // because it was a modifier key that was pressed/released + if (event.type != NSEventTypeFlagsChanged) { + NSString* specialchar; + switch ([[event charactersIgnoringModifiers] characterAtIndex:0]) { + case NSLeftArrowFunctionKey: specialchar = @"left"; break; + case NSRightArrowFunctionKey: specialchar = @"right"; break; + case NSUpArrowFunctionKey: specialchar = @"up"; break; + case NSDownArrowFunctionKey: specialchar = @"down"; break; + case NSF1FunctionKey: specialchar = @"f1"; break; + case NSF2FunctionKey: specialchar = @"f2"; break; + case NSF3FunctionKey: specialchar = @"f3"; break; + case NSF4FunctionKey: specialchar = @"f4"; break; + case NSF5FunctionKey: specialchar = @"f5"; break; + case NSF6FunctionKey: specialchar = @"f6"; break; + case NSF7FunctionKey: specialchar = @"f7"; break; + case NSF8FunctionKey: specialchar = @"f8"; break; + case NSF9FunctionKey: specialchar = @"f9"; break; + case NSF10FunctionKey: specialchar = @"f10"; break; + case NSF11FunctionKey: specialchar = @"f11"; break; + case NSF12FunctionKey: specialchar = @"f12"; break; + case NSF13FunctionKey: specialchar = @"f13"; break; + case NSF14FunctionKey: specialchar = @"f14"; break; + case NSF15FunctionKey: specialchar = @"f15"; break; + case NSF16FunctionKey: specialchar = @"f16"; break; + case NSF17FunctionKey: specialchar = @"f17"; break; + case NSF18FunctionKey: specialchar = @"f18"; break; + case NSF19FunctionKey: specialchar = @"f19"; break; + case NSScrollLockFunctionKey: specialchar = @"scroll_lock"; break; + case NSBreakFunctionKey: specialchar = @"break"; break; + case NSInsertFunctionKey: specialchar = @"insert"; break; + case NSDeleteFunctionKey: specialchar = @"delete"; break; + case NSHomeFunctionKey: specialchar = @"home"; break; + case NSEndFunctionKey: specialchar = @"end"; break; + case NSPageDownFunctionKey: specialchar = @"pagedown"; break; + case NSPageUpFunctionKey: specialchar = @"pageup"; break; + case NSDeleteCharacter: specialchar = @"backspace"; break; + case NSEnterCharacter: specialchar = @"enter"; break; + case NSTabCharacter: specialchar = @"tab"; break; + case NSCarriageReturnCharacter: specialchar = @"enter"; break; + case NSBackTabCharacter: specialchar = @"backtab"; break; + case 27: specialchar = @"escape"; break; + default: specialchar = nil; + } + if (specialchar) { + if (([event modifierFlags] & NSEventModifierFlagShift) || keyChangeShift) { + [returnkey appendString:@"shift+"]; + } + [returnkey appendString:specialchar]; + } else { + [returnkey appendString:[event charactersIgnoringModifiers]]; + } + } else { + if (([event modifierFlags] & NSEventModifierFlagShift) || keyChangeShift) { + [returnkey appendString:@"shift+"]; + } + // Since it was a modifier event trim the final character of the string + // because we added in "+" earlier + [returnkey setString: [returnkey substringToIndex:[returnkey length] - 1]]; + } return [returnkey UTF8String]; } - (void)keyDown:(NSEvent*)event { - PyObject* result; const char* s = [self convertKeyEvent: event]; - PyGILState_STATE gstate = PyGILState_Ensure(); - if (s==NULL) - { - result = PyObject_CallMethod(canvas, "key_press_event", "O", Py_None); - } - else - { - result = PyObject_CallMethod(canvas, "key_press_event", "s", s); + NSPoint location = [[self window] mouseLocationOutsideOfEventStream]; + location = [self convertPoint: location fromView: nil]; + int x = location.x * device_scale, + y = location.y * device_scale; + if (s) { + process_event( + "KeyEvent", "{s:s, s:O, s:s, s:i, s:i}", + "name", "key_press_event", "canvas", canvas, "key", s, "x", x, "y", y); + } else { + process_event( + "KeyEvent", "{s:s, s:O, s:O, s:i, s:i}", + "name", "key_press_event", "canvas", canvas, "key", Py_None, "x", x, "y", y); } - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); } - (void)keyUp:(NSEvent*)event { - PyObject* result; const char* s = [self convertKeyEvent: event]; - PyGILState_STATE gstate = PyGILState_Ensure(); - if (s==NULL) - { - result = PyObject_CallMethod(canvas, "key_release_event", "O", Py_None); - } - else - { - result = PyObject_CallMethod(canvas, "key_release_event", "s", s); + NSPoint location = [[self window] mouseLocationOutsideOfEventStream]; + location = [self convertPoint: location fromView: nil]; + int x = location.x * device_scale, + y = location.y * device_scale; + if (s) { + process_event( + "KeyEvent", "{s:s, s:O, s:s, s:i, s:i}", + "name", "key_release_event", "canvas", canvas, "key", s, "x", x, "y", y); + } else { + process_event( + "KeyEvent", "{s:s, s:O, s:O, s:i, s:i}", + "name", "key_release_event", "canvas", canvas, "key", Py_None, "x", x, "y", y); } - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); } - (void)scrollWheel:(NSEvent*)event { int step; float d = [event deltaY]; - if (d > 0) step = 1; - else if (d < 0) step = -1; + if (d > 0) { step = 1; } + else if (d < 0) { step = -1; } else return; NSPoint location = [event locationInWindow]; NSPoint point = [self convertPoint: location fromView: nil]; int x = (int)round(point.x * device_scale); int y = (int)round(point.y * device_scale - 1); - - PyObject* result; - PyGILState_STATE gstate = PyGILState_Ensure(); - result = PyObject_CallMethod(canvas, "scroll_event", "iii", x, y, step); - if(result) - Py_DECREF(result); - else - PyErr_Print(); - - PyGILState_Release(gstate); + process_event( + "MouseEvent", "{s:s, s:O, s:i, s:i, s:i, s:N}", + "name", "scroll_event", "canvas", canvas, + "x", x, "y", y, "step", step, "modifiers", mpl_modifiers(event)); } - (BOOL)acceptsFirstResponder @@ -2155,29 +1760,60 @@ - (BOOL)acceptsFirstResponder return YES; } -/* This is all wrong. Address of pointer is being passed instead of pointer, keynames don't - match up with what the front-end and does the front-end even handle modifier keys by themselves? +// flagsChanged gets called whenever a modifier key is pressed OR released +// so we need to handle both cases here +- (void)flagsChanged:(NSEvent *)event +{ + bool isPress = false; // true if key is pressed, false if key was released + + // Each if clause tests the two cases for each of the keys we can handle + // 1. If the modifier flag "command key" is pressed and it was not previously + // 2. If the modifier flag "command key" is not pressed and it was previously + // !! converts the result of the bitwise & operator to a logical boolean, + // which allows us to then bitwise xor (^) the result with a boolean (lastCommand). + if (!!([event modifierFlags] & NSEventModifierFlagCommand) ^ lastCommand) { + // Command pressed/released + lastCommand = !lastCommand; + keyChangeCommand = true; + isPress = lastCommand; + } else if (!!([event modifierFlags] & NSEventModifierFlagControl) ^ lastControl) { + // Control pressed/released + lastControl = !lastControl; + keyChangeControl = true; + isPress = lastControl; + } else if (!!([event modifierFlags] & NSEventModifierFlagShift) ^ lastShift) { + // Shift pressed/released + lastShift = !lastShift; + keyChangeShift = true; + isPress = lastShift; + } else if (!!([event modifierFlags] & NSEventModifierFlagOption) ^ lastOption) { + // Option pressed/released + lastOption = !lastOption; + keyChangeOption = true; + isPress = lastOption; + } else if (!!([event modifierFlags] & NSEventModifierFlagCapsLock) ^ lastCapsLock) { + // Capslock pressed/released + lastCapsLock = !lastCapsLock; + keyChangeCapsLock = true; + isPress = lastCapsLock; + } else { + // flag we don't handle + return; + } -- (void)flagsChanged:(NSEvent*)event -{ - const char *s = NULL; - if (([event modifierFlags] & NSControlKeyMask) == NSControlKeyMask) - s = "control"; - else if (([event modifierFlags] & NSShiftKeyMask) == NSShiftKeyMask) - s = "shift"; - else if (([event modifierFlags] & NSAlternateKeyMask) == NSAlternateKeyMask) - s = "alt"; - else return; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallMethod(canvas, "key_press_event", "s", &s); - if(result) - Py_DECREF(result); - else - PyErr_Print(); + if (isPress) { + [self keyDown:event]; + } else { + [self keyUp:event]; + } - PyGILState_Release(gstate); + // Reset the state for the key changes after handling the event + keyChangeCommand = false; + keyChangeControl = false; + keyChangeShift = false; + keyChangeOption = false; + keyChangeCapsLock = false; } - */ @end static PyObject* @@ -2206,7 +1842,7 @@ - (void)flagsChanged:(NSEvent*)event { lazy_init(); Timer* self = (Timer*)type->tp_alloc(type, 0); - if (!self) return NULL; + if (!self) { return NULL; } self->timer = NULL; return (PyObject*) self; } @@ -2218,20 +1854,9 @@ - (void)flagsChanged:(NSEvent*)event (void*) self, (void*)(self->timer)); } -static char Timer_doc[] = -"A Timer object wraps a CFRunLoopTimerRef and can add it to the event loop.\n"; - static void timer_callback(CFRunLoopTimerRef timer, void* info) { - PyObject* method = info; - PyGILState_STATE gstate = PyGILState_Ensure(); - PyObject* result = PyObject_CallFunction(method, NULL); - if (result) { - Py_DECREF(result); - } else { - PyErr_Print(); - } - PyGILState_Release(gstate); + gil_call_method(info, "_on_timer"); } static void context_cleanup(const void* info) @@ -2270,12 +1895,12 @@ static void context_cleanup(const void* info) PyErr_SetString(PyExc_RuntimeError, "_on_timer should be a Python method"); goto exit; } - Py_INCREF(py_on_timer); + Py_INCREF(self); context.version = 0; context.retain = NULL; context.release = context_cleanup; context.copyDescription = NULL; - context.info = py_on_timer; + context.info = self; timer = CFRunLoopTimerCreate(kCFAllocatorDefault, firstFire, interval, @@ -2325,126 +1950,62 @@ static void context_cleanup(const void* info) Py_TYPE(self)->tp_free((PyObject*)self); } -static PyMethodDef Timer_methods[] = { - {"_timer_start", - (PyCFunction)Timer__timer_start, - METH_VARARGS, - "Initialize and start the timer." - }, - {"_timer_stop", - (PyCFunction)Timer__timer_stop, - METH_NOARGS, - "Stop the timer." - }, - {NULL} /* Sentinel */ -}; - static PyTypeObject TimerType = { PyVarObject_HEAD_INIT(NULL, 0) - "_macosx.Timer", /*tp_name*/ - sizeof(Timer), /*tp_basicsize*/ - 0, /*tp_itemsize*/ - (destructor)Timer_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - (reprfunc)Timer_repr, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - 0, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - Timer_doc, /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - Timer_methods, /* tp_methods */ - 0, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - 0, /* tp_init */ - 0, /* tp_alloc */ - Timer_new, /* tp_new */ -}; - -static struct PyMethodDef methods[] = { - {"event_loop_is_running", - (PyCFunction)event_loop_is_running, - METH_NOARGS, - "Return whether the OSX backend has set up the NSApp main event loop." - }, - {"show", - (PyCFunction)show, - METH_NOARGS, - "Show all the figures and enter the main loop.\n" - "\n" - "This function does not return until all Matplotlib windows are closed,\n" - "and is normally not needed in interactive sessions." - }, - {"choose_save_file", - (PyCFunction)choose_save_file, - METH_VARARGS, - "Closes the window." - }, - {"set_cursor", - (PyCFunction)set_cursor, - METH_VARARGS, - "Sets the active cursor." - }, - {NULL, NULL, 0, NULL} /* sentinel */ + .tp_name = "_macosx.Timer", + .tp_basicsize = sizeof(Timer), + .tp_dealloc = (destructor)Timer_dealloc, + .tp_repr = (reprfunc)Timer_repr, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_new = (newfunc)Timer_new, + .tp_doc = "A Timer object wraps a CFRunLoopTimerRef and can add it to the event loop.", + .tp_methods = (PyMethodDef[]){ // All docstrings are inherited. + {"_timer_start", + (PyCFunction)Timer__timer_start, + METH_VARARGS}, + {"_timer_stop", + (PyCFunction)Timer__timer_stop, + METH_NOARGS}, + {} // sentinel + }, }; static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_macosx", - "Mac OS X native backend", - -1, - methods, - NULL, - NULL, - NULL, - NULL + PyModuleDef_HEAD_INIT, "_macosx", "Mac OS X native backend", -1, + (PyMethodDef[]){ + {"event_loop_is_running", + (PyCFunction)event_loop_is_running, + METH_NOARGS, + "Return whether the OSX backend has set up the NSApp main event loop."}, + {"show", + (PyCFunction)show, + METH_NOARGS, + "Show all the figures and enter the main loop.\n" + "\n" + "This function does not return until all Matplotlib windows are closed,\n" + "and is normally not needed in interactive sessions."}, + {"choose_save_file", + (PyCFunction)choose_save_file, + METH_VARARGS, + "Query the user for a location where to save a file."}, + {} /* Sentinel */ + }, }; #pragma GCC visibility push(default) PyObject* PyInit__macosx(void) { - PyObject *module; - - if (PyType_Ready(&FigureCanvasType) < 0 - || PyType_Ready(&FigureManagerType) < 0 - || PyType_Ready(&NavigationToolbar2Type) < 0 - || PyType_Ready(&TimerType) < 0) + PyObject *m; + if (!(m = PyModule_Create(&moduledef)) + || prepare_and_add_type(&FigureCanvasType, m) + || prepare_and_add_type(&FigureManagerType, m) + || prepare_and_add_type(&NavigationToolbar2Type, m) + || prepare_and_add_type(&TimerType, m)) { + Py_XDECREF(m); return NULL; - - module = PyModule_Create(&moduledef); - if (!module) - return NULL; - - Py_INCREF(&FigureCanvasType); - Py_INCREF(&FigureManagerType); - Py_INCREF(&NavigationToolbar2Type); - Py_INCREF(&TimerType); - PyModule_AddObject(module, "FigureCanvas", (PyObject*) &FigureCanvasType); - PyModule_AddObject(module, "FigureManager", (PyObject*) &FigureManagerType); - PyModule_AddObject(module, "NavigationToolbar2", (PyObject*) &NavigationToolbar2Type); - PyModule_AddObject(module, "Timer", (PyObject*) &TimerType); - - return module; + } + return m; } #pragma GCC visibility pop diff --git a/src/_path.h b/src/_path.h index 16a981f0e3a8..0c115e3d2735 100644 --- a/src/_path.h +++ b/src/_path.h @@ -259,7 +259,7 @@ inline void points_in_path(PointArray &points, } transformed_path_t trans_path(path, trans); - no_nans_t no_nans_path(trans_path, true, path.has_curves()); + no_nans_t no_nans_path(trans_path, true, path.has_codes()); curve_t curved_path(no_nans_path); if (r != 0.0) { contour_t contoured_path(curved_path); @@ -305,7 +305,7 @@ void points_on_path(PointArray &points, } transformed_path_t trans_path(path, trans); - no_nans_t nan_removed_path(trans_path, true, path.has_curves()); + no_nans_t nan_removed_path(trans_path, true, path.has_codes()); curve_t curved_path(nan_removed_path); stroke_t stroked_path(curved_path); stroked_path.width(r * 2.0); @@ -378,7 +378,7 @@ void update_path_extents(PathIterator &path, agg::trans_affine &trans, extent_li unsigned code; transformed_path_t tpath(path, trans); - nan_removed_t nan_removed(tpath, true, path.has_curves()); + nan_removed_t nan_removed(tpath, true, path.has_codes()); nan_removed.rewind(0); @@ -447,7 +447,6 @@ void point_in_path_collection(double x, OffsetArray &offsets, agg::trans_affine &offset_trans, bool filled, - e_offset_position offset_position, std::vector &result) { size_t Npaths = paths.size(); @@ -483,11 +482,7 @@ void point_in_path_collection(double x, double xo = offsets(i % Noffsets, 0); double yo = offsets(i % Noffsets, 1); offset_trans.transform(&xo, &yo); - if (offset_position == OFFSET_POSITION_DATA) { - trans = agg::trans_affine_translation(xo, yo) * trans; - } else { - trans *= agg::trans_affine_translation(xo, yo); - } + trans *= agg::trans_affine_translation(xo, yo); } if (filled) { @@ -517,7 +512,7 @@ bool path_in_path(PathIterator1 &a, } transformed_path_t b_path_trans(b, btrans); - no_nans_t b_no_nans(b_path_trans, true, b.has_curves()); + no_nans_t b_no_nans(b_path_trans, true, b.has_codes()); curve_t b_curved(b_no_nans); double x, y; @@ -534,7 +529,7 @@ bool path_in_path(PathIterator1 &a, /** The clip_path_to_rect code here is a clean-room implementation of the Sutherland-Hodgman clipping algorithm described here: - http://en.wikipedia.org/wiki/Sutherland-Hodgman_clipping_algorithm + https://en.wikipedia.org/wiki/Sutherland-Hodgman_clipping_algorithm */ namespace clip_to_rect_filters @@ -845,7 +840,7 @@ inline bool segments_intersect(const double &x1, // If den == 0 we have two possibilities: if (isclose(den, 0.0)) { - float t_area = (x2*y3 - x3*y2) - x1*(y3 - y2) + y1*(x3 - x2); + double t_area = (x2*y3 - x3*y2) - x1*(y3 - y2) + y1*(x3 - x2); // 1 - If the area of the triangle made by the 3 first points (2 from the first segment // plus one from the second) is zero, they are collinear if (isclose(t_area, 0.0)) { @@ -857,7 +852,6 @@ inline bool segments_intersect(const double &x1, else { return (fmin(x1, x2) <= fmin(x3, x4) && fmin(x3, x4) <= fmax(x1, x2)) || (fmin(x3, x4) <= fmin(x1, x2) && fmin(x1, x2) <= fmax(x3, x4)); - } } // 2 - If t_area is not zero, the segments are parallel, but not collinear @@ -881,7 +875,6 @@ inline bool segments_intersect(const double &x1, template bool path_intersects_path(PathIterator1 &p1, PathIterator2 &p2) { - typedef PathNanRemover no_nans_t; typedef agg::conv_curve curve_t; @@ -889,8 +882,8 @@ bool path_intersects_path(PathIterator1 &p1, PathIterator2 &p2) return false; } - no_nans_t n1(p1, true, p1.has_curves()); - no_nans_t n2(p2, true, p2.has_curves()); + no_nans_t n1(p1, true, p1.has_codes()); + no_nans_t n2(p2, true, p2.has_codes()); curve_t c1(n1); curve_t c2(n2); @@ -906,7 +899,6 @@ bool path_intersects_path(PathIterator1 &p1, PathIterator2 &p2) } c2.rewind(0); c2.vertex(&x21, &y21); - while (c2.vertex(&x22, &y22) != agg::path_cmd_stop) { // if the segment in path 2 is (almost) 0 length, skip to next vertex @@ -954,7 +946,7 @@ bool path_intersects_rectangle(PathIterator &path, return false; } - no_nans_t no_nans(path, true, path.has_curves()); + no_nans_t no_nans(path, true, path.has_codes()); curve_t curve(no_nans); double cx = (rect_x1 + rect_x2) * 0.5, cy = (rect_y1 + rect_y2) * 0.5; @@ -1003,8 +995,8 @@ void convert_path_to_polygons(PathIterator &path, bool simplify = path.should_simplify(); transformed_path_t tpath(path, trans); - nan_removal_t nan_removed(tpath, true, path.has_curves()); - clipped_t clipped(nan_removed, do_clip && !path.has_curves(), width, height); + nan_removal_t nan_removed(tpath, true, path.has_codes()); + clipped_t clipped(nan_removed, do_clip, width, height); simplify_t simplified(clipped, simplify, path.simplify_threshold()); curve_t curve(simplified); @@ -1068,8 +1060,8 @@ void cleanup_path(PathIterator &path, typedef Sketch sketch_t; transformed_path_t tpath(path, trans); - nan_removal_t nan_removed(tpath, remove_nans, path.has_curves()); - clipped_t clipped(nan_removed, do_clip && !path.has_curves(), rect); + nan_removal_t nan_removed(tpath, remove_nans, path.has_codes()); + clipped_t clipped(nan_removed, do_clip, rect); snapped_t snapped(clipped, snap_mode, path.total_vertices(), stroke_width); simplify_t simplified(snapped, do_simplify, path.simplify_threshold()); @@ -1152,17 +1144,15 @@ bool __convert_to_string(PathIterator &path, double last_x = 0.0; double last_y = 0.0; - const int sizes[] = { 1, 1, 2, 3 }; - int size = 0; unsigned code; while ((code = path.vertex(&x[0], &y[0])) != agg::path_cmd_stop) { - if (code == 0x4f) { + if (code == CLOSEPOLY) { buffer += codes[4]; } else if (code < 5) { - size = sizes[code - 1]; + size_t size = NUM_VERTICES[code]; - for (int i = 1; i < size; ++i) { + for (size_t i = 1; i < size; ++i) { unsigned subcode = path.vertex(&x[i], &y[i]); if (subcode != code) { return false; @@ -1182,7 +1172,7 @@ bool __convert_to_string(PathIterator &path, buffer += ' '; } - for (int i = 0; i < size; ++i) { + for (size_t i = 0; i < size; ++i) { __add_number(x[i], format_code, precision, buffer); buffer += ' '; __add_number(y[i], format_code, precision, buffer); @@ -1228,11 +1218,11 @@ bool convert_to_string(PathIterator &path, bool do_clip = (clip_rect.x1 < clip_rect.x2 && clip_rect.y1 < clip_rect.y2); transformed_path_t tpath(path, trans); - nan_removal_t nan_removed(tpath, true, path.has_curves()); - clipped_t clipped(nan_removed, do_clip && !path.has_curves(), clip_rect); + nan_removal_t nan_removed(tpath, true, path.has_codes()); + clipped_t clipped(nan_removed, do_clip, clip_rect); simplify_t simplified(clipped, simplify, path.simplify_threshold()); - buffersize = path.total_vertices() * (precision + 5) * 4; + buffersize = (size_t) path.total_vertices() * (precision + 5) * 4; if (buffersize == 0) { return true; } @@ -1254,70 +1244,24 @@ bool convert_to_string(PathIterator &path, } template -struct _is_sorted +bool is_sorted(PyArrayObject *array) { - bool operator()(PyArrayObject *array) - { - npy_intp size; - npy_intp i; - T last_value; - T current_value; - - size = PyArray_DIM(array, 0); - - // std::isnan is only in C++11, which we don't yet require, - // so we use the "self == self" trick - for (i = 0; i < size; ++i) { - last_value = *((T *)PyArray_GETPTR1(array, i)); - if (last_value == last_value) { - break; - } - } - - if (i == size) { - // The whole array is non-finite - return false; - } - - for (; i < size; ++i) { - current_value = *((T *)PyArray_GETPTR1(array, i)); - if (current_value == current_value) { - if (current_value < last_value) { - return false; - } - last_value = current_value; - } - } - - return true; - } -}; - - -template -struct _is_sorted_int -{ - bool operator()(PyArrayObject *array) - { - npy_intp size; - npy_intp i; - T last_value; - T current_value; - - size = PyArray_DIM(array, 0); - - last_value = *((T *)PyArray_GETPTR1(array, 0)); - - for (i = 1; i < size; ++i) { - current_value = *((T *)PyArray_GETPTR1(array, i)); - if (current_value < last_value) { + npy_intp size = PyArray_DIM(array, 0); + using limits = std::numeric_limits; + T last = limits::has_infinity ? -limits::infinity() : limits::min(); + + for (npy_intp i = 0; i < size; ++i) { + T current = *(T *)PyArray_GETPTR1(array, i); + // The following tests !isnan(current), but also works for integral + // types. (The isnan(IntegralType) overload is absent on MSVC.) + if (current == current) { + if (current < last) { return false; } - last_value = current_value; + last = current; } - - return true; } + return true; }; diff --git a/src/_path_wrapper.cpp b/src/_path_wrapper.cpp index 2b58aec0cfdd..8c297907ab98 100644 --- a/src/_path_wrapper.cpp +++ b/src/_path_wrapper.cpp @@ -32,7 +32,7 @@ const char *Py_point_in_path__doc__ = "point_in_path(x, y, radius, path, trans)\n" "--\n\n"; -static PyObject *Py_point_in_path(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_point_in_path(PyObject *self, PyObject *args) { double x, y, r; py::PathIterator path; @@ -64,7 +64,7 @@ const char *Py_points_in_path__doc__ = "points_in_path(points, radius, path, trans)\n" "--\n\n"; -static PyObject *Py_points_in_path(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_points_in_path(PyObject *self, PyObject *args) { numpy::array_view points; double r; @@ -95,7 +95,7 @@ const char *Py_point_on_path__doc__ = "point_on_path(x, y, radius, path, trans)\n" "--\n\n"; -static PyObject *Py_point_on_path(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_point_on_path(PyObject *self, PyObject *args) { double x, y, r; py::PathIterator path; @@ -127,7 +127,7 @@ const char *Py_points_on_path__doc__ = "points_on_path(points, radius, path, trans)\n" "--\n\n"; -static PyObject *Py_points_on_path(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_points_on_path(PyObject *self, PyObject *args) { numpy::array_view points; double r; @@ -158,7 +158,7 @@ const char *Py_get_path_extents__doc__ = "get_path_extents(path, trans)\n" "--\n\n"; -static PyObject *Py_get_path_extents(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_get_path_extents(PyObject *self, PyObject *args) { py::PathIterator path; agg::trans_affine trans; @@ -187,7 +187,7 @@ const char *Py_update_path_extents__doc__ = "update_path_extents(path, trans, rect, minpos, ignore)\n" "--\n\n"; -static PyObject *Py_update_path_extents(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_update_path_extents(PyObject *self, PyObject *args) { py::PathIterator path; agg::trans_affine trans; @@ -266,20 +266,21 @@ const char *Py_get_path_collection_extents__doc__ = "master_transform, paths, transforms, offsets, offset_transform)\n" "--\n\n"; -static PyObject *Py_get_path_collection_extents(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_get_path_collection_extents(PyObject *self, PyObject *args) { agg::trans_affine master_transform; - PyObject *pathsobj; + py::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; agg::trans_affine offset_trans; extent_limits e; if (!PyArg_ParseTuple(args, - "O&OO&O&O&:get_path_collection_extents", + "O&O&O&O&O&:get_path_collection_extents", &convert_trans_affine, &master_transform, - &pathsobj, + &convert_pathgen, + &paths, &convert_transforms, &transforms, &convert_points, @@ -289,18 +290,9 @@ static PyObject *Py_get_path_collection_extents(PyObject *self, PyObject *args, return NULL; } - try - { - py::PathGenerator paths(pathsobj); - - CALL_CPP("get_path_collection_extents", - (get_path_collection_extents( - master_transform, paths, transforms, offsets, offset_trans, e))); - } - catch (const py::exception &) - { - return NULL; - } + CALL_CPP("get_path_collection_extents", + (get_path_collection_extents( + master_transform, paths, transforms, offsets, offset_trans, e))); npy_intp dims[] = { 2, 2 }; numpy::array_view extents(dims); @@ -320,29 +312,29 @@ static PyObject *Py_get_path_collection_extents(PyObject *self, PyObject *args, const char *Py_point_in_path_collection__doc__ = "point_in_path_collection(" "x, y, radius, master_transform, paths, transforms, offsets, " - "offset_trans, filled, offset_position)\n" + "offset_trans, filled)\n" "--\n\n"; -static PyObject *Py_point_in_path_collection(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_point_in_path_collection(PyObject *self, PyObject *args) { double x, y, radius; agg::trans_affine master_transform; - PyObject *pathsobj; + py::PathGenerator paths; numpy::array_view transforms; numpy::array_view offsets; agg::trans_affine offset_trans; bool filled; - e_offset_position offset_position; std::vector result; if (!PyArg_ParseTuple(args, - "dddO&OO&O&O&O&O&:point_in_path_collection", + "dddO&O&O&O&O&O&:point_in_path_collection", &x, &y, &radius, &convert_trans_affine, &master_transform, - &pathsobj, + &convert_pathgen, + &paths, &convert_transforms, &transforms, &convert_points, @@ -350,33 +342,21 @@ static PyObject *Py_point_in_path_collection(PyObject *self, PyObject *args, PyO &convert_trans_affine, &offset_trans, &convert_bool, - &filled, - &convert_offset_position, - &offset_position)) { + &filled)) { return NULL; } - try - { - py::PathGenerator paths(pathsobj); - - CALL_CPP("point_in_path_collection", - (point_in_path_collection(x, - y, - radius, - master_transform, - paths, - transforms, - offsets, - offset_trans, - filled, - offset_position, - result))); - } - catch (const py::exception &) - { - return NULL; - } + CALL_CPP("point_in_path_collection", + (point_in_path_collection(x, + y, + radius, + master_transform, + paths, + transforms, + offsets, + offset_trans, + filled, + result))); npy_intp dims[] = {(npy_intp)result.size() }; numpy::array_view pyresult(dims); @@ -390,7 +370,7 @@ const char *Py_path_in_path__doc__ = "path_in_path(path_a, trans_a, path_b, trans_b)\n" "--\n\n"; -static PyObject *Py_path_in_path(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_path_in_path(PyObject *self, PyObject *args) { py::PathIterator a; agg::trans_affine atrans; @@ -424,7 +404,7 @@ const char *Py_clip_path_to_rect__doc__ = "clip_path_to_rect(path, rect, inside)\n" "--\n\n"; -static PyObject *Py_clip_path_to_rect(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_clip_path_to_rect(PyObject *self, PyObject *args) { py::PathIterator path; agg::rect_d rect; @@ -451,7 +431,7 @@ const char *Py_affine_transform__doc__ = "affine_transform(points, trans)\n" "--\n\n"; -static PyObject *Py_affine_transform(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_affine_transform(PyObject *self, PyObject *args) { PyObject *vertices_obj; agg::trans_affine trans; @@ -492,7 +472,7 @@ const char *Py_count_bboxes_overlapping_bbox__doc__ = "count_bboxes_overlapping_bbox(bbox, bboxes)\n" "--\n\n"; -static PyObject *Py_count_bboxes_overlapping_bbox(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_count_bboxes_overlapping_bbox(PyObject *self, PyObject *args) { agg::rect_d bbox; numpy::array_view bboxes; @@ -634,7 +614,7 @@ const char *Py_cleanup_path__doc__ = "return_curves, sketch)\n" "--\n\n"; -static PyObject *Py_cleanup_path(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_cleanup_path(PyObject *self, PyObject *args) { py::PathIterator path; agg::trans_affine trans; @@ -717,7 +697,7 @@ const char *Py_convert_to_string__doc__ = "--\n\n" "Convert *path* to a bytestring.\n" "\n" - "The first five parameters (up to *sketch*) are interpreted as in \n" + "The first five parameters (up to *sketch*) are interpreted as in\n" "`.cleanup_path`. The following ones are detailed below.\n" "\n" "Parameters\n" @@ -729,7 +709,7 @@ const char *Py_convert_to_string__doc__ = "sketch : tuple of 3 floats, or None\n" "precision : int\n" " The precision used to \"%.*f\"-format the values. Trailing zeros\n" - " and decimal points are always removed. (precision=-1 is a special \n" + " and decimal points are always removed. (precision=-1 is a special\n" " case used to implement ttconv-back-compatible conversion.)\n" "codes : sequence of 5 bytestrings\n" " The bytes representation of each opcode (MOVETO, LINETO, CURVE3,\n" @@ -741,7 +721,7 @@ const char *Py_convert_to_string__doc__ = " Whether the opcode comes after the values (True) or before (False).\n" ; -static PyObject *Py_convert_to_string(PyObject *self, PyObject *args, PyObject *kwds) +static PyObject *Py_convert_to_string(PyObject *self, PyObject *args) { py::PathIterator path; agg::trans_affine trans; @@ -827,54 +807,29 @@ static PyObject *Py_is_sorted(PyObject *self, PyObject *obj) /* Handle just the most common types here, otherwise coerce to double */ - switch(PyArray_TYPE(array)) { + switch (PyArray_TYPE(array)) { case NPY_INT: - { - _is_sorted_int is_sorted; - result = is_sorted(array); - } + result = is_sorted(array); break; - case NPY_LONG: - { - _is_sorted_int is_sorted; - result = is_sorted(array); - } + result = is_sorted(array); break; - case NPY_LONGLONG: - { - _is_sorted_int is_sorted; - result = is_sorted(array); - } + result = is_sorted(array); break; - case NPY_FLOAT: - { - _is_sorted is_sorted; - result = is_sorted(array); - } + result = is_sorted(array); break; - case NPY_DOUBLE: - { - _is_sorted is_sorted; - result = is_sorted(array); - } + result = is_sorted(array); break; - default: - { - Py_DECREF(array); - array = (PyArrayObject *)PyArray_FromObject(obj, NPY_DOUBLE, 1, 1); - - if (array == NULL) { - return NULL; - } - - _is_sorted is_sorted; - result = is_sorted(array); + Py_DECREF(array); + array = (PyArrayObject *)PyArray_FromObject(obj, NPY_DOUBLE, 1, 1); + if (array == NULL) { + return NULL; } + result = is_sorted(array); } Py_DECREF(array); @@ -910,31 +865,15 @@ static PyMethodDef module_functions[] = { }; static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_path", - NULL, - 0, - module_functions, - NULL, - NULL, - NULL, - NULL + PyModuleDef_HEAD_INIT, "_path", NULL, 0, module_functions }; #pragma GCC visibility push(default) PyMODINIT_FUNC PyInit__path(void) { - PyObject *m; - m = PyModule_Create(&moduledef); - - if (m == NULL) { - return NULL; - } - import_array(); - - return m; + return PyModule_Create(&moduledef); } #pragma GCC visibility pop diff --git a/src/qhull_wrap.c b/src/_qhull_wrapper.cpp similarity index 53% rename from src/qhull_wrap.c rename to src/_qhull_wrapper.cpp index 0f7b3938299c..e27c4215b96e 100644 --- a/src/qhull_wrap.c +++ b/src/_qhull_wrapper.cpp @@ -7,9 +7,18 @@ */ #define PY_SSIZE_T_CLEAN #include "Python.h" -#include "numpy/ndarrayobject.h" +#include "numpy_cpp.h" +#ifdef _MSC_VER +/* The Qhull header does not declare this as extern "C", but only MSVC seems to + * do name mangling on global variables. We thus need to declare this before + * the header so that it treats it correctly, and doesn't mangle the name. */ +extern "C" { +extern const char qh_version[]; +} +#endif #include "libqhull_r/qhull_ra.h" -#include +#include +#include #ifndef MPL_DEVNULL @@ -35,88 +44,104 @@ static void get_facet_vertices(qhT* qh, const facetT* facet, int indices[3]) { vertexT *vertex, **vertexp; - FOREACHvertex_(facet->vertices) + FOREACHvertex_(facet->vertices) { *indices++ = qh_pointid(qh, vertex->point); + } } /* Return the indices of the 3 triangles that are neighbors of the specified * facet (triangle). */ static void -get_facet_neighbours(const facetT* facet, const int* tri_indices, +get_facet_neighbours(const facetT* facet, std::vector& tri_indices, int indices[3]) { facetT *neighbor, **neighborp; - FOREACHneighbor_(facet) + FOREACHneighbor_(facet) { *indices++ = (neighbor->upperdelaunay ? -1 : tri_indices[neighbor->id]); + } } -/* Return 1 if the specified points arrays contain at least 3 unique points, - * or 0 otherwise. */ -static int -at_least_3_unique_points(int npoints, const double* x, const double* y) +/* Return true if the specified points arrays contain at least 3 unique points, + * or false otherwise. */ +static bool +at_least_3_unique_points(npy_intp npoints, const double* x, const double* y) { int i; const int unique1 = 0; /* First unique point has index 0. */ int unique2 = 0; /* Second unique point index is 0 until set. */ - if (npoints < 3) - return 0; + if (npoints < 3) { + return false; + } for (i = 1; i < npoints; ++i) { if (unique2 == 0) { /* Looking for second unique point. */ - if (x[i] != x[unique1] || y[i] != y[unique1]) + if (x[i] != x[unique1] || y[i] != y[unique1]) { unique2 = i; + } } else { /* Looking for third unique point. */ if ( (x[i] != x[unique1] || y[i] != y[unique1]) && (x[i] != x[unique2] || y[i] != y[unique2]) ) { /* 3 unique points found, with indices 0, unique2 and i. */ - return 1; + return true; } } } /* Run out of points before 3 unique points found. */ - return 0; + return false; } -/* Delaunay implementation methyod. If hide_qhull_errors is 1 then qhull error - * messages are discarded; if it is 0 then they are written to stderr. */ +/* Holds on to info from Qhull so that it can be destructed automatically. */ +class QhullInfo { +public: + QhullInfo(FILE *error_file, qhT* qh) { + this->error_file = error_file; + this->qh = qh; + } + + ~QhullInfo() { + qh_freeqhull(this->qh, !qh_ALL); + int curlong, totlong; /* Memory remaining. */ + qh_memfreeshort(this->qh, &curlong, &totlong); + if (curlong || totlong) { + PyErr_WarnEx(PyExc_RuntimeWarning, + "Qhull could not free all allocated memory", 1); + } + + if (this->error_file != stderr) { + fclose(error_file); + } + } + +private: + FILE* error_file; + qhT* qh; +}; + +/* Delaunay implementation method. + * If hide_qhull_errors is true then qhull error messages are discarded; + * if it is false then they are written to stderr. */ static PyObject* -delaunay_impl(int npoints, const double* x, const double* y, - int hide_qhull_errors) +delaunay_impl(npy_intp npoints, const double* x, const double* y, + bool hide_qhull_errors) { - qhT qh_qh; /* qh variable type and name must be like */ - qhT* qh = &qh_qh; /* this for Qhull macros to work correctly. */ - coordT* points = NULL; + qhT qh_qh; /* qh variable type and name must be like */ + qhT* qh = &qh_qh; /* this for Qhull macros to work correctly. */ facetT* facet; int i, ntri, max_facet_id; - FILE* error_file = NULL; /* qhull expects a FILE* to write errors to. */ int exitcode; /* Value returned from qh_new_qhull(). */ - int* tri_indices = NULL; /* Maps qhull facet id to triangle index. */ - int indices[3]; - int curlong, totlong; /* Memory remaining after qh_memfreeshort. */ - PyObject* tuple; /* Return tuple (triangles, neighbors). */ const int ndim = 2; - npy_intp dims[2]; - PyArrayObject* triangles = NULL; - PyArrayObject* neighbors = NULL; - int* triangles_ptr; - int* neighbors_ptr; double x_mean = 0.0; double y_mean = 0.0; QHULL_LIB_CHECK /* Allocate points. */ - points = (coordT*)malloc(npoints*ndim*sizeof(coordT)); - if (points == NULL) { - PyErr_SetString(PyExc_MemoryError, - "Could not allocate points array in qhull.delaunay"); - goto error_before_qhull; - } + std::vector points(npoints * ndim); /* Determine mean x, y coordinates. */ for (i = 0; i < npoints; ++i) { @@ -133,15 +158,14 @@ delaunay_impl(int npoints, const double* x, const double* y, } /* qhull expects a FILE* to write errors to. */ + FILE* error_file = NULL; if (hide_qhull_errors) { /* qhull errors are ignored by writing to OS-equivalent of /dev/null. * Rather than have OS-specific code here, instead it is determined by * setupext.py and passed in via the macro MPL_DEVNULL. */ error_file = fopen(STRINGIFY(MPL_DEVNULL), "w"); if (error_file == NULL) { - PyErr_SetString(PyExc_RuntimeError, - "Could not open devnull in qhull.delaunay"); - goto error_before_qhull; + throw std::runtime_error("Could not open devnull"); } } else { @@ -150,15 +174,16 @@ delaunay_impl(int npoints, const double* x, const double* y, } /* Perform Delaunay triangulation. */ + QhullInfo info(error_file, qh); qh_zero(qh, error_file); - exitcode = qh_new_qhull(qh, ndim, npoints, points, False, - "qhull d Qt Qbb Qc Qz", NULL, error_file); + exitcode = qh_new_qhull(qh, ndim, (int)npoints, points.data(), False, + (char*)"qhull d Qt Qbb Qc Qz", NULL, error_file); if (exitcode != qh_ERRnone) { PyErr_Format(PyExc_RuntimeError, "Error in qhull Delaunay triangulation calculation: %s (exitcode=%d)%s", qhull_error_msg[exitcode], exitcode, hide_qhull_errors ? "; use python verbose option (-v) to see original qhull error." : ""); - goto error; + return NULL; } /* Split facets so that they only have 3 points each. */ @@ -168,57 +193,44 @@ delaunay_impl(int npoints, const double* x, const double* y, Note that libqhull uses macros to iterate through collections. */ ntri = 0; FORALLfacets { - if (!facet->upperdelaunay) + if (!facet->upperdelaunay) { ++ntri; + } } max_facet_id = qh->facet_id - 1; /* Create array to map facet id to triangle index. */ - tri_indices = (int*)malloc((max_facet_id+1)*sizeof(int)); - if (tri_indices == NULL) { - PyErr_SetString(PyExc_MemoryError, - "Could not allocate triangle map in qhull.delaunay"); - goto error; - } + std::vector tri_indices(max_facet_id+1); - /* Allocate python arrays to return. */ - dims[0] = ntri; - dims[1] = 3; - triangles = (PyArrayObject*)PyArray_SimpleNew(ndim, dims, NPY_INT); - if (triangles == NULL) { - PyErr_SetString(PyExc_MemoryError, - "Could not allocate triangles array in qhull.delaunay"); - goto error; - } + /* Allocate Python arrays to return. */ + npy_intp dims[2] = {ntri, 3}; + numpy::array_view triangles(dims); + int* triangles_ptr = triangles.data(); - neighbors = (PyArrayObject*)PyArray_SimpleNew(ndim, dims, NPY_INT); - if (neighbors == NULL) { - PyErr_SetString(PyExc_MemoryError, - "Could not allocate neighbors array in qhull.delaunay"); - goto error; - } - - triangles_ptr = (int*)PyArray_DATA(triangles); - neighbors_ptr = (int*)PyArray_DATA(neighbors); + numpy::array_view neighbors(dims); + int* neighbors_ptr = neighbors.data(); /* Determine triangles array and set tri_indices array. */ i = 0; FORALLfacets { if (!facet->upperdelaunay) { + int indices[3]; tri_indices[facet->id] = i++; get_facet_vertices(qh, facet, indices); *triangles_ptr++ = (facet->toporient ? indices[0] : indices[2]); *triangles_ptr++ = indices[1]; *triangles_ptr++ = (facet->toporient ? indices[2] : indices[0]); } - else + else { tri_indices[facet->id] = -1; + } } /* Determine neighbors array. */ FORALLfacets { if (!facet->upperdelaunay) { + int indices[3]; get_facet_neighbours(facet, tri_indices, indices); *neighbors_ptr++ = (facet->toporient ? indices[2] : indices[0]); *neighbors_ptr++ = (facet->toporient ? indices[0] : indices[2]); @@ -226,118 +238,94 @@ delaunay_impl(int npoints, const double* x, const double* y, } } - /* Clean up. */ - qh_freeqhull(qh, !qh_ALL); - qh_memfreeshort(qh, &curlong, &totlong); - if (curlong || totlong) - PyErr_WarnEx(PyExc_RuntimeWarning, - "Qhull could not free all allocated memory", 1); - if (hide_qhull_errors) - fclose(error_file); - free(tri_indices); - free(points); - - tuple = PyTuple_New(2); - PyTuple_SetItem(tuple, 0, (PyObject*)triangles); - PyTuple_SetItem(tuple, 1, (PyObject*)neighbors); - return tuple; + PyObject* tuple = PyTuple_New(2); + if (tuple == 0) { + throw std::runtime_error("Failed to create Python tuple"); + } -error: - /* Clean up. */ - Py_XDECREF(triangles); - Py_XDECREF(neighbors); - qh_freeqhull(qh, !qh_ALL); - qh_memfreeshort(qh, &curlong, &totlong); - /* Don't bother checking curlong and totlong as raising error anyway. */ - if (hide_qhull_errors) - fclose(error_file); - free(tri_indices); - -error_before_qhull: - free(points); - - return NULL; + PyTuple_SET_ITEM(tuple, 0, triangles.pyobj()); + PyTuple_SET_ITEM(tuple, 1, neighbors.pyobj()); + return tuple; } -/* Process python arguments and call Delaunay implementation method. */ +/* Process Python arguments and call Delaunay implementation method. */ static PyObject* delaunay(PyObject *self, PyObject *args) { - PyObject* xarg; - PyObject* yarg; - PyArrayObject* xarray; - PyArrayObject* yarray; + numpy::array_view xarray; + numpy::array_view yarray; PyObject* ret; - int npoints; + npy_intp npoints; const double* x; const double* y; - if (!PyArg_ParseTuple(args, "OO", &xarg, &yarg)) { - PyErr_SetString(PyExc_ValueError, "expecting x and y arrays"); + if (!PyArg_ParseTuple(args, "O&O&", + &xarray.converter_contiguous, &xarray, + &yarray.converter_contiguous, &yarray)) { return NULL; } - xarray = (PyArrayObject*)PyArray_ContiguousFromObject(xarg, NPY_DOUBLE, - 1, 1); - yarray = (PyArrayObject*)PyArray_ContiguousFromObject(yarg, NPY_DOUBLE, - 1, 1); - if (xarray == 0 || yarray == 0 || - PyArray_DIM(xarray,0) != PyArray_DIM(yarray, 0)) { - Py_XDECREF(xarray); - Py_XDECREF(yarray); + npoints = xarray.dim(0); + if (npoints != yarray.dim(0)) { PyErr_SetString(PyExc_ValueError, "x and y must be 1D arrays of the same length"); return NULL; } - npoints = PyArray_DIM(xarray, 0); - if (npoints < 3) { - Py_XDECREF(xarray); - Py_XDECREF(yarray); PyErr_SetString(PyExc_ValueError, "x and y arrays must have a length of at least 3"); return NULL; } - x = (const double*)PyArray_DATA(xarray); - y = (const double*)PyArray_DATA(yarray); + x = xarray.data(); + y = yarray.data(); if (!at_least_3_unique_points(npoints, x, y)) { - Py_XDECREF(xarray); - Py_XDECREF(yarray); PyErr_SetString(PyExc_ValueError, "x and y arrays must consist of at least 3 unique points"); return NULL; } - ret = delaunay_impl(npoints, x, y, Py_VerboseFlag == 0); + CALL_CPP("qhull.delaunay", + (ret = delaunay_impl(npoints, x, y, Py_VerboseFlag == 0))); - Py_XDECREF(xarray); - Py_XDECREF(yarray); return ret; } /* Return qhull version string for assistance in debugging. */ static PyObject* -version(void) +version(PyObject *self, PyObject *arg) { return PyBytes_FromString(qh_version); } static PyMethodDef qhull_methods[] = { - {"delaunay", (PyCFunction)delaunay, METH_VARARGS, ""}, - {"version", (PyCFunction)version, METH_NOARGS, ""}, + {"delaunay", delaunay, METH_VARARGS, + "delaunay(x, y, /)\n" + "--\n\n" + "Compute a Delaunay triangulation.\n" + "\n" + "Parameters\n" + "----------\n" + "x, y : 1d arrays\n" + " The coordinates of the point set, which must consist of at least\n" + " three unique points.\n" + "\n" + "Returns\n" + "-------\n" + "triangles, neighbors : int arrays, shape (ntri, 3)\n" + " Indices of triangle vertices and indices of triangle neighbors.\n" + }, + {"version", version, METH_NOARGS, + "version()\n--\n\n" + "Return the qhull version string."}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef qhull_module = { PyModuleDef_HEAD_INIT, - "qhull", - "Computing Delaunay triangulations.\n", - -1, - qhull_methods, - NULL, NULL, NULL, NULL + "qhull", "Computing Delaunay triangulations.\n", -1, qhull_methods }; #pragma GCC visibility push(default) @@ -345,17 +333,8 @@ static struct PyModuleDef qhull_module = { PyMODINIT_FUNC PyInit__qhull(void) { - PyObject* m; - - m = PyModule_Create(&qhull_module); - - if (m == NULL) { - return NULL; - } - import_array(); - - return m; + return PyModule_Create(&qhull_module); } #pragma GCC visibility pop diff --git a/src/_tkagg.cpp b/src/_tkagg.cpp index 5f058d14e0f0..663c06fd0474 100644 --- a/src/_tkagg.cpp +++ b/src/_tkagg.cpp @@ -19,15 +19,21 @@ /* * Unfortunately cygwin's libdl inherits restrictions from the underlying * Windows OS, at least currently. Therefore, a symbol may be loaded from a - * module by dlsym() only if it is really located in the given modile, + * module by dlsym() only if it is really located in the given module, * dependencies are not included. So we have to use native WinAPI on Cygwin * also. */ #define WIN32_DLL +static inline PyObject *PyErr_SetFromWindowsErr(int ierr) { + PyErr_SetString(PyExc_OSError, "Call to EnumProcessModules failed"); + return NULL; +} #endif #ifdef WIN32_DLL +#include #include +#include #define PSAPI_VERSION 1 #include // Must be linked with 'psapi' library #define dlsym GetProcAddress @@ -48,7 +54,10 @@ static int convert_voidptr(PyObject *obj, void *p) // Global vars for Tk functions. We load these symbols from the tkinter // extension module or loaded Tk libraries at run-time. static Tk_FindPhoto_t TK_FIND_PHOTO; -static Tk_PhotoPutBlock_NoComposite_t TK_PHOTO_PUT_BLOCK_NO_COMPOSITE; +static Tk_PhotoPutBlock_t TK_PHOTO_PUT_BLOCK; +// Global vars for Tcl functions. We load these symbols from the tkinter +// extension module or loaded Tcl libraries at run-time. +static Tcl_SetVar_t TCL_SETVAR; static PyObject *mpl_tk_blit(PyObject *self, PyObject *args) { @@ -56,13 +65,16 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args) char const *photo_name; int height, width; unsigned char *data_ptr; + int comp_rule; + int put_retval; int o0, o1, o2, o3; int x1, x2, y1, y2; Tk_PhotoHandle photo; Tk_PhotoImageBlock block; - if (!PyArg_ParseTuple(args, "O&s(iiO&)(iiii)(iiii):blit", + if (!PyArg_ParseTuple(args, "O&s(iiO&)i(iiii)(iiii):blit", convert_voidptr, &interp, &photo_name, &height, &width, convert_voidptr, &data_ptr, + &comp_rule, &o0, &o1, &o2, &o3, &x1, &x2, &y1, &y2)) { goto exit; @@ -75,7 +87,12 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args) PyErr_SetString(PyExc_ValueError, "Attempting to draw out of bounds"); goto exit; } + if (comp_rule != TK_PHOTO_COMPOSITE_OVERLAY && comp_rule != TK_PHOTO_COMPOSITE_SET) { + PyErr_SetString(PyExc_ValueError, "Invalid comp_rule argument"); + goto exit; + } + Py_BEGIN_ALLOW_THREADS block.pixelPtr = data_ptr + 4 * ((height - y2) * width + x1); block.width = x2 - x1; block.height = y2 - y1; @@ -85,8 +102,13 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args) block.offset[1] = o1; block.offset[2] = o2; block.offset[3] = o3; - TK_PHOTO_PUT_BLOCK_NO_COMPOSITE( - photo, &block, x1, height - y2, x2 - x1, y2 - y1); + put_retval = TK_PHOTO_PUT_BLOCK( + interp, photo, &block, x1, height - y2, x2 - x1, y2 - y1, comp_rule); + Py_END_ALLOW_THREADS + if (put_retval == TCL_ERROR) { + return PyErr_NoMemory(); + } + exit: if (PyErr_Occurred()) { return NULL; @@ -95,51 +117,165 @@ static PyObject *mpl_tk_blit(PyObject *self, PyObject *args) } } +#ifdef WIN32_DLL +LRESULT CALLBACK +DpiSubclassProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam, + UINT_PTR uIdSubclass, DWORD_PTR dwRefData) +{ + switch (uMsg) { + case WM_DPICHANGED: + // This function is a subclassed window procedure, and so is run during + // the Tcl/Tk event loop. Unfortunately, Tkinter has a *second* lock on + // Tcl threading that is not exposed publicly, but is currently taken + // while we're in the window procedure. So while we can take the GIL to + // call Python code, we must not also call *any* Tk code from Python. + // So stay with Tcl calls in C only. + { + // This variable naming must match the name used in + // lib/matplotlib/backends/_backend_tk.py:FigureManagerTk. + std::string var_name("window_dpi"); + var_name += std::to_string((unsigned long long)hwnd); + + // X is high word, Y is low word, but they are always equal. + std::string dpi = std::to_string(LOWORD(wParam)); + + Tcl_Interp* interp = (Tcl_Interp*)dwRefData; + TCL_SETVAR(interp, var_name.c_str(), dpi.c_str(), 0); + } + return 0; + case WM_NCDESTROY: + RemoveWindowSubclass(hwnd, DpiSubclassProc, uIdSubclass); + break; + } + + return DefSubclassProc(hwnd, uMsg, wParam, lParam); +} +#endif + +static PyObject* +mpl_tk_enable_dpi_awareness(PyObject* self, PyObject*const* args, + Py_ssize_t nargs) +{ + if (nargs != 2) { + return PyErr_Format(PyExc_TypeError, + "enable_dpi_awareness() takes 2 positional " + "arguments but %zd were given", + nargs); + } + +#ifdef WIN32_DLL + HWND frame_handle = NULL; + Tcl_Interp *interp = NULL; + + if (!convert_voidptr(args[0], &frame_handle)) { + return NULL; + } + if (!convert_voidptr(args[1], &interp)) { + return NULL; + } + +#ifdef _DPI_AWARENESS_CONTEXTS_ + HMODULE user32 = LoadLibrary("user32.dll"); + + typedef DPI_AWARENESS_CONTEXT (WINAPI *GetWindowDpiAwarenessContext_t)(HWND); + GetWindowDpiAwarenessContext_t GetWindowDpiAwarenessContextPtr = + (GetWindowDpiAwarenessContext_t)GetProcAddress( + user32, "GetWindowDpiAwarenessContext"); + if (GetWindowDpiAwarenessContextPtr == NULL) { + FreeLibrary(user32); + Py_RETURN_FALSE; + } + + typedef BOOL (WINAPI *AreDpiAwarenessContextsEqual_t)(DPI_AWARENESS_CONTEXT, + DPI_AWARENESS_CONTEXT); + AreDpiAwarenessContextsEqual_t AreDpiAwarenessContextsEqualPtr = + (AreDpiAwarenessContextsEqual_t)GetProcAddress( + user32, "AreDpiAwarenessContextsEqual"); + if (AreDpiAwarenessContextsEqualPtr == NULL) { + FreeLibrary(user32); + Py_RETURN_FALSE; + } + + DPI_AWARENESS_CONTEXT ctx = GetWindowDpiAwarenessContextPtr(frame_handle); + bool per_monitor = ( + AreDpiAwarenessContextsEqualPtr( + ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2) || + AreDpiAwarenessContextsEqualPtr( + ctx, DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE)); + + if (per_monitor) { + // Per monitor aware means we need to handle WM_DPICHANGED by wrapping + // the Window Procedure, and the Python side needs to trace the Tk + // window_dpi variable stored on interp. + SetWindowSubclass(frame_handle, DpiSubclassProc, 0, (DWORD_PTR)interp); + } + FreeLibrary(user32); + return PyBool_FromLong(per_monitor); +#endif +#endif + + Py_RETURN_NONE; +} + static PyMethodDef functions[] = { { "blit", (PyCFunction)mpl_tk_blit, METH_VARARGS }, + { "enable_dpi_awareness", (PyCFunction)mpl_tk_enable_dpi_awareness, + METH_FASTCALL }, { NULL, NULL } /* sentinel */ }; -// Functions to fill global Tk function pointers by dynamic loading +// Functions to fill global Tcl/Tk function pointers by dynamic loading. template -int load_tk(T lib) +bool load_tcl_tk(T lib) { - // Try to fill Tk global vars with function pointers. Return the number of - // functions found. - return - !!(TK_FIND_PHOTO = - (Tk_FindPhoto_t)dlsym(lib, "Tk_FindPhoto")) + - !!(TK_PHOTO_PUT_BLOCK_NO_COMPOSITE = - (Tk_PhotoPutBlock_NoComposite_t)dlsym(lib, "Tk_PhotoPutBlock_NoComposite")); + // Try to fill Tcl/Tk global vars with function pointers. Return whether + // all of them have been filled. + if (auto ptr = dlsym(lib, "Tcl_SetVar")) { + TCL_SETVAR = (Tcl_SetVar_t)ptr; + } + if (auto ptr = dlsym(lib, "Tk_FindPhoto")) { + TK_FIND_PHOTO = (Tk_FindPhoto_t)ptr; + } + if (auto ptr = dlsym(lib, "Tk_PhotoPutBlock")) { + TK_PHOTO_PUT_BLOCK = (Tk_PhotoPutBlock_t)ptr; + } + return TCL_SETVAR && TK_FIND_PHOTO && TK_PHOTO_PUT_BLOCK; } #ifdef WIN32_DLL -/* - * On Windows, we can't load the tkinter module to get the Tk symbols, because - * Windows does not load symbols into the library name-space of importing - * modules. So, knowing that tkinter has already been imported by Python, we - * scan all modules in the running process for the Tk function names. +/* On Windows, we can't load the tkinter module to get the Tcl/Tk symbols, + * because Windows does not load symbols into the library name-space of + * importing modules. So, knowing that tkinter has already been imported by + * Python, we scan all modules in the running process for the Tcl/Tk function + * names. */ void load_tkinter_funcs(void) { - // Load Tk functions by searching all modules in current process. - HMODULE hMods[1024]; - HANDLE hProcess; - DWORD cbNeeded; - unsigned int i; - // Returns pseudo-handle that does not need to be closed - hProcess = GetCurrentProcess(); - // Iterate through modules in this process looking for Tk names. - if (EnumProcessModules(hProcess, hMods, sizeof(hMods), &cbNeeded)) { - for (i = 0; i < (cbNeeded / sizeof(HMODULE)); i++) { - if (load_tk(hMods[i])) { - return; - } + HANDLE process = GetCurrentProcess(); // Pseudo-handle, doesn't need closing. + HMODULE* modules = NULL; + DWORD size; + if (!EnumProcessModules(process, NULL, 0, &size)) { + PyErr_SetFromWindowsErr(0); + goto exit; + } + if (!(modules = static_cast(malloc(size)))) { + PyErr_NoMemory(); + goto exit; + } + if (!EnumProcessModules(process, modules, size, &size)) { + PyErr_SetFromWindowsErr(0); + goto exit; + } + for (unsigned i = 0; i < size / sizeof(HMODULE); ++i) { + if (load_tcl_tk(modules[i])) { + return; } } +exit: + free(modules); } #else // not Windows @@ -159,7 +295,7 @@ void load_tkinter_funcs(void) // Try loading from the main program namespace first. main_program = dlopen(NULL, RTLD_LAZY); - if (load_tk(main_program)) { + if (load_tcl_tk(main_program)) { goto exit; } // Clear exception triggered when we didn't find symbols above. @@ -182,7 +318,7 @@ void load_tkinter_funcs(void) PyErr_SetString(PyExc_RuntimeError, dlerror()); goto exit; } - if (load_tk(tkinter_lib)) { + if (load_tcl_tk(tkinter_lib)) { goto exit; } @@ -201,7 +337,7 @@ void load_tkinter_funcs(void) #endif // end not Windows static PyModuleDef _tkagg_module = { - PyModuleDef_HEAD_INIT, "_tkagg", "", -1, functions, NULL, NULL, NULL, NULL + PyModuleDef_HEAD_INIT, "_tkagg", NULL, -1, functions }; #pragma GCC visibility push(default) @@ -209,13 +345,22 @@ static PyModuleDef _tkagg_module = { PyMODINIT_FUNC PyInit__tkagg(void) { load_tkinter_funcs(); - if (PyErr_Occurred()) { + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); + // Always raise ImportError (normalizing a previously set exception if + // needed) to interact properly with backend auto-fallback. + if (value) { + PyErr_NormalizeException(&type, &value, &traceback); + PyErr_SetObject(PyExc_ImportError, value); + return NULL; + } else if (!TCL_SETVAR) { + PyErr_SetString(PyExc_ImportError, "Failed to load Tcl_SetVar"); return NULL; } else if (!TK_FIND_PHOTO) { - PyErr_SetString(PyExc_RuntimeError, "Failed to load Tk_FindPhoto"); + PyErr_SetString(PyExc_ImportError, "Failed to load Tk_FindPhoto"); return NULL; - } else if (!TK_PHOTO_PUT_BLOCK_NO_COMPOSITE) { - PyErr_SetString(PyExc_RuntimeError, "Failed to load Tk_PhotoPutBlock_NoComposite"); + } else if (!TK_PHOTO_PUT_BLOCK) { + PyErr_SetString(PyExc_ImportError, "Failed to load Tk_PhotoPutBlock"); return NULL; } return PyModule_Create(&_tkagg_module); diff --git a/src/_tkmini.h b/src/_tkmini.h index d99b73291987..85f245815e4c 100644 --- a/src/_tkmini.h +++ b/src/_tkmini.h @@ -86,14 +86,24 @@ typedef struct Tk_PhotoImageBlock int offset[4]; } Tk_PhotoImageBlock; +#define TK_PHOTO_COMPOSITE_OVERLAY 0 // apply transparency rules pixel-wise +#define TK_PHOTO_COMPOSITE_SET 1 // set image buffer directly +#define TCL_OK 0 +#define TCL_ERROR 1 + /* Typedefs derived from function signatures in Tk header */ /* Tk_FindPhoto typedef */ typedef Tk_PhotoHandle (*Tk_FindPhoto_t) (Tcl_Interp *interp, const char *imageName); -/* Tk_PhotoPutBLock_NoComposite typedef */ -typedef void (*Tk_PhotoPutBlock_NoComposite_t) (Tk_PhotoHandle handle, +/* Tk_PhotoPutBLock typedef */ +typedef int (*Tk_PhotoPutBlock_t) (Tcl_Interp *interp, Tk_PhotoHandle handle, Tk_PhotoImageBlock *blockPtr, int x, int y, - int width, int height); + int width, int height, int compRule); + +/* Typedefs derived from function signatures in Tcl header */ +/* Tcl_SetVar typedef */ +typedef const char *(*Tcl_SetVar_t)(Tcl_Interp *interp, const char *varName, + const char *newValue, int flags); #ifdef __cplusplus } diff --git a/src/_ttconv.cpp b/src/_ttconv.cpp index 2fadb9bfa5fa..b88edbd2883d 100644 --- a/src/_ttconv.cpp +++ b/src/_ttconv.cpp @@ -150,81 +150,6 @@ static PyObject *convert_ttf_to_ps(PyObject *self, PyObject *args, PyObject *kwd return Py_None; } -class PythonDictionaryCallback : public TTDictionaryCallback -{ - PyObject *_dict; - - public: - PythonDictionaryCallback(PyObject *dict) - { - _dict = dict; - } - - virtual void add_pair(const char *a, const char *b) - { - assert(a != NULL); - assert(b != NULL); - PyObject *value = PyBytes_FromString(b); - if (!value) { - throw py::exception(); - } - if (PyDict_SetItemString(_dict, a, value)) { - Py_DECREF(value); - throw py::exception(); - } - Py_DECREF(value); - } -}; - -static PyObject *py_get_pdf_charprocs(PyObject *self, PyObject *args, PyObject *kwds) -{ - const char *filename; - std::vector glyph_ids; - PyObject *result; - - static const char *kwlist[] = { "filename", "glyph_ids", NULL }; - if (!PyArg_ParseTupleAndKeywords(args, - kwds, - "y|O&:get_pdf_charprocs", - (char **)kwlist, - &filename, - pyiterable_to_vector_int, - &glyph_ids)) { - return NULL; - } - - result = PyDict_New(); - if (!result) { - return NULL; - } - - PythonDictionaryCallback dict(result); - - try - { - ::get_pdf_charprocs(filename, glyph_ids, dict); - } - catch (TTException &e) - { - Py_DECREF(result); - PyErr_SetString(PyExc_RuntimeError, e.getMessage()); - return NULL; - } - catch (const py::exception &) - { - Py_DECREF(result); - return NULL; - } - catch (...) - { - Py_DECREF(result); - PyErr_SetString(PyExc_RuntimeError, "Unknown C++ exception"); - return NULL; - } - - return result; -} - static PyMethodDef ttconv_methods[] = { { @@ -239,26 +164,12 @@ static PyMethodDef ttconv_methods[] = "font data will be written to.\n" "fonttype may be either 3 or 42. Type 3 is a \"raw Postscript\" font. " "Type 42 is an embedded Truetype font. Glyph subsetting is not supported " - "for Type 42 fonts.\n" + "for Type 42 fonts within this module (needs to be done externally).\n" "glyph_ids (optional) is a list of glyph ids (integers) to keep when " "subsetting to a Type 3 font. If glyph_ids is not provided or is None, " "then all glyphs will be included. If any of the glyphs specified are " "composite glyphs, then the component glyphs will also be included." }, - { - "get_pdf_charprocs", (PyCFunction)py_get_pdf_charprocs, METH_VARARGS | METH_KEYWORDS, - "get_pdf_charprocs(filename, glyph_ids)\n" - "\n" - "Given a Truetype font file, returns a dictionary containing the PDF Type 3\n" - "representation of its paths. Useful for subsetting a Truetype font inside\n" - "of a PDF file.\n" - "\n" - "filename is the path to a TTF font file.\n" - "glyph_ids is a list of the numeric glyph ids to include.\n" - "The return value is a dictionary where the keys are glyph names and\n" - "the values are the stream content needed to render that glyph. This\n" - "is useful to generate the CharProcs dictionary in a PDF Type 3 font.\n" - }, {0, 0, 0, 0} /* Sentinel */ }; @@ -273,7 +184,6 @@ static PyModuleDef ttconv_module = { module_docstring, -1, ttconv_methods, - NULL, NULL, NULL, NULL }; #pragma GCC visibility push(default) @@ -281,11 +191,7 @@ static PyModuleDef ttconv_module = { PyMODINIT_FUNC PyInit__ttconv(void) { - PyObject* m; - - m = PyModule_Create(&ttconv_module); - - return m; + return PyModule_Create(&ttconv_module); } #pragma GCC visibility pop diff --git a/src/checkdep_freetype2.c b/src/checkdep_freetype2.c index d0e8fd34fbe9..8d9d8ca24a07 100644 --- a/src/checkdep_freetype2.c +++ b/src/checkdep_freetype2.c @@ -1,7 +1,7 @@ #ifdef __has_include #if !__has_include() #error "FreeType version 2.3 or higher is required. \ -You may unset the system_freetype entry in setup.cfg to let Matplotlib download it." +You may unset the system_freetype entry in mplsetup.cfg to let Matplotlib download it." #endif #endif @@ -15,5 +15,5 @@ You may unset the system_freetype entry in setup.cfg to let Matplotlib download XSTR(FREETYPE_MAJOR) "." XSTR(FREETYPE_MINOR) "." XSTR(FREETYPE_PATCH) ".") #if FREETYPE_MAJOR << 16 + FREETYPE_MINOR << 8 + FREETYPE_PATCH < 0x020300 #error "FreeType version 2.3 or higher is required. \ -You may unset the system_freetype entry in setup.cfg to let Matplotlib download it." +You may unset the system_freetype entry in mplsetup.cfg to let Matplotlib download it." #endif diff --git a/src/ft2font.cpp b/src/ft2font.cpp index 720d8a622df5..1dc831545554 100644 --- a/src/ft2font.cpp +++ b/src/ft2font.cpp @@ -3,6 +3,7 @@ #define NO_IMPORT_ARRAY #include +#include #include #include #include @@ -20,7 +21,7 @@ To improve the hinting of the fonts, this code uses a hack presented here: - http://antigrain.com/research/font_rasterization/index.html + http://agg.sourceforge.net/antigrain.com/research/font_rasterization/index.html The idea is to limit the effect of hinting in the x-direction, while preserving hinting in the y-direction. Since freetype does not @@ -43,9 +44,23 @@ FT_Library _ft2Library; +// FreeType error codes; loaded as per fterror.h. +static char const* ft_error_string(FT_Error error) { +#undef __FTERRORS_H__ +#define FT_ERROR_START_LIST switch (error) { +#define FT_ERRORDEF( e, v, s ) case v: return s; +#define FT_ERROR_END_LIST default: return NULL; } +#include FT_ERRORS_H +} + void throw_ft_error(std::string message, FT_Error error) { + char const* s = ft_error_string(error); std::ostringstream os(""); - os << message << " (error code 0x" << std::hex << error << ")"; + if (s) { + os << message << " (" << s << "; error code 0x" << std::hex << error << ")"; + } else { // Should not occur, but don't add another error from failed lookup. + os << message << " (error code 0x" << std::hex << error << ")"; + } throw std::runtime_error(os.str()); } @@ -99,13 +114,13 @@ void FT2Image::draw_bitmap(FT_Bitmap *bitmap, FT_Int x, FT_Int y) FT_Int char_width = bitmap->width; FT_Int char_height = bitmap->rows; - FT_Int x1 = CLAMP(x, 0, image_width); - FT_Int y1 = CLAMP(y, 0, image_height); - FT_Int x2 = CLAMP(x + char_width, 0, image_width); - FT_Int y2 = CLAMP(y + char_height, 0, image_height); + FT_Int x1 = std::min(std::max(x, 0), image_width); + FT_Int y1 = std::min(std::max(y, 0), image_height); + FT_Int x2 = std::min(std::max(x + char_width, 0), image_width); + FT_Int y2 = std::min(std::max(y + char_height, 0), image_height); - FT_Int x_start = MAX(0, -x); - FT_Int y_offset = y1 - MAX(0, -y); + FT_Int x_start = std::max(0, -x); + FT_Int y_offset = y1 - std::max(0, -y); if (bitmap->pixel_mode == FT_PIXEL_MODE_GRAY) { for (FT_Int i = y1; i < y2; ++i) { @@ -169,20 +184,33 @@ FT2Image::draw_rect_filled(unsigned long x0, unsigned long y0, unsigned long x1, m_dirty = true; } -static FT_UInt ft_get_char_index_or_warn(FT_Face face, FT_ULong charcode) +static void ft_glyph_warn(FT_ULong charcode) { - FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); - if (!glyph_index) { - PyErr_WarnFormat(NULL, 1, "Glyph %lu missing from current font.", charcode); - // Apparently PyErr_WarnFormat returns 0 even if the exception propagates - // due to running with -Werror, so check the error flag directly instead. - if (PyErr_Occurred()) { - throw py::exception(); - } + PyObject *text_helpers = NULL, *tmp = NULL; + if (!(text_helpers = PyImport_ImportModule("matplotlib._text_helpers")) || + !(tmp = PyObject_CallMethod(text_helpers, "warn_on_missing_glyph", "k", charcode))) { + goto exit; + } +exit: + Py_XDECREF(text_helpers); + Py_XDECREF(tmp); + if (PyErr_Occurred()) { + throw py::exception(); } - return glyph_index; } +static FT_UInt +ft_get_char_index_or_warn(FT_Face face, FT_ULong charcode, bool warn = true) +{ + FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); + if (glyph_index) { + return glyph_index; + } + if (warn) { + ft_glyph_warn(charcode); + } + return 0; +} // ft_outline_decomposer should be passed to FT_Outline_Decompose. On the // first pass, vertices and codes are set to NULL, and index is simply @@ -203,10 +231,10 @@ ft_outline_move_to(FT_Vector const* to, void* user) ft_outline_decomposer* d = reinterpret_cast(user); if (d->codes) { if (d->index) { - // Appending ENDPOLY is important to make patheffects work. + // Appending CLOSEPOLY is important to make patheffects work. *(d->vertices++) = 0; *(d->vertices++) = 0; - *(d->codes++) = ENDPOLY; + *(d->codes++) = CLOSEPOLY; } *(d->vertices++) = to->x / 64.; *(d->vertices++) = to->y / 64.; @@ -286,7 +314,7 @@ FT2Font::get_path() "FT_Outline_Decompose failed with error 0x%x", error); return NULL; } - if (!decomposer.index) { // Don't append ENDPOLY to null glyphs. + if (!decomposer.index) { // Don't append CLOSEPOLY to null glyphs. npy_intp vertices_dims[2] = { 0, 2 }; numpy::array_view vertices(vertices_dims); npy_intp codes_dims[1] = { 0 }; @@ -309,23 +337,19 @@ FT2Font::get_path() } *(decomposer.vertices++) = 0; *(decomposer.vertices++) = 0; - *(decomposer.codes++) = ENDPOLY; + *(decomposer.codes++) = CLOSEPOLY; return Py_BuildValue("NN", vertices.pyobj(), codes.pyobj()); } -FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_) : image(), face(NULL) +FT2Font::FT2Font(FT_Open_Args &open_args, + long hinting_factor_, + std::vector &fallback_list) + : image(), face(NULL) { clear(); FT_Error error = FT_Open_Face(_ft2Library, &open_args, 0, &face); - - if (error == FT_Err_Unknown_File_Format) { - throw std::runtime_error("Can not load face. Unknown file format."); - } else if (error == FT_Err_Cannot_Open_Resource) { - throw std::runtime_error("Can not load face. Can not open resource."); - } else if (error == FT_Err_Invalid_File_Format) { - throw std::runtime_error("Can not load face. Invalid file format."); - } else if (error) { + if (error) { throw_ft_error("Can not load face", error); } @@ -347,6 +371,9 @@ FT2Font::FT2Font(FT_Open_Args &open_args, long hinting_factor_) : image(), face( FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 }; FT_Set_Transform(face, &transform, 0); + + // Set fallbacks + std::copy(fallback_list.begin(), fallback_list.end(), std::back_inserter(fallbacks)); } FT2Font::~FT2Font() @@ -370,6 +397,12 @@ void FT2Font::clear() } glyphs.clear(); + glyph_to_font.clear(); + char_to_font.clear(); + + for (size_t i = 0; i < fallbacks.size(); i++) { + fallbacks[i]->clear(); + } } void FT2Font::set_size(double ptsize, double dpi) @@ -381,6 +414,10 @@ void FT2Font::set_size(double ptsize, double dpi) } FT_Matrix transform = { 65536 / hinting_factor, 0, 0, 65536 }; FT_Set_Transform(face, &transform, 0); + + for (size_t i = 0; i < fallbacks.size(); i++) { + fallbacks[i]->set_size(ptsize, dpi); + } } void FT2Font::set_charmap(int i) @@ -401,12 +438,32 @@ void FT2Font::select_charmap(unsigned long i) } } -int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode) +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback = false) +{ + if (fallback && glyph_to_font.find(left) != glyph_to_font.end() && + glyph_to_font.find(right) != glyph_to_font.end()) { + FT2Font *left_ft_object = glyph_to_font[left]; + FT2Font *right_ft_object = glyph_to_font[right]; + if (left_ft_object != right_ft_object) { + // we do not know how to do kerning between different fonts + return 0; + } + // if left_ft_object is the same as right_ft_object, + // do the exact same thing which set_text does. + return right_ft_object->get_kerning(left, right, mode, false); + } + else + { + FT_Vector delta; + return get_kerning(left, right, mode, delta); + } +} + +int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta) { if (!FT_HAS_KERNING(face)) { return 0; } - FT_Vector delta; if (!FT_Get_Kerning(face, left, right, mode, &delta)) { return (int)(delta.x) / (hinting_factor << kerning_factor); @@ -418,6 +475,9 @@ int FT2Font::get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode) void FT2Font::set_kerning_factor(int factor) { kerning_factor = factor; + for (size_t i = 0; i < fallbacks.size(); i++) { + fallbacks[i]->set_kerning_factor(factor); + } } void FT2Font::set_text( @@ -427,47 +487,54 @@ void FT2Font::set_text( angle = angle / 360.0 * 2 * M_PI; - // this computes width and height in subpixels so we have to divide by 64 + // this computes width and height in subpixels so we have to multiply by 64 matrix.xx = (FT_Fixed)(cos(angle) * 0x10000L); matrix.xy = (FT_Fixed)(-sin(angle) * 0x10000L); matrix.yx = (FT_Fixed)(sin(angle) * 0x10000L); matrix.yy = (FT_Fixed)(cos(angle) * 0x10000L); - FT_Bool use_kerning = FT_HAS_KERNING(face); - FT_UInt previous = 0; - clear(); bbox.xMin = bbox.yMin = 32000; bbox.xMax = bbox.yMax = -32000; - for (unsigned int n = 0; n < N; n++) { - FT_UInt glyph_index; + FT_UInt previous = 0; + FT2Font *previous_ft_object = NULL; + + for (size_t n = 0; n < N; n++) { + FT_UInt glyph_index = 0; FT_BBox glyph_bbox; FT_Pos last_advance; - glyph_index = ft_get_char_index_or_warn(face, codepoints[n]); + FT_Error charcode_error, glyph_error; + FT2Font *ft_object_with_glyph = this; + bool was_found = load_char_with_fallback(ft_object_with_glyph, glyph_index, glyphs, + char_to_font, glyph_to_font, codepoints[n], flags, + charcode_error, glyph_error, false); + if (!was_found) { + ft_glyph_warn((FT_ULong)codepoints[n]); + + // render missing glyph tofu + // come back to top-most font + ft_object_with_glyph = this; + char_to_font[codepoints[n]] = ft_object_with_glyph; + glyph_to_font[glyph_index] = ft_object_with_glyph; + ft_object_with_glyph->load_glyph(glyph_index, flags, ft_object_with_glyph, false); + } // retrieve kerning distance and move pen position - if (use_kerning && previous && glyph_index) { + if ((ft_object_with_glyph == previous_ft_object) && // if both fonts are the same + ft_object_with_glyph->has_kerning() && // if the font knows how to kern + previous && glyph_index // and we really have 2 glyphs + ) { FT_Vector delta; - FT_Get_Kerning(face, previous, glyph_index, FT_KERNING_DEFAULT, &delta); - pen.x += delta.x / (hinting_factor << kerning_factor); - } - if (FT_Error error = FT_Load_Glyph(face, glyph_index, flags)) { - throw_ft_error("Could not load glyph", error); + pen.x += ft_object_with_glyph->get_kerning(previous, glyph_index, FT_KERNING_DEFAULT, delta); } - // ignore errors, jump to next glyph // extract glyph image and store it in our table + FT_Glyph &thisGlyph = glyphs[glyphs.size() - 1]; - FT_Glyph thisGlyph; - if (FT_Error error = FT_Get_Glyph(face->glyph, &thisGlyph)) { - throw_ft_error("Could not get glyph", error); - } - // ignore errors, jump to next glyph - - last_advance = face->glyph->advance.x; + last_advance = ft_object_with_glyph->get_face()->glyph->advance.x; FT_Glyph_Transform(thisGlyph, 0, &pen); FT_Glyph_Transform(thisGlyph, &matrix, 0); xys.push_back(pen.x); @@ -483,7 +550,8 @@ void FT2Font::set_text( pen.x += last_advance; previous = glyph_index; - glyphs.push_back(thisGlyph); + previous_ft_object = ft_object_with_glyph; + } FT_Vector_Transform(&pen, &matrix); @@ -494,17 +562,135 @@ void FT2Font::set_text( } } -void FT2Font::load_char(long charcode, FT_Int32 flags) +void FT2Font::load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback = false) +{ + // if this is parent FT2Font, cache will be filled in 2 ways: + // 1. set_text was previously called + // 2. set_text was not called and fallback was enabled + if (fallback && char_to_font.find(charcode) != char_to_font.end()) { + ft_object = char_to_font[charcode]; + // since it will be assigned to ft_object anyway + FT2Font *throwaway = NULL; + ft_object->load_char(charcode, flags, throwaway, false); + } else if (fallback) { + FT_UInt final_glyph_index; + FT_Error charcode_error, glyph_error; + FT2Font *ft_object_with_glyph = this; + bool was_found = load_char_with_fallback(ft_object_with_glyph, final_glyph_index, glyphs, char_to_font, + glyph_to_font, charcode, flags, charcode_error, glyph_error, true); + if (!was_found) { + ft_glyph_warn(charcode); + if (charcode_error) { + throw_ft_error("Could not load charcode", charcode_error); + } + else if (glyph_error) { + throw_ft_error("Could not load charcode", glyph_error); + } + } + ft_object = ft_object_with_glyph; + } else { + ft_object = this; + FT_UInt glyph_index = ft_get_char_index_or_warn(face, (FT_ULong)charcode); + + if (FT_Error error = FT_Load_Glyph(face, glyph_index, flags)) { + throw_ft_error("Could not load charcode", error); + } + FT_Glyph thisGlyph; + if (FT_Error error = FT_Get_Glyph(face->glyph, &thisGlyph)) { + throw_ft_error("Could not get glyph", error); + } + glyphs.push_back(thisGlyph); + } +} + + +bool FT2Font::get_char_fallback_index(FT_ULong charcode, int& index) const { - FT_UInt glyph_index = ft_get_char_index_or_warn(face, (FT_ULong)charcode); - if (FT_Error error = FT_Load_Glyph(face, glyph_index, flags)) { - throw_ft_error("Could not load charcode", error); + FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); + if (glyph_index) { + // -1 means the host has the char and we do not need to fallback + index = -1; + return true; + } else { + int inner_index = 0; + bool was_found; + + for (size_t i = 0; i < fallbacks.size(); ++i) { + // TODO handle recursion somehow! + was_found = fallbacks[i]->get_char_fallback_index(charcode, inner_index); + if (was_found) { + index = i; + return true; + } + } } - FT_Glyph thisGlyph; - if (FT_Error error = FT_Get_Glyph(face->glyph, &thisGlyph)) { - throw_ft_error("Could not get glyph", error); + return false; +} + + +bool FT2Font::load_char_with_fallback(FT2Font *&ft_object_with_glyph, + FT_UInt &final_glyph_index, + std::vector &parent_glyphs, + std::unordered_map &parent_char_to_font, + std::unordered_map &parent_glyph_to_font, + long charcode, + FT_Int32 flags, + FT_Error &charcode_error, + FT_Error &glyph_error, + bool override = false) +{ + FT_UInt glyph_index = FT_Get_Char_Index(face, charcode); + + if (glyph_index || override) { + charcode_error = FT_Load_Glyph(face, glyph_index, flags); + if (charcode_error) { + return false; + } + + FT_Glyph thisGlyph; + glyph_error = FT_Get_Glyph(face->glyph, &thisGlyph); + if (glyph_error) { + return false; + } + + final_glyph_index = glyph_index; + + // cache the result for future + // need to store this for anytime a character is loaded from a parent + // FT2Font object or to generate a mapping of individual characters to fonts + ft_object_with_glyph = this; + parent_glyph_to_font[final_glyph_index] = this; + parent_char_to_font[charcode] = this; + parent_glyphs.push_back(thisGlyph); + return true; + } + + else { + for (size_t i = 0; i < fallbacks.size(); ++i) { + bool was_found = fallbacks[i]->load_char_with_fallback( + ft_object_with_glyph, final_glyph_index, parent_glyphs, parent_char_to_font, + parent_glyph_to_font, charcode, flags, charcode_error, glyph_error, override); + if (was_found) { + return true; + } + } + return false; } - glyphs.push_back(thisGlyph); +} + +void FT2Font::load_glyph(FT_UInt glyph_index, + FT_Int32 flags, + FT2Font *&ft_object, + bool fallback = false) +{ + // cache is only for parent FT2Font + if (fallback && glyph_to_font.find(glyph_index) != glyph_to_font.end()) { + ft_object = glyph_to_font[glyph_index]; + } else { + ft_object = this; + } + + ft_object->load_glyph(glyph_index, flags); } void FT2Font::load_glyph(FT_UInt glyph_index, FT_Int32 flags) @@ -519,6 +705,28 @@ void FT2Font::load_glyph(FT_UInt glyph_index, FT_Int32 flags) glyphs.push_back(thisGlyph); } +FT_UInt FT2Font::get_char_index(FT_ULong charcode, bool fallback = false) +{ + FT2Font *ft_object = NULL; + if (fallback && char_to_font.find(charcode) != char_to_font.end()) { + // fallback denotes whether we want to search fallback list. + // should call set_text/load_char_with_fallback to parent FT2Font before + // wanting to use fallback list here. (since that populates the cache) + ft_object = char_to_font[charcode]; + } else { + // set as self + ft_object = this; + } + + // historically, get_char_index never raises a warning + return ft_get_char_index_or_warn(ft_object->get_face(), charcode, false); +} + +void FT2Font::get_cbox(FT_BBox &bbox) +{ + FT_Glyph_Get_CBox(glyphs.back(), ft_glyph_bbox_subpixels, &bbox); +} + void FT2Font::get_width_height(long *width, long *height) { *width = advance; @@ -538,8 +746,8 @@ void FT2Font::get_bitmap_offset(long *x, long *y) void FT2Font::draw_glyphs_to_bitmap(bool antialiased) { - size_t width = (bbox.xMax - bbox.xMin) / 64 + 2; - size_t height = (bbox.yMax - bbox.yMin) / 64 + 2; + long width = (bbox.xMax - bbox.xMin) / 64 + 2; + long height = (bbox.yMax - bbox.yMin) / 64 + 2; image.resize(width, height); @@ -609,8 +817,14 @@ void FT2Font::draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, im.draw_bitmap(&bitmap->bitmap, x + bitmap->left, y); } -void FT2Font::get_glyph_name(unsigned int glyph_number, char *buffer) +void FT2Font::get_glyph_name(unsigned int glyph_number, char *buffer, bool fallback = false) { + if (fallback && glyph_to_font.find(glyph_number) != glyph_to_font.end()) { + // cache is only for parent FT2Font + FT2Font *ft_object = glyph_to_font[glyph_number]; + ft_object->get_glyph_name(glyph_number, buffer, false); + return; + } if (!FT_HAS_GLYPH_NAMES(face)) { /* Note that this generated name must match the name that is generated by ttconv in ttfont_CharStrings_getname. */ diff --git a/src/ft2font.h b/src/ft2font.h index 0863f3450b36..dc157f0e2887 100644 --- a/src/ft2font.h +++ b/src/ft2font.h @@ -1,10 +1,12 @@ /* -*- mode: c++; c-basic-offset: 4 -*- */ /* A python interface to FreeType */ +#pragma once #ifndef MPL_FT2FONT_H #define MPL_FT2FONT_H #include #include +#include extern "C" { #include @@ -69,7 +71,7 @@ class FT2Font { public: - FT2Font(FT_Open_Args &open_args, long hinting_factor); + FT2Font(FT_Open_Args &open_args, long hinting_factor, std::vector &fallback_list); virtual ~FT2Font(); void clear(); void set_size(double ptsize, double dpi); @@ -77,9 +79,21 @@ class FT2Font void select_charmap(unsigned long i); void set_text( size_t N, uint32_t *codepoints, double angle, FT_Int32 flags, std::vector &xys); - int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode); + int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, bool fallback); + int get_kerning(FT_UInt left, FT_UInt right, FT_UInt mode, FT_Vector &delta); void set_kerning_factor(int factor); - void load_char(long charcode, FT_Int32 flags); + void load_char(long charcode, FT_Int32 flags, FT2Font *&ft_object, bool fallback); + bool load_char_with_fallback(FT2Font *&ft_object_with_glyph, + FT_UInt &final_glyph_index, + std::vector &parent_glyphs, + std::unordered_map &parent_char_to_font, + std::unordered_map &parent_glyph_to_font, + long charcode, + FT_Int32 flags, + FT_Error &charcode_error, + FT_Error &glyph_error, + bool override); + void load_glyph(FT_UInt glyph_index, FT_Int32 flags, FT2Font *&ft_object, bool fallback); void load_glyph(FT_UInt glyph_index, FT_Int32 flags); void get_width_height(long *width, long *height); void get_bitmap_offset(long *x, long *y); @@ -89,40 +103,51 @@ class FT2Font void get_xys(bool antialiased, std::vector &xys); void draw_glyphs_to_bitmap(bool antialiased); void draw_glyph_to_bitmap(FT2Image &im, int x, int y, size_t glyphInd, bool antialiased); - void get_glyph_name(unsigned int glyph_number, char *buffer); + void get_glyph_name(unsigned int glyph_number, char *buffer, bool fallback); long get_name_index(char *name); + FT_UInt get_char_index(FT_ULong charcode, bool fallback); + void get_cbox(FT_BBox &bbox); PyObject* get_path(); + bool get_char_fallback_index(FT_ULong charcode, int& index) const; - FT_Face &get_face() + FT_Face const &get_face() const { return face; } + FT2Image &get_image() { return image; } - FT_Glyph &get_last_glyph() + FT_Glyph const &get_last_glyph() const { return glyphs.back(); } - size_t get_last_glyph_index() + size_t get_last_glyph_index() const { return glyphs.size() - 1; } - size_t get_num_glyphs() + size_t get_num_glyphs() const { return glyphs.size(); } - long get_hinting_factor() + long get_hinting_factor() const { return hinting_factor; } + FT_Bool has_kerning() const + { + return FT_HAS_KERNING(face); + } private: FT2Image image; FT_Face face; FT_Vector pen; /* untransformed origin */ std::vector glyphs; + std::vector fallbacks; + std::unordered_map glyph_to_font; + std::unordered_map char_to_font; FT_BBox bbox; FT_Pos advance; long hinting_factor; diff --git a/src/ft2font_wrapper.cpp b/src/ft2font_wrapper.cpp index 362df3a2d3aa..8b415bf3efdb 100644 --- a/src/ft2font_wrapper.cpp +++ b/src/ft2font_wrapper.cpp @@ -7,6 +7,9 @@ // From Python #include +#include +#include + #define STRINGIFY(s) XSTRINGIFY(s) #define XSTRINGIFY(s) #s @@ -33,6 +36,8 @@ typedef struct Py_ssize_t suboffsets[2]; } PyFT2Image; +static PyTypeObject PyFT2ImageType; + static PyObject *PyFT2Image_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyFT2Image *self; @@ -62,11 +67,11 @@ static void PyFT2Image_dealloc(PyFT2Image *self) } const char *PyFT2Image_draw_rect__doc__ = - "draw_rect(x0, y0, x1, y1)\n" + "draw_rect(self, x0, y0, x1, y1)\n" "--\n\n" "Draw an empty rectangle to the image.\n"; -static PyObject *PyFT2Image_draw_rect(PyFT2Image *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Image_draw_rect(PyFT2Image *self, PyObject *args) { double x0, y0, x1, y1; @@ -80,11 +85,11 @@ static PyObject *PyFT2Image_draw_rect(PyFT2Image *self, PyObject *args, PyObject } const char *PyFT2Image_draw_rect_filled__doc__ = - "draw_rect_filled(x0, y0, x1, y1)\n" + "draw_rect_filled(self, x0, y0, x1, y1)\n" "--\n\n" "Draw a filled rectangle to the image.\n"; -static PyObject *PyFT2Image_draw_rect_filled(PyFT2Image *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Image_draw_rect_filled(PyFT2Image *self, PyObject *args) { double x0, y0, x1, y1; @@ -121,9 +126,7 @@ static int PyFT2Image_get_buffer(PyFT2Image *self, Py_buffer *buf, int flags) return 1; } -static PyTypeObject PyFT2ImageType; - -static PyTypeObject *PyFT2Image_init_type(PyObject *m, PyTypeObject *type) +static PyTypeObject* PyFT2Image_init_type() { static PyMethodDef methods[] = { {"draw_rect", (PyCFunction)PyFT2Image_draw_rect, METH_VARARGS, PyFT2Image_draw_rect__doc__}, @@ -132,28 +135,18 @@ static PyTypeObject *PyFT2Image_init_type(PyObject *m, PyTypeObject *type) }; static PyBufferProcs buffer_procs; - memset(&buffer_procs, 0, sizeof(PyBufferProcs)); buffer_procs.bf_getbuffer = (getbufferproc)PyFT2Image_get_buffer; - memset(type, 0, sizeof(PyTypeObject)); - type->tp_name = "matplotlib.ft2font.FT2Image"; - type->tp_basicsize = sizeof(PyFT2Image); - type->tp_dealloc = (destructor)PyFT2Image_dealloc; - type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - type->tp_methods = methods; - type->tp_new = PyFT2Image_new; - type->tp_init = (initproc)PyFT2Image_init; - type->tp_as_buffer = &buffer_procs; - - if (PyType_Ready(type) < 0) { - return NULL; - } + PyFT2ImageType.tp_name = "matplotlib.ft2font.FT2Image"; + PyFT2ImageType.tp_basicsize = sizeof(PyFT2Image); + PyFT2ImageType.tp_dealloc = (destructor)PyFT2Image_dealloc; + PyFT2ImageType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + PyFT2ImageType.tp_methods = methods; + PyFT2ImageType.tp_new = PyFT2Image_new; + PyFT2ImageType.tp_init = (initproc)PyFT2Image_init; + PyFT2ImageType.tp_as_buffer = &buffer_procs; - if (PyModule_AddObject(m, "FT2Image", (PyObject *)type)) { - return NULL; - } - - return type; + return &PyFT2ImageType; } /********************************************************************** @@ -178,14 +171,16 @@ typedef struct static PyTypeObject PyGlyphType; -static PyObject * -PyGlyph_new(const FT_Face &face, const FT_Glyph &glyph, size_t ind, long hinting_factor) +static PyObject *PyGlyph_from_FT2Font(const FT2Font *font) { + const FT_Face &face = font->get_face(); + const long hinting_factor = font->get_hinting_factor(); + const FT_Glyph &glyph = font->get_last_glyph(); + PyGlyph *self; self = (PyGlyph *)PyGlyphType.tp_alloc(&PyGlyphType, 0); - self->glyphInd = ind; - + self->glyphInd = font->get_last_glyph_index(); FT_Glyph_Get_CBox(glyph, ft_glyph_bbox_subpixels, &self->bbox); self->width = face->glyph->metrics.width / hinting_factor; @@ -212,7 +207,7 @@ static PyObject *PyGlyph_get_bbox(PyGlyph *self, void *closure) "llll", self->bbox.xMin, self->bbox.yMin, self->bbox.xMax, self->bbox.yMax); } -static PyTypeObject *PyGlyph_init_type(PyObject *m, PyTypeObject *type) +static PyTypeObject *PyGlyph_init_type() { static PyMemberDef members[] = { {(char *)"width", T_LONG, offsetof(PyGlyph, width), READONLY, (char *)""}, @@ -232,39 +227,33 @@ static PyTypeObject *PyGlyph_init_type(PyObject *m, PyTypeObject *type) {NULL} }; - memset(type, 0, sizeof(PyTypeObject)); - type->tp_name = "matplotlib.ft2font.Glyph"; - type->tp_basicsize = sizeof(PyGlyph); - type->tp_dealloc = (destructor)PyGlyph_dealloc; - type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - type->tp_members = members; - type->tp_getset = getset; - - if (PyType_Ready(type) < 0) { - return NULL; - } - - /* Don't need to add to module, since you can't create glyphs - directly from Python */ + PyGlyphType.tp_name = "matplotlib.ft2font.Glyph"; + PyGlyphType.tp_basicsize = sizeof(PyGlyph); + PyGlyphType.tp_dealloc = (destructor)PyGlyph_dealloc; + PyGlyphType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + PyGlyphType.tp_members = members; + PyGlyphType.tp_getset = getset; - return type; + return &PyGlyphType; } /********************************************************************** * FT2Font * */ -typedef struct +struct PyFT2Font { PyObject_HEAD FT2Font *x; - PyObject *fname; PyObject *py_file; FT_StreamRec stream; Py_ssize_t shape[2]; Py_ssize_t strides[2]; Py_ssize_t suboffsets[2]; -} PyFT2Font; + std::vector fallbacks; +}; + +static PyTypeObject PyFT2FontType; static unsigned long read_from_file_callback(FT_Stream stream, unsigned long offset, @@ -292,11 +281,13 @@ static unsigned long read_from_file_callback(FT_Stream stream, return 1; // Non-zero signals error, when count == 0. } } - return n_read; + return (unsigned long)n_read; } static void close_file_callback(FT_Stream stream) { + PyObject *type, *value, *traceback; + PyErr_Fetch(&type, &value, &traceback); PyFT2Font *self = (PyFT2Font *)stream->descriptor.pointer; PyObject *close_result = NULL; if (!(close_result = PyObject_CallMethod(self->py_file, "close", ""))) { @@ -308,26 +299,38 @@ static void close_file_callback(FT_Stream stream) if (PyErr_Occurred()) { PyErr_WriteUnraisable((PyObject*)self); } + PyErr_Restore(type, value, traceback); } -static PyTypeObject PyFT2FontType; - static PyObject *PyFT2Font_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { PyFT2Font *self; self = (PyFT2Font *)type->tp_alloc(type, 0); self->x = NULL; - self->fname = NULL; self->py_file = NULL; memset(&self->stream, 0, sizeof(FT_StreamRec)); return (PyObject *)self; } const char *PyFT2Font_init__doc__ = - "FT2Font(ttffile)\n" + "FT2Font(filename, hinting_factor=8, *, _fallback_list=None, _kerning_factor=0)\n" "--\n\n" "Create a new FT2Font object.\n" "\n" + "Parameters\n" + "----------\n" + "filename : str or file-like\n" + " The source of the font data in a format (ttf or ttc) that FreeType can read\n\n" + "hinting_factor : int, optional\n" + " Must be positive. Used to scale the hinting in the x-direction\n" + "_fallback_list : list of FT2Font, optional\n" + " A list of FT2Font objects used to find missing glyphs.\n\n" + " .. warning::\n" + " This API is both private and provisional: do not use it directly\n\n" + "_kerning_factor : int, optional\n" + " Used to adjust the degree of kerning.\n\n" + " .. warning::\n" + " This API is private: do not use it directly\n\n" "Attributes\n" "----------\n" "num_faces\n" @@ -361,17 +364,24 @@ const char *PyFT2Font_init__doc__ = static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) { - PyObject *filename = NULL, *open = NULL, *data = NULL; + PyObject *filename = NULL, *open = NULL, *data = NULL, *fallback_list = NULL; FT_Open_Args open_args; long hinting_factor = 8; int kerning_factor = 0; - const char *names[] = { "filename", "hinting_factor", "_kerning_factor", NULL }; - + const char *names[] = { + "filename", "hinting_factor", "_fallback_list", "_kerning_factor", NULL + }; + std::vector fallback_fonts; if (!PyArg_ParseTupleAndKeywords( - args, kwds, "O|l$i:FT2Font", (char **)names, &filename, - &hinting_factor, &kerning_factor)) { + args, kwds, "O|l$Oi:FT2Font", (char **)names, &filename, + &hinting_factor, &fallback_list, &kerning_factor)) { return -1; } + if (hinting_factor <= 0) { + PyErr_SetString(PyExc_ValueError, + "hinting_factor must be greater than 0"); + goto exit; + } self->stream.base = NULL; self->stream.size = 0x7fffffff; // Unknown size. @@ -382,6 +392,37 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) open_args.flags = FT_OPEN_STREAM; open_args.stream = &self->stream; + if (fallback_list) { + if (!PyList_Check(fallback_list)) { + PyErr_SetString(PyExc_TypeError, "Fallback list must be a list"); + goto exit; + } + Py_ssize_t size = PyList_Size(fallback_list); + + // go through fallbacks once to make sure the types are right + for (Py_ssize_t i = 0; i < size; ++i) { + // this returns a borrowed reference + PyObject* item = PyList_GetItem(fallback_list, i); + if (!PyObject_IsInstance(item, PyObject_Type(reinterpret_cast(self)))) { + PyErr_SetString(PyExc_TypeError, "Fallback fonts must be FT2Font objects."); + goto exit; + } + } + // go through a second time to add them to our lists + for (Py_ssize_t i = 0; i < size; ++i) { + // this returns a borrowed reference + PyObject* item = PyList_GetItem(fallback_list, i); + // Increase the ref count, we will undo this in dealloc this makes + // sure things do not get gc'd under us! + Py_INCREF(item); + self->fallbacks.push_back(item); + // Also (locally) cache the underlying FT2Font objects. As long as + // the Python objects are kept alive, these pointer are good. + FT2Font *fback = reinterpret_cast(item)->x; + fallback_fonts.push_back(fback); + } + } + if (PyBytes_Check(filename) || PyUnicode_Check(filename)) { if (!(open = PyDict_GetItemString(PyEval_GetBuiltins(), "open")) // Borrowed reference. || !(self->py_file = PyObject_CallFunction(open, "Os", filename, "rb"))) { @@ -392,7 +433,7 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) || !(data = PyObject_CallMethod(filename, "read", "i", 0)) || !PyBytes_Check(data)) { PyErr_SetString(PyExc_TypeError, - "First argument must be a path or binary-mode file object"); + "First argument must be a path to a font file or a binary-mode file object"); Py_CLEAR(data); goto exit; } else { @@ -403,14 +444,11 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) Py_CLEAR(data); CALL_CPP_FULL( - "FT2Font", (self->x = new FT2Font(open_args, hinting_factor)), + "FT2Font", (self->x = new FT2Font(open_args, hinting_factor, fallback_fonts)), Py_CLEAR(self->py_file), -1); CALL_CPP_INIT("FT2Font->set_kerning_factor", (self->x->set_kerning_factor(kerning_factor))); - Py_INCREF(filename); - self->fname = filename; - exit: return PyErr_Occurred() ? -1 : 0; } @@ -418,17 +456,20 @@ static int PyFT2Font_init(PyFT2Font *self, PyObject *args, PyObject *kwds) static void PyFT2Font_dealloc(PyFT2Font *self) { delete self->x; + for (size_t i = 0; i < self->fallbacks.size(); i++) { + Py_DECREF(self->fallbacks[i]); + } + Py_XDECREF(self->py_file); - Py_XDECREF(self->fname); Py_TYPE(self)->tp_free((PyObject *)self); } const char *PyFT2Font_clear__doc__ = - "clear()\n" + "clear(self)\n" "--\n\n" "Clear all the glyphs, reset for a new call to `.set_text`.\n"; -static PyObject *PyFT2Font_clear(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_clear(PyFT2Font *self, PyObject *args) { CALL_CPP("clear", (self->x->clear())); @@ -436,11 +477,11 @@ static PyObject *PyFT2Font_clear(PyFT2Font *self, PyObject *args, PyObject *kwds } const char *PyFT2Font_set_size__doc__ = - "set_size(ptsize, dpi)\n" + "set_size(self, ptsize, dpi)\n" "--\n\n" "Set the point size and dpi of the text.\n"; -static PyObject *PyFT2Font_set_size(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_set_size(PyFT2Font *self, PyObject *args) { double ptsize; double dpi; @@ -455,11 +496,11 @@ static PyObject *PyFT2Font_set_size(PyFT2Font *self, PyObject *args, PyObject *k } const char *PyFT2Font_set_charmap__doc__ = - "set_charmap(i)\n" + "set_charmap(self, i)\n" "--\n\n" "Make the i-th charmap current.\n"; -static PyObject *PyFT2Font_set_charmap(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_set_charmap(PyFT2Font *self, PyObject *args) { int i; @@ -473,11 +514,11 @@ static PyObject *PyFT2Font_set_charmap(PyFT2Font *self, PyObject *args, PyObject } const char *PyFT2Font_select_charmap__doc__ = - "select_charmap(i)\n" + "select_charmap(self, i)\n" "--\n\n" "Select a charmap by its FT_Encoding number.\n"; -static PyObject *PyFT2Font_select_charmap(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_select_charmap(PyFT2Font *self, PyObject *args) { unsigned long i; @@ -491,30 +532,101 @@ static PyObject *PyFT2Font_select_charmap(PyFT2Font *self, PyObject *args, PyObj } const char *PyFT2Font_get_kerning__doc__ = - "get_kerning(left, right, mode)\n" + "get_kerning(self, left, right, mode)\n" "--\n\n" "Get the kerning between *left* and *right* glyph indices.\n" - "*mode* is a kerning mode constant:\n" - " KERNING_DEFAULT - Return scaled and grid-fitted kerning distances\n" - " KERNING_UNFITTED - Return scaled but un-grid-fitted kerning distances\n" - " KERNING_UNSCALED - Return the kerning vector in original font units\n"; + "*mode* is a kerning mode constant:\n\n" + " - KERNING_DEFAULT - Return scaled and grid-fitted kerning distances\n" + " - KERNING_UNFITTED - Return scaled but un-grid-fitted kerning distances\n" + " - KERNING_UNSCALED - Return the kerning vector in original font units\n"; -static PyObject *PyFT2Font_get_kerning(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_kerning(PyFT2Font *self, PyObject *args) { FT_UInt left, right, mode; int result; + int fallback = 1; if (!PyArg_ParseTuple(args, "III:get_kerning", &left, &right, &mode)) { return NULL; } - CALL_CPP("get_kerning", (result = self->x->get_kerning(left, right, mode))); + CALL_CPP("get_kerning", (result = self->x->get_kerning(left, right, mode, (bool)fallback))); return PyLong_FromLong(result); } +const char *PyFT2Font_get_fontmap__doc__ = + "_get_fontmap(self, string)\n" + "--\n\n" + "Get a mapping between characters and the font that includes them.\n" + "A dictionary mapping unicode characters to PyFT2Font objects."; +static PyObject *PyFT2Font_get_fontmap(PyFT2Font *self, PyObject *args, PyObject *kwds) +{ + PyObject *textobj; + const char *names[] = { "string", NULL }; + + if (!PyArg_ParseTupleAndKeywords( + args, kwds, "O:_get_fontmap", (char **)names, &textobj)) { + return NULL; + } + + std::set codepoints; + size_t size; + + if (PyUnicode_Check(textobj)) { + size = PyUnicode_GET_LENGTH(textobj); +#if defined(PYPY_VERSION) && (PYPY_VERSION_NUM < 0x07040000) + // PyUnicode_ReadChar is available from PyPy 7.3.2, but wheels do not + // specify the micro-release version, so put the version bound at 7.4 + // to prevent generating wheels unusable on PyPy 7.3.{0,1}. + Py_UNICODE *unistr = PyUnicode_AsUnicode(textobj); + for (size_t i = 0; i < size; ++i) { + codepoints.insert(unistr[i]); + } +#else + for (size_t i = 0; i < size; ++i) { + codepoints.insert(PyUnicode_ReadChar(textobj, i)); + } +#endif + } else { + PyErr_SetString(PyExc_TypeError, "string must be str"); + return NULL; + } + PyObject *char_to_font; + if (!(char_to_font = PyDict_New())) { + return NULL; + } + for (auto it = codepoints.begin(); it != codepoints.end(); ++it) { + auto x = *it; + PyObject* target_font; + int index; + if (self->x->get_char_fallback_index(x, index)) { + if (index >= 0) { + target_font = self->fallbacks[index]; + } else { + target_font = (PyObject *)self; + } + } else { + // TODO Handle recursion! + target_font = (PyObject *)self; + } + + PyObject *key = NULL; + bool error = (!(key = PyUnicode_FromFormat("%c", x)) + || (PyDict_SetItem(char_to_font, key, target_font) == -1)); + Py_XDECREF(key); + if (error) { + Py_DECREF(char_to_font); + PyErr_SetString(PyExc_ValueError, "Something went very wrong"); + return NULL; + } + } + return char_to_font; +} + + const char *PyFT2Font_set_text__doc__ = - "set_text(string, angle, flags=32)\n" + "set_text(self, string, angle, flags=32)\n" "--\n\n" "Set the text *string* and *angle*.\n" "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" @@ -557,22 +669,8 @@ static PyObject *PyFT2Font_set_text(PyFT2Font *self, PyObject *args, PyObject *k codepoints[i] = PyUnicode_ReadChar(textobj, i); } #endif - } else if (PyBytes_Check(textobj)) { - if (PyErr_WarnEx( - PyExc_FutureWarning, - "Passing bytes to FTFont.set_text is deprecated since Matplotlib " - "3.4 and support will be removed in Matplotlib 3.6; pass str instead", - 1)) { - return NULL; - } - size = PyBytes_Size(textobj); - codepoints.resize(size); - char *bytestr = PyBytes_AsString(textobj); - for (size_t i = 0; i < size; ++i) { - codepoints[i] = bytestr[i]; - } } else { - PyErr_SetString(PyExc_TypeError, "String must be str or bytes"); + PyErr_SetString(PyExc_TypeError, "set_text requires str-input."); return NULL; } @@ -586,101 +684,99 @@ static PyObject *PyFT2Font_set_text(PyFT2Font *self, PyObject *args, PyObject *k } const char *PyFT2Font_get_num_glyphs__doc__ = - "get_num_glyphs()\n" + "get_num_glyphs(self)\n" "--\n\n" "Return the number of loaded glyphs.\n"; -static PyObject *PyFT2Font_get_num_glyphs(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_num_glyphs(PyFT2Font *self, PyObject *args) { - return PyLong_FromLong(self->x->get_num_glyphs()); + return PyLong_FromSize_t(self->x->get_num_glyphs()); } const char *PyFT2Font_load_char__doc__ = - "load_char(charcode, flags=32)\n" + "load_char(self, charcode, flags=32)\n" "--\n\n" "Load character with *charcode* in current fontfile and set glyph.\n" "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" "the default value is LOAD_FORCE_AUTOHINT.\n" - "Return value is a Glyph object, with attributes\n" - " width # glyph width\n" - " height # glyph height\n" - " bbox # the glyph bbox (xmin, ymin, xmax, ymax)\n" - " horiBearingX # left side bearing in horizontal layouts\n" - " horiBearingY # top side bearing in horizontal layouts\n" - " horiAdvance # advance width for horizontal layout\n" - " vertBearingX # left side bearing in vertical layouts\n" - " vertBearingY # top side bearing in vertical layouts\n" - " vertAdvance # advance height for vertical layout\n"; + "Return value is a Glyph object, with attributes\n\n" + "- width: glyph width\n" + "- height: glyph height\n" + "- bbox: the glyph bbox (xmin, ymin, xmax, ymax)\n" + "- horiBearingX: left side bearing in horizontal layouts\n" + "- horiBearingY: top side bearing in horizontal layouts\n" + "- horiAdvance: advance width for horizontal layout\n" + "- vertBearingX: left side bearing in vertical layouts\n" + "- vertBearingY: top side bearing in vertical layouts\n" + "- vertAdvance: advance height for vertical layout\n"; static PyObject *PyFT2Font_load_char(PyFT2Font *self, PyObject *args, PyObject *kwds) { long charcode; + int fallback = 1; FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; const char *names[] = { "charcode", "flags", NULL }; /* This makes a technically incorrect assumption that FT_Int32 is int. In theory it can also be long, if the size of int is less than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "l|i:load_char", (char **)names, &charcode, &flags)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|i:load_char", (char **)names, &charcode, + &flags)) { return NULL; } - CALL_CPP("load_char", (self->x->load_char(charcode, flags))); + FT2Font *ft_object = NULL; + CALL_CPP("load_char", (self->x->load_char(charcode, flags, ft_object, (bool)fallback))); - return PyGlyph_new(self->x->get_face(), - self->x->get_last_glyph(), - self->x->get_last_glyph_index(), - self->x->get_hinting_factor()); + return PyGlyph_from_FT2Font(ft_object); } const char *PyFT2Font_load_glyph__doc__ = - "load_glyph(glyphindex, flags=32)\n" + "load_glyph(self, glyphindex, flags=32)\n" "--\n\n" "Load character with *glyphindex* in current fontfile and set glyph.\n" "*flags* can be a bitwise-or of the LOAD_XXX constants;\n" "the default value is LOAD_FORCE_AUTOHINT.\n" - "Return value is a Glyph object, with attributes\n" - " width # glyph width\n" - " height # glyph height\n" - " bbox # the glyph bbox (xmin, ymin, xmax, ymax)\n" - " horiBearingX # left side bearing in horizontal layouts\n" - " horiBearingY # top side bearing in horizontal layouts\n" - " horiAdvance # advance width for horizontal layout\n" - " vertBearingX # left side bearing in vertical layouts\n" - " vertBearingY # top side bearing in vertical layouts\n" - " vertAdvance # advance height for vertical layout\n"; + "Return value is a Glyph object, with attributes\n\n" + "- width: glyph width\n" + "- height: glyph height\n" + "- bbox: the glyph bbox (xmin, ymin, xmax, ymax)\n" + "- horiBearingX: left side bearing in horizontal layouts\n" + "- horiBearingY: top side bearing in horizontal layouts\n" + "- horiAdvance: advance width for horizontal layout\n" + "- vertBearingX: left side bearing in vertical layouts\n" + "- vertBearingY: top side bearing in vertical layouts\n" + "- vertAdvance: advance height for vertical layout\n"; static PyObject *PyFT2Font_load_glyph(PyFT2Font *self, PyObject *args, PyObject *kwds) { FT_UInt glyph_index; FT_Int32 flags = FT_LOAD_FORCE_AUTOHINT; + int fallback = 1; const char *names[] = { "glyph_index", "flags", NULL }; /* This makes a technically incorrect assumption that FT_Int32 is int. In theory it can also be long, if the size of int is less than 32 bits. This is very unlikely on modern platforms. */ - if (!PyArg_ParseTupleAndKeywords( - args, kwds, "I|i:load_glyph", (char **)names, &glyph_index, &flags)) { + if (!PyArg_ParseTupleAndKeywords(args, kwds, "I|i:load_glyph", (char **)names, &glyph_index, + &flags)) { return NULL; } - CALL_CPP("load_glyph", (self->x->load_glyph(glyph_index, flags))); + FT2Font *ft_object = NULL; + CALL_CPP("load_glyph", (self->x->load_glyph(glyph_index, flags, ft_object, (bool)fallback))); - return PyGlyph_new(self->x->get_face(), - self->x->get_last_glyph(), - self->x->get_last_glyph_index(), - self->x->get_hinting_factor()); + return PyGlyph_from_FT2Font(ft_object); } const char *PyFT2Font_get_width_height__doc__ = - "get_width_height()\n" + "get_width_height(self)\n" "--\n\n" "Get the width and height in 26.6 subpixels of the current string set by `.set_text`.\n" "The rotation of the string is accounted for. To get width and height\n" "in pixels, divide these values by 64.\n"; -static PyObject *PyFT2Font_get_width_height(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_width_height(PyFT2Font *self, PyObject *args) { long width, height; @@ -690,12 +786,12 @@ static PyObject *PyFT2Font_get_width_height(PyFT2Font *self, PyObject *args, PyO } const char *PyFT2Font_get_bitmap_offset__doc__ = - "get_bitmap_offset()\n" + "get_bitmap_offset(self)\n" "--\n\n" "Get the (x, y) offset in 26.6 subpixels for the bitmap if ink hangs left or below (0, 0).\n" "Since Matplotlib only supports left-to-right text, y is always 0.\n"; -static PyObject *PyFT2Font_get_bitmap_offset(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_bitmap_offset(PyFT2Font *self, PyObject *args) { long x, y; @@ -705,13 +801,13 @@ static PyObject *PyFT2Font_get_bitmap_offset(PyFT2Font *self, PyObject *args, Py } const char *PyFT2Font_get_descent__doc__ = - "get_descent()\n" + "get_descent(self)\n" "--\n\n" "Get the descent in 26.6 subpixels of the current string set by `.set_text`.\n" "The rotation of the string is accounted for. To get the descent\n" "in pixels, divide this value by 64.\n"; -static PyObject *PyFT2Font_get_descent(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_descent(PyFT2Font *self, PyObject *args) { long descent; @@ -721,7 +817,7 @@ static PyObject *PyFT2Font_get_descent(PyFT2Font *self, PyObject *args, PyObject } const char *PyFT2Font_draw_glyphs_to_bitmap__doc__ = - "draw_glyphs_to_bitmap()\n" + "draw_glyphs_to_bitmap(self, antialiased=True)\n" "--\n\n" "Draw the glyphs that were loaded by `.set_text` to the bitmap.\n" "The bitmap size will be automatically set to include the glyphs.\n"; @@ -742,7 +838,7 @@ static PyObject *PyFT2Font_draw_glyphs_to_bitmap(PyFT2Font *self, PyObject *args } const char *PyFT2Font_get_xys__doc__ = - "get_xys()\n" + "get_xys(self, antialiased=True)\n" "--\n\n" "Get the xy locations of the current glyphs.\n"; @@ -763,7 +859,7 @@ static PyObject *PyFT2Font_get_xys(PyFT2Font *self, PyObject *args, PyObject *kw } const char *PyFT2Font_draw_glyph_to_bitmap__doc__ = - "draw_glyph_to_bitmap(bitmap, x, y, glyph)\n" + "draw_glyph_to_bitmap(self, image, x, y, glyph, antialiased=True)\n" "--\n\n" "Draw a single glyph to the bitmap at pixel locations x, y\n" "Note it is your responsibility to set up the bitmap manually\n" @@ -804,7 +900,7 @@ static PyObject *PyFT2Font_draw_glyph_to_bitmap(PyFT2Font *self, PyObject *args, } const char *PyFT2Font_get_glyph_name__doc__ = - "get_glyph_name(index)\n" + "get_glyph_name(self, index)\n" "--\n\n" "Retrieve the ASCII name of a given glyph *index* in a face.\n" "\n" @@ -812,24 +908,26 @@ const char *PyFT2Font_get_glyph_name__doc__ = "names (per FT_FACE_FLAG_GLYPH_NAMES), this returns a made-up name which\n" "does *not* roundtrip through `.get_name_index`.\n"; -static PyObject *PyFT2Font_get_glyph_name(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_glyph_name(PyFT2Font *self, PyObject *args) { unsigned int glyph_number; char buffer[128]; + int fallback = 1; + if (!PyArg_ParseTuple(args, "I:get_glyph_name", &glyph_number)) { return NULL; } - CALL_CPP("get_glyph_name", (self->x->get_glyph_name(glyph_number, buffer))); + CALL_CPP("get_glyph_name", (self->x->get_glyph_name(glyph_number, buffer, (bool)fallback))); return PyUnicode_FromString(buffer); } const char *PyFT2Font_get_charmap__doc__ = - "get_charmap()\n" + "get_charmap(self)\n" "--\n\n" "Return a dict that maps the character codes of the selected charmap\n" "(Unicode by default) to their corresponding glyph indices.\n"; -static PyObject *PyFT2Font_get_charmap(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_charmap(PyFT2Font *self, PyObject *args) { PyObject *charmap; if (!(charmap = PyDict_New())) { @@ -855,33 +953,34 @@ static PyObject *PyFT2Font_get_charmap(PyFT2Font *self, PyObject *args, PyObject const char *PyFT2Font_get_char_index__doc__ = - "get_char_index(codepoint)\n" + "get_char_index(self, codepoint)\n" "--\n\n" "Return the glyph index corresponding to a character *codepoint*.\n"; -static PyObject *PyFT2Font_get_char_index(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_char_index(PyFT2Font *self, PyObject *args) { FT_UInt index; FT_ULong ccode; + int fallback = 1; if (!PyArg_ParseTuple(args, "k:get_char_index", &ccode)) { return NULL; } - index = FT_Get_Char_Index(self->x->get_face(), ccode); + CALL_CPP("get_char_index", index = self->x->get_char_index(ccode, (bool)fallback)); return PyLong_FromLong(index); } const char *PyFT2Font_get_sfnt__doc__ = - "get_sfnt()\n" + "get_sfnt(self)\n" "--\n\n" "Load the entire SFNT names table, as a dict whose keys are\n" "(platform-ID, ISO-encoding-scheme, language-code, and description)\n" "tuples.\n"; -static PyObject *PyFT2Font_get_sfnt(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_sfnt(PyFT2Font *self, PyObject *args) { PyObject *names; @@ -936,12 +1035,12 @@ static PyObject *PyFT2Font_get_sfnt(PyFT2Font *self, PyObject *args, PyObject *k } const char *PyFT2Font_get_name_index__doc__ = - "get_name_index(name)\n" + "get_name_index(self, name)\n" "--\n\n" "Return the glyph index of a given glyph *name*.\n" "The glyph index 0 means 'undefined character code'.\n"; -static PyObject *PyFT2Font_get_name_index(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_name_index(PyFT2Font *self, PyObject *args) { char *glyphname; long name_index; @@ -953,11 +1052,11 @@ static PyObject *PyFT2Font_get_name_index(PyFT2Font *self, PyObject *args, PyObj } const char *PyFT2Font_get_ps_font_info__doc__ = - "get_ps_font_info()\n" + "get_ps_font_info(self)\n" "--\n\n" "Return the information in the PS Font Info structure.\n"; -static PyObject *PyFT2Font_get_ps_font_info(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_ps_font_info(PyFT2Font *self, PyObject *args) { PS_FontInfoRec fontinfo; @@ -980,12 +1079,12 @@ static PyObject *PyFT2Font_get_ps_font_info(PyFT2Font *self, PyObject *args, PyO } const char *PyFT2Font_get_sfnt_table__doc__ = - "get_sfnt_table(name)\n" + "get_sfnt_table(self, name)\n" "--\n\n" "Return one of the following SFNT tables: head, maxp, OS/2, hhea, " "vhea, post, or pclt.\n"; -static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args) { char *tagname; if (!PyArg_ParseTuple(args, "s:get_sfnt_table", &tagname)) { @@ -1282,22 +1381,22 @@ static PyObject *PyFT2Font_get_sfnt_table(PyFT2Font *self, PyObject *args, PyObj } const char *PyFT2Font_get_path__doc__ = - "get_path()\n" + "get_path(self)\n" "--\n\n" "Get the path data from the currently loaded glyph as a tuple of vertices, " "codes.\n"; -static PyObject *PyFT2Font_get_path(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_path(PyFT2Font *self, PyObject *args) { CALL_CPP("get_path", return self->x->get_path()); } const char *PyFT2Font_get_image__doc__ = - "get_image()\n" + "get_image(self)\n" "--\n\n" "Return the underlying image buffer for this font object.\n"; -static PyObject *PyFT2Font_get_image(PyFT2Font *self, PyObject *args, PyObject *kwds) +static PyObject *PyFT2Font_get_image(PyFT2Font *self, PyObject *args) { FT2Image &im = self->x->get_image(); npy_intp dims[] = {(npy_intp)im.get_height(), (npy_intp)im.get_width() }; @@ -1420,12 +1519,12 @@ static PyObject *PyFT2Font_underline_thickness(PyFT2Font *self, void *closure) static PyObject *PyFT2Font_fname(PyFT2Font *self, void *closure) { - if (self->fname) { - Py_INCREF(self->fname); - return self->fname; + if (self->stream.close) { // Called passed a filename to the constructor. + return PyObject_GetAttrString(self->py_file, "name"); + } else { + Py_INCREF(self->py_file); + return self->py_file; } - - Py_RETURN_NONE; } static int PyFT2Font_get_buffer(PyFT2Font *self, Py_buffer *buf, int flags) @@ -1452,7 +1551,7 @@ static int PyFT2Font_get_buffer(PyFT2Font *self, Py_buffer *buf, int flags) return 1; } -static PyTypeObject *PyFT2Font_init_type(PyObject *m, PyTypeObject *type) +static PyTypeObject *PyFT2Font_init_type() { static PyGetSetDef getset[] = { {(char *)"postscript_name", (getter)PyFT2Font_postscript_name, NULL, NULL, NULL}, @@ -1485,6 +1584,7 @@ static PyTypeObject *PyFT2Font_init_type(PyObject *m, PyTypeObject *type) {"select_charmap", (PyCFunction)PyFT2Font_select_charmap, METH_VARARGS, PyFT2Font_select_charmap__doc__}, {"get_kerning", (PyCFunction)PyFT2Font_get_kerning, METH_VARARGS, PyFT2Font_get_kerning__doc__}, {"set_text", (PyCFunction)PyFT2Font_set_text, METH_VARARGS|METH_KEYWORDS, PyFT2Font_set_text__doc__}, + {"_get_fontmap", (PyCFunction)PyFT2Font_get_fontmap, METH_VARARGS|METH_KEYWORDS, PyFT2Font_get_fontmap__doc__}, {"get_num_glyphs", (PyCFunction)PyFT2Font_get_num_glyphs, METH_NOARGS, PyFT2Font_get_num_glyphs__doc__}, {"load_char", (PyCFunction)PyFT2Font_load_char, METH_VARARGS|METH_KEYWORDS, PyFT2Font_load_char__doc__}, {"load_glyph", (PyCFunction)PyFT2Font_load_glyph, METH_VARARGS|METH_KEYWORDS, PyFT2Font_load_glyph__doc__}, @@ -1507,134 +1607,88 @@ static PyTypeObject *PyFT2Font_init_type(PyObject *m, PyTypeObject *type) }; static PyBufferProcs buffer_procs; - memset(&buffer_procs, 0, sizeof(PyBufferProcs)); buffer_procs.bf_getbuffer = (getbufferproc)PyFT2Font_get_buffer; - memset(type, 0, sizeof(PyTypeObject)); - type->tp_name = "matplotlib.ft2font.FT2Font"; - type->tp_doc = PyFT2Font_init__doc__; - type->tp_basicsize = sizeof(PyFT2Font); - type->tp_dealloc = (destructor)PyFT2Font_dealloc; - type->tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; - type->tp_methods = methods; - type->tp_getset = getset; - type->tp_new = PyFT2Font_new; - type->tp_init = (initproc)PyFT2Font_init; - type->tp_as_buffer = &buffer_procs; - - if (PyType_Ready(type) < 0) { - return NULL; - } + PyFT2FontType.tp_name = "matplotlib.ft2font.FT2Font"; + PyFT2FontType.tp_doc = PyFT2Font_init__doc__; + PyFT2FontType.tp_basicsize = sizeof(PyFT2Font); + PyFT2FontType.tp_dealloc = (destructor)PyFT2Font_dealloc; + PyFT2FontType.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE; + PyFT2FontType.tp_methods = methods; + PyFT2FontType.tp_getset = getset; + PyFT2FontType.tp_new = PyFT2Font_new; + PyFT2FontType.tp_init = (initproc)PyFT2Font_init; + PyFT2FontType.tp_as_buffer = &buffer_procs; - if (PyModule_AddObject(m, "FT2Font", (PyObject *)type)) { - return NULL; - } - - return type; + return &PyFT2FontType; } -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "ft2font", - NULL, - 0, - NULL, - NULL, - NULL, - NULL, - NULL -}; +static struct PyModuleDef moduledef = { PyModuleDef_HEAD_INIT, "ft2font" }; #pragma GCC visibility push(default) PyMODINIT_FUNC PyInit_ft2font(void) { - PyObject *m; - - m = PyModule_Create(&moduledef); - - if (m == NULL) { - return NULL; - } - - if (!PyFT2Image_init_type(m, &PyFT2ImageType)) { - return NULL; - } - - if (!PyGlyph_init_type(m, &PyGlyphType)) { - return NULL; - } - - if (!PyFT2Font_init_type(m, &PyFT2FontType)) { - return NULL; - } - - PyObject *d = PyModule_GetDict(m); - - if (add_dict_int(d, "SCALABLE", FT_FACE_FLAG_SCALABLE) || - add_dict_int(d, "FIXED_SIZES", FT_FACE_FLAG_FIXED_SIZES) || - add_dict_int(d, "FIXED_WIDTH", FT_FACE_FLAG_FIXED_WIDTH) || - add_dict_int(d, "SFNT", FT_FACE_FLAG_SFNT) || - add_dict_int(d, "HORIZONTAL", FT_FACE_FLAG_HORIZONTAL) || - add_dict_int(d, "VERTICAL", FT_FACE_FLAG_VERTICAL) || - add_dict_int(d, "KERNING", FT_FACE_FLAG_KERNING) || - add_dict_int(d, "FAST_GLYPHS", FT_FACE_FLAG_FAST_GLYPHS) || - add_dict_int(d, "MULTIPLE_MASTERS", FT_FACE_FLAG_MULTIPLE_MASTERS) || - add_dict_int(d, "GLYPH_NAMES", FT_FACE_FLAG_GLYPH_NAMES) || - add_dict_int(d, "EXTERNAL_STREAM", FT_FACE_FLAG_EXTERNAL_STREAM) || - add_dict_int(d, "ITALIC", FT_STYLE_FLAG_ITALIC) || - add_dict_int(d, "BOLD", FT_STYLE_FLAG_BOLD) || - add_dict_int(d, "KERNING_DEFAULT", FT_KERNING_DEFAULT) || - add_dict_int(d, "KERNING_UNFITTED", FT_KERNING_UNFITTED) || - add_dict_int(d, "KERNING_UNSCALED", FT_KERNING_UNSCALED) || - add_dict_int(d, "LOAD_DEFAULT", FT_LOAD_DEFAULT) || - add_dict_int(d, "LOAD_NO_SCALE", FT_LOAD_NO_SCALE) || - add_dict_int(d, "LOAD_NO_HINTING", FT_LOAD_NO_HINTING) || - add_dict_int(d, "LOAD_RENDER", FT_LOAD_RENDER) || - add_dict_int(d, "LOAD_NO_BITMAP", FT_LOAD_NO_BITMAP) || - add_dict_int(d, "LOAD_VERTICAL_LAYOUT", FT_LOAD_VERTICAL_LAYOUT) || - add_dict_int(d, "LOAD_FORCE_AUTOHINT", FT_LOAD_FORCE_AUTOHINT) || - add_dict_int(d, "LOAD_CROP_BITMAP", FT_LOAD_CROP_BITMAP) || - add_dict_int(d, "LOAD_PEDANTIC", FT_LOAD_PEDANTIC) || - add_dict_int(d, "LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH", FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH) || - add_dict_int(d, "LOAD_NO_RECURSE", FT_LOAD_NO_RECURSE) || - add_dict_int(d, "LOAD_IGNORE_TRANSFORM", FT_LOAD_IGNORE_TRANSFORM) || - add_dict_int(d, "LOAD_MONOCHROME", FT_LOAD_MONOCHROME) || - add_dict_int(d, "LOAD_LINEAR_DESIGN", FT_LOAD_LINEAR_DESIGN) || - add_dict_int(d, "LOAD_NO_AUTOHINT", (unsigned long)FT_LOAD_NO_AUTOHINT) || - add_dict_int(d, "LOAD_TARGET_NORMAL", (unsigned long)FT_LOAD_TARGET_NORMAL) || - add_dict_int(d, "LOAD_TARGET_LIGHT", (unsigned long)FT_LOAD_TARGET_LIGHT) || - add_dict_int(d, "LOAD_TARGET_MONO", (unsigned long)FT_LOAD_TARGET_MONO) || - add_dict_int(d, "LOAD_TARGET_LCD", (unsigned long)FT_LOAD_TARGET_LCD) || - add_dict_int(d, "LOAD_TARGET_LCD_V", (unsigned long)FT_LOAD_TARGET_LCD_V)) { - return NULL; - } - - // initialize library - int error = FT_Init_FreeType(&_ft2Library); - - if (error) { - PyErr_SetString(PyExc_RuntimeError, "Could not initialize the freetype2 library"); - return NULL; - } - - { - FT_Int major, minor, patch; - char version_string[64]; + import_array(); - FT_Library_Version(_ft2Library, &major, &minor, &patch); - sprintf(version_string, "%d.%d.%d", major, minor, patch); - if (PyModule_AddStringConstant(m, "__freetype_version__", version_string)) { - return NULL; - } + if (FT_Init_FreeType(&_ft2Library)) { // initialize library + return PyErr_Format( + PyExc_RuntimeError, "Could not initialize the freetype2 library"); } + FT_Int major, minor, patch; + char version_string[64]; + FT_Library_Version(_ft2Library, &major, &minor, &patch); + snprintf(version_string, sizeof(version_string), "%d.%d.%d", major, minor, patch); - if (PyModule_AddStringConstant(m, "__freetype_build_type__", STRINGIFY(FREETYPE_BUILD_TYPE))) { + PyObject *m; + if (!(m = PyModule_Create(&moduledef)) || + prepare_and_add_type(PyFT2Image_init_type(), m) || + prepare_and_add_type(PyFT2Font_init_type(), m) || + // Glyph is not constructible from Python, thus not added to the module. + PyType_Ready(PyGlyph_init_type()) || + PyModule_AddStringConstant(m, "__freetype_version__", version_string) || + PyModule_AddStringConstant(m, "__freetype_build_type__", STRINGIFY(FREETYPE_BUILD_TYPE)) || + PyModule_AddIntConstant(m, "SCALABLE", FT_FACE_FLAG_SCALABLE) || + PyModule_AddIntConstant(m, "FIXED_SIZES", FT_FACE_FLAG_FIXED_SIZES) || + PyModule_AddIntConstant(m, "FIXED_WIDTH", FT_FACE_FLAG_FIXED_WIDTH) || + PyModule_AddIntConstant(m, "SFNT", FT_FACE_FLAG_SFNT) || + PyModule_AddIntConstant(m, "HORIZONTAL", FT_FACE_FLAG_HORIZONTAL) || + PyModule_AddIntConstant(m, "VERTICAL", FT_FACE_FLAG_VERTICAL) || + PyModule_AddIntConstant(m, "KERNING", FT_FACE_FLAG_KERNING) || + PyModule_AddIntConstant(m, "FAST_GLYPHS", FT_FACE_FLAG_FAST_GLYPHS) || + PyModule_AddIntConstant(m, "MULTIPLE_MASTERS", FT_FACE_FLAG_MULTIPLE_MASTERS) || + PyModule_AddIntConstant(m, "GLYPH_NAMES", FT_FACE_FLAG_GLYPH_NAMES) || + PyModule_AddIntConstant(m, "EXTERNAL_STREAM", FT_FACE_FLAG_EXTERNAL_STREAM) || + PyModule_AddIntConstant(m, "ITALIC", FT_STYLE_FLAG_ITALIC) || + PyModule_AddIntConstant(m, "BOLD", FT_STYLE_FLAG_BOLD) || + PyModule_AddIntConstant(m, "KERNING_DEFAULT", FT_KERNING_DEFAULT) || + PyModule_AddIntConstant(m, "KERNING_UNFITTED", FT_KERNING_UNFITTED) || + PyModule_AddIntConstant(m, "KERNING_UNSCALED", FT_KERNING_UNSCALED) || + PyModule_AddIntConstant(m, "LOAD_DEFAULT", FT_LOAD_DEFAULT) || + PyModule_AddIntConstant(m, "LOAD_NO_SCALE", FT_LOAD_NO_SCALE) || + PyModule_AddIntConstant(m, "LOAD_NO_HINTING", FT_LOAD_NO_HINTING) || + PyModule_AddIntConstant(m, "LOAD_RENDER", FT_LOAD_RENDER) || + PyModule_AddIntConstant(m, "LOAD_NO_BITMAP", FT_LOAD_NO_BITMAP) || + PyModule_AddIntConstant(m, "LOAD_VERTICAL_LAYOUT", FT_LOAD_VERTICAL_LAYOUT) || + PyModule_AddIntConstant(m, "LOAD_FORCE_AUTOHINT", FT_LOAD_FORCE_AUTOHINT) || + PyModule_AddIntConstant(m, "LOAD_CROP_BITMAP", FT_LOAD_CROP_BITMAP) || + PyModule_AddIntConstant(m, "LOAD_PEDANTIC", FT_LOAD_PEDANTIC) || + PyModule_AddIntConstant(m, "LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH", FT_LOAD_IGNORE_GLOBAL_ADVANCE_WIDTH) || + PyModule_AddIntConstant(m, "LOAD_NO_RECURSE", FT_LOAD_NO_RECURSE) || + PyModule_AddIntConstant(m, "LOAD_IGNORE_TRANSFORM", FT_LOAD_IGNORE_TRANSFORM) || + PyModule_AddIntConstant(m, "LOAD_MONOCHROME", FT_LOAD_MONOCHROME) || + PyModule_AddIntConstant(m, "LOAD_LINEAR_DESIGN", FT_LOAD_LINEAR_DESIGN) || + PyModule_AddIntConstant(m, "LOAD_NO_AUTOHINT", (unsigned long)FT_LOAD_NO_AUTOHINT) || + PyModule_AddIntConstant(m, "LOAD_TARGET_NORMAL", (unsigned long)FT_LOAD_TARGET_NORMAL) || + PyModule_AddIntConstant(m, "LOAD_TARGET_LIGHT", (unsigned long)FT_LOAD_TARGET_LIGHT) || + PyModule_AddIntConstant(m, "LOAD_TARGET_MONO", (unsigned long)FT_LOAD_TARGET_MONO) || + PyModule_AddIntConstant(m, "LOAD_TARGET_LCD", (unsigned long)FT_LOAD_TARGET_LCD) || + PyModule_AddIntConstant(m, "LOAD_TARGET_LCD_V", (unsigned long)FT_LOAD_TARGET_LCD_V)) { + FT_Done_FreeType(_ft2Library); + Py_XDECREF(m); return NULL; } - import_array(); - return m; } diff --git a/src/mplutils.cpp b/src/mplutils.cpp deleted file mode 100644 index 237def047d8c..000000000000 --- a/src/mplutils.cpp +++ /dev/null @@ -1,21 +0,0 @@ -/* -*- mode: c++; c-basic-offset: 4 -*- */ - -#include "mplutils.h" - -int add_dict_int(PyObject *dict, const char *key, long val) -{ - PyObject *valobj; - valobj = PyLong_FromLong(val); - if (valobj == NULL) { - return 1; - } - - if (PyDict_SetItemString(dict, key, valobj)) { - Py_DECREF(valobj); - return 1; - } - - Py_DECREF(valobj); - - return 0; -} diff --git a/src/mplutils.h b/src/mplutils.h index 925ec32c082f..39d98ed02e8f 100644 --- a/src/mplutils.h +++ b/src/mplutils.h @@ -29,28 +29,37 @@ #include -#undef CLAMP -#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x))) - -#undef MAX -#define MAX(a, b) (((a) > (b)) ? (a) : (b)) - inline double mpl_round(double v) { return (double)(int)(v + ((v >= 0.0) ? 0.5 : -0.5)); } +// 'kind' codes for paths. enum { STOP = 0, MOVETO = 1, LINETO = 2, CURVE3 = 3, CURVE4 = 4, - ENDPOLY = 0x4f + CLOSEPOLY = 0x4f }; const size_t NUM_VERTICES[] = { 1, 1, 1, 2, 3, 1 }; -extern "C" int add_dict_int(PyObject *dict, const char *key, long val); +inline int prepare_and_add_type(PyTypeObject *type, PyObject *module) +{ + if (PyType_Ready(type)) { + return -1; + } + char const* ptr = strrchr(type->tp_name, '.'); + if (!ptr) { + PyErr_SetString(PyExc_ValueError, "tp_name should be a qualified name"); + return -1; + } + if (PyModule_AddObject(module, ptr + 1, (PyObject *)type)) { + return -1; + } + return 0; +} #endif diff --git a/src/path_converters.h b/src/path_converters.h index 5a1d28964662..6cbbf9c14115 100644 --- a/src/path_converters.h +++ b/src/path_converters.h @@ -162,16 +162,22 @@ class PathNanRemover : protected EmbeddedQueue<4> { VertexSource *m_source; bool m_remove_nans; - bool m_has_curves; + bool m_has_codes; bool valid_segment_exists; + bool m_last_segment_valid; + bool m_was_broken; + double m_initX; + double m_initY; public: - /* has_curves should be true if the path contains bezier curve - segments, as this requires a slower algorithm to remove the - NaNs. When in doubt, set to true. + /* has_codes should be true if the path contains bezier curve segments, or + * closed loops, as this requires a slower algorithm to remove the NaNs. + * When in doubt, set to true. */ - PathNanRemover(VertexSource &source, bool remove_nans, bool has_curves) - : m_source(&source), m_remove_nans(remove_nans), m_has_curves(has_curves) + PathNanRemover(VertexSource &source, bool remove_nans, bool has_codes) + : m_source(&source), m_remove_nans(remove_nans), m_has_codes(has_codes), + m_last_segment_valid(false), m_was_broken(false), + m_initX(nan("")), m_initY(nan("")) { // ignore all close/end_poly commands until after the first valid // (nan-free) command is encountered @@ -192,8 +198,9 @@ class PathNanRemover : protected EmbeddedQueue<4> return m_source->vertex(x, y); } - if (m_has_curves) { - /* This is the slow method for when there might be curves. */ + if (m_has_codes) { + /* This is the slow method for when there might be curves or closed + * loops. */ if (queue_pop(&code, x, y)) { return code; } @@ -205,14 +212,41 @@ class PathNanRemover : protected EmbeddedQueue<4> are found along the way, the queue is emptied, and the next curve segment is handled. */ code = m_source->vertex(x, y); - /* The vertices attached to STOP and CLOSEPOLY left are never - used, so we leave them as-is even if NaN. However, CLOSEPOLY - only makes sense if a valid MOVETO command has already been - emitted. */ - if (code == agg::path_cmd_stop || - (code == (agg::path_cmd_end_poly | agg::path_flags_close) && - valid_segment_exists)) { + /* The vertices attached to STOP and CLOSEPOLY are never used, + * so we leave them as-is even if NaN. */ + if (code == agg::path_cmd_stop) { return code; + } else if (code == (agg::path_cmd_end_poly | + agg::path_flags_close) && + valid_segment_exists) { + /* However, CLOSEPOLY only makes sense if a valid MOVETO + * command has already been emitted. But if a NaN was + * removed in the path, then we cannot close it as it is no + * longer a loop. We must emulate that by inserting a + * LINETO instead. */ + if (m_was_broken) { + if (m_last_segment_valid && ( + std::isfinite(m_initX) && + std::isfinite(m_initY))) { + /* Join to start if both ends are valid. */ + queue_push(agg::path_cmd_line_to, m_initX, m_initY); + break; + } else { + /* Skip the close, in case there are additional + * subpaths. */ + continue; + } + m_was_broken = false; + break; + } else { + return code; + } + } else if (code == agg::path_cmd_move_to) { + /* Save the initial point in order to produce the last + * segment closing a loop, *if* we broke the loop. */ + m_initX = *x; + m_initY = *y; + m_was_broken = false; } if (needs_move_to) { @@ -220,22 +254,24 @@ class PathNanRemover : protected EmbeddedQueue<4> } size_t num_extra_points = num_extra_points_map[code & 0xF]; - bool has_nan = (!(std::isfinite(*x) && std::isfinite(*y))); + m_last_segment_valid = (std::isfinite(*x) && std::isfinite(*y)); queue_push(code, *x, *y); /* Note: this test can not be short-circuited, since we need to advance through the entire curve no matter what */ for (size_t i = 0; i < num_extra_points; ++i) { m_source->vertex(x, y); - has_nan = has_nan || !(std::isfinite(*x) && std::isfinite(*y)); + m_last_segment_valid = m_last_segment_valid && + (std::isfinite(*x) && std::isfinite(*y)); queue_push(code, *x, *y); } - if (!has_nan) { + if (m_last_segment_valid) { valid_segment_exists = true; break; } + m_was_broken = true; queue_clear(); /* If the last point is finite, we use that for the @@ -254,9 +290,9 @@ class PathNanRemover : protected EmbeddedQueue<4> } else { return agg::path_cmd_stop; } - } else // !m_has_curves + } else // !m_has_codes { - /* This is the fast path for when we know we have no curves */ + /* This is the fast path for when we know we have no codes. */ code = m_source->vertex(x, y); if (code == agg::path_cmd_stop || @@ -300,6 +336,7 @@ class PathClipper : public EmbeddedQueue<3> double m_initX; double m_initY; bool m_has_init; + bool m_was_clipped; public: PathClipper(VertexSource &source, bool do_clipping, double width, double height) @@ -311,7 +348,8 @@ class PathClipper : public EmbeddedQueue<3> m_moveto(true), m_initX(nan("")), m_initY(nan("")), - m_has_init(false) + m_has_init(false), + m_was_clipped(false) { // empty } @@ -325,7 +363,8 @@ class PathClipper : public EmbeddedQueue<3> m_moveto(true), m_initX(nan("")), m_initY(nan("")), - m_has_init(false) + m_has_init(false), + m_was_clipped(false) { m_cliprect.x1 -= 1.0; m_cliprect.y1 -= 1.0; @@ -336,21 +375,29 @@ class PathClipper : public EmbeddedQueue<3> inline void rewind(unsigned path_id) { m_has_init = false; + m_was_clipped = false; m_moveto = true; m_source->rewind(path_id); } - int draw_clipped_line(double x0, double y0, double x1, double y1) + int draw_clipped_line(double x0, double y0, double x1, double y1, + bool closed=false) { unsigned moved = agg::clip_line_segment(&x0, &y0, &x1, &y1, m_cliprect); // moved >= 4 - Fully clipped // moved & 1 != 0 - First point has been moved // moved & 2 != 0 - Second point has been moved + m_was_clipped = m_was_clipped || (moved != 0); if (moved < 4) { if (moved & 1 || m_moveto) { queue_push(agg::path_cmd_move_to, x0, y0); } queue_push(agg::path_cmd_line_to, x1, y1); + if (closed && !m_was_clipped) { + // Close the path only if the end point hasn't moved. + queue_push(agg::path_cmd_end_poly | agg::path_flags_close, + x1, y1); + } m_moveto = false; return 1; @@ -364,75 +411,91 @@ class PathClipper : public EmbeddedQueue<3> unsigned code; bool emit_moveto = false; - if (m_do_clipping) { - /* This is the slow path where we actually do clipping */ + if (!m_do_clipping) { + // If not doing any clipping, just pass along the vertices verbatim + return m_source->vertex(x, y); + } - if (queue_pop(&code, x, y)) { - return code; - } + /* This is the slow path where we actually do clipping */ - while ((code = m_source->vertex(x, y)) != agg::path_cmd_stop) { - emit_moveto = false; + if (queue_pop(&code, x, y)) { + return code; + } - switch (code) { - case (agg::path_cmd_end_poly | agg::path_flags_close): - if (m_has_init) { - draw_clipped_line(m_lastX, m_lastY, m_initX, m_initY); - } + while ((code = m_source->vertex(x, y)) != agg::path_cmd_stop) { + emit_moveto = false; + + switch (code) { + case (agg::path_cmd_end_poly | agg::path_flags_close): + if (m_has_init) { + // Queue the line from last point to the initial point, and + // if never clipped, add a close code. + draw_clipped_line(m_lastX, m_lastY, m_initX, m_initY, + true); + } else { + // An empty path that is immediately closed. queue_push( agg::path_cmd_end_poly | agg::path_flags_close, m_lastX, m_lastY); + } + // If paths were not clipped, then the above code queued + // something, and we should exit the loop. Otherwise, continue + // to the next point, as there may be a new subpath. + if (queue_nonempty()) { goto exit_loop; + } + break; + + case agg::path_cmd_move_to: + + // was the last command a moveto (and we have + // seen at least one command ? + // if so, shove it in the queue if in clip box + if (m_moveto && m_has_init && + m_lastX >= m_cliprect.x1 && + m_lastX <= m_cliprect.x2 && + m_lastY >= m_cliprect.y1 && + m_lastY <= m_cliprect.y2) { + // push the last moveto onto the queue + queue_push(agg::path_cmd_move_to, m_lastX, m_lastY); + // flag that we need to emit it + emit_moveto = true; + } + // update the internal state for this moveto + m_initX = m_lastX = *x; + m_initY = m_lastY = *y; + m_has_init = true; + m_moveto = true; + m_was_clipped = false; + // if the last command was moveto exit the loop to emit the code + if (emit_moveto) { + goto exit_loop; + } + // else, break and get the next point + break; - case agg::path_cmd_move_to: - - // was the last command a moveto (and we have - // seen at least one command ? - // if so, shove it in the queue if in clip box - if (m_moveto && m_has_init && - m_lastX >= m_cliprect.x1 && - m_lastX <= m_cliprect.x2 && - m_lastY >= m_cliprect.y1 && - m_lastY <= m_cliprect.y2) { - // push the last moveto onto the queue - queue_push(agg::path_cmd_move_to, m_lastX, m_lastY); - // flag that we need to emit it - emit_moveto = true; - } - // update the internal state for this moveto - m_initX = m_lastX = *x; - m_initY = m_lastY = *y; - m_has_init = true; - m_moveto = true; - // if the last command was moveto exit the loop to emit the code - if (emit_moveto) { - goto exit_loop; - } - // else, break and get the next point - break; - - case agg::path_cmd_line_to: - if (draw_clipped_line(m_lastX, m_lastY, *x, *y)) { - m_lastX = *x; - m_lastY = *y; - goto exit_loop; - } - m_lastX = *x; - m_lastY = *y; - break; - - default: - if (m_moveto) { - queue_push(agg::path_cmd_move_to, m_lastX, m_lastY); - m_moveto = false; - } - - queue_push(code, *x, *y); + case agg::path_cmd_line_to: + if (draw_clipped_line(m_lastX, m_lastY, *x, *y)) { m_lastX = *x; m_lastY = *y; goto exit_loop; } + m_lastX = *x; + m_lastY = *y; + break; + + default: + if (m_moveto) { + queue_push(agg::path_cmd_move_to, m_lastX, m_lastY); + m_moveto = false; + } + + queue_push(code, *x, *y); + m_lastX = *x; + m_lastY = *y; + goto exit_loop; } + } exit_loop: @@ -452,11 +515,6 @@ class PathClipper : public EmbeddedQueue<3> } return agg::path_cmd_stop; - } else { - // If not doing any clipping, just pass along the vertices - // verbatim - return m_source->vertex(x, y); - } } }; diff --git a/src/py_adaptors.h b/src/py_adaptors.h index 912c93c8bf12..7722137dc6e1 100644 --- a/src/py_adaptors.h +++ b/src/py_adaptors.h @@ -175,7 +175,7 @@ class PathIterator return m_simplify_threshold; } - inline bool has_curves() const + inline bool has_codes() const { return m_codes != NULL; } @@ -194,12 +194,7 @@ class PathGenerator public: typedef PathIterator path_iterator; - PathGenerator(PyObject *obj) : m_paths(NULL), m_npaths(0) - { - if (!set(obj)) { - throw py::exception(); - } - } + PathGenerator() : m_paths(NULL), m_npaths(0) {} ~PathGenerator() { @@ -212,6 +207,7 @@ class PathGenerator return 0; } + Py_XDECREF(m_paths); m_paths = obj; Py_INCREF(m_paths); diff --git a/src/py_converters.cpp b/src/py_converters.cpp index a4c0b1909940..515291aa302d 100644 --- a/src/py_converters.cpp +++ b/src/py_converters.cpp @@ -191,54 +191,41 @@ int convert_rect(PyObject *rectobj, void *rectp) int convert_rgba(PyObject *rgbaobj, void *rgbap) { agg::rgba *rgba = (agg::rgba *)rgbap; - + PyObject *rgbatuple = NULL; + int success = 1; if (rgbaobj == NULL || rgbaobj == Py_None) { rgba->r = 0.0; rgba->g = 0.0; rgba->b = 0.0; rgba->a = 0.0; } else { + if (!(rgbatuple = PySequence_Tuple(rgbaobj))) { + success = 0; + goto exit; + } rgba->a = 1.0; if (!PyArg_ParseTuple( - rgbaobj, "ddd|d:rgba", &(rgba->r), &(rgba->g), &(rgba->b), &(rgba->a))) { - return 0; + rgbatuple, "ddd|d:rgba", &(rgba->r), &(rgba->g), &(rgba->b), &(rgba->a))) { + success = 0; + goto exit; } } - - return 1; +exit: + Py_XDECREF(rgbatuple); + return success; } int convert_dashes(PyObject *dashobj, void *dashesp) { Dashes *dashes = (Dashes *)dashesp; - if (dashobj == NULL && dashobj == Py_None) { - return 1; - } - - PyObject *dash_offset_obj = NULL; double dash_offset = 0.0; PyObject *dashes_seq = NULL; - if (!PyArg_ParseTuple(dashobj, "OO:dashes", &dash_offset_obj, &dashes_seq)) { + if (!PyArg_ParseTuple(dashobj, "dO:dashes", &dash_offset, &dashes_seq)) { return 0; } - if (dash_offset_obj != Py_None) { - dash_offset = PyFloat_AsDouble(dash_offset_obj); - if (PyErr_Occurred()) { - return 0; - } - } else { - if (PyErr_WarnEx(PyExc_FutureWarning, - "Passing the dash offset as None is deprecated since " - "Matplotlib 3.3 and will be removed in Matplotlib 3.5; " - "pass it as zero instead.", - 1)) { - return 0; - } - } - if (dashes_seq == Py_None) { return 1; } @@ -415,6 +402,16 @@ int convert_path(PyObject *obj, void *pathp) return status; } +int convert_pathgen(PyObject *obj, void *pathgenp) +{ + py::PathGenerator *paths = (py::PathGenerator *)pathgenp; + if (!paths->set(obj)) { + PyErr_SetString(PyExc_TypeError, "Not an iterable of paths"); + return 0; + } + return 1; +} + int convert_clippath(PyObject *clippath_tuple, void *clippathp) { ClipPath *clippath = (ClipPath *)clippathp; @@ -492,22 +489,6 @@ int convert_gcagg(PyObject *pygc, void *gcp) return 1; } -int convert_offset_position(PyObject *obj, void *offsetp) -{ - e_offset_position *offset = (e_offset_position *)offsetp; - const char *names[] = {"data", NULL}; - int values[] = {OFFSET_POSITION_DATA}; - int result = (int)OFFSET_POSITION_FIGURE; - - if (!convert_string_enum(obj, "offset_position", names, values, &result)) { - PyErr_Clear(); - } - - *offset = (e_offset_position)result; - - return 1; -} - int convert_face(PyObject *color, GCAgg &gc, agg::rgba *rgba) { if (!convert_rgba(color, rgba)) { diff --git a/src/py_converters.h b/src/py_converters.h index 2c19acdaaf80..2c9dc6d1b860 100644 --- a/src/py_converters.h +++ b/src/py_converters.h @@ -32,9 +32,9 @@ int convert_dashes(PyObject *dashobj, void *gcp); int convert_dashes_vector(PyObject *obj, void *dashesp); int convert_trans_affine(PyObject *obj, void *transp); int convert_path(PyObject *obj, void *pathp); +int convert_pathgen(PyObject *obj, void *pathgenp); int convert_clippath(PyObject *clippath_tuple, void *clippathp); int convert_snap(PyObject *obj, void *snapp); -int convert_offset_position(PyObject *obj, void *offsetp); int convert_sketch_params(PyObject *obj, void *sketchp); int convert_gcagg(PyObject *pygc, void *gcp); int convert_points(PyObject *pygc, void *pointsp); diff --git a/src/tri/_tri.cpp b/src/tri/_tri.cpp index 075ab7658327..548e65b3e52d 100644 --- a/src/tri/_tri.cpp +++ b/src/tri/_tri.cpp @@ -5,17 +5,13 @@ * undef_macros=['NDEBUG'] * to the appropriate make_extension call in setupext.py, and then rebuilding. */ -#define NO_IMPORT_ARRAY - +#include "../mplutils.h" #include "_tri.h" #include +#include #include -#define MOVETO 1 -#define LINETO 2 - - TriEdge::TriEdge() : tri(-1), edge(-1) @@ -223,7 +219,7 @@ Triangulation::Triangulation(const CoordinateArray& x, const MaskArray& mask, const EdgeArray& edges, const NeighborArray& neighbors, - int correct_triangle_orientations) + bool correct_triangle_orientations) : _x(x), _y(y), _triangles(triangles), @@ -231,6 +227,29 @@ Triangulation::Triangulation(const CoordinateArray& x, _edges(edges), _neighbors(neighbors) { + if (_x.ndim() != 1 || _y.ndim() != 1 || _x.shape(0) != _y.shape(0)) + throw std::invalid_argument("x and y must be 1D arrays of the same length"); + + if (_triangles.ndim() != 2 || _triangles.shape(1) != 3) + throw std::invalid_argument("triangles must be a 2D array of shape (?,3)"); + + // Optional mask. + if (_mask.size() > 0 && + (_mask.ndim() != 1 || _mask.shape(0) != _triangles.shape(0))) + throw std::invalid_argument( + "mask must be a 1D array with the same length as the triangles array"); + + // Optional edges. + if (_edges.size() > 0 && + (_edges.ndim() != 2 || _edges.shape(1) != 2)) + throw std::invalid_argument("edges must be a 2D array with shape (?,2)"); + + // Optional neighbors. + if (_neighbors.size() > 0 && + (_neighbors.ndim() != 2 || _neighbors.shape() != _triangles.shape())) + throw std::invalid_argument( + "neighbors must be a 2D array with the same shape as the triangles array"); + if (correct_triangle_orientations) correct_triangles(); } @@ -293,7 +312,7 @@ void Triangulation::calculate_boundaries() void Triangulation::calculate_edges() { - assert(_edges.empty() && "Expected empty edges array"); + assert(!has_edges() && "Expected empty edges array"); // Create set of all edges, storing them with start point index less than // end point index. @@ -310,29 +329,28 @@ void Triangulation::calculate_edges() } // Convert to python _edges array. - npy_intp dims[2] = {static_cast(edge_set.size()), 2}; + py::ssize_t dims[2] = {static_cast(edge_set.size()), 2}; _edges = EdgeArray(dims); + auto edges = _edges.mutable_data(); int i = 0; for (EdgeSet::const_iterator it = edge_set.begin(); it != edge_set.end(); ++it) { - _edges(i, 0) = it->start; - _edges(i++, 1) = it->end; + edges[i++] = it->start; + edges[i++] = it->end; } } void Triangulation::calculate_neighbors() { - assert(_neighbors.empty() && "Expected empty neighbors array"); + assert(!has_neighbors() && "Expected empty neighbors array"); // Create _neighbors array with shape (ntri,3) and initialise all to -1. - npy_intp dims[2] = {get_ntri(), 3}; + py::ssize_t dims[2] = {get_ntri(), 3}; _neighbors = NeighborArray(dims); + auto* neighbors = _neighbors.mutable_data(); int tri, edge; - for (tri = 0; tri < get_ntri(); ++tri) { - for (edge = 0; edge < 3; ++edge) - _neighbors(tri, edge) = -1; - } + std::fill(neighbors, neighbors+3*get_ntri(), -1); // For each triangle edge (start to end point), find corresponding neighbor // edge from end to start point. Do this by traversing all edges and @@ -355,8 +373,8 @@ void Triangulation::calculate_neighbors() } else { // Neighbor edge found, set the two elements of _neighbors // and remove edge from edge_to_tri_edge_map. - _neighbors(tri, edge)= it->second.tri; - _neighbors(it->second.tri, it->second.edge) = tri; + neighbors[3*tri + edge] = it->second.tri; + neighbors[3*it->second.tri + it->second.edge] = tri; edge_to_tri_edge_map.erase(it); } } @@ -370,8 +388,17 @@ void Triangulation::calculate_neighbors() Triangulation::TwoCoordinateArray Triangulation::calculate_plane_coefficients( const CoordinateArray& z) { - npy_intp dims[2] = {get_ntri(), 3}; - Triangulation::TwoCoordinateArray planes(dims); + if (z.ndim() != 1 || z.shape(0) != _x.shape(0)) + throw std::invalid_argument( + "z must be a 1D array with the same length as the triangulation x and y arrays"); + + int dims[2] = {get_ntri(), 3}; + Triangulation::TwoCoordinateArray planes_array(dims); + auto planes = planes_array.mutable_unchecked<2>(); + auto triangles = _triangles.unchecked<2>(); + auto x = _x.unchecked<1>(); + auto y = _y.unchecked<1>(); + auto z_ptr = z.unchecked<1>(); int point; for (int tri = 0; tri < get_ntri(); ++tri) { @@ -388,12 +415,12 @@ Triangulation::TwoCoordinateArray Triangulation::calculate_plane_coefficients( // and rearrange to give // r_z = (-normal_x/normal_z)*r_x + (-normal_y/normal_z)*r_y + // p/normal_z - point = _triangles(tri, 0); - XYZ point0(_x(point), _y(point), z(point)); - point = _triangles(tri, 1); - XYZ side01 = XYZ(_x(point), _y(point), z(point)) - point0; - point = _triangles(tri, 2); - XYZ side02 = XYZ(_x(point), _y(point), z(point)) - point0; + point = triangles(tri, 0); + XYZ point0(x(point), y(point), z_ptr(point)); + point = triangles(tri, 1); + XYZ side01 = XYZ(x(point), y(point), z_ptr(point)) - point0; + point = triangles(tri, 2); + XYZ side02 = XYZ(x(point), y(point), z_ptr(point)) - point0; XYZ normal = side01.cross(side02); @@ -417,20 +444,23 @@ Triangulation::TwoCoordinateArray Triangulation::calculate_plane_coefficients( } } - return planes; + return planes_array; } void Triangulation::correct_triangles() { + auto triangles = _triangles.mutable_data(); + auto neighbors = _neighbors.mutable_data(); + for (int tri = 0; tri < get_ntri(); ++tri) { - XY point0 = get_point_coords(_triangles(tri, 0)); - XY point1 = get_point_coords(_triangles(tri, 1)); - XY point2 = get_point_coords(_triangles(tri, 2)); + XY point0 = get_point_coords(triangles[3*tri]); + XY point1 = get_point_coords(triangles[3*tri+1]); + XY point2 = get_point_coords(triangles[3*tri+2]); if ( (point1 - point0).cross_z(point2 - point0) < 0.0) { // Triangle points are clockwise, so change them to anticlockwise. - std::swap(_triangles(tri, 1), _triangles(tri, 2)); - if (!_neighbors.empty()) - std::swap(_neighbors(tri, 1), _neighbors(tri, 2)); + std::swap(triangles[3*tri+1], triangles[3*tri+2]); + if (has_neighbors()) + std::swap(neighbors[3*tri+1], neighbors[3*tri+2]); } } } @@ -459,8 +489,11 @@ int Triangulation::get_edge_in_triangle(int tri, int point) const { assert(tri >= 0 && tri < get_ntri() && "Triangle index out of bounds"); assert(point >= 0 && point < get_npoints() && "Point index out of bounds."); + + auto triangles = _triangles.data(); + for (int edge = 0; edge < 3; ++edge) { - if (_triangles(tri, edge) == point) + if (triangles[3*tri + edge] == point) return edge; } return -1; // point is not in triangle. @@ -468,7 +501,7 @@ int Triangulation::get_edge_in_triangle(int tri, int point) const Triangulation::EdgeArray& Triangulation::get_edges() { - if (_edges.empty()) + if (!has_edges()) calculate_edges(); return _edges; } @@ -477,9 +510,9 @@ int Triangulation::get_neighbor(int tri, int edge) const { assert(tri >= 0 && tri < get_ntri() && "Triangle index out of bounds"); assert(edge >= 0 && edge < 3 && "Edge index out of bounds"); - if (_neighbors.empty()) + if (!has_neighbors()) const_cast(*this).calculate_neighbors(); - return _neighbors(tri, edge); + return _neighbors.data()[3*tri + edge]; } TriEdge Triangulation::get_neighbor_edge(int tri, int edge) const @@ -496,32 +529,32 @@ TriEdge Triangulation::get_neighbor_edge(int tri, int edge) const Triangulation::NeighborArray& Triangulation::get_neighbors() { - if (_neighbors.empty()) + if (!has_neighbors()) calculate_neighbors(); return _neighbors; } int Triangulation::get_npoints() const { - return _x.size(); + return _x.shape(0); } int Triangulation::get_ntri() const { - return _triangles.size(); + return _triangles.shape(0); } XY Triangulation::get_point_coords(int point) const { assert(point >= 0 && point < get_npoints() && "Point index out of bounds."); - return XY(_x(point), _y(point)); + return XY(_x.data()[point], _y.data()[point]); } int Triangulation::get_triangle_point(int tri, int edge) const { assert(tri >= 0 && tri < get_ntri() && "Triangle index out of bounds"); assert(edge >= 0 && edge < 3 && "Edge index out of bounds"); - return _triangles(tri, edge); + return _triangles.data()[3*tri + edge]; } int Triangulation::get_triangle_point(const TriEdge& tri_edge) const @@ -529,15 +562,34 @@ int Triangulation::get_triangle_point(const TriEdge& tri_edge) const return get_triangle_point(tri_edge.tri, tri_edge.edge); } +bool Triangulation::has_edges() const +{ + return _edges.size() > 0; +} + +bool Triangulation::has_mask() const +{ + return _mask.size() > 0; +} + +bool Triangulation::has_neighbors() const +{ + return _neighbors.size() > 0; +} + bool Triangulation::is_masked(int tri) const { assert(tri >= 0 && tri < get_ntri() && "Triangle index out of bounds."); - const npy_bool* mask_ptr = reinterpret_cast(_mask.data()); - return !_mask.empty() && mask_ptr[tri]; + return has_mask() && _mask.data()[tri]; } void Triangulation::set_mask(const MaskArray& mask) { + if (mask.size() > 0 && + (mask.ndim() != 1 || mask.shape(0) != _triangles.shape(0))) + throw std::invalid_argument( + "mask must be a 1D array with the same length as the triangles array"); + _mask = mask; // Clear derived fields so they are recalculated when needed. @@ -569,7 +621,11 @@ TriContourGenerator::TriContourGenerator(Triangulation& triangulation, _interior_visited(2*_triangulation.get_ntri()), _boundaries_visited(0), _boundaries_used(0) -{} +{ + if (_z.ndim() != 1 || _z.shape(0) != _triangulation.get_npoints()) + throw std::invalid_argument( + "z must be a 1D array with the same length as the x and y arrays"); +} void TriContourGenerator::clear_visited_flags(bool include_boundaries) { @@ -600,71 +656,106 @@ void TriContourGenerator::clear_visited_flags(bool include_boundaries) } } -PyObject* TriContourGenerator::contour_to_segs(const Contour& contour) +py::tuple TriContourGenerator::contour_line_to_segs_and_kinds(const Contour& contour) { - PyObject* segs = PyList_New(contour.size()); + // Convert all of the lines generated by a call to create_contour() into + // their Python equivalents for return to the calling function. + // A line is either a closed line loop (in which case the last point is + // identical to the first) or an open line strip. Two NumPy arrays are + // created for each line: + // vertices is a double array of shape (npoints, 2) containing the (x, y) + // coordinates of the points in the line + // codes is a uint8 array of shape (npoints,) containing the 'kind codes' + // which are defined in the Path class + // and they are appended to the Python lists vertices_list and codes_list + // respectively for return to the Python calling function. + + py::list vertices_list(contour.size()); + py::list codes_list(contour.size()); + for (Contour::size_type i = 0; i < contour.size(); ++i) { - const ContourLine& line = contour[i]; - npy_intp dims[2] = {static_cast(line.size()),2}; - PyArrayObject* py_line = (PyArrayObject*)PyArray_SimpleNew( - 2, dims, NPY_DOUBLE); - double* p = (double*)PyArray_DATA(py_line); - for (ContourLine::const_iterator it = line.begin(); it != line.end(); ++it) { - *p++ = it->x; - *p++ = it->y; - } - if (PyList_SetItem(segs, i, (PyObject*)py_line)) { - Py_XDECREF(segs); - PyErr_SetString(PyExc_RuntimeError, - "Unable to set contour segments"); - return NULL; + const ContourLine& contour_line = contour[i]; + py::ssize_t npoints = static_cast(contour_line.size()); + + py::ssize_t segs_dims[2] = {npoints, 2}; + CoordinateArray segs(segs_dims); + double* segs_ptr = segs.mutable_data(); + + py::ssize_t codes_dims[1] = {npoints}; + CodeArray codes(codes_dims); + unsigned char* codes_ptr = codes.mutable_data(); + + for (ContourLine::const_iterator it = contour_line.begin(); + it != contour_line.end(); ++it) { + *segs_ptr++ = it->x; + *segs_ptr++ = it->y; + *codes_ptr++ = (it == contour_line.begin() ? MOVETO : LINETO); } + + // Closed line loop has identical first and last (x, y) points. + if (contour_line.size() > 1 && + contour_line.front() == contour_line.back()) + *(codes_ptr-1) = CLOSEPOLY; + + vertices_list[i] = segs; + codes_list[i] = codes; } - return segs; + + return py::make_tuple(vertices_list, codes_list); } -PyObject* TriContourGenerator::contour_to_segs_and_kinds(const Contour& contour) +py::tuple TriContourGenerator::contour_to_segs_and_kinds(const Contour& contour) { + // Convert all of the polygons generated by a call to + // create_filled_contour() into their Python equivalents for return to the + // calling function. All of the polygons' points and kinds codes are + // combined into single NumPy arrays for each; this avoids having + // to determine which polygons are holes as this will be determined by the + // renderer. If there are ntotal points in all of the polygons, the two + // NumPy arrays created are: + // vertices is a double array of shape (ntotal, 2) containing the (x, y) + // coordinates of the points in the polygons + // codes is a uint8 array of shape (ntotal,) containing the 'kind codes' + // which are defined in the Path class + // and they are returned in the Python lists vertices_list and codes_list + // respectively. + Contour::const_iterator line; ContourLine::const_iterator point; // Find total number of points in all contour lines. - npy_intp n_points = 0; + py::ssize_t n_points = 0; for (line = contour.begin(); line != contour.end(); ++line) - n_points += (npy_intp)line->size(); + n_points += static_cast(line->size()); // Create segs array for point coordinates. - npy_intp segs_dims[2] = {n_points, 2}; - PyArrayObject* segs = (PyArrayObject*)PyArray_SimpleNew( - 2, segs_dims, NPY_DOUBLE); - double* segs_ptr = (double*)PyArray_DATA(segs); + py::ssize_t segs_dims[2] = {n_points, 2}; + TwoCoordinateArray segs(segs_dims); + double* segs_ptr = segs.mutable_data(); // Create kinds array for code types. - npy_intp kinds_dims[1] = {n_points}; - PyArrayObject* kinds = (PyArrayObject*)PyArray_SimpleNew( - 1, kinds_dims, NPY_UBYTE); - unsigned char* kinds_ptr = (unsigned char*)PyArray_DATA(kinds); + py::ssize_t codes_dims[1] = {n_points}; + CodeArray codes(codes_dims); + unsigned char* codes_ptr = codes.mutable_data(); for (line = contour.begin(); line != contour.end(); ++line) { for (point = line->begin(); point != line->end(); point++) { *segs_ptr++ = point->x; *segs_ptr++ = point->y; - *kinds_ptr++ = (point == line->begin() ? MOVETO : LINETO); + *codes_ptr++ = (point == line->begin() ? MOVETO : LINETO); } } - PyObject* result = PyTuple_New(2); - if (PyTuple_SetItem(result, 0, (PyObject*)segs) || - PyTuple_SetItem(result, 1, (PyObject*)kinds)) { - Py_XDECREF(result); - PyErr_SetString(PyExc_RuntimeError, - "Unable to set contour segments and kinds"); - return NULL; - } - return result; + py::list vertices_list(1); + vertices_list[0] = segs; + + py::list codes_list(1); + codes_list[0] = codes; + + return py::make_tuple(vertices_list, codes_list); } -PyObject* TriContourGenerator::create_contour(const double& level) +py::tuple TriContourGenerator::create_contour(const double& level) { clear_visited_flags(false); Contour contour; @@ -672,12 +763,15 @@ PyObject* TriContourGenerator::create_contour(const double& level) find_boundary_lines(contour, level); find_interior_lines(contour, level, false, false); - return contour_to_segs(contour); + return contour_line_to_segs_and_kinds(contour); } -PyObject* TriContourGenerator::create_filled_contour(const double& lower_level, +py::tuple TriContourGenerator::create_filled_contour(const double& lower_level, const double& upper_level) { + if (lower_level >= upper_level) + throw std::invalid_argument("filled contour levels must be increasing"); + clear_visited_flags(true); Contour contour; @@ -974,7 +1068,7 @@ const double& TriContourGenerator::get_z(int point) const { assert(point >= 0 && point < _triangulation.get_npoints() && "Point index out of bounds."); - return _z(point); + return _z.data()[point]; } XY TriContourGenerator::interp(int point1, @@ -1235,16 +1329,22 @@ TrapezoidMapTriFinder::TriIndexArray TrapezoidMapTriFinder::find_many(const CoordinateArray& x, const CoordinateArray& y) { + if (x.ndim() != 1 || x.shape(0) != y.shape(0)) + throw std::invalid_argument( + "x and y must be array-like with same shape"); + // Create integer array to return. - npy_intp n = x.dim(0); - npy_intp dims[1] = {n}; - TriIndexArray tri_indices(dims); + auto n = x.shape(0); + TriIndexArray tri_indices_array(n); + auto tri_indices = tri_indices_array.mutable_unchecked<1>(); + auto x_data = x.data(); + auto y_data = y.data(); // Fill returned array. - for (npy_intp i = 0; i < n; ++i) - tri_indices(i) = find_one(XY(x(i), y(i))); + for (py::ssize_t i = 0; i < n; ++i) + tri_indices(i) = find_one(XY(x_data[i], y_data[i])); - return tri_indices; + return tri_indices_array; } int @@ -1298,20 +1398,21 @@ TrapezoidMapTriFinder::find_trapezoids_intersecting_edge( return true; } -PyObject* +py::list TrapezoidMapTriFinder::get_tree_stats() { NodeStats stats; _tree->get_stats(0, stats); - return Py_BuildValue("[l,l,l,l,l,l,d]", - stats.node_count, - stats.unique_nodes.size(), - stats.trapezoid_count, - stats.unique_trapezoid_nodes.size(), - stats.max_parent_count, - stats.max_depth, - stats.sum_trapezoid_depth / stats.trapezoid_count); + py::list ret(7); + ret[0] = stats.node_count; + ret[1] = stats.unique_nodes.size(), + ret[2] = stats.trapezoid_count, + ret[3] = stats.unique_trapezoid_nodes.size(), + ret[4] = stats.max_parent_count, + ret[5] = stats.max_depth, + ret[6] = stats.sum_trapezoid_depth / stats.trapezoid_count; + return ret; } void @@ -1393,8 +1494,8 @@ TrapezoidMapTriFinder::initialize() _tree->assert_valid(false); // Randomly shuffle all edges other than first 2. - RandomNumberGenerator rng(1234); - std::random_shuffle(_edges.begin()+2, _edges.end(), rng); + std::mt19937 rng(1234); + std::shuffle(_edges.begin()+2, _edges.end(), rng); // Add edges, one at a time, to tree. size_t nedges = _edges.size(); @@ -1984,16 +2085,3 @@ TrapezoidMapTriFinder::Trapezoid::set_upper_right(Trapezoid* upper_right_) if (upper_right != 0) upper_right->upper_left = this; } - - - -RandomNumberGenerator::RandomNumberGenerator(unsigned long seed) - : _m(21870), _a(1291), _c(4621), _seed(seed % _m) -{} - -unsigned long -RandomNumberGenerator::operator()(unsigned long max_value) -{ - _seed = (_seed*_a + _c) % _m; - return (_seed*max_value) / _m; -} diff --git a/src/tri/_tri.h b/src/tri/_tri.h index 13aad01fb668..6c6c66a01120 100644 --- a/src/tri/_tri.h +++ b/src/tri/_tri.h @@ -63,7 +63,8 @@ #ifndef MPL_TRI_H #define MPL_TRI_H -#include "../numpy_cpp.h" +#include +#include #include #include @@ -71,6 +72,7 @@ #include #include +namespace py = pybind11; /* An edge of a triangle consisting of an triangle index in the range 0 to @@ -161,12 +163,12 @@ void write_contour(const Contour& contour); class Triangulation { public: - typedef numpy::array_view CoordinateArray; - typedef numpy::array_view TwoCoordinateArray; - typedef numpy::array_view TriangleArray; - typedef numpy::array_view MaskArray; - typedef numpy::array_view EdgeArray; - typedef numpy::array_view NeighborArray; + typedef py::array_t CoordinateArray; + typedef py::array_t TwoCoordinateArray; + typedef py::array_t TriangleArray; + typedef py::array_t MaskArray; + typedef py::array_t EdgeArray; + typedef py::array_t NeighborArray; /* A single boundary is a vector of the TriEdges that make up that boundary * following it around with unmasked triangles on the left. */ @@ -196,7 +198,7 @@ class Triangulation const MaskArray& mask, const EdgeArray& edges, const NeighborArray& neighbors, - int correct_triangle_orientations); + bool correct_triangle_orientations); /* Calculate plane equation coefficients for all unmasked triangles from * the point (x,y) coordinates and point z-array of shape (npoints) passed @@ -296,7 +298,11 @@ class Triangulation * the specified triangle, or -1 if the point is not in the triangle. */ int get_edge_in_triangle(int tri, int point) const; + bool has_edges() const; + bool has_mask() const; + + bool has_neighbors() const; // Variables shared with python, always set. @@ -304,11 +310,11 @@ class Triangulation TriangleArray _triangles; // int array (ntri,3) of triangle point indices, // ordered anticlockwise. - // Variables shared with python, may be zero. + // Variables shared with python, may be unset (size == 0). MaskArray _mask; // bool array (ntri). - // Derived variables shared with python, may be zero. If zero, are - // recalculated when needed. + // Derived variables shared with python, may be unset (size == 0). + // If unset, are recalculated when needed. EdgeArray _edges; // int array (?,2) of start & end point indices. NeighborArray _neighbors; // int array (ntri,3), neighbor triangle indices // or -1 if no neighbor. @@ -329,6 +335,8 @@ class TriContourGenerator { public: typedef Triangulation::CoordinateArray CoordinateArray; + typedef Triangulation::TwoCoordinateArray TwoCoordinateArray; + typedef py::array_t CodeArray; /* Constructor. * triangulation: Triangulation to generate contours for. @@ -342,7 +350,7 @@ class TriContourGenerator * Returns new python list [segs0, segs1, ...] where * segs0: double array of shape (?,2) of point coordinates of first * contour line, etc. */ - PyObject* create_contour(const double& level); + py::tuple create_contour(const double& level); /* Create and return a filled contour. * lower_level: Lower contour level. @@ -350,7 +358,7 @@ class TriContourGenerator * Returns new python tuple (segs, kinds) where * segs: double array of shape (n_points,2) of all point coordinates, * kinds: ubyte array of shape (n_points) of all point code types. */ - PyObject* create_filled_contour(const double& lower_level, + py::tuple create_filled_contour(const double& lower_level, const double& upper_level); private: @@ -363,16 +371,19 @@ class TriContourGenerator void clear_visited_flags(bool include_boundaries); /* Convert a non-filled Contour from C++ to Python. - * Returns new python list [segs0, segs1, ...] where - * segs0: double array of shape (?,2) of point coordinates of first - * contour line, etc. */ - PyObject* contour_to_segs(const Contour& contour); + * Returns new python tuple ([segs0, segs1, ...], [kinds0, kinds1...]) + * where + * segs0: double array of shape (n_points,2) of point coordinates of first + * contour line, etc. + * kinds0: ubyte array of shape (n_points) of kinds codes of first contour + * line, etc. */ + py::tuple contour_line_to_segs_and_kinds(const Contour& contour); /* Convert a filled Contour from C++ to Python. - * Returns new python tuple (segs, kinds) where + * Returns new python tuple ([segs], [kinds]) where * segs: double array of shape (n_points,2) of all point coordinates, * kinds: ubyte array of shape (n_points) of all point code types. */ - PyObject* contour_to_segs_and_kinds(const Contour& contour); + py::tuple contour_to_segs_and_kinds(const Contour& contour); /* Return the point on the specified TriEdge that intersects the specified * level. */ @@ -457,7 +468,7 @@ class TriContourGenerator // Variables shared with python, always set. - Triangulation& _triangulation; + Triangulation _triangulation; CoordinateArray _z; // double array (npoints). // Variables internal to C++ only. @@ -504,7 +515,7 @@ class TrapezoidMapTriFinder { public: typedef Triangulation::CoordinateArray CoordinateArray; - typedef numpy::array_view TriIndexArray; + typedef py::array_t TriIndexArray; /* Constructor. A separate call to initialize() is required to initialize * the object before use. @@ -530,7 +541,7 @@ class TrapezoidMapTriFinder * comparisons needed to search through the tree) * 6: mean of all trapezoid depths (one more than the average number of * comparisons needed to search through the tree) */ - PyObject* get_tree_stats(); + py::list get_tree_stats(); /* Initialize this object before use. May be called multiple times, if, * for example, the triangulation is changed by setting the mask. */ @@ -788,28 +799,4 @@ class TrapezoidMapTriFinder Node* _tree; // Root node of the trapezoid map search tree. Owned. }; - - -/* Linear congruential random number generator. Edges in the triangulation are - * randomly shuffled before being added to the trapezoid map. Want the - * shuffling to be identical across different operating systems and the same - * regardless of previous random number use. Would prefer to use a STL or - * Boost random number generator, but support is not consistent across - * different operating systems so implementing own here. - * - * This is not particularly random, but is perfectly adequate for the use here. - * Coefficients taken from Numerical Recipes in C. */ -class RandomNumberGenerator -{ -public: - RandomNumberGenerator(unsigned long seed); - - // Return random integer in the range 0 to max_value-1. - unsigned long operator()(unsigned long max_value); - -private: - const unsigned long _m, _a, _c; - unsigned long _seed; -}; - #endif diff --git a/src/tri/_tri_wrapper.cpp b/src/tri/_tri_wrapper.cpp index fcf155721ecc..1b0c3d75555e 100644 --- a/src/tri/_tri_wrapper.cpp +++ b/src/tri/_tri_wrapper.cpp @@ -1,534 +1,58 @@ #include "_tri.h" -#include "../mplutils.h" -#include "../py_exceptions.h" - -/* Triangulation */ - -typedef struct -{ - PyObject_HEAD - Triangulation* ptr; -} PyTriangulation; - -static PyTypeObject PyTriangulationType; - -static PyObject* PyTriangulation_new(PyTypeObject* type, PyObject* args, PyObject* kwds) -{ - PyTriangulation* self; - self = (PyTriangulation*)type->tp_alloc(type, 0); - self->ptr = NULL; - return (PyObject*)self; -} - -const char* PyTriangulation_init__doc__ = - "Triangulation(x, y, triangles, mask, edges, neighbors)\n" - "--\n\n" - "Create a new C++ Triangulation object\n" - "This should not be called directly, instead use the python class\n" - "matplotlib.tri.Triangulation instead.\n"; - -static int PyTriangulation_init(PyTriangulation* self, PyObject* args, PyObject* kwds) -{ - Triangulation::CoordinateArray x, y; - Triangulation::TriangleArray triangles; - Triangulation::MaskArray mask; - Triangulation::EdgeArray edges; - Triangulation::NeighborArray neighbors; - int correct_triangle_orientations; - - if (!PyArg_ParseTuple(args, - "O&O&O&O&O&O&i", - &x.converter, &x, - &y.converter, &y, - &triangles.converter, &triangles, - &mask.converter, &mask, - &edges.converter, &edges, - &neighbors.converter, &neighbors, - &correct_triangle_orientations)) { - return -1; - } - - // x and y. - if (x.empty() || y.empty() || x.dim(0) != y.dim(0)) { - PyErr_SetString(PyExc_ValueError, - "x and y must be 1D arrays of the same length"); - return -1; - } - - // triangles. - if (triangles.empty() || triangles.dim(1) != 3) { - PyErr_SetString(PyExc_ValueError, - "triangles must be a 2D array of shape (?,3)"); - return -1; - } - - // Optional mask. - if (!mask.empty() && mask.dim(0) != triangles.dim(0)) { - PyErr_SetString(PyExc_ValueError, - "mask must be a 1D array with the same length as the triangles array"); - return -1; - } - - // Optional edges. - if (!edges.empty() && edges.dim(1) != 2) { - PyErr_SetString(PyExc_ValueError, - "edges must be a 2D array with shape (?,2)"); - return -1; - } - - // Optional neighbors. - if (!neighbors.empty() && (neighbors.dim(0) != triangles.dim(0) || - neighbors.dim(1) != triangles.dim(1))) { - PyErr_SetString(PyExc_ValueError, - "neighbors must be a 2D array with the same shape as the triangles array"); - return -1; - } - - CALL_CPP_INIT("Triangulation", - (self->ptr = new Triangulation(x, y, triangles, mask, - edges, neighbors, - correct_triangle_orientations))); - return 0; -} - -static void PyTriangulation_dealloc(PyTriangulation* self) -{ - delete self->ptr; - Py_TYPE(self)->tp_free((PyObject*)self); -} - -const char* PyTriangulation_calculate_plane_coefficients__doc__ = - "calculate_plane_coefficients(z, plane_coefficients)\n" - "--\n\n" - "Calculate plane equation coefficients for all unmasked triangles"; - -static PyObject* PyTriangulation_calculate_plane_coefficients(PyTriangulation* self, PyObject* args, PyObject* kwds) -{ - Triangulation::CoordinateArray z; - if (!PyArg_ParseTuple(args, "O&:calculate_plane_coefficients", - &z.converter, &z)) { - return NULL; - } - - if (z.empty() || z.dim(0) != self->ptr->get_npoints()) { - PyErr_SetString(PyExc_ValueError, - "z array must have same length as triangulation x and y arrays"); - return NULL; - } - - Triangulation::TwoCoordinateArray result; - CALL_CPP("calculate_plane_coefficients", - (result = self->ptr->calculate_plane_coefficients(z))); - return result.pyobj(); -} - -const char* PyTriangulation_get_edges__doc__ = - "get_edges()\n" - "--\n\n" - "Return edges array"; - -static PyObject* PyTriangulation_get_edges(PyTriangulation* self, PyObject* args, PyObject* kwds) -{ - Triangulation::EdgeArray* result; - CALL_CPP("get_edges", (result = &self->ptr->get_edges())); - - if (result->empty()) { - Py_RETURN_NONE; - } - else - return result->pyobj(); -} - -const char* PyTriangulation_get_neighbors__doc__ = - "get_neighbors()\n" - "--\n\n" - "Return neighbors array"; - -static PyObject* PyTriangulation_get_neighbors(PyTriangulation* self, PyObject* args, PyObject* kwds) -{ - Triangulation::NeighborArray* result; - CALL_CPP("get_neighbors", (result = &self->ptr->get_neighbors())); - - if (result->empty()) { - Py_RETURN_NONE; - } - else - return result->pyobj(); -} - -const char* PyTriangulation_set_mask__doc__ = - "set_mask(mask)\n" - "--\n\n" - "Set or clear the mask array."; - -static PyObject* PyTriangulation_set_mask(PyTriangulation* self, PyObject* args, PyObject* kwds) -{ - Triangulation::MaskArray mask; - - if (!PyArg_ParseTuple(args, "O&:set_mask", &mask.converter, &mask)) { - return NULL; - } - - if (!mask.empty() && mask.dim(0) != self->ptr->get_ntri()) { - PyErr_SetString(PyExc_ValueError, - "mask must be a 1D array with the same length as the triangles array"); - return NULL; - } - - CALL_CPP("set_mask", (self->ptr->set_mask(mask))); - Py_RETURN_NONE; -} - -static PyTypeObject* PyTriangulation_init_type(PyObject* m, PyTypeObject* type) -{ - static PyMethodDef methods[] = { - {"calculate_plane_coefficients", (PyCFunction)PyTriangulation_calculate_plane_coefficients, METH_VARARGS, PyTriangulation_calculate_plane_coefficients__doc__}, - {"get_edges", (PyCFunction)PyTriangulation_get_edges, METH_NOARGS, PyTriangulation_get_edges__doc__}, - {"get_neighbors", (PyCFunction)PyTriangulation_get_neighbors, METH_NOARGS, PyTriangulation_get_neighbors__doc__}, - {"set_mask", (PyCFunction)PyTriangulation_set_mask, METH_VARARGS, PyTriangulation_set_mask__doc__}, - {NULL} - }; - - memset(type, 0, sizeof(PyTypeObject)); - type->tp_name = "matplotlib._tri.Triangulation"; - type->tp_doc = PyTriangulation_init__doc__; - type->tp_basicsize = sizeof(PyTriangulation); - type->tp_dealloc = (destructor)PyTriangulation_dealloc; - type->tp_flags = Py_TPFLAGS_DEFAULT; - type->tp_methods = methods; - type->tp_new = PyTriangulation_new; - type->tp_init = (initproc)PyTriangulation_init; - - if (PyType_Ready(type) < 0) { - return NULL; - } - - if (PyModule_AddObject(m, "Triangulation", (PyObject*)type)) { - return NULL; - } - - return type; -} - - -/* TriContourGenerator */ - -typedef struct -{ - PyObject_HEAD - TriContourGenerator* ptr; - PyTriangulation* py_triangulation; -} PyTriContourGenerator; - -static PyTypeObject PyTriContourGeneratorType; - -static PyObject* PyTriContourGenerator_new(PyTypeObject* type, PyObject* args, PyObject* kwds) -{ - PyTriContourGenerator* self; - self = (PyTriContourGenerator*)type->tp_alloc(type, 0); - self->ptr = NULL; - self->py_triangulation = NULL; - return (PyObject*)self; -} - -const char* PyTriContourGenerator_init__doc__ = - "TriContourGenerator(triangulation, z)\n" - "--\n\n" - "Create a new C++ TriContourGenerator object\n" - "This should not be called directly, instead use the functions\n" - "matplotlib.axes.tricontour and tricontourf instead.\n"; - -static int PyTriContourGenerator_init(PyTriContourGenerator* self, PyObject* args, PyObject* kwds) -{ - PyObject* triangulation_arg; - TriContourGenerator::CoordinateArray z; - - if (!PyArg_ParseTuple(args, "O!O&", - &PyTriangulationType, &triangulation_arg, - &z.converter, &z)) { - return -1; - } - - PyTriangulation* py_triangulation = (PyTriangulation*)triangulation_arg; - Py_INCREF(py_triangulation); - self->py_triangulation = py_triangulation; - Triangulation& triangulation = *(py_triangulation->ptr); - - if (z.empty() || z.dim(0) != triangulation.get_npoints()) { - PyErr_SetString(PyExc_ValueError, - "z must be a 1D array with the same length as the x and y arrays"); - return -1; - } - - CALL_CPP_INIT("TriContourGenerator", - (self->ptr = new TriContourGenerator(triangulation, z))); - return 0; -} - -static void PyTriContourGenerator_dealloc(PyTriContourGenerator* self) -{ - delete self->ptr; - Py_XDECREF(self->py_triangulation); - Py_TYPE(self)->tp_free((PyObject *)self); -} - -const char* PyTriContourGenerator_create_contour__doc__ = - "create_contour(level)\n" - "\n" - "Create and return a non-filled contour."; - -static PyObject* PyTriContourGenerator_create_contour(PyTriContourGenerator* self, PyObject* args, PyObject* kwds) -{ - double level; - if (!PyArg_ParseTuple(args, "d:create_contour", &level)) { - return NULL; - } - - PyObject* result; - CALL_CPP("create_contour", (result = self->ptr->create_contour(level))); - return result; -} - -const char* PyTriContourGenerator_create_filled_contour__doc__ = - "create_filled_contour(lower_level, upper_level)\n" - "\n" - "Create and return a filled contour"; - -static PyObject* PyTriContourGenerator_create_filled_contour(PyTriContourGenerator* self, PyObject* args, PyObject* kwds) -{ - double lower_level, upper_level; - if (!PyArg_ParseTuple(args, "dd:create_filled_contour", - &lower_level, &upper_level)) { - return NULL; - } - - if (lower_level >= upper_level) - { - PyErr_SetString(PyExc_ValueError, - "filled contour levels must be increasing"); - return NULL; - } - - PyObject* result; - CALL_CPP("create_filled_contour", - (result = self->ptr->create_filled_contour(lower_level, - upper_level))); - return result; -} - -static PyTypeObject* PyTriContourGenerator_init_type(PyObject* m, PyTypeObject* type) -{ - static PyMethodDef methods[] = { - {"create_contour", (PyCFunction)PyTriContourGenerator_create_contour, METH_VARARGS, PyTriContourGenerator_create_contour__doc__}, - {"create_filled_contour", (PyCFunction)PyTriContourGenerator_create_filled_contour, METH_VARARGS, PyTriContourGenerator_create_filled_contour__doc__}, - {NULL} - }; - - memset(type, 0, sizeof(PyTypeObject)); - type->tp_name = "matplotlib._tri.TriContourGenerator"; - type->tp_doc = PyTriContourGenerator_init__doc__; - type->tp_basicsize = sizeof(PyTriContourGenerator); - type->tp_dealloc = (destructor)PyTriContourGenerator_dealloc; - type->tp_flags = Py_TPFLAGS_DEFAULT; - type->tp_methods = methods; - type->tp_new = PyTriContourGenerator_new; - type->tp_init = (initproc)PyTriContourGenerator_init; - - if (PyType_Ready(type) < 0) { - return NULL; - } - - if (PyModule_AddObject(m, "TriContourGenerator", (PyObject*)type)) { - return NULL; - } - - return type; -} - - -/* TrapezoidMapTriFinder */ - -typedef struct -{ - PyObject_HEAD - TrapezoidMapTriFinder* ptr; - PyTriangulation* py_triangulation; -} PyTrapezoidMapTriFinder; - -static PyTypeObject PyTrapezoidMapTriFinderType; - -static PyObject* PyTrapezoidMapTriFinder_new(PyTypeObject* type, PyObject* args, PyObject* kwds) -{ - PyTrapezoidMapTriFinder* self; - self = (PyTrapezoidMapTriFinder*)type->tp_alloc(type, 0); - self->ptr = NULL; - self->py_triangulation = NULL; - return (PyObject*)self; -} - -const char* PyTrapezoidMapTriFinder_init__doc__ = - "TrapezoidMapTriFinder(triangulation)\n" - "--\n\n" - "Create a new C++ TrapezoidMapTriFinder object\n" - "This should not be called directly, instead use the python class\n" - "matplotlib.tri.TrapezoidMapTriFinder instead.\n"; - -static int PyTrapezoidMapTriFinder_init(PyTrapezoidMapTriFinder* self, PyObject* args, PyObject* kwds) -{ - PyObject* triangulation_arg; - if (!PyArg_ParseTuple(args, "O!", - &PyTriangulationType, &triangulation_arg)) { - return -1; - } - - PyTriangulation* py_triangulation = (PyTriangulation*)triangulation_arg; - Py_INCREF(py_triangulation); - self->py_triangulation = py_triangulation; - Triangulation& triangulation = *(py_triangulation->ptr); - - CALL_CPP_INIT("TrapezoidMapTriFinder", - (self->ptr = new TrapezoidMapTriFinder(triangulation))); - return 0; -} - -static void PyTrapezoidMapTriFinder_dealloc(PyTrapezoidMapTriFinder* self) -{ - delete self->ptr; - Py_XDECREF(self->py_triangulation); - Py_TYPE(self)->tp_free((PyObject *)self); -} - -const char* PyTrapezoidMapTriFinder_find_many__doc__ = - "find_many(x, y)\n" - "\n" - "Find indices of triangles containing the point coordinates (x, y)"; - -static PyObject* PyTrapezoidMapTriFinder_find_many(PyTrapezoidMapTriFinder* self, PyObject* args, PyObject* kwds) -{ - TrapezoidMapTriFinder::CoordinateArray x, y; - if (!PyArg_ParseTuple(args, "O&O&:find_many", - &x.converter, &x, - &y.converter, &y)) { - return NULL; - } - - if (x.empty() || y.empty() || x.dim(0) != y.dim(0)) { - PyErr_SetString(PyExc_ValueError, - "x and y must be array-like with same shape"); - return NULL; - } - - TrapezoidMapTriFinder::TriIndexArray result; - CALL_CPP("find_many", (result = self->ptr->find_many(x, y))); - return result.pyobj(); -} - -const char* PyTrapezoidMapTriFinder_get_tree_stats__doc__ = - "get_tree_stats()\n" - "\n" - "Return statistics about the tree used by the trapezoid map"; - -static PyObject* PyTrapezoidMapTriFinder_get_tree_stats(PyTrapezoidMapTriFinder* self, PyObject* args, PyObject* kwds) -{ - PyObject* result; - CALL_CPP("get_tree_stats", (result = self->ptr->get_tree_stats())); - return result; -} - -const char* PyTrapezoidMapTriFinder_initialize__doc__ = - "initialize()\n" - "\n" - "Initialize this object, creating the trapezoid map from the triangulation"; - -static PyObject* PyTrapezoidMapTriFinder_initialize(PyTrapezoidMapTriFinder* self, PyObject* args, PyObject* kwds) -{ - CALL_CPP("initialize", (self->ptr->initialize())); - Py_RETURN_NONE; +PYBIND11_MODULE(_tri, m) { + py::class_(m, "Triangulation") + .def(py::init(), + py::arg("x"), + py::arg("y"), + py::arg("triangles"), + py::arg("mask"), + py::arg("edges"), + py::arg("neighbors"), + py::arg("correct_triangle_orientations"), + "Create a new C++ Triangulation object.\n" + "This should not be called directly, use the python class\n" + "matplotlib.tri.Triangulation instead.\n") + .def("calculate_plane_coefficients", &Triangulation::calculate_plane_coefficients, + "Calculate plane equation coefficients for all unmasked triangles.") + .def("get_edges", &Triangulation::get_edges, + "Return edges array.") + .def("get_neighbors", &Triangulation::get_neighbors, + "Return neighbors array.") + .def("set_mask", &Triangulation::set_mask, + "Set or clear the mask array."); + + py::class_(m, "TriContourGenerator") + .def(py::init(), + py::arg("triangulation"), + py::arg("z"), + "Create a new C++ TriContourGenerator object.\n" + "This should not be called directly, use the functions\n" + "matplotlib.axes.tricontour and tricontourf instead.\n") + .def("create_contour", &TriContourGenerator::create_contour, + "Create and return a non-filled contour.") + .def("create_filled_contour", &TriContourGenerator::create_filled_contour, + "Create and return a filled contour."); + + py::class_(m, "TrapezoidMapTriFinder") + .def(py::init(), + py::arg("triangulation"), + "Create a new C++ TrapezoidMapTriFinder object.\n" + "This should not be called directly, use the python class\n" + "matplotlib.tri.TrapezoidMapTriFinder instead.\n") + .def("find_many", &TrapezoidMapTriFinder::find_many, + "Find indices of triangles containing the point coordinates (x, y).") + .def("get_tree_stats", &TrapezoidMapTriFinder::get_tree_stats, + "Return statistics about the tree used by the trapezoid map.") + .def("initialize", &TrapezoidMapTriFinder::initialize, + "Initialize this object, creating the trapezoid map from the triangulation.") + .def("print_tree", &TrapezoidMapTriFinder::print_tree, + "Print the search tree as text to stdout; useful for debug purposes."); } - -const char* PyTrapezoidMapTriFinder_print_tree__doc__ = - "print_tree()\n" - "\n" - "Print the search tree as text to stdout; useful for debug purposes"; - -static PyObject* PyTrapezoidMapTriFinder_print_tree(PyTrapezoidMapTriFinder* self, PyObject* args, PyObject* kwds) -{ - CALL_CPP("print_tree", (self->ptr->print_tree())); - Py_RETURN_NONE; -} - -static PyTypeObject* PyTrapezoidMapTriFinder_init_type(PyObject* m, PyTypeObject* type) -{ - static PyMethodDef methods[] = { - {"find_many", (PyCFunction)PyTrapezoidMapTriFinder_find_many, METH_VARARGS, PyTrapezoidMapTriFinder_find_many__doc__}, - {"get_tree_stats", (PyCFunction)PyTrapezoidMapTriFinder_get_tree_stats, METH_NOARGS, PyTrapezoidMapTriFinder_get_tree_stats__doc__}, - {"initialize", (PyCFunction)PyTrapezoidMapTriFinder_initialize, METH_NOARGS, PyTrapezoidMapTriFinder_initialize__doc__}, - {"print_tree", (PyCFunction)PyTrapezoidMapTriFinder_print_tree, METH_NOARGS, PyTrapezoidMapTriFinder_print_tree__doc__}, - {NULL} - }; - - memset(type, 0, sizeof(PyTypeObject)); - type->tp_name = "matplotlib._tri.TrapezoidMapTriFinder"; - type->tp_doc = PyTrapezoidMapTriFinder_init__doc__; - type->tp_basicsize = sizeof(PyTrapezoidMapTriFinder); - type->tp_dealloc = (destructor)PyTrapezoidMapTriFinder_dealloc; - type->tp_flags = Py_TPFLAGS_DEFAULT; - type->tp_methods = methods; - type->tp_new = PyTrapezoidMapTriFinder_new; - type->tp_init = (initproc)PyTrapezoidMapTriFinder_init; - - if (PyType_Ready(type) < 0) { - return NULL; - } - - if (PyModule_AddObject(m, "TrapezoidMapTriFinder", (PyObject*)type)) { - return NULL; - } - - return type; -} - - -/* Module */ - -static struct PyModuleDef moduledef = { - PyModuleDef_HEAD_INIT, - "_tri", - NULL, - 0, - NULL, - NULL, - NULL, - NULL, - NULL -}; - -#pragma GCC visibility push(default) - -PyMODINIT_FUNC PyInit__tri(void) -{ - PyObject *m; - - m = PyModule_Create(&moduledef); - - if (m == NULL) { - return NULL; - } - - if (!PyTriangulation_init_type(m, &PyTriangulationType)) { - return NULL; - } - if (!PyTriContourGenerator_init_type(m, &PyTriContourGeneratorType)) { - return NULL; - } - if (!PyTrapezoidMapTriFinder_init_type(m, &PyTrapezoidMapTriFinderType)) { - return NULL; - } - - import_array(); - - return m; -} - -#pragma GCC visibility pop diff --git a/tools/boilerplate.py b/tools/boilerplate.py index cf013ef0db8d..0b00d7a12b4a 100644 --- a/tools/boilerplate.py +++ b/tools/boilerplate.py @@ -79,7 +79,16 @@ def {name}{signature}: return gcf().{called_name}{call} """ -CMAP_TEMPLATE = "def {name}(): set_cmap({name!r})\n" # Colormap functions. +CMAP_TEMPLATE = ''' +def {name}(): + """ + Set the colormap to {name!r}. + + This changes the default colormap as well as the colormap of the current + image if there is one. See ``help(colormaps)`` for more information. + """ + set_cmap({name!r}) +''' # Colormap functions. class value_formatter: @@ -138,7 +147,13 @@ def generate_function(name, called_fullname, template, **kwargs): class_name, called_name = called_fullname.split('.') class_ = {'Axes': Axes, 'Figure': Figure}[class_name] - signature = inspect.signature(getattr(class_, called_name)) + meth = getattr(class_, called_name) + decorator = _api.deprecation.DECORATORS.get(meth) + # Generate the wrapper with the non-kwonly signature, as it will get + # redecorated with make_keyword_only by _copy_docstring_and_deprecators. + if decorator and decorator.func is _api.make_keyword_only: + meth = meth.__wrapped__ + signature = inspect.signature(meth) # Replace self argument. params = list(signature.parameters.values())[1:] signature = str(signature.replace(parameters=[ @@ -167,11 +182,12 @@ def generate_function(name, called_fullname, template, **kwargs): if param.kind in [ Parameter.POSITIONAL_OR_KEYWORD, Parameter.KEYWORD_ONLY] else + '{0}' + if param.kind is Parameter.POSITIONAL_ONLY else '*{0}' if param.kind is Parameter.VAR_POSITIONAL else '**{0}' if param.kind is Parameter.VAR_KEYWORD else - # Intentionally crash for Parameter.POSITIONAL_ONLY. None).format(param.name) for param in params) + ')' MAX_CALL_PREFIX = 18 # len(' __ret = gca().') @@ -202,6 +218,7 @@ def boilerplate_gen(): 'ginput', 'subplots_adjust', 'suptitle', + 'tight_layout', 'waitforbuttonpress', ) @@ -323,8 +340,6 @@ def boilerplate_gen(): yield generate_function(name, f'Axes.{called_name}', template, sci_command=cmappable.get(name)) - yield AUTOGEN_MSG - yield '\n' cmaps = ( 'autumn', 'bone', @@ -348,11 +363,9 @@ def boilerplate_gen(): ) # add all the colormaps (autumn, hsv, ....) for name in cmaps: + yield AUTOGEN_MSG yield CMAP_TEMPLATE.format(name=name) - yield '\n\n' - yield '_setup_pyplot_info_docstrings()' - def build_pyplot(pyplot_path): pyplot_orig = pyplot_path.read_text().splitlines(keepends=True) @@ -365,7 +378,6 @@ def build_pyplot(pyplot_path): with pyplot_path.open('w') as pyplot: pyplot.writelines(pyplot_orig) pyplot.writelines(boilerplate_gen()) - pyplot.write('\n') if __name__ == '__main__': diff --git a/tools/cache_zenodo_svg.py b/tools/cache_zenodo_svg.py index e00f4eb3d128..a7a80603c602 100644 --- a/tools/cache_zenodo_svg.py +++ b/tools/cache_zenodo_svg.py @@ -50,7 +50,8 @@ def _get_xdg_cache_dir(): """ Return the XDG cache directory. - See https://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html + See + https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html """ cache_dir = os.environ.get("XDG_CACHE_HOME") if not cache_dir: @@ -62,6 +63,16 @@ def _get_xdg_cache_dir(): if __name__ == "__main__": data = { + "v3.7.0": "7637593", + "v3.6.3": "7527665", + "v3.6.2": "7275322", + "v3.6.1": "7162185", + "v3.6.0": "7084615", + "v3.5.3": "6982547", + "v3.5.2": "6513224", + "v3.5.1": "5773480", + "v3.5.0": "5706396", + "v3.4.3": "5194481", "v3.4.2": "4743323", "v3.4.1": "4649959", "v3.4.0": "4638398", @@ -104,7 +115,7 @@ def _get_xdg_cache_dir(): } doc_dir = Path(__file__).parent.parent.absolute() / "doc" target_dir = doc_dir / "_static/zenodo_cache" - citing = doc_dir / "citing.rst" + citing = doc_dir / "users/project/citing.rst" target_dir.mkdir(exist_ok=True, parents=True) header = [] footer = [] @@ -132,7 +143,7 @@ def _get_xdg_cache_dir(): fout.write( f""" {version} - .. image:: _static/zenodo_cache/{doi}.svg + .. image:: ../../_static/zenodo_cache/{doi}.svg :target: https://doi.org/10.5281/zenodo.{doi}""" ) fout.write("\n\n") diff --git a/tools/create_DejaVuDisplay.sh b/tools/create_DejaVuDisplay.sh index 1b9d49935fd0..8ee3b1659e62 100755 --- a/tools/create_DejaVuDisplay.sh +++ b/tools/create_DejaVuDisplay.sh @@ -2,10 +2,10 @@ # Subsetting DejaVu fonts to create a display-math-only font -# The DejaVu fonts include math display variants outside of the unicode range, -# and it is currently hard to access them from matploltib. The subset.py script +# The DejaVu fonts include math display variants outside of the Unicode range, +# and it is currently hard to access them from matplotlib. The subset.py script # in `tools` has been modified to move the math display variants found in DejaVu -# fonts into a new TTF font with these variants in the unicode range. +# fonts into a new TTF font with these variants in the Unicode range. # This bash script calls the subset.py scripts with the appropriate options to # generate the new font files `DejaVuSansDisplay.ttf` and diff --git a/tools/gh_api.py b/tools/gh_api.py index 71879264a4ac..dad57df9f119 100644 --- a/tools/gh_api.py +++ b/tools/gh_api.py @@ -216,7 +216,7 @@ def encode_multipart_formdata(fields, boundary=None): bytes. If the value is a tuple of two elements, then the first element is treated as the filename of the form-data section. - Field names and filenames must be unicode. + Field names and filenames must be str. :param boundary: If not specified, then a random boundary will be generated using diff --git a/tools/github_stats.py b/tools/github_stats.py index 3b9a4cbb5377..f6e190324194 100755 --- a/tools/github_stats.py +++ b/tools/github_stats.py @@ -78,8 +78,8 @@ def issues_closed_since(period=timedelta(days=365), project="matplotlib/matplotl filtered = [ i for i in allclosed if _parse_datetime(i['closed_at']) > since ] if pulls: filtered = [ i for i in filtered if _parse_datetime(i['merged_at']) > since ] - # filter out PRs not against master (backports) - filtered = [ i for i in filtered if i['base']['ref'] == 'master' ] + # filter out PRs not against main (backports) + filtered = [ i for i in filtered if i['base']['ref'] == 'main' ] else: filtered = [ i for i in filtered if not is_pull_request(i) ] @@ -172,17 +172,18 @@ def report(issues, show_urls=False): n_issues, n_pulls = map(len, (issues, pulls)) n_total = n_issues + n_pulls + since_day = since.strftime("%Y/%m/%d") + today = datetime.today() # Print summary report we can directly include into release notes. print('.. _github-stats:') print() - print('GitHub Stats') - print('============') + title = 'GitHub statistics ' + today.strftime('(%b %d, %Y)') + print(title) + print('=' * len(title)) print() - since_day = since.strftime("%Y/%m/%d") - today = datetime.today().strftime("%Y/%m/%d") - print("GitHub stats for %s - %s (tag: %s)" % (since_day, today, tag)) + print("GitHub statistics for %s (tag: %s) - %s" % (since_day, tag, today.strftime("%Y/%m/%d"), )) print() print("These lists are automatically generated, and may be incomplete or contain duplicates.") print() @@ -229,8 +230,9 @@ def report(issues, show_urls=False): report(issues, show_urls) print() print() - print("""Previous GitHub Stats ---------------------- + print("""\ +Previous GitHub statistics +-------------------------- .. toctree:: diff --git a/tools/make_icons.py b/tools/make_icons.py index 773b4c717e28..0424c0d03dad 100755 --- a/tools/make_icons.py +++ b/tools/make_icons.py @@ -46,8 +46,8 @@ def save_icon(fig, dest_dir, name): def make_icon(font_path, ccode): fig = plt.figure(figsize=(1, 1)) fig.patch.set_alpha(0.0) - text = fig.text(0.5, 0.48, chr(ccode), ha='center', va='center', - font=font_path, fontsize=68) + fig.text(0.5, 0.48, chr(ccode), ha='center', va='center', + font=font_path, fontsize=68) return fig diff --git a/tools/run_examples.py b/tools/run_examples.py index 97dfe5be7ec5..e6542e9ddf23 100644 --- a/tools/run_examples.py +++ b/tools/run_examples.py @@ -10,6 +10,7 @@ import sys from tempfile import TemporaryDirectory import time +import tokenize _preamble = """\ @@ -73,8 +74,9 @@ def main(): cwd.mkdir(parents=True) else: cwd = stack.enter_context(TemporaryDirectory()) - Path(cwd, relpath.name).write_text( - _preamble + (root / relpath).read_text()) + with tokenize.open(root / relpath) as src: + Path(cwd, relpath.name).write_text( + _preamble + src.read(), encoding="utf-8") for backend in args.backend or [None]: env = {**os.environ} if backend is not None: diff --git a/tools/subset.py b/tools/subset.py index 5b1494f1b515..9fdf3789b0df 100644 --- a/tools/subset.py +++ b/tools/subset.py @@ -9,7 +9,7 @@ # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0.txt # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -21,8 +21,10 @@ # # A script for subsetting a font, using FontForge. See README for details. -# TODO 2013-04-08 ensure the menu files are as compact as possible by default, similar to subset.pl -# TODO 2013-05-22 in Arimo, the latin subset doesn't include ; but the greek does. why on earth is this happening? +# TODO 2013-04-08 ensure the menu files are as compact as possible by default, +# similar to subset.pl +# TODO 2013-05-22 in Arimo, the latin subset doesn't include ; but the greek +# does. why on earth is this happening? import getopt import os @@ -33,34 +35,37 @@ import fontforge -def log_namelist(nam, unicode): - if nam and isinstance(unicode, int): - print("0x%0.4X" % unicode, fontforge.nameFromUnicode(unicode), file=nam) +def log_namelist(name, unicode): + if name and isinstance(unicode, int): + print(f"0x{unicode:04X}", fontforge.nameeFromUnicode(unicode), + file=name) -def select_with_refs(font, unicode, newfont, pe = None, nam = None): + +def select_with_refs(font, unicode, newfont, pe=None, name=None): newfont.selection.select(('more', 'unicode'), unicode) - log_namelist(nam, unicode) + log_namelist(name, unicode) if pe: - print("SelectMore(%d)" % unicode, file=pe) + print(f"SelectMore({unicode})", file=pe) try: for ref in font[unicode].references: newfont.selection.select(('more',), ref[0]) - log_namelist(nam, ref[0]) + log_namelist(name, ref[0]) if pe: - print('SelectMore("%s")' % ref[0], file=pe) + print(f'SelectMore("{ref[0]}")', file=pe) except Exception: - print('Resolving references on u+%04x failed' % unicode) + print(f'Resolving references on u+{unicode:04x} failed') + def subset_font_raw(font_in, font_out, unicodes, opts): if '--namelist' in opts: # 2010-12-06 DC To allow setting namelist filenames, # change getopt.gnu_getopt from namelist to namelist= # and invert comments on following 2 lines - # nam_fn = opts['--namelist'] - nam_fn = font_out + '.nam' - nam = open(nam_fn, 'w') + # name_fn = opts['--namelist'] + name_fn = f'{font_out}.name' + name = open(name_fn, 'w') else: - nam = None + name = None if '--script' in opts: pe_fn = "/tmp/script.pe" pe = open(pe_fn, 'w') @@ -68,10 +73,10 @@ def subset_font_raw(font_in, font_out, unicodes, opts): pe = None font = fontforge.open(font_in) if pe: - print('Open("' + font_in + '")', file=pe) + print(f'Open("{font_in}")', file=pe) extract_vert_to_script(font_in, pe) for i in unicodes: - select_with_refs(font, i, font, pe, nam) + select_with_refs(font, i, font, pe, name) addl_glyphs = [] if '--nmr' in opts: @@ -82,10 +87,11 @@ def subset_font_raw(font_in, font_out, unicodes, opts): addl_glyphs.append('.notdef') for glyph in addl_glyphs: font.selection.select(('more',), glyph) - if nam: - print("0x%0.4X" % fontforge.unicodeFromName(glyph), glyph, file=nam) + if name: + print(f"0x{fontforge.unicodeFromName(glyph):0.4X}", glyph, + file=name) if pe: - print('SelectMore("%s")' % glyph, file=pe) + print(f'SelectMore("{glyph}")', file=pe) flags = () @@ -107,7 +113,7 @@ def subset_font_raw(font_in, font_out, unicodes, opts): new.em = font.em new.layers['Fore'].is_quadratic = font.layers['Fore'].is_quadratic for i in unicodes: - select_with_refs(font, i, new, pe, nam) + select_with_refs(font, i, new, pe, name) new.paste() # This is a hack - it should have been taken care of above. font.selection.select('space') @@ -123,7 +129,7 @@ def subset_font_raw(font_in, font_out, unicodes, opts): print("Clear()", file=pe) if '--move-display' in opts: - print("Moving display glyphs into unicode ranges...") + print("Moving display glyphs into Unicode ranges...") font.familyname += " Display" font.fullname += " Display" font.fontname += "Display" @@ -144,16 +150,16 @@ def subset_font_raw(font_in, font_out, unicodes, opts): font.selection.select(glname) font.cut() - if nam: + if name: print("Writing NameList", end="") - nam.close() + name.close() if pe: - print('Generate("' + font_out + '")', file=pe) + print(f'Generate("{font_out}")', file=pe) pe.close() subprocess.call(["fontforge", "-script", pe_fn]) else: - font.generate(font_out, flags = flags) + font.generate(font_out, flags=flags) font.close() if '--roundtrip' in opts: @@ -161,7 +167,8 @@ def subset_font_raw(font_in, font_out, unicodes, opts): # the advanceWidthMax in the hhea table, and a workaround is to open # and re-generate font2 = fontforge.open(font_out) - font2.generate(font_out, flags = flags) + font2.generate(font_out, flags=flags) + def subset_font(font_in, font_out, unicodes, opts): font_out_raw = font_out @@ -171,61 +178,71 @@ def subset_font(font_in, font_out, unicodes, opts): if font_out != font_out_raw: os.rename(font_out_raw, font_out) # 2011-02-14 DC this needs to only happen with --namelist is used -# os.rename(font_out_raw + '.nam', font_out + '.nam') +# os.rename(font_out_raw + '.name', font_out + '.name') + def getsubset(subset, font_in): subsets = subset.split('+') - - quotes = [0x2013] # endash - quotes += [0x2014] # emdash - quotes += [0x2018] # quoteleft - quotes += [0x2019] # quoteright - quotes += [0x201A] # quotesinglbase - quotes += [0x201C] # quotedblleft - quotes += [0x201D] # quotedblright - quotes += [0x201E] # quotedblbase - quotes += [0x2022] # bullet - quotes += [0x2039] # guilsinglleft - quotes += [0x203A] # guilsinglright - - latin = range(0x20, 0x7f) # Basic Latin (A-Z, a-z, numbers) - latin += range(0xa0, 0x100) # Western European symbols and diacritics - latin += [0x20ac] # Euro - latin += [0x0152] # OE - latin += [0x0153] # oe - latin += [0x003b] # semicolon - latin += [0x00b7] # periodcentered - latin += [0x0131] # dotlessi - latin += [0x02c6] # circumflex - latin += [0x02da] # ring - latin += [0x02dc] # tilde - latin += [0x2074] # foursuperior - latin += [0x2215] # division slash - latin += [0x2044] # fraction slash - latin += [0xe0ff] # PUA: Font logo - latin += [0xeffd] # PUA: Font version number - latin += [0xf000] # PUA: font ppem size indicator: run `ftview -f 1255 10 Ubuntu-Regular.ttf` to see it in action! + quotes = [ + 0x2013, # endash + 0x2014, # emdash + 0x2018, # quoteleft + 0x2019, # quoteright + 0x201A, # quotesinglbase + 0x201C, # quotedblleft + 0x201D, # quotedblright + 0x201E, # quotedblbase + 0x2022, # bullet + 0x2039, # guilsinglleft + 0x203A, # guilsinglright + ] + + latin = [ + *range(0x20, 0x7f), # Basic Latin (A-Z, a-z, numbers) + *range(0xa0, 0x100), # Western European symbols and diacritics + 0x20ac, # Euro + 0x0152, # OE + 0x0153, # oe + 0x003b, # semicolon + 0x00b7, # periodcentered + 0x0131, # dotlessi + 0x02c6, # circumflex + 0x02da, # ring + 0x02dc, # tilde + 0x2074, # foursuperior + 0x2215, # division slash + 0x2044, # fraction slash + 0xe0ff, # PUA: Font logo + 0xeffd, # PUA: Font version number + 0xf000, # PUA: font ppem size indicator: run + # `ftview -f 1255 10 Ubuntu-Regular.ttf` to see it in action! + ] result = quotes - if 'menu' in subset: + if 'menu' in subsets: font = fontforge.open(font_in) - result = map(ord, font.familyname) - result += [0x0020] + result = [ + *map(ord, font.familyname), + 0x0020, + ] - if 'latin' in subset: + if 'latin' in subsets: result += latin - if 'latin-ext' in subset: + if 'latin-ext' in subsets: # These ranges include Extended A, B, C, D, and Additional with the # exception of Vietnamese, which is a separate range - result += (range(0x100, 0x370) + - range(0x1d00, 0x1ea0) + - range(0x1ef2, 0x1f00) + - range(0x2070, 0x20d0) + - range(0x2c60, 0x2c80) + - range(0xa700, 0xa800)) - if 'vietnamese' in subset: - # 2011-07-16 DC: Charset from http://vietunicode.sourceforge.net/charset/ + U+1ef9 from Fontaine + result += [ + *range(0x100, 0x370), + *range(0x1d00, 0x1ea0), + *range(0x1ef2, 0x1f00), + *range(0x2070, 0x20d0), + *range(0x2c60, 0x2c80), + *range(0xa700, 0xa800), + ] + if 'vietnamese' in subsets: + # 2011-07-16 DC: Charset from + # http://vietunicode.sourceforge.net/charset/ + U+1ef9 from Fontaine result += [0x00c0, 0x00c1, 0x00c2, 0x00c3, 0x00C8, 0x00C9, 0x00CA, 0x00CC, 0x00CD, 0x00D2, 0x00D3, 0x00D4, 0x00D5, 0x00D9, 0x00DA, 0x00DD, 0x00E0, 0x00E1, @@ -233,22 +250,27 @@ def getsubset(subset, font_in): 0x00ED, 0x00F2, 0x00F3, 0x00F4, 0x00F5, 0x00F9, 0x00FA, 0x00FD, 0x0102, 0x0103, 0x0110, 0x0111, 0x0128, 0x0129, 0x0168, 0x0169, 0x01A0, 0x01A1, - 0x01AF, 0x01B0, 0x20AB] + range(0x1EA0, 0x1EFA) - if 'greek' in subset: - # Could probably be more aggressive here and exclude archaic characters, - # but lack data - result += range(0x370, 0x400) - if 'greek-ext' in subset: - result += range(0x370, 0x400) + range(0x1f00, 0x2000) - if 'cyrillic' in subset: + 0x01AF, 0x01B0, 0x20AB, *range(0x1EA0, 0x1EFA)] + if 'greek' in subsets: + # Could probably be more aggressive here and exclude archaic + # characters, but lack data + result += [*range(0x370, 0x400)] + if 'greek-ext' in subsets: + result += [*range(0x370, 0x400), *range(0x1f00, 0x2000)] + if 'cyrillic' in subsets: # Based on character frequency analysis - result += range(0x400, 0x460) + [0x490, 0x491, 0x4b0, 0x4b1, 0x2116] - if 'cyrillic-ext' in subset: - result += (range(0x400, 0x530) + - [0x20b4, 0x2116] + # 0x2116 is the russian No, a number abbreviation similar to the latin #, suggested by Alexei Vanyashin - range(0x2de0, 0x2e00) + - range(0xa640, 0xa6a0)) - if 'arabic' in subset: + result += [*range(0x400, 0x460), 0x490, 0x491, 0x4b0, 0x4b1, 0x2116] + if 'cyrillic-ext' in subsets: + result += [ + *range(0x400, 0x530), + 0x20b4, + # 0x2116 is the russian No, a number abbreviation similar to the + # latin #, suggested by Alexei Vanyashin + 0x2116, + *range(0x2de0, 0x2e00), + *range(0xa640, 0xa6a0), + ] + if 'arabic' in subsets: # Based on Droid Arabic Kufi 1.0 result += [0x000D, 0x0020, 0x0621, 0x0627, 0x062D, 0x062F, 0x0631, 0x0633, 0x0635, 0x0637, 0x0639, @@ -295,11 +317,12 @@ def getsubset(subset, font_in): 0x06C8, 0x06C9, 0x06CA, 0x06CB, 0x06CF, 0x06CE, 0x06D0, 0x06D1, 0x06D4, 0x06FA, 0x06DD, 0x06DE, 0x06E0, 0x06E9, 0x060D, 0xFD3E, 0xFD3F, 0x25CC, - # Added from https://groups.google.com/d/topic/googlefontdirectory-discuss/MwlMWMPNCXs/discussion + # Added from + # https://groups.google.com/d/topic/googlefontdirectory-discuss/MwlMWMPNCXs/discussion 0x063b, 0x063c, 0x063d, 0x063e, 0x063f, 0x0620, 0x0674, 0x0674, 0x06EC] - if 'dejavu-ext' in subset: + if 'dejavu-ext' in subsets: # add all glyphnames ending in .display font = fontforge.open(font_in) for glyph in font.glyphs(): @@ -308,42 +331,48 @@ def getsubset(subset, font_in): return result -# code for extracting vertical metrics from a TrueType font +# code for extracting vertical metrics from a TrueType font class Sfnt: def __init__(self, data): - version, numTables, _, _, _ = struct.unpack('>IHHHH', data[:12]) + _, numTables, _, _, _ = struct.unpack('>IHHHH', data[:12]) self.tables = {} for i in range(numTables): - tag, checkSum, offset, length = struct.unpack('>4sIII', data[12 + 16 * i: 28 + 16 * i]) + tag, _, offset, length = struct.unpack( + '>4sIII', data[12 + 16 * i: 28 + 16 * i]) self.tables[tag] = data[offset: offset + length] def hhea(self): r = {} d = self.tables['hhea'] - r['Ascender'], r['Descender'], r['LineGap'] = struct.unpack('>hhh', d[4:10]) + r['Ascender'], r['Descender'], r['LineGap'] = struct.unpack( + '>hhh', d[4:10]) return r def os2(self): r = {} d = self.tables['OS/2'] r['fsSelection'], = struct.unpack('>H', d[62:64]) - r['sTypoAscender'], r['sTypoDescender'], r['sTypoLineGap'] = struct.unpack('>hhh', d[68:74]) - r['usWinAscender'], r['usWinDescender'] = struct.unpack('>HH', d[74:78]) + r['sTypoAscender'], r['sTypoDescender'], r['sTypoLineGap'] = \ + struct.unpack('>hhh', d[68:74]) + r['usWinAscender'], r['usWinDescender'] = struct.unpack( + '>HH', d[74:78]) return r + def set_os2(pe, name, val): - print('SetOS2Value("' + name + '", %d)' % val, file=pe) + print(f'SetOS2Value("{name}", {val:d})', file=pe) + def set_os2_vert(pe, name, val): set_os2(pe, name + 'IsOffset', 0) set_os2(pe, name, val) + # Extract vertical metrics data directly out of font file, and emit # script code to set the values in the generated font. This is a (rather # ugly) workaround for the issue described in: -# http://sourceforge.net/mailarchive/forum.php?thread_name=20100906085718.GB1907%40khaled-laptop&forum_name=fontforge-users - +# https://sourceforge.net/p/fontforge/mailman/fontforge-users/thread/20100906085718.GB1907@khaled-laptop/ def extract_vert_to_script(font_in, pe): with open(font_in, 'rb') as in_file: data = in_file.read() @@ -357,11 +386,12 @@ def extract_vert_to_script(font_in, pe): set_os2_vert(pe, "HHeadAscent", hhea['Ascender']) set_os2_vert(pe, "HHeadDescent", hhea['Descender']) + def main(argv): - optlist, args = getopt.gnu_getopt(argv, '', ['string=', 'strip_names', 'opentype-features', - 'simplify', 'new', 'script', - 'nmr', 'roundtrip', 'subset=', - 'namelist', 'null', 'nd', 'move-display']) + optlist, args = getopt.gnu_getopt(argv, '', [ + 'string=', 'strip_names', 'opentype-features', 'simplify', 'new', + 'script', 'nmr', 'roundtrip', 'subset=', 'namelist', 'null', 'nd', + 'move-display']) font_in, font_out = args opts = dict(optlist) @@ -371,5 +401,6 @@ def main(argv): subset = getsubset(opts.get('--subset', 'latin'), font_in) subset_font(font_in, font_out, subset, opts) + if __name__ == '__main__': main(sys.argv[1:]) diff --git a/tools/triage_tests.py b/tools/triage_tests.py index 02b8f600761d..fc245fea1a2f 100644 --- a/tools/triage_tests.py +++ b/tools/triage_tests.py @@ -30,6 +30,7 @@ import sys from matplotlib.backends.qt_compat import QtCore, QtGui, QtWidgets +from matplotlib.backends.qt_compat import _enum, _exec # matplotlib stores the baseline images under two separate subtrees, @@ -39,13 +40,13 @@ BASELINE_IMAGES = [ Path('lib/matplotlib/tests/baseline_images'), - Path('lib/mpl_toolkits/tests/baseline_images'), + *Path('lib/mpl_toolkits').glob('*/tests/baseline_images'), ] # Non-png image extensions -exts = ['pdf', 'svg'] +exts = ['pdf', 'svg', 'eps'] class Thumbnail(QtWidgets.QFrame): @@ -61,13 +62,13 @@ def __init__(self, parent, index, name): layout = QtWidgets.QVBoxLayout() label = QtWidgets.QLabel(name) - label.setAlignment(QtCore.Qt.AlignHCenter | - QtCore.Qt.AlignVCenter) + label.setAlignment(_enum('QtCore.Qt.AlignmentFlag').AlignHCenter | + _enum('QtCore.Qt.AlignmentFlag').AlignVCenter) layout.addWidget(label, 0) self.image = QtWidgets.QLabel() - self.image.setAlignment(QtCore.Qt.AlignHCenter | - QtCore.Qt.AlignVCenter) + self.image.setAlignment(_enum('QtCore.Qt.AlignmentFlag').AlignHCenter | + _enum('QtCore.Qt.AlignmentFlag').AlignVCenter) self.image.setMinimumSize(800 // 3, 600 // 3) layout.addWidget(self.image) self.setLayout(layout) @@ -85,7 +86,7 @@ def __init__(self, window): self.window = window def eventFilter(self, receiver, event): - if event.type() == QtCore.QEvent.KeyPress: + if event.type() == _enum('QtCore.QEvent.Type').KeyPress: self.window.keyPressEvent(event) return True else: @@ -125,8 +126,9 @@ def __init__(self, entries): images_layout = QtWidgets.QVBoxLayout() images_box = QtWidgets.QWidget() self.image_display = QtWidgets.QLabel() - self.image_display.setAlignment(QtCore.Qt.AlignHCenter | - QtCore.Qt.AlignVCenter) + self.image_display.setAlignment( + _enum('QtCore.Qt.AlignmentFlag').AlignHCenter | + _enum('QtCore.Qt.AlignmentFlag').AlignVCenter) self.image_display.setMinimumSize(800, 600) images_layout.addWidget(self.image_display, 6) images_box.setLayout(images_layout) @@ -164,8 +166,9 @@ def set_entry(self, index): for fname, thumbnail in zip(entry.thumbnails, self.thumbnails): pixmap = QtGui.QPixmap(os.fspath(fname)) scaled_pixmap = pixmap.scaled( - thumbnail.size(), QtCore.Qt.KeepAspectRatio, - QtCore.Qt.SmoothTransformation) + thumbnail.size(), + _enum('QtCore.Qt.AspectRatioMode').KeepAspectRatio, + _enum('QtCore.Qt.TransformationMode').SmoothTransformation) thumbnail.image.setPixmap(scaled_pixmap) self.pixmaps.append(scaled_pixmap) @@ -173,13 +176,15 @@ def set_entry(self, index): self.filelist.setCurrentRow(self.current_entry) def set_large_image(self, index): - self.thumbnails[self.current_thumbnail].setFrameShape(0) + self.thumbnails[self.current_thumbnail].setFrameShape( + _enum('QtWidgets.QFrame.Shape').NoFrame) self.current_thumbnail = index pixmap = QtGui.QPixmap(os.fspath( self.entries[self.current_entry] .thumbnails[self.current_thumbnail])) self.image_display.setPixmap(pixmap) - self.thumbnails[self.current_thumbnail].setFrameShape(1) + self.thumbnails[self.current_thumbnail].setFrameShape( + _enum('QtWidgets.QFrame.Shape').Box) def accept_test(self): entry = self.entries[self.current_entry] @@ -204,17 +209,17 @@ def reject_test(self): self.set_entry(min((self.current_entry + 1), len(self.entries) - 1)) def keyPressEvent(self, e): - if e.key() == QtCore.Qt.Key_Left: + if e.key() == _enum('QtCore.Qt.Key').Key_Left: self.set_large_image((self.current_thumbnail - 1) % 3) - elif e.key() == QtCore.Qt.Key_Right: + elif e.key() == _enum('QtCore.Qt.Key').Key_Right: self.set_large_image((self.current_thumbnail + 1) % 3) - elif e.key() == QtCore.Qt.Key_Up: + elif e.key() == _enum('QtCore.Qt.Key').Key_Up: self.set_entry(max(self.current_entry - 1, 0)) - elif e.key() == QtCore.Qt.Key_Down: + elif e.key() == _enum('QtCore.Qt.Key').Key_Down: self.set_entry(min(self.current_entry + 1, len(self.entries) - 1)) - elif e.key() == QtCore.Qt.Key_A: + elif e.key() == _enum('QtCore.Qt.Key').Key_A: self.accept_test() - elif e.key() == QtCore.Qt.Key_R: + elif e.key() == _enum('QtCore.Qt.Key').Key_R: self.reject_test() else: super().keyPressEvent(e) @@ -233,8 +238,8 @@ def __init__(self, path, root, source): basename = self.diff[:-len('-failed-diff.png')] for ext in exts: - if basename.endswith('_' + ext): - display_extension = '_' + ext + if basename.endswith(f'_{ext}'): + display_extension = f'_{ext}' extension = ext basename = basename[:-4] break @@ -244,11 +249,10 @@ def __init__(self, path, root, source): self.basename = basename self.extension = extension - self.generated = basename + '.' + extension - self.expected = basename + '-expected.' + extension - self.expected_display = (basename + '-expected' + display_extension + - '.png') - self.generated_display = basename + display_extension + '.png' + self.generated = f'{basename}.{extension}' + self.expected = f'{basename}-expected.{extension}' + self.expected_display = f'{basename}-expected{display_extension}.png' + self.generated_display = f'{basename}{display_extension}.png' self.name = self.reldir / self.basename self.destdir = self.get_dest_dir(self.reldir) @@ -277,7 +281,7 @@ def get_dest_dir(self, reldir): path = self.source / baseline_dir / reldir if path.is_dir(): return path - raise ValueError("Can't find baseline dir for {}".format(reldir)) + raise ValueError(f"Can't find baseline dir for {reldir}") @property def display(self): @@ -292,7 +296,7 @@ def display(self): 'autogen': '\N{WHITE SQUARE CONTAINING BLACK SMALL SQUARE}', } box = status_map[self.status] - return '{} {} [{}]'.format(box, self.name, self.extension) + return f'{box} {self.name} [{self.extension}]' def accept(self): """ @@ -305,7 +309,9 @@ def reject(self): """ Reject this test by copying the expected result to the source tree. """ - copy_file(self.dir / self.expected, self.destdir / self.generated) + expected = self.dir / self.expected + if not expected.is_symlink(): + copy_file(expected, self.destdir / self.generated) self.status = 'reject' @@ -339,7 +345,7 @@ def launch(result_images, source): dialog.show() filter = EventFilter(dialog) app.installEventFilter(filter) - sys.exit(app.exec_()) + sys.exit(_exec(app)) if __name__ == '__main__': diff --git a/tools/visualize_tests.py b/tools/visualize_tests.py index 7786a232f09f..239c1b53de69 100644 --- a/tools/visualize_tests.py +++ b/tools/visualize_tests.py @@ -126,7 +126,7 @@ def run(show_browser=True): show_message = True if show_message: - print("Open {} in a browser for a visual comparison.".format(index)) + print(f"Open {index} in a browser for a visual comparison.") if __name__ == '__main__': diff --git a/tox.ini b/tox.ini index b6b94c41b5fc..fc8479269160 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py37, py38 +envlist = py38, py39, py310 [testenv] changedir = /tmp diff --git a/tutorials/README.txt b/tutorials/README.txt index 74d339265bdb..da744b3224c7 100644 --- a/tutorials/README.txt +++ b/tutorials/README.txt @@ -7,6 +7,6 @@ This page contains more in-depth guides for using Matplotlib. It is broken up into beginner, intermediate, and advanced sections, as well as sections covering specific topics. -For shorter examples, see our `examples page <../gallery/index.html>`_. -You can also find `external resources <../resources/index.html>`_ and -a `FAQ <../faq/index.html>`_ in our `user guide <../contents.html>`_. +For shorter examples, see our :ref:`examples page `. +You can also find :ref:`external resources ` and +a :ref:`FAQ ` in our :ref:`user guide `. diff --git a/tutorials/advanced/blitting.py b/tutorials/advanced/blitting.py index 868b48a497bd..54aeddecc6ef 100644 --- a/tutorials/advanced/blitting.py +++ b/tutorials/advanced/blitting.py @@ -7,7 +7,7 @@ `__ in raster graphics that, in the context of Matplotlib, can be used to (drastically) improve performance of interactive figures. For example, the -:mod:`~.animation` and :mod:`~.widgets` modules use blitting +:mod:`.animation` and :mod:`.widgets` modules use blitting internally. Here, we demonstrate how to implement your own blitting, outside of these classes. @@ -41,7 +41,7 @@ .. warning:: This code does not work with the OSX backend (but does work with other - GUI backends on mac). + GUI backends on Mac). Minimal example --------------- @@ -106,7 +106,7 @@ # pixels changes (due to either the size or dpi of the figure # changing) , the background will be invalid and result in incorrect # (but sometimes cool looking!) images. There is also a global -# variable and a fair amount of boiler plate which suggests we should +# variable and a fair amount of boilerplate which suggests we should # wrap this in a class. # # Class-based example @@ -125,7 +125,7 @@ def __init__(self, canvas, animated_artists=()): Parameters ---------- canvas : FigureCanvasAgg - The canvas to work with, this only works for sub-classes of the Agg + The canvas to work with, this only works for subclasses of the Agg canvas which have the `~FigureCanvasAgg.copy_from_bbox` and `~FigureCanvasAgg.restore_region` methods. diff --git a/tutorials/advanced/path_tutorial.py b/tutorials/advanced/path_tutorial.py index 19632ce42964..caf5f04b5a1e 100644 --- a/tutorials/advanced/path_tutorial.py +++ b/tutorials/advanced/path_tutorial.py @@ -9,7 +9,7 @@ the :class:`~matplotlib.path.Path`, which supports the standard set of moveto, lineto, curveto commands to draw simple and compound outlines consisting of line segments and splines. The ``Path`` is instantiated -with a (N, 2) array of (x, y) vertices, and a N-length array of path +with a (N, 2) array of (x, y) vertices, and an N-length array of path codes. For example to draw the unit rectangle from (0, 0) to (1, 1), we could use this code: """ @@ -76,11 +76,11 @@ # ============== # # Some of the path components require multiple vertices to specify them: -# for example CURVE 3 is a `bézier +# for example CURVE 3 is a `Bézier # `_ curve with one # control point and one end point, and CURVE4 has three vertices for the # two control points and the end point. The example below shows a -# CURVE4 Bézier spline -- the bézier curve will be contained in the +# CURVE4 Bézier spline -- the Bézier curve will be contained in the # convex hull of the start point, the two control points, and the end # point @@ -139,8 +139,8 @@ # for each histogram bar: the rectangle width is the bin width and the # rectangle height is the number of datapoints in that bin. First we'll # create some random normally distributed data and compute the -# histogram. Because numpy returns the bin edges and not centers, the -# length of ``bins`` is 1 greater than the length of ``n`` in the +# histogram. Because NumPy returns the bin edges and not centers, the +# length of ``bins`` is one greater than the length of ``n`` in the # example below:: # # # histogram our data with numpy @@ -148,7 +148,7 @@ # n, bins = np.histogram(data, 100) # # We'll now extract the corners of the rectangles. Each of the -# ``left``, ``bottom``, etc, arrays below is ``len(n)``, where ``n`` is +# ``left``, ``bottom``, etc., arrays below is ``len(n)``, where ``n`` is # the array of counts for each histogram bar:: # # # get the corners of the rectangles for the histogram @@ -159,10 +159,10 @@ # # Now we have to construct our compound path, which will consist of a # series of ``MOVETO``, ``LINETO`` and ``CLOSEPOLY`` for each rectangle. -# For each rectangle, we need 5 vertices: 1 for the ``MOVETO``, 3 for -# the ``LINETO``, and 1 for the ``CLOSEPOLY``. As indicated in the -# table above, the vertex for the closepoly is ignored but we still need -# it to keep the codes aligned with the vertices:: +# For each rectangle, we need five vertices: one for the ``MOVETO``, +# three for the ``LINETO``, and one for the ``CLOSEPOLY``. As indicated +# in the table above, the vertex for the closepoly is ignored, but we still +# need it to keep the codes aligned with the vertices:: # # nverts = nrects*(1+3+1) # verts = np.zeros((nverts, 2)) diff --git a/tutorials/advanced/transforms_tutorial.py b/tutorials/advanced/transforms_tutorial.py index 3fe230f3fd6a..26b936ef5b95 100644 --- a/tutorials/advanced/transforms_tutorial.py +++ b/tutorials/advanced/transforms_tutorial.py @@ -3,80 +3,100 @@ Transformations Tutorial ======================== -Like any graphics packages, Matplotlib is built on top of a -transformation framework to easily move between coordinate systems, -the userland *data* coordinate system, the *axes* coordinate system, -the *figure* coordinate system, and the *display* coordinate system. -In 95% of your plotting, you won't need to think about this, as it -happens under the hood, but as you push the limits of custom figure -generation, it helps to have an understanding of these objects so you -can reuse the existing transformations Matplotlib makes available to -you, or create your own (see :mod:`matplotlib.transforms`). The table -below summarizes the some useful coordinate systems, the transformation -object you should use to work in that coordinate system, and the -description of that system. In the ``Transformation Object`` column, -``ax`` is a :class:`~matplotlib.axes.Axes` instance, and ``fig`` is a -:class:`~matplotlib.figure.Figure` instance. - -+----------------+-----------------------------+-----------------------------------+ -|Coordinates |Transformation object |Description | -+================+=============================+===================================+ -|"data" |``ax.transData`` |The coordinate system for the data,| -| | |controlled by xlim and ylim. | -+----------------+-----------------------------+-----------------------------------+ -|"axes" |``ax.transAxes`` |The coordinate system of the | -| | |`~matplotlib.axes.Axes`; (0, 0) | -| | |is bottom left of the axes, and | -| | |(1, 1) is top right of the axes. | -+----------------+-----------------------------+-----------------------------------+ -|"subfigure" |``subfigure.transSubfigure`` |The coordinate system of the | -| | |`.SubFigure`; (0, 0) is bottom left| -| | |of the subfigure, and (1, 1) is top| -| | |right of the subfigure. If a | -| | |figure has no subfigures, this is | -| | |the same as ``transFigure``. | -+----------------+-----------------------------+-----------------------------------+ -|"figure" |``fig.transFigure`` |The coordinate system of the | -| | |`.Figure`; (0, 0) is bottom left | -| | |of the figure, and (1, 1) is top | -| | |right of the figure. | -+----------------+-----------------------------+-----------------------------------+ -|"figure-inches" |``fig.dpi_scale_trans`` |The coordinate system of the | -| | |`.Figure` in inches; (0, 0) is | -| | |bottom left of the figure, and | -| | |(width, height) is the top right | -| | |of the figure in inches. | -+----------------+-----------------------------+-----------------------------------+ -|"display" |``None``, or |The pixel coordinate system of the | -| |``IdentityTransform()`` |display window; (0, 0) is bottom | -| | |left of the window, and (width, | -| | |height) is top right of the | -| | |display window in pixels. | -+----------------+-----------------------------+-----------------------------------+ -|"xaxis", |``ax.get_xaxis_transform()``,|Blended coordinate systems; use | -|"yaxis" |``ax.get_yaxis_transform()`` |data coordinates on one of the axis| -| | |and axes coordinates on the other. | -+----------------+-----------------------------+-----------------------------------+ - -All of the transformation objects in the table above take inputs in -their coordinate system, and transform the input to the *display* -coordinate system. That is why the *display* coordinate system has -``None`` for the ``Transformation Object`` column -- it already is in -*display* coordinates. The transformations also know how to invert -themselves, to go from *display* back to the native coordinate system. -This is particularly useful when processing events from the user -interface, which typically occur in display space, and you want to -know where the mouse click or key-press occurred in your *data* -coordinate system. - -Note that specifying objects in *display* coordinates will change their -location if the ``dpi`` of the figure changes. This can cause confusion when -printing or changing screen resolution, because the object can change location -and size. Therefore it is most common -for artists placed in an axes or figure to have their transform set to -something *other* than the `~.transforms.IdentityTransform()`; the default when -an artist is placed on an axes using `~.axes.Axes.add_artist` is for the -transform to be ``ax.transData``. +Like any graphics packages, Matplotlib is built on top of a transformation +framework to easily move between coordinate systems, the userland *data* +coordinate system, the *axes* coordinate system, the *figure* coordinate +system, and the *display* coordinate system. In 95% of your plotting, you +won't need to think about this, as it happens under the hood, but as you push +the limits of custom figure generation, it helps to have an understanding of +these objects, so you can reuse the existing transformations Matplotlib makes +available to you, or create your own (see :mod:`matplotlib.transforms`). The +table below summarizes some useful coordinate systems, a description of each +system, and the transformation object for going from each coordinate system to +the *display* coordinates. In the "Transformation Object" column, ``ax`` is a +:class:`~matplotlib.axes.Axes` instance, ``fig`` is a +:class:`~matplotlib.figure.Figure` instance, and ``subfigure`` is a +:class:`~matplotlib.figure.SubFigure` instance. + + ++----------------+-----------------------------------+---------------------------------------------------+ +|Coordinate |Description |Transformation object | +|system | |from system to display | ++================+===================================+===================================================+ +|"data" |The coordinate system of the data |``ax.transData`` | +| |in the Axes. | | ++----------------+-----------------------------------+---------------------------------------------------+ +|"axes" |The coordinate system of the |``ax.transAxes`` | +| |`~matplotlib.axes.Axes`; (0, 0) | | +| |is bottom left of the axes, and | | +| |(1, 1) is top right of the axes. | | ++----------------+-----------------------------------+---------------------------------------------------+ +|"subfigure" |The coordinate system of the |``subfigure.transSubfigure`` | +| |`.SubFigure`; (0, 0) is bottom left| | +| |of the subfigure, and (1, 1) is top| | +| |right of the subfigure. If a | | +| |figure has no subfigures, this is | | +| |the same as ``transFigure``. | | ++----------------+-----------------------------------+---------------------------------------------------+ +|"figure" |The coordinate system of the |``fig.transFigure`` | +| |`.Figure`; (0, 0) is bottom left | | +| |of the figure, and (1, 1) is top | | +| |right of the figure. | | ++----------------+-----------------------------------+---------------------------------------------------+ +|"figure-inches" |The coordinate system of the |``fig.dpi_scale_trans`` | +| |`.Figure` in inches; (0, 0) is | | +| |bottom left of the figure, and | | +| |(width, height) is the top right | | +| |of the figure in inches. | | ++----------------+-----------------------------------+---------------------------------------------------+ +|"xaxis", |Blended coordinate systems, using |``ax.get_xaxis_transform()``, | +|"yaxis" |data coordinates on one direction |``ax.get_yaxis_transform()`` | +| |and axes coordinates on the other. | | ++----------------+-----------------------------------+---------------------------------------------------+ +|"display" |The native coordinate system of the|`None`, or | +| |output ; (0, 0) is the bottom left |:class:`~matplotlib.transforms.IdentityTransform()`| +| |of the window, and (width, height) | | +| |is top right of the output in | | +| |"display units". | | +| | | | +| |The exact interpretation of the | | +| |units depends on the back end. For | | +| |example it is pixels for Agg and | | +| |points for svg/pdf. | | ++----------------+-----------------------------------+---------------------------------------------------+ + + + + + +The `~matplotlib.transforms.Transform` objects are naive to the source and +destination coordinate systems, however the objects referred to in the table +above are constructed to take inputs in their coordinate system, and transform +the input to the *display* coordinate system. That is why the *display* +coordinate system has `None` for the "Transformation Object" column -- it +already is in *display* coordinates. The naming and destination conventions +are an aid to keeping track of the available "standard" coordinate systems and +transforms. + +The transformations also know how to invert themselves (via +`.Transform.inverted`) to generate a transform from output coordinate system +back to the input coordinate system. For example, ``ax.transData`` converts +values in data coordinates to display coordinates and +``ax.transData.inversed()`` is a :class:`matplotlib.transforms.Transform` that +goes from display coordinates to data coordinates. This is particularly useful +when processing events from the user interface, which typically occur in +display space, and you want to know where the mouse click or key-press occurred +in your *data* coordinate system. + +Note that specifying the position of Artists in *display* coordinates may +change their relative location if the ``dpi`` or size of the figure changes. +This can cause confusion when printing or changing screen resolution, because +the object can change location and size. Therefore, it is most common for +artists placed in an Axes or figure to have their transform set to something +*other* than the `~.transforms.IdentityTransform()`; the default when an artist +is added to an Axes using `~.axes.Axes.add_artist` is for the transform to be +``ax.transData`` so that you can work and think in *data* coordinates and let +Matplotlib take care of the transformation to *display*. .. _data-coords: @@ -89,6 +109,7 @@ :meth:`~matplotlib.axes.Axes.set_ylim` methods. For example, in the figure below, the data limits stretch from 0 to 10 on the x-axis, and -1 to 1 on the y-axis. + """ import numpy as np @@ -270,7 +291,7 @@ # coordinates is extremely useful, for example to create a horizontal # span which highlights some region of the y-data but spans across the # x-axis regardless of the data limits, pan or zoom level, etc. In fact -# these blended lines and spans are so useful, we have built in +# these blended lines and spans are so useful, we have built-in # functions to make them easy to plot (see # :meth:`~matplotlib.axes.Axes.axhline`, # :meth:`~matplotlib.axes.Axes.axvline`, @@ -409,7 +430,7 @@ # Another use of :class:`~matplotlib.transforms.ScaledTranslation` is to create # a new transformation that is # offset from another transformation, e.g., to place one object shifted a -# bit relative to another object. Typically you want the shift to be in +# bit relative to another object. Typically, you want the shift to be in # some physical dimension, like points or inches rather than in *data* # coordinates, so that the shift effect is constant at different zoom # levels and dpi settings. @@ -458,7 +479,7 @@ # a new transform with an added offset. So above we could have done:: # # shadow_transform = transforms.offset_copy(ax.transData, -# fig=fig, dx, dy, units='inches') +# fig, dx, dy, units='inches') # # # .. _transformation-pipeline: @@ -546,8 +567,9 @@ # the typical separable matplotlib Axes, with one additional piece # ``transProjection``:: # -# self.transData = self.transScale + self.transProjection + \ -# (self.transProjectionAffine + self.transAxes) +# self.transData = ( +# self.transScale + self.transShift + self.transProjection + +# (self.transProjectionAffine + self.transWedge + self.transAxes)) # # ``transProjection`` handles the projection from the space, # e.g., latitude and longitude for map data, or radius and theta for polar diff --git a/tutorials/colors/colorbar_only.py b/tutorials/colors/colorbar_only.py index daa44b752085..ce48ec50c26b 100644 --- a/tutorials/colors/colorbar_only.py +++ b/tutorials/colors/colorbar_only.py @@ -77,8 +77,7 @@ # `~.Figure.colorbar`. For the out-of-range values to display on the colorbar # without using the *extend* keyword with # `.colors.BoundaryNorm`, we have to use the *extend* keyword argument directly -# in the colorbar call, and supply an additional boundary on each end of the -# range. Here we also +# in the colorbar call. Here we also # use the spacing argument to make # the length of each colorbar segment proportional to its corresponding # interval. @@ -94,7 +93,6 @@ fig.colorbar( mpl.cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax, - boundaries=[0] + bounds + [13], # Adding values for extensions. extend='both', ticks=bounds, spacing='proportional', @@ -121,7 +119,6 @@ fig.colorbar( mpl.cm.ScalarMappable(cmap=cmap, norm=norm), cax=ax, - boundaries=[-10] + bounds + [10], extend='both', extendfrac='auto', ticks=bounds, diff --git a/tutorials/colors/colormap-manipulation.py b/tutorials/colors/colormap-manipulation.py index 7273ca25b08b..8b2cbc784bc0 100644 --- a/tutorials/colors/colormap-manipulation.py +++ b/tutorials/colors/colormap-manipulation.py @@ -4,7 +4,7 @@ ******************************** Matplotlib has a number of built-in colormaps accessible via -`.matplotlib.cm.get_cmap`. There are also external libraries like +`.matplotlib.colormaps`. There are also external libraries like palettable_ that have many extra colormaps. .. _palettable: https://jiffyclub.github.io/palettable/ @@ -24,19 +24,19 @@ ============================================ First, getting a named colormap, most of which are listed in -:doc:`/tutorials/colors/colormaps`, may be done using -`.matplotlib.cm.get_cmap`, which returns a colormap object. -The second argument gives the size of the list of colors used to define the -colormap, and below we use a modest value of 8 so there are not a lot of -values to look at. +:doc:`/tutorials/colors/colormaps`, may be done using `.matplotlib.colormaps`, +which returns a colormap object. The length of the list of colors used +internally to define the colormap can be adjusted via `.Colormap.resampled`. +Below we use a modest value of 8 so there are not a lot of values to look at. + """ import numpy as np import matplotlib.pyplot as plt -from matplotlib import cm +import matplotlib as mpl from matplotlib.colors import ListedColormap, LinearSegmentedColormap -viridis = cm.get_cmap('viridis', 8) +viridis = mpl.colormaps['viridis'].resampled(8) ############################################################################## # The object ``viridis`` is a callable, that when passed a float between @@ -48,7 +48,7 @@ # ListedColormap # -------------- # -# `.ListedColormap` s store their color values in a ``.colors`` attribute. +# `.ListedColormap`\s store their color values in a ``.colors`` attribute. # The list of colors that comprise the colormap can be directly accessed using # the ``colors`` property, # or it can be accessed indirectly by calling ``viridis`` with an array @@ -68,11 +68,11 @@ ############################################################################## # LinearSegmentedColormap # ----------------------- -# `.LinearSegmentedColormap` s do not have a ``.colors`` attribute. +# `.LinearSegmentedColormap`\s do not have a ``.colors`` attribute. # However, one may still call the colormap with an integer array, or with a # float array between 0 and 1. -copper = cm.get_cmap('copper', 8) +copper = mpl.colormaps['copper'].resampled(8) print('copper(range(8))', copper(range(8))) print('copper(np.linspace(0, 1, 8))', copper(np.linspace(0, 1, 8))) @@ -114,7 +114,7 @@ def plot_examples(colormaps): ############################################################################## # In fact, that list may contain any valid -# :doc:`matplotlib color specification `. +# :doc:`Matplotlib color specification `. # Particularly useful for creating custom colormaps are Nx4 numpy arrays. # Because with the variety of numpy operations that we can do on a such an # array, carpentry of new colormaps from existing colormaps become quite @@ -123,7 +123,7 @@ def plot_examples(colormaps): # For example, suppose we want to make the first 25 entries of a 256-length # "viridis" colormap pink for some reason: -viridis = cm.get_cmap('viridis', 256) +viridis = mpl.colormaps['viridis'].resampled(256) newcolors = viridis(np.linspace(0, 1, 256)) pink = np.array([248/256, 24/256, 148/256, 1]) newcolors[:25, :] = pink @@ -132,19 +132,21 @@ def plot_examples(colormaps): plot_examples([viridis, newcmp]) ############################################################################## -# We can easily reduce the dynamic range of a colormap; here we choose the -# middle 0.5 of the colormap. However, we need to interpolate from a larger -# colormap, otherwise the new colormap will have repeated values. - -viridis_big = cm.get_cmap('viridis', 512) -newcmp = ListedColormap(viridis_big(np.linspace(0.25, 0.75, 256))) +# We can reduce the dynamic range of a colormap; here we choose the +# middle half of the colormap. Note, however, that because viridis is a +# listed colormap, we will end up with 128 discrete values instead of the 256 +# values that were in the original colormap. This method does not interpolate +# in color-space to add new colors. + +viridis_big = mpl.colormaps['viridis'] +newcmp = ListedColormap(viridis_big(np.linspace(0.25, 0.75, 128))) plot_examples([viridis, newcmp]) ############################################################################## # and we can easily concatenate two colormaps: -top = cm.get_cmap('Oranges_r', 128) -bottom = cm.get_cmap('Blues', 128) +top = mpl.colormaps['Oranges_r'].resampled(128) +bottom = mpl.colormaps['Blues'].resampled(128) newcolors = np.vstack((top(np.linspace(0, 1, 128)), bottom(np.linspace(0, 1, 128)))) @@ -168,7 +170,7 @@ def plot_examples(colormaps): # Creating linear segmented colormaps # =================================== # -# `.LinearSegmentedColormap` class specifies colormaps using anchor points +# The `.LinearSegmentedColormap` class specifies colormaps using anchor points # between which RGB(A) values are interpolated. # # The format to specify these colormaps allows discontinuities at the anchor @@ -177,7 +179,7 @@ def plot_examples(colormaps): # ``yleft[i]`` and ``yright[i]`` are the values of the color on either # side of the anchor point. # -# If there are no discontinuities, then ``yleft[i]=yright[i]``: +# If there are no discontinuities, then ``yleft[i] == yright[i]``: cdict = {'red': [[0.0, 0.0, 0.0], [0.5, 1.0, 1.0], @@ -221,9 +223,10 @@ def plot_linearmap(cdict): # # In the example below there is a discontinuity in red at 0.5. The # interpolation between 0 and 0.5 goes from 0.3 to 1, and between 0.5 and 1 -# it goes from 0.9 to 1. Note that red[0, 1], and red[2, 2] are both -# superfluous to the interpolation because red[0, 1] is the value to the -# left of 0, and red[2, 2] is the value to the right of 1.0. +# it goes from 0.9 to 1. Note that ``red[0, 1]``, and ``red[2, 2]`` are both +# superfluous to the interpolation because ``red[0, 1]`` (i.e., ``yleft[0]``) +# is the value to the left of 0, and ``red[2, 2]`` (i.e., ``yright[2]``) is the +# value to the right of 1, which are outside the color mapping domain. cdict['red'] = [[0.0, 0.0, 0.3], [0.5, 1.0, 0.9], @@ -234,7 +237,7 @@ def plot_linearmap(cdict): # Directly creating a segmented colormap from a list # -------------------------------------------------- # -# The above described is a very versatile approach, but admittedly a bit +# The approach described above is very versatile, but admittedly a bit # cumbersome to implement. For some basic cases, the use of # `.LinearSegmentedColormap.from_list` may be easier. This creates a segmented # colormap with equal spacings from a supplied list of colors. @@ -243,8 +246,8 @@ def plot_linearmap(cdict): cmap1 = LinearSegmentedColormap.from_list("mycmap", colors) ############################################################################# -# If desired, the nodes of the colormap can be given as numbers -# between 0 and 1. E.g. one could have the reddish part take more space in the +# If desired, the nodes of the colormap can be given as numbers between 0 and +# 1. For example, one could have the reddish part take more space in the # colormap. nodes = [0.0, 0.4, 0.8, 1.0] @@ -252,6 +255,48 @@ def plot_linearmap(cdict): plot_examples([cmap1, cmap2]) +############################################################################# +# .. _reversing-colormap: +# +# Reversing a colormap +# ==================== +# +# `.Colormap.reversed` creates a new colormap that is a reversed version of +# the original colormap. + +colors = ["#ffffcc", "#a1dab4", "#41b6c4", "#2c7fb8", "#253494"] +my_cmap = ListedColormap(colors, name="my_cmap") + +my_cmap_r = my_cmap.reversed() + +plot_examples([my_cmap, my_cmap_r]) +# %% +# If no name is passed in, ``.reversed`` also names the copy by +# :ref:`appending '_r' ` to the original colormap's +# name. + +############################################################################## +# .. _registering-colormap: +# +# Registering a colormap +# ====================== +# +# Colormaps can be added to the `matplotlib.colormaps` list of named colormaps. +# This allows the colormaps to be accessed by name in plotting functions: + +# my_cmap, my_cmap_r from reversing a colormap +mpl.colormaps.register(cmap=my_cmap) +mpl.colormaps.register(cmap=my_cmap_r) + +data = [[1, 2, 3, 4, 5]] + +fig, (ax1, ax2) = plt.subplots(nrows=2) + +ax1.imshow(data, cmap='my_cmap') +ax2.imshow(data, cmap='my_cmap_r') + +plt.show() + ############################################################################# # # .. admonition:: References @@ -265,4 +310,4 @@ def plot_linearmap(cdict): # - `matplotlib.colors.LinearSegmentedColormap` # - `matplotlib.colors.ListedColormap` # - `matplotlib.cm` -# - `matplotlib.cm.get_cmap` +# - `matplotlib.colormaps` diff --git a/tutorials/colors/colormapnorms.py b/tutorials/colors/colormapnorms.py index 41aa6310949a..e7ca0e61bfc7 100644 --- a/tutorials/colors/colormapnorms.py +++ b/tutorials/colors/colormapnorms.py @@ -20,14 +20,12 @@ Artists that map data to color pass the arguments *vmin* and *vmax* to construct a :func:`matplotlib.colors.Normalize` instance, then call it: -.. ipython:: +.. code-block:: pycon - In [1]: import matplotlib as mpl - - In [2]: norm = mpl.colors.Normalize(vmin=-1, vmax=1) - - In [3]: norm(0) - Out[3]: 0.5 + >>> import matplotlib as mpl + >>> norm = mpl.colors.Normalize(vmin=-1, vmax=1) + >>> norm(0) + 0.5 However, there are sometimes cases where it is useful to map data to colormaps in a non-linear fashion. @@ -53,7 +51,7 @@ X, Y = np.mgrid[-3:3:complex(0, N), -2:2:complex(0, N)] # A low hump with a spike coming out of the top right. Needs to have -# z/colour axis on a log scale so we see both hump and spike. linear +# z/colour axis on a log scale, so we see both hump and spike. A linear # scale only shows the spike. Z1 = np.exp(-X**2 - Y**2) Z2 = np.exp(-(X * 10)**2 - (Y * 10)**2) @@ -192,15 +190,12 @@ # lower out-of-bounds values to the range over which the colors are # distributed. For instance: # -# .. ipython:: -# -# In [2]: import matplotlib.colors as colors -# -# In [3]: bounds = np.array([-0.25, -0.125, 0, 0.5, 1]) +# .. code-block:: pycon # -# In [4]: norm = colors.BoundaryNorm(boundaries=bounds, ncolors=4) -# -# In [5]: print(norm([-0.2, -0.15, -0.02, 0.3, 0.8, 0.99])) +# >>> import matplotlib.colors as colors +# >>> bounds = np.array([-0.25, -0.125, 0, 0.5, 1]) +# >>> norm = colors.BoundaryNorm(boundaries=bounds, ncolors=4) +# >>> print(norm([-0.2, -0.15, -0.02, 0.3, 0.8, 0.99])) # [0 0 1 2 3 3] # # Note: Unlike the other norms, this norm returns values from 0 to *ncolors*-1. @@ -273,11 +268,12 @@ pcm = ax.pcolormesh(longitude, latitude, topo, rasterized=True, norm=divnorm, cmap=terrain_map, shading='auto') -# Simple geographic plot, set aspect ratio beecause distance between lines of +# Simple geographic plot, set aspect ratio because distance between lines of # longitude depends on latitude. ax.set_aspect(1 / np.cos(np.deg2rad(49))) ax.set_title('TwoSlopeNorm(x)') -fig.colorbar(pcm, shrink=0.6) +cb = fig.colorbar(pcm, shrink=0.6) +cb.set_ticks([-500, 0, 1000, 2000, 3000, 4000]) plt.show() @@ -312,7 +308,8 @@ def _inverse(x): # ---------------------------------------------------------- # # The `.TwoSlopeNorm` described above makes a useful example for -# defining your own norm. +# defining your own norm. Note for the colorbar to work, you must +# define an inverse for your norm: class MidpointNormalize(colors.Normalize): @@ -323,8 +320,14 @@ def __init__(self, vmin=None, vmax=None, vcenter=None, clip=False): def __call__(self, value, clip=None): # I'm ignoring masked values and all kinds of edge cases to make a # simple example... - x, y = [self.vmin, self.vcenter, self.vmax], [0, 0.5, 1] - return np.ma.masked_array(np.interp(value, x, y)) + # Note also that we must extrapolate beyond vmin/vmax + x, y = [self.vmin, self.vcenter, self.vmax], [0, 0.5, 1.] + return np.ma.masked_array(np.interp(value, x, y, + left=-np.inf, right=np.inf)) + + def inverse(self, value): + y, x = [self.vmin, self.vcenter, self.vmax], [0, 0.5, 1] + return np.interp(value, x, y, left=-np.inf, right=np.inf) fig, ax = plt.subplots() @@ -334,5 +337,7 @@ def __call__(self, value, clip=None): cmap=terrain_map, shading='auto') ax.set_aspect(1 / np.cos(np.deg2rad(49))) ax.set_title('Custom norm') -fig.colorbar(pcm, shrink=0.6, extend='both') +cb = fig.colorbar(pcm, shrink=0.6, extend='both') +cb.set_ticks([-500, 0, 1000, 2000, 3000, 4000]) + plt.show() diff --git a/tutorials/colors/colormaps.py b/tutorials/colors/colormaps.py index f8764a6a7367..68862d834f68 100644 --- a/tutorials/colors/colormaps.py +++ b/tutorials/colors/colormaps.py @@ -4,8 +4,9 @@ ******************************** Matplotlib has a number of built-in colormaps accessible via -`.matplotlib.cm.get_cmap`. There are also external libraries like -[palettable]_ and [colorcet]_ that have many extra colormaps. +`.matplotlib.colormaps`. There are also external libraries that +have many extra colormaps, which can be viewed in the +`Third-party colormaps`_ section of the Matplotlib documentation. Here we briefly discuss how to choose between the many options. For help on creating your own colormaps, see :doc:`/tutorials/colors/colormap-manipulation`. @@ -32,8 +33,9 @@ perceives changes in the lightness parameter as changes in the data much better than, for example, changes in hue. Therefore, colormaps which have monotonically increasing lightness through the colormap -will be better interpreted by the viewer. A wonderful example of -perceptually uniform colormaps is [colorcet]_. +will be better interpreted by the viewer. Wonderful examples of +perceptually uniform colormaps can be found in the +`Third-party colormaps`_ section as well. Color can be represented in 3D space in various ways. One way to represent color is using CIELAB. In CIELAB, color space is represented by lightness, @@ -45,6 +47,8 @@ is from [IBM]_. +.. _color-colormaps_reference: + Classes of colormaps ==================== @@ -76,11 +80,41 @@ import numpy as np import matplotlib as mpl import matplotlib.pyplot as plt -from matplotlib import cm from colorspacious import cspace_converter -from collections import OrderedDict -cmaps = OrderedDict() + +############################################################################### +# +# First, we'll show the range of each colormap. Note that some seem +# to change more "quickly" than others. + +cmaps = {} + +gradient = np.linspace(0, 1, 256) +gradient = np.vstack((gradient, gradient)) + + +def plot_color_gradients(category, cmap_list): + # Create figure and adjust figure height to number of colormaps + nrows = len(cmap_list) + figh = 0.35 + 0.15 + (nrows + (nrows - 1) * 0.1) * 0.22 + fig, axs = plt.subplots(nrows=nrows + 1, figsize=(6.4, figh)) + fig.subplots_adjust(top=1 - 0.35 / figh, bottom=0.15 / figh, + left=0.2, right=0.99) + axs[0].set_title(f'{category} colormaps', fontsize=14) + + for ax, name in zip(axs, cmap_list): + ax.imshow(gradient, aspect='auto', cmap=mpl.colormaps[name]) + ax.text(-0.01, 0.5, name, va='center', ha='right', fontsize=10, + transform=ax.transAxes) + + # Turn off *all* ticks & spines, not just the ones with colormaps. + for ax in axs: + ax.set_axis_off() + + # Save colormap list for later. + cmaps[category] = cmap_list + ############################################################################### # Sequential @@ -94,13 +128,15 @@ # amongst the colormaps: some are approximately linear in :math:`L^*` and others # are more curved. -cmaps['Perceptually Uniform Sequential'] = [ - 'viridis', 'plasma', 'inferno', 'magma', 'cividis'] +plot_color_gradients('Perceptually Uniform Sequential', + ['viridis', 'plasma', 'inferno', 'magma', 'cividis']) + +############################################################################### -cmaps['Sequential'] = [ - 'Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', - 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', - 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn'] +plot_color_gradients('Sequential', + ['Greys', 'Purples', 'Blues', 'Greens', 'Oranges', 'Reds', + 'YlOrBr', 'YlOrRd', 'OrRd', 'PuRd', 'RdPu', 'BuPu', + 'GnBu', 'PuBu', 'YlGnBu', 'PuBuGn', 'BuGn', 'YlGn']) ############################################################################### # Sequential2 @@ -114,10 +150,10 @@ # banding of the data in those values in the colormap (see [mycarta-banding]_ for # an excellent example of this). -cmaps['Sequential (2)'] = [ - 'binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', 'pink', - 'spring', 'summer', 'autumn', 'winter', 'cool', 'Wistia', - 'hot', 'afmhot', 'gist_heat', 'copper'] +plot_color_gradients('Sequential (2)', + ['binary', 'gist_yarg', 'gist_gray', 'gray', 'bone', + 'pink', 'spring', 'summer', 'autumn', 'winter', 'cool', + 'Wistia', 'hot', 'afmhot', 'gist_heat', 'copper']) ############################################################################### # Diverging @@ -130,9 +166,9 @@ # measures, BrBG and RdBu are good options. coolwarm is a good option, but it # doesn't span a wide range of :math:`L^*` values (see grayscale section below). -cmaps['Diverging'] = [ - 'PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', - 'RdYlBu', 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic'] +plot_color_gradients('Diverging', + ['PiYG', 'PRGn', 'BrBG', 'PuOr', 'RdGy', 'RdBu', 'RdYlBu', + 'RdYlGn', 'Spectral', 'coolwarm', 'bwr', 'seismic']) ############################################################################### # Cyclic @@ -152,7 +188,7 @@ # for viewers to see perceptually. See an extension on this idea at # [mycarta-jet]_. -cmaps['Cyclic'] = ['twilight', 'twilight_shifted', 'hsv'] +plot_color_gradients('Cyclic', ['twilight', 'twilight_shifted', 'hsv']) ############################################################################### # Qualitative @@ -163,9 +199,10 @@ # the place throughout the colormap, and are clearly not monotonically increasing. # These would not be good options for use as perceptual colormaps. -cmaps['Qualitative'] = ['Pastel1', 'Pastel2', 'Paired', 'Accent', - 'Dark2', 'Set1', 'Set2', 'Set3', - 'tab10', 'tab20', 'tab20b', 'tab20c'] +plot_color_gradients('Qualitative', + ['Pastel1', 'Pastel2', 'Paired', 'Accent', 'Dark2', + 'Set1', 'Set2', 'Set3', 'tab10', 'tab20', 'tab20b', + 'tab20c']) ############################################################################### # Miscellaneous @@ -187,43 +224,12 @@ # poor choice for representing data for viewers to see perceptually. See an # extension on this idea at [mycarta-jet]_ and [turbo]_. -cmaps['Miscellaneous'] = [ - 'flag', 'prism', 'ocean', 'gist_earth', 'terrain', 'gist_stern', - 'gnuplot', 'gnuplot2', 'CMRmap', 'cubehelix', 'brg', - 'gist_rainbow', 'rainbow', 'jet', 'turbo', 'nipy_spectral', - 'gist_ncar'] - -############################################################################### -# .. _color-colormaps_reference: -# -# First, we'll show the range of each colormap. Note that some seem -# to change more "quickly" than others. -gradient = np.linspace(0, 1, 256) -gradient = np.vstack((gradient, gradient)) - - -def plot_color_gradients(cmap_category, cmap_list): - # Create figure and adjust figure height to number of colormaps - nrows = len(cmap_list) - figh = 0.35 + 0.15 + (nrows + (nrows - 1) * 0.1) * 0.22 - fig, axs = plt.subplots(nrows=nrows + 1, figsize=(6.4, figh)) - fig.subplots_adjust(top=1 - 0.35 / figh, bottom=0.15 / figh, - left=0.2, right=0.99) - axs[0].set_title(cmap_category + ' colormaps', fontsize=14) - - for ax, name in zip(axs, cmap_list): - ax.imshow(gradient, aspect='auto', cmap=plt.get_cmap(name)) - ax.text(-0.01, 0.5, name, va='center', ha='right', fontsize=10, - transform=ax.transAxes) - - # Turn off *all* ticks & spines, not just the ones with colormaps. - for ax in axs: - ax.set_axis_off() - - -for cmap_category, cmap_list in cmaps.items(): - plot_color_gradients(cmap_category, cmap_list) +plot_color_gradients('Miscellaneous', + ['flag', 'prism', 'ocean', 'gist_earth', 'terrain', + 'gist_stern', 'gnuplot', 'gnuplot2', 'CMRmap', + 'cubehelix', 'brg', 'gist_rainbow', 'rainbow', 'jet', + 'turbo', 'nipy_spectral', 'gist_ncar']) plt.show() @@ -270,16 +276,16 @@ def plot_color_gradients(cmap_category, cmap_list): # Get RGB values for colormap and convert the colormap in # CAM02-UCS colorspace. lab[0, :, 0] is the lightness. - rgb = cm.get_cmap(cmap)(x)[np.newaxis, :, :3] + rgb = mpl.colormaps[cmap](x)[np.newaxis, :, :3] lab = cspace_converter("sRGB1", "CAM02-UCS")(rgb) # Plot colormap L values. Do separately for each category # so each plot can be pretty. To make scatter markers change # color along plot: - # http://stackoverflow.com/questions/8202605/ + # https://stackoverflow.com/q/8202605/ if cmap_category == 'Sequential': - # These colormaps all start at high lightness but we want them + # These colormaps all start at high lightness, but we want them # reversed to look nice in the plot, so reverse the order. y_ = lab[0, ::-1, 0] c_ = x[::-1] @@ -373,14 +379,14 @@ def plot_color_gradients(cmap_category, cmap_list): for ax, name in zip(axs, cmap_list): # Get RGB values for colormap. - rgb = cm.get_cmap(plt.get_cmap(name))(x)[np.newaxis, :, :3] + rgb = mpl.colormaps[name](x)[np.newaxis, :, :3] # Get colormap in CAM02-UCS colorspace. We want the lightness. lab = cspace_converter("sRGB1", "CAM02-UCS")(rgb) L = lab[0, :, 0] L = np.float32(np.vstack((L, L, L))) - ax[0].imshow(gradient, aspect='auto', cmap=plt.get_cmap(name)) + ax[0].imshow(gradient, aspect='auto', cmap=mpl.colormaps[name]) ax[1].imshow(L, aspect='auto', cmap='binary_r', vmin=0., vmax=100.) pos = list(ax[0].get_position().bounds) x_text = pos[0] - 0.01 @@ -414,15 +420,14 @@ def plot_color_gradients(cmap_category, cmap_list): # References # ========== # -# .. [colorcet] https://colorcet.pyviz.org +# .. _Third-party colormaps: https://matplotlib.org/mpl-third-party/#colormaps-and-styles # .. [Ware] http://ccom.unh.edu/sites/default/files/publications/Ware_1988_CGA_Color_sequences_univariate_maps.pdf # .. [Moreland] http://www.kennethmoreland.com/color-maps/ColorMapsExpanded.pdf # .. [list-colormaps] https://gist.github.com/endolith/2719900#id7 # .. [mycarta-banding] https://mycarta.wordpress.com/2012/10/14/the-rainbow-is-deadlong-live-the-rainbow-part-4-cie-lab-heated-body/ # .. [mycarta-jet] https://mycarta.wordpress.com/2012/10/06/the-rainbow-is-deadlong-live-the-rainbow-part-3/ # .. [kovesi-colormaps] https://arxiv.org/abs/1509.03700 -# .. [bw] http://www.tannerhelland.com/3643/grayscale-image-algorithm-vb6/ +# .. [bw] https://tannerhelland.com/3643/grayscale-image-algorithm-vb6/ # .. [colorblindness] http://www.color-blindness.com/ # .. [IBM] https://doi.org/10.1109/VISUAL.1995.480803 -# .. [palettable] https://jiffyclub.github.io/palettable/ # .. [turbo] https://ai.googleblog.com/2019/08/turbo-improved-rainbow-colormap-for.html diff --git a/tutorials/colors/colors.py b/tutorials/colors/colors.py index dac7b800e47a..224886c20483 100644 --- a/tutorials/colors/colors.py +++ b/tutorials/colors/colors.py @@ -1,10 +1,12 @@ """ ***************** -Specifying Colors +Specifying colors ***************** -Matplotlib recognizes the following formats in the table below to specify a -color. +Color formats +============= + +Matplotlib recognizes the following formats to specify a color. +--------------------------------------+--------------------------------------+ | Format | Example | @@ -20,18 +22,20 @@ | equivalent hex shorthand of | - ``'#fb1'`` as ``'#ffbb11'`` | | duplicated characters. | | +--------------------------------------+--------------------------------------+ -| String representation of float value | - ``'0.8'`` as light gray | -| in closed interval ``[0, 1]`` for | - ``'0'`` as black | -| black and white, respectively. | - ``'1'`` as white | +| String representation of float value | - ``'0'`` as black | +| in closed interval ``[0, 1]`` for | - ``'1'`` as white | +| grayscale values. | - ``'0.8'`` as light gray | +--------------------------------------+--------------------------------------+ | Single character shorthand notation | - ``'b'`` as blue | -| for shades of colors. | - ``'g'`` as green | +| for some basic colors. | - ``'g'`` as green | | | - ``'r'`` as red | -| .. note:: The colors green, cyan, | - ``'c'`` as cyan | -| magenta, and yellow do not | - ``'m'`` as magenta | -| coincide with X11/CSS4 | - ``'y'`` as yellow | -| colors. | - ``'k'`` as black | -| | - ``'w'`` as white | +| .. note:: | - ``'c'`` as cyan | +| The colors green, cyan, magenta, | - ``'m'`` as magenta | +| and yellow do not coincide with | - ``'y'`` as yellow | +| X11/CSS4 colors. Their particular | - ``'k'`` as black | +| shades were chosen for better | - ``'w'`` as white | +| visibility of colored lines | | +| against typical backgrounds. | | +--------------------------------------+--------------------------------------+ | Case-insensitive X11/CSS4 color name | - ``'aquamarine'`` | | with no spaces. | - ``'mediumseagreen'`` | @@ -73,45 +77,57 @@ "Red", "Green", and "Blue" are the intensities of those colors. In combination, they represent the colorspace. -Matplotlib draws Artists based on the ``zorder`` parameter. If there are no -specified values, Matplotlib defaults to the order of the Artists added to the -Axes. - -The alpha for an Artist controls opacity. It indicates how the RGB color of the -new Artist combines with RGB colors already on the Axes. +Transparency +============ -The two Artists combine with alpha compositing. Matplotlib uses the equation -below to compute the result of blending a new Artist. +The *alpha* value of a color specifies its transparency, where 0 is fully +transparent and 1 is fully opaque. When a color is semi-transparent, the +background color will show through. -:: +The *alpha* value determines the resulting color by blending the +foreground color with the background color according to the formula - RGB_{new} = RGB_{below} * (1 - \\alpha) + RGB_{artist} * \\alpha +.. math:: -Alpha of 1 indicates the new Artist completely covers the previous color. -Alpha of 0 for top color is not visible; however, it contributes to blending -for intermediate values as the cumulative result of all previous Artists. The -following table contains examples. + RGB_{result} = RGB_{background} * (1 - \\alpha) + RGB_{foreground} * \\alpha -+---------------+-------------------------------------------------------------+ -| Alpha value | Visual | -+===============+=============================================================+ -| ``0.3`` | .. image:: ../../_static/color_zorder_A.png | -+---------------+-------------------------------------------------------------+ -| ``1`` | .. image:: ../../_static/color_zorder_B.png | -+---------------+-------------------------------------------------------------+ - -.. note:: +The following plot illustrates the effect of transparency. +""" - Re-ordering Artists is not commutative in Matplotlib. +import matplotlib.pyplot as plt +from matplotlib.patches import Rectangle +import numpy as np +fig, ax = plt.subplots(figsize=(6.5, 1.65), layout='constrained') +ax.add_patch(Rectangle((-0.2, -0.35), 11.2, 0.7, color='C1', alpha=0.8)) +for i, alpha in enumerate(np.linspace(0, 1, 11)): + ax.add_patch(Rectangle((i, 0.05), 0.8, 0.6, alpha=alpha, zorder=0)) + ax.text(i+0.4, 0.85, f"{alpha:.1f}", ha='center') + ax.add_patch(Rectangle((i, -0.05), 0.8, -0.6, alpha=alpha, zorder=2)) +ax.set_xlim(-0.2, 13) +ax.set_ylim(-1, 1) +ax.set_title('alpha values') +ax.text(11.3, 0.6, 'zorder=1', va='center', color='C0') +ax.text(11.3, 0, 'zorder=2\nalpha=0.8', va='center', color='C1') +ax.text(11.3, -0.6, 'zorder=3', va='center', color='C0') +ax.axis('off') -"CN" color selection --------------------- -Matplotlib converts "CN" colors to RGBA when drawing Artists. The -:doc:`/tutorials/intermediate/color_cycle` section contains additional -information about controlling colors and style properties. -""" +############################################################################### +# +# The orange rectangle is semi-transparent with *alpha* = 0.8. The top row of +# blue squares is drawn below and the bottom row of blue squares is drawn on +# top of the orange rectangle. +# +# See also :doc:`/gallery/misc/zorder_demo` to learn more on the drawing order. +# +# +# "CN" color selection +# ==================== +# +# Matplotlib converts "CN" colors to RGBA when drawing Artists. The +# :doc:`/tutorials/intermediate/color_cycle` section contains additional +# information about controlling colors and style properties. import numpy as np @@ -133,7 +149,7 @@ def demo(sty): demo('default') -demo('seaborn') +demo('seaborn-v0_8') ############################################################################### # The first color ``'C0'`` is the title. Each plot uses the second and third @@ -143,7 +159,7 @@ def demo(sty): # .. _xkcd-colors: # # Comparison between X11/CSS4 and xkcd colors -# ------------------------------------------- +# =========================================== # # The xkcd colors come from a `user survey conducted by the webcomic xkcd # `__. @@ -159,11 +175,11 @@ def demo(sty): # The visual below shows name collisions. Color names where color values agree # are in bold. -import matplotlib._color_data as mcd +import matplotlib.colors as mcolors import matplotlib.patches as mpatch -overlap = {name for name in mcd.CSS4_COLORS - if "xkcd:" + name in mcd.XKCD_COLORS} +overlap = {name for name in mcolors.CSS4_COLORS + if f'xkcd:{name}' in mcolors.XKCD_COLORS} fig = plt.figure(figsize=[9, 5]) ax = fig.add_axes([0, 0, 1, 1]) @@ -172,23 +188,30 @@ def demo(sty): n_rows = len(overlap) // n_groups + 1 for j, color_name in enumerate(sorted(overlap)): - css4 = mcd.CSS4_COLORS[color_name] - xkcd = mcd.XKCD_COLORS["xkcd:" + color_name].upper() + css4 = mcolors.CSS4_COLORS[color_name] + xkcd = mcolors.XKCD_COLORS[f'xkcd:{color_name}'].upper() + + # Pick text colour based on perceived luminance. + rgba = mcolors.to_rgba_array([css4, xkcd]) + luma = 0.299 * rgba[:, 0] + 0.587 * rgba[:, 1] + 0.114 * rgba[:, 2] + css4_text_color = 'k' if luma[0] > 0.5 else 'w' + xkcd_text_color = 'k' if luma[1] > 0.5 else 'w' col_shift = (j // n_rows) * 3 y_pos = j % n_rows - text_args = dict(va='center', fontsize=10, - weight='bold' if css4 == xkcd else None) + text_args = dict(fontsize=10, weight='bold' if css4 == xkcd else None) ax.add_patch(mpatch.Rectangle((0 + col_shift, y_pos), 1, 1, color=css4)) ax.add_patch(mpatch.Rectangle((1 + col_shift, y_pos), 1, 1, color=xkcd)) - ax.text(0 + col_shift, y_pos + .5, ' ' + css4, alpha=0.5, **text_args) - ax.text(1 + col_shift, y_pos + .5, ' ' + xkcd, alpha=0.5, **text_args) - ax.text(2 + col_shift, y_pos + .5, ' ' + color_name, **text_args) + ax.text(0.5 + col_shift, y_pos + .7, css4, + color=css4_text_color, ha='center', **text_args) + ax.text(1.5 + col_shift, y_pos + .7, xkcd, + color=xkcd_text_color, ha='center', **text_args) + ax.text(2 + col_shift, y_pos + .7, f' {color_name}', **text_args) for g in range(n_groups): ax.hlines(range(n_rows), 3*g, 3*g + 2.8, color='0.7', linewidth=1) - ax.text(0.5 + 3*g, -0.5, 'X11', ha='center', va='center') - ax.text(1.5 + 3*g, -0.5, 'xkcd', ha='center', va='center') + ax.text(0.5 + 3*g, -0.3, 'X11/CSS4', ha='center') + ax.text(1.5 + 3*g, -0.3, 'xkcd', ha='center') ax.set_xlim(0, 3 * n_groups) ax.set_ylim(n_rows, -1) diff --git a/tutorials/intermediate/arranging_axes.py b/tutorials/intermediate/arranging_axes.py new file mode 100644 index 000000000000..3fbfb7b6e239 --- /dev/null +++ b/tutorials/intermediate/arranging_axes.py @@ -0,0 +1,414 @@ +""" +=================================== +Arranging multiple Axes in a Figure +=================================== + +Often more than one Axes is wanted on a figure at a time, usually +organized into a regular grid. Matplotlib has a variety of tools for +working with grids of Axes that have evolved over the history of the library. +Here we will discuss the tools we think users should use most often, the tools +that underpin how Axes are organized, and mention some of the older tools. + +.. note:: + + Matplotlib uses *Axes* to refer to the drawing area that contains + data, x- and y-axis, ticks, labels, title, etc. See :ref:`figure_parts` + for more details. Another term that is often used is "subplot", which + refers to an Axes that is in a grid with other Axes objects. + +Overview +======== + +Create grid-shaped combinations of Axes +--------------------------------------- + +`~matplotlib.pyplot.subplots` + The primary function used to create figures and a grid of Axes. It + creates and places all Axes on the figure at once, and returns an + object array with handles for the Axes in the grid. See + `.Figure.subplots`. + +or + +`~matplotlib.pyplot.subplot_mosaic` + A simple way to create figures and a grid of Axes, with the added + flexibility that Axes can also span rows or columns. The Axes are returned + in a labelled dictionary instead of an array. See also + `.Figure.subplot_mosaic` and + :doc:`/gallery/subplots_axes_and_figures/mosaic`. + +Sometimes it is natural to have more than one distinct group of Axes grids, +in which case Matplotlib has the concept of `.SubFigure`: + +`~matplotlib.figure.SubFigure` + A virtual figure within a figure. + +Underlying tools +---------------- + +Underlying these are the concept of a `~.gridspec.GridSpec` and +a `~.SubplotSpec`: + +`~matplotlib.gridspec.GridSpec` + Specifies the geometry of the grid that a subplot will be + placed. The number of rows and number of columns of the grid + need to be set. Optionally, the subplot layout parameters + (e.g., left, right, etc.) can be tuned. + +`~matplotlib.gridspec.SubplotSpec` + Specifies the location of the subplot in the given `.GridSpec`. + +Adding single Axes at a time +---------------------------- + +The above functions create all Axes in a single function call. It is also +possible to add Axes one at a time, and this was originally how Matplotlib +used to work. Doing so is generally less elegant and flexible, though +sometimes useful for interactive work or to place an Axes in a custom +location: + +`~matplotlib.figure.Figure.add_axes` + Adds a single axes at a location specified by + ``[left, bottom, width, height]`` in fractions of figure width or height. + +`~matplotlib.pyplot.subplot` or `.Figure.add_subplot` + Adds a single subplot on a figure, with 1-based indexing (inherited from + Matlab). Columns and rows can be spanned by specifying a range of grid + cells. + +`~matplotlib.pyplot.subplot2grid` + Similar to `.pyplot.subplot`, but uses 0-based indexing and two-d python + slicing to choose cells. + +.. redirect-from:: /tutorials/intermediate/gridspec + +""" +############################################################################ +# High-level methods for making grids +# =================================== +# +# Basic 2x2 grid +# -------------- +# +# We can create a basic 2-by-2 grid of Axes using +# `~matplotlib.pyplot.subplots`. It returns a `~matplotlib.figure.Figure` +# instance and an array of `~matplotlib.axes.Axes` objects. The Axes +# objects can be used to access methods to place artists on the Axes; here +# we use `~.Axes.annotate`, but other examples could be `~.Axes.plot`, +# `~.Axes.pcolormesh`, etc. + +import matplotlib.pyplot as plt +import numpy as np + +fig, axs = plt.subplots(ncols=2, nrows=2, figsize=(5.5, 3.5), + layout="constrained") +# add an artist, in this case a nice label in the middle... +for row in range(2): + for col in range(2): + axs[row, col].annotate(f'axs[{row}, {col}]', (0.5, 0.5), + transform=axs[row, col].transAxes, + ha='center', va='center', fontsize=18, + color='darkgrey') +fig.suptitle('plt.subplots()') + +############################################################################## +# We will annotate a lot of Axes, so let's encapsulate the annotation, rather +# than having that large piece of annotation code every time we need it: + + +def annotate_axes(ax, text, fontsize=18): + ax.text(0.5, 0.5, text, transform=ax.transAxes, + ha="center", va="center", fontsize=fontsize, color="darkgrey") + + +############################################################################## +# The same effect can be achieved with `~.pyplot.subplot_mosaic`, +# but the return type is a dictionary instead of an array, where the user +# can give the keys useful meanings. Here we provide two lists, each list +# representing a row, and each element in the list a key representing the +# column. + +fig, axd = plt.subplot_mosaic([['upper left', 'upper right'], + ['lower left', 'lower right']], + figsize=(5.5, 3.5), layout="constrained") +for k in axd: + annotate_axes(axd[k], f'axd["{k}"]', fontsize=14) +fig.suptitle('plt.subplot_mosaic()') + +############################################################################# +# +# Grids of fixed-aspect ratio Axes +# -------------------------------- +# +# Fixed-aspect ratio axes are common for images or maps. However, they +# present a challenge to layout because two sets of constraints are being +# imposed on the size of the Axes - that they fit in the figure and that they +# have a set aspect ratio. This leads to large gaps between Axes by default: +# + +fig, axs = plt.subplots(2, 2, layout="constrained", figsize=(5.5, 3.5)) +for ax in axs.flat: + ax.set_aspect(1) +fig.suptitle('Fixed aspect Axes') + +############################################################################ +# One way to address this is to change the aspect of the figure to be close +# to the aspect ratio of the Axes, however that requires trial and error. +# Matplotlib also supplies ``layout="compressed"``, which will work with +# simple grids to reduce the gaps between Axes. (The ``mpl_toolkits`` also +# provides `~.mpl_toolkits.axes_grid1.axes_grid.ImageGrid` to accomplish +# a similar effect, but with a non-standard Axes class). + +fig, axs = plt.subplots(2, 2, layout="compressed", figsize=(5.5, 3.5)) +for ax in axs.flat: + ax.set_aspect(1) +fig.suptitle('Fixed aspect Axes: compressed') + + +############################################################################ +# Axes spanning rows or columns in a grid +# --------------------------------------- +# +# Sometimes we want Axes to span rows or columns of the grid. +# There are actually multiple ways to accomplish this, but the most +# convenient is probably to use `~.pyplot.subplot_mosaic` by repeating one +# of the keys: + +fig, axd = plt.subplot_mosaic([['upper left', 'right'], + ['lower left', 'right']], + figsize=(5.5, 3.5), layout="constrained") +for k in axd: + annotate_axes(axd[k], f'axd["{k}"]', fontsize=14) +fig.suptitle('plt.subplot_mosaic()') + +############################################################################ +# See below for the description of how to do the same thing using +# `~matplotlib.gridspec.GridSpec` or `~matplotlib.pyplot.subplot2grid`. +# +# Variable widths or heights in a grid +# ------------------------------------ +# +# Both `~.pyplot.subplots` and `~.pyplot.subplot_mosaic` allow the rows +# in the grid to be different heights, and the columns to be different +# widths using the *gridspec_kw* keyword argument. +# Spacing parameters accepted by `~matplotlib.gridspec.GridSpec` +# can be passed to `~matplotlib.pyplot.subplots` and +# `~matplotlib.pyplot.subplot_mosaic`: + +gs_kw = dict(width_ratios=[1.4, 1], height_ratios=[1, 2]) +fig, axd = plt.subplot_mosaic([['upper left', 'right'], + ['lower left', 'right']], + gridspec_kw=gs_kw, figsize=(5.5, 3.5), + layout="constrained") +for k in axd: + annotate_axes(axd[k], f'axd["{k}"]', fontsize=14) +fig.suptitle('plt.subplot_mosaic()') + +############################################################################ +# Nested Axes layouts +# ------------------- +# +# Sometimes it is helpful to have two or more grids of Axes that +# may not need to be related to one another. The most simple way to +# accomplish this is to use `.Figure.subfigures`. Note that the subfigure +# layouts are independent, so the Axes spines in each subfigure are not +# necessarily aligned. See below for a more verbose way to achieve the same +# effect with `~.gridspec.GridSpecFromSubplotSpec`. + +fig = plt.figure(layout="constrained") +subfigs = fig.subfigures(1, 2, wspace=0.07, width_ratios=[1.5, 1.]) +axs0 = subfigs[0].subplots(2, 2) +subfigs[0].set_facecolor('0.9') +subfigs[0].suptitle('subfigs[0]\nLeft side') +subfigs[0].supxlabel('xlabel for subfigs[0]') + +axs1 = subfigs[1].subplots(3, 1) +subfigs[1].suptitle('subfigs[1]') +subfigs[1].supylabel('ylabel for subfigs[1]') + +############################################################################ +# It is also possible to nest Axes using `~.pyplot.subplot_mosaic` using +# nested lists. This method does not use subfigures, like above, so lacks +# the ability to add per-subfigure ``suptitle`` and ``supxlabel``, etc. +# Rather it is a convenience wrapper around the `~.SubplotSpec.subgridspec` +# method described below. + +inner = [['innerA'], + ['innerB']] +outer = [['upper left', inner], + ['lower left', 'lower right']] + +fig, axd = plt.subplot_mosaic(outer, layout="constrained") +for k in axd: + annotate_axes(axd[k], f'axd["{k}"]') + +############################################################################ +# Low-level and advanced grid methods +# =================================== +# +# Internally, the arrangement of a grid of Axes is controlled by creating +# instances of `~.GridSpec` and `~.SubplotSpec`. *GridSpec* defines a +# (possibly non-uniform) grid of cells. Indexing into the *GridSpec* returns +# a SubplotSpec that covers one or more grid cells, and can be used to +# specify the location of an Axes. +# +# The following examples show how to use low-level methods to arrange Axes +# using *GridSpec* objects. +# +# Basic 2x2 grid +# -------------- +# +# We can accomplish a 2x2 grid in the same manner as +# ``plt.subplots(2, 2)``: + +fig = plt.figure(figsize=(5.5, 3.5), layout="constrained") +spec = fig.add_gridspec(ncols=2, nrows=2) + +ax0 = fig.add_subplot(spec[0, 0]) +annotate_axes(ax0, 'ax0') + +ax1 = fig.add_subplot(spec[0, 1]) +annotate_axes(ax1, 'ax1') + +ax2 = fig.add_subplot(spec[1, 0]) +annotate_axes(ax2, 'ax2') + +ax3 = fig.add_subplot(spec[1, 1]) +annotate_axes(ax3, 'ax3') + +fig.suptitle('Manually added subplots using add_gridspec') + +############################################################################## +# Axes spanning rows or grids in a grid +# ------------------------------------- +# +# We can index the *spec* array using `NumPy slice syntax +# `_ +# and the new Axes will span the slice. This would be the same +# as ``fig, axd = plt.subplot_mosaic([['ax0', 'ax0'], ['ax1', 'ax2']], ...)``: + +fig = plt.figure(figsize=(5.5, 3.5), layout="constrained") +spec = fig.add_gridspec(2, 2) + +ax0 = fig.add_subplot(spec[0, :]) +annotate_axes(ax0, 'ax0') + +ax10 = fig.add_subplot(spec[1, 0]) +annotate_axes(ax10, 'ax10') + +ax11 = fig.add_subplot(spec[1, 1]) +annotate_axes(ax11, 'ax11') + +fig.suptitle('Manually added subplots, spanning a column') + +############################################################################### +# Manual adjustments to a *GridSpec* layout +# ----------------------------------------- +# +# When a *GridSpec* is explicitly used, you can adjust the layout +# parameters of subplots that are created from the *GridSpec*. Note this +# option is not compatible with ``constrained_layout`` or +# `.Figure.tight_layout` which both ignore *left* and *right* and adjust +# subplot sizes to fill the figure. Usually such manual placement +# requires iterations to make the Axes tick labels not overlap the Axes. +# +# These spacing parameters can also be passed to `~.pyplot.subplots` and +# `~.pyplot.subplot_mosaic` as the *gridspec_kw* argument. + +fig = plt.figure(layout=None, facecolor='0.9') +gs = fig.add_gridspec(nrows=3, ncols=3, left=0.05, right=0.75, + hspace=0.1, wspace=0.05) +ax0 = fig.add_subplot(gs[:-1, :]) +annotate_axes(ax0, 'ax0') +ax1 = fig.add_subplot(gs[-1, :-1]) +annotate_axes(ax1, 'ax1') +ax2 = fig.add_subplot(gs[-1, -1]) +annotate_axes(ax2, 'ax2') +fig.suptitle('Manual gridspec with right=0.75') + +############################################################################### +# Nested layouts with SubplotSpec +# ------------------------------- +# +# You can create nested layout similar to `~.Figure.subfigures` using +# `~.gridspec.SubplotSpec.subgridspec`. Here the Axes spines *are* +# aligned. +# +# Note this is also available from the more verbose +# `.gridspec.GridSpecFromSubplotSpec`. + +fig = plt.figure(layout="constrained") +gs0 = fig.add_gridspec(1, 2) + +gs00 = gs0[0].subgridspec(2, 2) +gs01 = gs0[1].subgridspec(3, 1) + +for a in range(2): + for b in range(2): + ax = fig.add_subplot(gs00[a, b]) + annotate_axes(ax, f'axLeft[{a}, {b}]', fontsize=10) + if a == 1 and b == 1: + ax.set_xlabel('xlabel') +for a in range(3): + ax = fig.add_subplot(gs01[a]) + annotate_axes(ax, f'axRight[{a}, {b}]') + if a == 2: + ax.set_ylabel('ylabel') + +fig.suptitle('nested gridspecs') + +############################################################################### +# Here's a more sophisticated example of nested *GridSpec*: We create an outer +# 4x4 grid with each cell containing an inner 3x3 grid of Axes. We outline +# the outer 4x4 grid by hiding appropriate spines in each of the inner 3x3 +# grids. + + +def squiggle_xy(a, b, c, d, i=np.arange(0.0, 2*np.pi, 0.05)): + return np.sin(i*a)*np.cos(i*b), np.sin(i*c)*np.cos(i*d) + +fig = plt.figure(figsize=(8, 8), constrained_layout=False) +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) + axs = inner_grid.subplots() # Create all subplots for the inner grid. + for (c, d), ax in np.ndenumerate(axs): + ax.plot(*squiggle_xy(a + 1, b + 1, c + 1, d + 1)) + ax.set(xticks=[], yticks=[]) + +# show only the outside spines +for ax in fig.get_axes(): + ss = ax.get_subplotspec() + ax.spines.top.set_visible(ss.is_first_row()) + ax.spines.bottom.set_visible(ss.is_last_row()) + ax.spines.left.set_visible(ss.is_first_col()) + ax.spines.right.set_visible(ss.is_last_col()) + +plt.show() + +############################################################################# +# +# More reading +# ============ +# +# - More details about :doc:`subplot mosaic +# `. +# - More details about :doc:`constrained layout +# `, used to align +# spacing in most of these examples. +# +# .. admonition:: References +# +# The use of the following functions, methods, classes and modules is shown +# in this example: +# +# - `matplotlib.pyplot.subplots` +# - `matplotlib.pyplot.subplot_mosaic` +# - `matplotlib.figure.Figure.add_gridspec` +# - `matplotlib.figure.Figure.add_subplot` +# - `matplotlib.gridspec.GridSpec` +# - `matplotlib.gridspec.SubplotSpec.subgridspec` +# - `matplotlib.gridspec.GridSpecFromSubplotSpec` diff --git a/tutorials/intermediate/artists.py b/tutorials/intermediate/artists.py index 1df2544cafcb..1a186ccb4222 100644 --- a/tutorials/intermediate/artists.py +++ b/tutorials/intermediate/artists.py @@ -29,8 +29,8 @@ the containers are places to put them (:class:`~matplotlib.axis.Axis`, :class:`~matplotlib.axes.Axes` and :class:`~matplotlib.figure.Figure`). The standard use is to create a :class:`~matplotlib.figure.Figure` instance, use -the ``Figure`` to create one or more :class:`~matplotlib.axes.Axes` or -:class:`~matplotlib.axes.Subplot` instances, and use the ``Axes`` instance +the ``Figure`` to create one or more :class:`~matplotlib.axes.Axes` +instances, and use the ``Axes`` instance helper methods to create the primitives. In the example below, we create a ``Figure`` instance using :func:`matplotlib.pyplot.figure`, which is a convenience method for instantiating ``Figure`` instances and connecting them @@ -59,10 +59,7 @@ class in the Matplotlib API, and the one you will be working with most :class:`~matplotlib.image.AxesImage`, respectively). These helper methods will take your data (e.g., ``numpy`` arrays and strings) and create primitive ``Artist`` instances as needed (e.g., ``Line2D``), add them to -the relevant containers, and draw them when requested. Most of you -are probably familiar with the :class:`~matplotlib.axes.Subplot`, -which is just a special case of an ``Axes`` that lives on a regular -rows by columns grid of ``Subplot`` instances. If you want to create +the relevant containers, and draw them when requested. If you want to create an ``Axes`` at an arbitrary location, simply use the :meth:`~matplotlib.figure.Figure.add_axes` method which takes a list of ``[left, bottom, width, height]`` values in 0-1 relative figure @@ -79,13 +76,11 @@ class in the Matplotlib API, and the one you will be working with most line, = ax.plot(t, s, color='blue', lw=2) In this example, ``ax`` is the ``Axes`` instance created by the -``fig.add_subplot`` call above (remember ``Subplot`` is just a -subclass of ``Axes``) and when you call ``ax.plot``, it creates a -``Line2D`` instance and adds it to the :attr:`Axes.lines -` list. In the interactive `IPython -`_ session below, you can see that the -``Axes.lines`` list is length one and contains the same line that was -returned by the ``line, = ax.plot...`` call: +``fig.add_subplot`` call above and when you call ``ax.plot``, it creates a +``Line2D`` instance and +adds it to the ``Axes``. In the interactive `IPython `_ +session below, you can see that the ``Axes.lines`` list is length one and +contains the same line that was returned by the ``line, = ax.plot...`` call: .. sourcecode:: ipython @@ -97,11 +92,10 @@ class in the Matplotlib API, and the one you will be working with most If you make subsequent calls to ``ax.plot`` (and the hold state is "on" which is the default) then additional lines will be added to the list. -You can remove lines later simply by calling the list methods; either -of these will work:: +You can remove a line later by calling its ``remove`` method:: - del ax.lines[0] - ax.lines.remove(line) # one or the other, not both! + line = ax.lines[0] + line.remove() The Axes also has helper methods to configure and decorate the x-axis and y-axis tick, tick labels and axis labels:: @@ -118,6 +112,7 @@ class in the Matplotlib API, and the one you will be working with most Try creating the figure below. """ +# sphinx_gallery_capture_repr = ('__repr__',) import numpy as np import matplotlib.pyplot as plt @@ -125,8 +120,8 @@ class in the Matplotlib API, and the one you will be working with most fig = plt.figure() fig.subplots_adjust(top=0.8) ax1 = fig.add_subplot(211) -ax1.set_ylabel('volts') -ax1.set_title('a sine wave') +ax1.set_ylabel('Voltage [V]') +ax1.set_title('A sine wave') t = np.arange(0.0, 1.0, 0.01) s = np.sin(2*np.pi*t) @@ -138,7 +133,7 @@ class in the Matplotlib API, and the one you will be working with most ax2 = fig.add_axes([0.15, 0.1, 0.7, 0.3]) n, bins, patches = ax2.hist(np.random.randn(1000), 50, facecolor='yellow', edgecolor='yellow') -ax2.set_xlabel('time (s)') +ax2.set_xlabel('Time [s]') plt.show() @@ -301,10 +296,10 @@ class in the Matplotlib API, and the one you will be working with most # In [158]: ax2 = fig.add_axes([0.1, 0.1, 0.7, 0.3]) # # In [159]: ax1 -# Out[159]: +# Out[159]: # # In [160]: print(fig.axes) -# [, ] +# [, ] # # Because the figure maintains the concept of the "current Axes" (see # :meth:`Figure.gca ` and @@ -313,7 +308,7 @@ class in the Matplotlib API, and the one you will be working with most # directly from the Axes list, but rather use the # :meth:`~matplotlib.figure.Figure.add_subplot` and # :meth:`~matplotlib.figure.Figure.add_axes` methods to insert, and the -# :meth:`~matplotlib.figure.Figure.delaxes` method to delete. You are +# `Axes.remove ` method to delete. You are # free however, to iterate over the list of Axes or index into it to get # access to ``Axes`` instances you want to customize. Here is an # example which turns all the Axes grids on:: @@ -351,12 +346,12 @@ class in the Matplotlib API, and the one you will be working with most # ================ ============================================================ # Figure attribute Description # ================ ============================================================ -# axes A list of `~.axes.Axes` instances (includes Subplot) +# axes A list of `~.axes.Axes` instances # patch The `.Rectangle` background # images A list of `.FigureImage` patches - # useful for raw pixel display # legends A list of Figure `.Legend` instances -# (different from ``Axes.legends``) +# (different from ``Axes.get_legend()``) # lines A list of Figure `.Line2D` instances # (rarely used, see ``Axes.lines``) # patches A list of Figure `.Patch`\s @@ -386,11 +381,10 @@ class in the Matplotlib API, and the one you will be working with most # rect.set_facecolor('green') # # When you call a plotting method, e.g., the canonical -# :meth:`~matplotlib.axes.Axes.plot` and pass in arrays or lists of -# values, the method will create a :meth:`matplotlib.lines.Line2D` -# instance, update the line with all the ``Line2D`` properties passed as -# keyword arguments, add the line to the :attr:`Axes.lines -# ` container, and returns it to you: +# `~matplotlib.axes.Axes.plot` and pass in arrays or lists of values, the +# method will create a `matplotlib.lines.Line2D` instance, update the line with +# all the ``Line2D`` properties passed as keyword arguments, add the line to +# the ``Axes``, and return it to you: # # .. sourcecode:: ipython # @@ -423,19 +417,20 @@ class in the Matplotlib API, and the one you will be working with most # In [235]: print(len(ax.patches)) # Out[235]: 50 # -# You should not add objects directly to the ``Axes.lines`` or -# ``Axes.patches`` lists unless you know exactly what you are doing, -# because the ``Axes`` needs to do a few things when it creates and adds -# an object. It sets the figure and axes property of the ``Artist``, as -# well as the default ``Axes`` transformation (unless a transformation -# is set). It also inspects the data contained in the ``Artist`` to -# update the data structures controlling auto-scaling, so that the view -# limits can be adjusted to contain the plotted data. You can, -# nonetheless, create objects yourself and add them directly to the -# ``Axes`` using helper methods like -# :meth:`~matplotlib.axes.Axes.add_line` and -# :meth:`~matplotlib.axes.Axes.add_patch`. Here is an annotated -# interactive session illustrating what is going on: +# You should not add objects directly to the ``Axes.lines`` or ``Axes.patches`` +# lists, because the ``Axes`` needs to do a few things when it creates and adds +# an object: +# +# - It sets the ``figure`` and ``axes`` property of the ``Artist``; +# - It sets the default ``Axes`` transformation (unless one is already set); +# - It inspects the data contained in the ``Artist`` to update the data +# structures controlling auto-scaling, so that the view limits can be +# adjusted to contain the plotted data. +# +# You can, nonetheless, create objects yourself and add them directly to the +# ``Axes`` using helper methods like `~matplotlib.axes.Axes.add_line` and +# `~matplotlib.axes.Axes.add_patch`. Here is an annotated interactive session +# illustrating what is going on: # # .. sourcecode:: ipython # @@ -546,7 +541,7 @@ class in the Matplotlib API, and the one you will be working with most # `~.axes.Axes.fill` - shared area `.Polygon` ax.patches # `~.axes.Axes.hist` - histograms `.Rectangle` ax.patches # `~.axes.Axes.imshow` - image data `.AxesImage` ax.images -# `~.axes.Axes.legend` - Axes legends `.Legend` ax.legends +# `~.axes.Axes.legend` - Axes legend `.Legend` ax.get_legend() # `~.axes.Axes.plot` - xy plots `.Line2D` ax.lines # `~.axes.Axes.scatter` - scatter charts `.PolyCollection` ax.collections # `~.axes.Axes.text` - text `.Text` ax.texts @@ -561,31 +556,31 @@ class in the Matplotlib API, and the one you will be working with most # :attr:`~matplotlib.axes.Axes.yaxis`. The ``XAxis`` and ``YAxis`` # containers will be detailed below, but note that the ``Axes`` contains # many helper methods which forward calls on to the -# :class:`~matplotlib.axis.Axis` instances so you often do not need to +# :class:`~matplotlib.axis.Axis` instances, so you often do not need to # work with them directly unless you want to. For example, you can set # the font color of the ``XAxis`` ticklabels using the ``Axes`` helper # method:: # -# for label in ax.get_xticklabels(): -# label.set_color('orange') +# ax.tick_params(axis='x', labelcolor='orange') # -# Below is a summary of the Artists that the Axes contains +# Below is a summary of the Artists that the `~.axes.Axes` contains # # ============== ========================================= # Axes attribute Description # ============== ========================================= -# artists A list of `.Artist` instances +# artists An `.ArtistList` of `.Artist` instances # patch `.Rectangle` instance for Axes background -# collections A list of `.Collection` instances -# images A list of `.AxesImage` -# legends A list of `.Legend` instances -# lines A list of `.Line2D` instances -# patches A list of `.Patch` instances -# texts A list of `.Text` instances +# collections An `.ArtistList` of `.Collection` instances +# images An `.ArtistList` of `.AxesImage` +# lines An `.ArtistList` of `.Line2D` instances +# patches An `.ArtistList` of `.Patch` instances +# texts An `.ArtistList` of `.Text` instances # xaxis A `matplotlib.axis.XAxis` instance # yaxis A `matplotlib.axis.YAxis` instance # ============== ========================================= # +# The legend can be accessed by `~.axes.Axes.get_legend`, +# # .. _axis-container: # # Axis containers @@ -721,6 +716,6 @@ class in the Matplotlib API, and the one you will be working with most # dollar signs and colors them green on the right side of the yaxis. # # -# .. include:: ../../gallery/pyplots/dollar_ticks.rst -# :start-after: y axis labels. +# .. include:: ../../gallery/ticks/dollar_ticks.rst +# :start-after: .. redirect-from:: /gallery/pyplots/dollar_ticks # :end-before: .. admonition:: References diff --git a/tutorials/intermediate/autoscale.py b/tutorials/intermediate/autoscale.py index 64cc2e22f014..3b563510aa1f 100644 --- a/tutorials/intermediate/autoscale.py +++ b/tutorials/intermediate/autoscale.py @@ -26,7 +26,7 @@ # ------- # The default margin around the data limits is 5%: -ax.margins() +print(ax.margins()) ############################################################################### # The margins can be made larger using `~matplotlib.axes.Axes.margins`: @@ -71,7 +71,7 @@ # `~matplotlib.axes.Axes.use_sticky_edges`. # Artists have a property `.Artist.sticky_edges`, and the values of # sticky edges can be changed by writing to ``Artist.sticky_edges.x`` or -# ``.Artist.sticky_edges.y``. +# ``Artist.sticky_edges.y``. # # The following example shows how overriding works and when it is needed. @@ -164,9 +164,9 @@ fig, ax = plt.subplots() collection = mpl.collections.StarPolygonCollection( - 5, 0, [250, ], # five point star, zero angle, size 250px + 5, rotation=0, sizes=(250,), # five point star, zero angle, size 250px offsets=np.column_stack([x, y]), # Set the positions - transOffset=ax.transData, # Propagate transformations of the Axes + offset_transform=ax.transData, # Propagate transformations of the Axes ) ax.add_collection(collection) ax.autoscale_view() diff --git a/tutorials/intermediate/constrainedlayout_guide.py b/tutorials/intermediate/constrainedlayout_guide.py index 3458667aef4e..8852f4067ff4 100644 --- a/tutorials/intermediate/constrainedlayout_guide.py +++ b/tutorials/intermediate/constrainedlayout_guide.py @@ -14,29 +14,21 @@ but uses a constraint solver to determine the size of axes that allows them to fit. -*constrained_layout* needs to be activated before any axes are added to -a figure. Two ways of doing so are +*constrained_layout* typically needs to be activated before any axes are +added to a figure. Two ways of doing so are * using the respective argument to :func:`~.pyplot.subplots` or :func:`~.pyplot.figure`, e.g.:: - plt.subplots(constrained_layout=True) + plt.subplots(layout="constrained") -* activate it via :ref:`rcParams`, like:: +* activate it via :ref:`rcParams`, + like:: plt.rcParams['figure.constrained_layout.use'] = True Those are described in detail throughout the following sections. -.. 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``. - Simple Example ============== @@ -71,32 +63,32 @@ def example_plot(ax, fontsize=12, hide_labels=False): ax.set_ylabel('y-label', fontsize=fontsize) ax.set_title('Title', fontsize=fontsize) - -fig, ax = plt.subplots(constrained_layout=False) +fig, ax = plt.subplots(layout=None) example_plot(ax, fontsize=24) ############################################################################### # To prevent this, the location of axes needs to be adjusted. For -# subplots, this can be done by adjusting the subplot params -# (:ref:`howto-subplots-adjust`). However, specifying your figure with the -# ``constrained_layout=True`` kwarg will do the adjusting automatically. +# subplots, this can be done manually by adjusting the subplot parameters +# using `.Figure.subplots_adjust`. However, specifying your figure with the +# # ``layout="constrained"`` keyword argument will do the adjusting +# # automatically. -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") example_plot(ax, fontsize=24) ############################################################################### # When you have multiple subplots, often you see labels of different # axes overlapping each other. -fig, axs = plt.subplots(2, 2, constrained_layout=False) +fig, axs = plt.subplots(2, 2, layout=None) for ax in axs.flat: example_plot(ax) ############################################################################### -# Specifying ``constrained_layout=True`` in the call to ``plt.subplots`` +# Specifying ``layout="constrained"`` in the call to ``plt.subplots`` # causes the layout to be properly constrained. -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: example_plot(ax) @@ -112,7 +104,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # # .. note:: # -# For the `~.axes.Axes.pcolormesh` kwargs (``pc_kwargs``) we use a +# For the `~.axes.Axes.pcolormesh` keyword arguments (``pc_kwargs``) we use a # dictionary. Below we will assign one colorbar to a number of axes each # containing a `~.cm.ScalarMappable`; specifying the norm and colormap # ensures the colorbar is accurate for all the axes. @@ -121,7 +113,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): norm = mcolors.Normalize(vmin=0., vmax=100.) # see note above: this makes all pcolormesh calls consistent: pc_kwargs = {'rasterized': True, 'cmap': 'viridis', 'norm': norm} -fig, ax = plt.subplots(figsize=(4, 4), constrained_layout=True) +fig, ax = plt.subplots(figsize=(4, 4), layout="constrained") im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=ax, shrink=0.6) @@ -130,7 +122,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # ``ax`` argument of ``colorbar``, constrained_layout will take space from # the specified axes. -fig, axs = plt.subplots(2, 2, figsize=(4, 4), constrained_layout=True) +fig, axs = plt.subplots(2, 2, figsize=(4, 4), layout="constrained") for ax in axs.flat: im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs, shrink=0.6) @@ -140,7 +132,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # will steal space appropriately, and leave a gap, but all subplots will # still be the same size. -fig, axs = plt.subplots(3, 3, figsize=(4, 4), constrained_layout=True) +fig, axs = plt.subplots(3, 3, figsize=(4, 4), layout="constrained") for ax in axs.flat: im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs[1:, ][:, 1], shrink=0.8) @@ -150,9 +142,9 @@ def example_plot(ax, fontsize=12, hide_labels=False): # Suptitle # ========= # -# ``constrained_layout`` can also make room for `~.figure.Figure.suptitle`. +# ``constrained_layout`` can also make room for `~.Figure.suptitle`. -fig, axs = plt.subplots(2, 2, figsize=(4, 4), constrained_layout=True) +fig, axs = plt.subplots(2, 2, figsize=(4, 4), layout="constrained") for ax in axs.flat: im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs, shrink=0.6) @@ -167,14 +159,14 @@ def example_plot(ax, fontsize=12, hide_labels=False): # However, constrained-layout does *not* handle legends being created via # :meth:`.Figure.legend` (yet). -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") ax.plot(np.arange(10), label='This is a plot') ax.legend(loc='center left', bbox_to_anchor=(0.8, 0.5)) ############################################# # However, this will steal space from a subplot layout: -fig, axs = plt.subplots(1, 2, figsize=(4, 2), constrained_layout=True) +fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained") axs[0].plot(np.arange(10)) axs[1].plot(np.arange(10), label='This is a plot') axs[1].legend(loc='center left', bbox_to_anchor=(0.8, 0.5)) @@ -190,7 +182,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # trigger a draw if we want constrained_layout to adjust the size # of the axes before printing. -fig, axs = plt.subplots(1, 2, figsize=(4, 2), constrained_layout=True) +fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained") axs[0].plot(np.arange(10)) axs[1].plot(np.arange(10), label='This is a plot') @@ -202,9 +194,14 @@ def example_plot(ax, fontsize=12, hide_labels=False): # we want the legend included in the bbox_inches='tight' calcs. leg.set_in_layout(True) # we don't want the layout to change at this point. -fig.set_constrained_layout(False) -fig.savefig('../../doc/_static/constrained_layout_1b.png', - bbox_inches='tight', dpi=100) +fig.set_layout_engine(None) +try: + fig.savefig('../../doc/_static/constrained_layout_1b.png', + bbox_inches='tight', dpi=100) +except FileNotFoundError: + # this allows the script to keep going if run interactively and + # the directory above doesn't exist + pass ############################################# # The saved file looks like: @@ -214,14 +211,20 @@ def example_plot(ax, fontsize=12, hide_labels=False): # # A better way to get around this awkwardness is to simply # use the legend method provided by `.Figure.legend`: -fig, axs = plt.subplots(1, 2, figsize=(4, 2), constrained_layout=True) +fig, axs = plt.subplots(1, 2, figsize=(4, 2), layout="constrained") axs[0].plot(np.arange(10)) lines = axs[1].plot(np.arange(10), label='This is a plot') labels = [l.get_label() for l in lines] leg = fig.legend(lines, labels, loc='center left', bbox_to_anchor=(0.8, 0.5), bbox_transform=axs[1].transAxes) -fig.savefig('../../doc/_static/constrained_layout_2b.png', - bbox_inches='tight', dpi=100) +try: + fig.savefig('../../doc/_static/constrained_layout_2b.png', + bbox_inches='tight', dpi=100) +except FileNotFoundError: + # this allows the script to keep going if run interactively and + # the directory above doesn't exist + pass + ############################################# # The saved file looks like: @@ -236,13 +239,14 @@ def example_plot(ax, fontsize=12, hide_labels=False): # # Padding between axes is controlled in the horizontal by *w_pad* and # *wspace*, and vertical by *h_pad* and *hspace*. These can be edited -# via `~.figure.Figure.set_constrained_layout_pads`. *w/h_pad* are +# via `~.layout_engine.ConstrainedLayoutEngine.set`. *w/h_pad* are # the minimum space around the axes in units of inches: -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: example_plot(ax, hide_labels=True) -fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0, wspace=0) +fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0, + wspace=0) ########################################## # Spacing between subplots is further set by *wspace* and *hspace*. These @@ -251,36 +255,35 @@ def example_plot(ax, fontsize=12, hide_labels=False): # used instead. Note in the below how the space at the edges doesn't change # from the above, but the space between subplots does. -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: example_plot(ax, hide_labels=True) -fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2, - wspace=0.2) +fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2, + wspace=0.2) ########################################## # If there are more than two columns, the *wspace* is shared between them, -# so here the wspace is divided in 2, with a *wspace* of 0.1 between each +# so here the wspace is divided in two, with a *wspace* of 0.1 between each # column: -fig, axs = plt.subplots(2, 3, constrained_layout=True) +fig, axs = plt.subplots(2, 3, layout="constrained") for ax in axs.flat: example_plot(ax, hide_labels=True) -fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2, - wspace=0.2) +fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.2, + wspace=0.2) ########################################## # GridSpecs also have optional *hspace* and *wspace* keyword arguments, # that will be used instead of the pads set by ``constrained_layout``: -fig, axs = plt.subplots(2, 2, constrained_layout=True, +fig, axs = plt.subplots(2, 2, layout="constrained", gridspec_kw={'wspace': 0.3, 'hspace': 0.2}) for ax in axs.flat: example_plot(ax, hide_labels=True) # this has no effect because the space set in the gridspec trumps the # space set in constrained_layout. -fig.set_constrained_layout_pads(w_pad=4 / 72, h_pad=4 / 72, hspace=0.0, - wspace=0.0) -plt.show() +fig.get_layout_engine().set(w_pad=4 / 72, h_pad=4 / 72, hspace=0.0, + wspace=0.0) ########################################## # Spacing with colorbars @@ -290,24 +293,24 @@ def example_plot(ax, fontsize=12, hide_labels=False): # is a fraction of the width of the parent(s). The spacing to the # next subplot is then given by *w/hspace*. -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") pads = [0, 0.05, 0.1, 0.2] for pad, ax in zip(pads, axs.flat): pc = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(pc, ax=ax, shrink=0.6, pad=pad) - ax.set_xticklabels('') - ax.set_yticklabels('') + ax.set_xticklabels([]) + ax.set_yticklabels([]) ax.set_title(f'pad: {pad}') -fig.set_constrained_layout_pads(w_pad=2 / 72, h_pad=2 / 72, hspace=0.2, - wspace=0.2) +fig.get_layout_engine().set(w_pad=2 / 72, h_pad=2 / 72, hspace=0.2, + wspace=0.2) ########################################## # rcParams # ======== # -# There are five :ref:`rcParams` that can be set, -# either in a script or in the :file:`matplotlibrc` file. -# They all have the prefix ``figure.constrained_layout``: +# There are five :ref:`rcParams` +# that can be set, either in a script or in the :file:`matplotlibrc` +# file. They all have the prefix ``figure.constrained_layout``: # # - *use*: Whether to use constrained_layout. Default is False # - *w_pad*, *h_pad*: Padding around axes objects. @@ -326,13 +329,15 @@ def example_plot(ax, fontsize=12, hide_labels=False): # ================= # # constrained_layout is meant to be used -# with :func:`~matplotlib.figure.Figure.subplots` or -# :func:`~matplotlib.gridspec.GridSpec` and +# with :func:`~matplotlib.figure.Figure.subplots`, +# :func:`~matplotlib.figure.Figure.subplot_mosaic`, or +# :func:`~matplotlib.gridspec.GridSpec` with # :func:`~matplotlib.figure.Figure.add_subplot`. # -# Note that in what follows ``constrained_layout=True`` +# Note that in what follows ``layout="constrained"`` -fig = plt.figure() +plt.rcParams['figure.constrained_layout.use'] = False +fig = plt.figure(layout="constrained") gs1 = gridspec.GridSpec(2, 1, figure=fig) ax1 = fig.add_subplot(gs1[0]) @@ -346,7 +351,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # convenience functions `~.Figure.add_gridspec` and # `~.SubplotSpec.subgridspec`. -fig = plt.figure() +fig = plt.figure(layout="constrained") gs0 = fig.add_gridspec(1, 2) @@ -373,7 +378,7 @@ def example_plot(ax, fontsize=12, hide_labels=False): # then they need to be in the same gridspec. We need to make this figure # larger as well in order for the axes not to collapse to zero height: -fig = plt.figure(figsize=(4, 6)) +fig = plt.figure(figsize=(4, 6), layout="constrained") gs0 = fig.add_gridspec(6, 2) @@ -391,52 +396,94 @@ def example_plot(ax, fontsize=12, hide_labels=False): example_plot(ax, hide_labels=True) fig.suptitle('Overlapping Gridspecs') - ############################################################################ # This example uses two gridspecs to have the colorbar only pertain to # one set of pcolors. Note how the left column is wider than the # two right-hand columns because of this. Of course, if you wanted the -# subplots to be the same size you only needed one gridspec. +# subplots to be the same size you only needed one gridspec. Note that +# the same effect can be achieved using `~.Figure.subfigures`. +fig = plt.figure(layout="constrained") +gs0 = fig.add_gridspec(1, 2, figure=fig, width_ratios=[1, 2]) +gs_left = gs0[0].subgridspec(2, 1) +gs_right = gs0[1].subgridspec(2, 2) -def docomplicated(suptitle=None): - fig = plt.figure() - gs0 = fig.add_gridspec(1, 2, figure=fig, width_ratios=[1., 2.]) - gsl = gs0[0].subgridspec(2, 1) - gsr = gs0[1].subgridspec(2, 2) +for gs in gs_left: + ax = fig.add_subplot(gs) + example_plot(ax) +axs = [] +for gs in gs_right: + ax = fig.add_subplot(gs) + pcm = ax.pcolormesh(arr, **pc_kwargs) + ax.set_xlabel('x-label') + ax.set_ylabel('y-label') + ax.set_title('title') + axs += [ax] +fig.suptitle('Nested plots using subgridspec') +fig.colorbar(pcm, ax=axs) - for gs in gsl: - ax = fig.add_subplot(gs) - example_plot(ax) - axs = [] - for gs in gsr: - ax = fig.add_subplot(gs) - pcm = ax.pcolormesh(arr, **pc_kwargs) - ax.set_xlabel('x-label') - ax.set_ylabel('y-label') - ax.set_title('title') +############################################################################### +# Rather than using subgridspecs, Matplotlib now provides `~.Figure.subfigures` +# which also work with ``constrained_layout``: - axs += [ax] - fig.colorbar(pcm, ax=axs) - if suptitle is not None: - fig.suptitle(suptitle) +fig = plt.figure(layout="constrained") +sfigs = fig.subfigures(1, 2, width_ratios=[1, 2]) +axs_left = sfigs[0].subplots(2, 1) +for ax in axs_left.flat: + example_plot(ax) -docomplicated() +axs_right = sfigs[1].subplots(2, 2) +for ax in axs_right.flat: + pcm = ax.pcolormesh(arr, **pc_kwargs) + ax.set_xlabel('x-label') + ax.set_ylabel('y-label') + ax.set_title('title') +fig.colorbar(pcm, ax=axs_right) +fig.suptitle('Nested plots using subfigures') ############################################################################### # Manually setting axes positions # ================================ # -# There can be good reasons to manually set an axes position. A manual call +# There can be good reasons to manually set an Axes position. A manual call # to `~.axes.Axes.set_position` will set the axes so constrained_layout has # no effect on it anymore. (Note that ``constrained_layout`` still leaves the # space for the axes that is moved). -fig, axs = plt.subplots(1, 2) +fig, axs = plt.subplots(1, 2, layout="constrained") example_plot(axs[0], fontsize=12) axs[1].set_position([0.2, 0.2, 0.4, 0.4]) +############################################################################### +# .. _compressed_layout: +# +# Grids of fixed aspect-ratio Axes: "compressed" layout +# ===================================================== +# +# ``constrained_layout`` operates on the grid of "original" positions for +# axes. However, when Axes have fixed aspect ratios, one side is usually made +# shorter, and leaves large gaps in the shortened direction. In the following, +# the Axes are square, but the figure quite wide so there is a horizontal gap: + +fig, axs = plt.subplots(2, 2, figsize=(5, 3), + sharex=True, sharey=True, layout="constrained") +for ax in axs.flat: + ax.imshow(arr) +fig.suptitle("fixed-aspect plots, layout='constrained'") + +############################################################################### +# One obvious way of fixing this is to make the figure size more square, +# however, closing the gaps exactly requires trial and error. For simple grids +# of Axes we can use ``layout="compressed"`` to do the job for us: + +fig, axs = plt.subplots(2, 2, figsize=(5, 3), + sharex=True, sharey=True, layout='compressed') +for ax in axs.flat: + ax.imshow(arr) +fig.suptitle("fixed-aspect plots, layout='compressed'") + + ############################################################################### # Manually turning off ``constrained_layout`` # =========================================== @@ -444,7 +491,7 @@ def docomplicated(suptitle=None): # ``constrained_layout`` usually adjusts the axes positions on each draw # of the figure. If you want to get the spacing provided by # ``constrained_layout`` but not have it update, then do the initial -# draw and then call ``fig.set_constrained_layout(False)``. +# draw and then call ``fig.set_layout_engine(None)``. # This is potentially useful for animations where the tick labels may # change length. # @@ -465,7 +512,7 @@ def docomplicated(suptitle=None): # `.GridSpec` instance if the geometry is not the same, and # ``constrained_layout``. So the following works fine: -fig = plt.figure() +fig = plt.figure(layout="constrained") ax1 = plt.subplot(2, 2, 1) ax2 = plt.subplot(2, 2, 3) @@ -480,7 +527,7 @@ def docomplicated(suptitle=None): ############################################################################### # but the following leads to a poor layout: -fig = plt.figure() +fig = plt.figure(layout="constrained") ax1 = plt.subplot(2, 2, 1) ax2 = plt.subplot(2, 2, 3) @@ -496,7 +543,7 @@ def docomplicated(suptitle=None): # `~matplotlib.pyplot.subplot2grid` works with the same limitation # that nrows and ncols cannot change for the layout to look good. -fig = plt.figure() +fig = plt.figure(layout="constrained") ax1 = plt.subplot2grid((3, 3), (0, 0)) ax2 = plt.subplot2grid((3, 3), (0, 1), colspan=2) @@ -552,10 +599,10 @@ def docomplicated(suptitle=None): # ====================== # # The algorithm for the constraint is relatively straightforward, but -# has some complexity due to the complex ways we can layout a figure. +# has some complexity due to the complex ways we can lay out a figure. # # Layout in Matplotlib is carried out with gridspecs -# via the `~.GridSpec` class. A gridspec is a logical division of the figure +# via the `.GridSpec` class. A gridspec is a logical division of the figure # into rows and columns, with the relative width of the Axes in those # rows and columns set by *width_ratios* and *height_ratios*. # @@ -564,7 +611,7 @@ def docomplicated(suptitle=None): # for each column, and ``bottom`` and ``top`` variables for each row, and # further it has a margin for each of left, right, bottom and top. In each # row, the bottom/top margins are widened until all the decorators -# in that row are accommodated. Similarly for columns and the left/right +# in that row are accommodated. Similarly, for columns and the left/right # margins. # # @@ -586,9 +633,9 @@ def docomplicated(suptitle=None): from matplotlib._layoutgrid import plot_children -fig, ax = plt.subplots(constrained_layout=True) +fig, ax = plt.subplots(layout="constrained") example_plot(ax, fontsize=24) -plot_children(fig, fig._layoutgrid) +plot_children(fig) ####################################################################### # Simple case: two Axes @@ -600,10 +647,10 @@ def docomplicated(suptitle=None): # margin. The left and right margins are not shared, and hence are # allowed to be different. -fig, ax = plt.subplots(1, 2, constrained_layout=True) +fig, ax = plt.subplots(1, 2, layout="constrained") example_plot(ax[0], fontsize=32) example_plot(ax[1], fontsize=8) -plot_children(fig, fig._layoutgrid, printit=False) +plot_children(fig) ####################################################################### # Two Axes and colorbar @@ -612,11 +659,11 @@ def docomplicated(suptitle=None): # A colorbar is simply another item that expands the margin of the parent # layoutgrid cell: -fig, ax = plt.subplots(1, 2, constrained_layout=True) +fig, ax = plt.subplots(1, 2, layout="constrained") im = ax[0].pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=ax[0], shrink=0.6) im = ax[1].pcolormesh(arr, **pc_kwargs) -plot_children(fig, fig._layoutgrid) +plot_children(fig) ####################################################################### # Colorbar associated with a Gridspec @@ -625,11 +672,11 @@ def docomplicated(suptitle=None): # If a colorbar belongs to more than one cell of the grid, then # it makes a larger margin for each: -fig, axs = plt.subplots(2, 2, constrained_layout=True) +fig, axs = plt.subplots(2, 2, layout="constrained") for ax in axs.flat: im = ax.pcolormesh(arr, **pc_kwargs) fig.colorbar(im, ax=axs, shrink=0.6) -plot_children(fig, fig._layoutgrid, printit=False) +plot_children(fig) ####################################################################### # Uneven sized Axes @@ -643,10 +690,10 @@ def docomplicated(suptitle=None): # ``bottom`` margins are not affected by the left-hand column. This # is a conscious decision of the algorithm, and leads to the case where # the two right-hand axes have the same height, but it is not 1/2 the height -# of the left-hand axes. This is consietent with how ``gridspec`` works +# of the left-hand axes. This is consistent with how ``gridspec`` works # without constrained layout. -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout="constrained") gs = gridspec.GridSpec(2, 2, figure=fig) ax = fig.add_subplot(gs[:, 0]) im = ax.pcolormesh(arr, **pc_kwargs) @@ -654,7 +701,7 @@ def docomplicated(suptitle=None): im = ax.pcolormesh(arr, **pc_kwargs) ax = fig.add_subplot(gs[1, 1]) im = ax.pcolormesh(arr, **pc_kwargs) -plot_children(fig, fig._layoutgrid, printit=False) +plot_children(fig) ####################################################################### # One case that requires finessing is if margins do not have any artists @@ -663,10 +710,11 @@ def docomplicated(suptitle=None): # so we take the maximum width of the margin widths that do have artists. # This makes all the axes have the same size: -fig = plt.figure(constrained_layout=True) +fig = plt.figure(layout="constrained") gs = fig.add_gridspec(2, 4) ax00 = fig.add_subplot(gs[0, 0:2]) ax01 = fig.add_subplot(gs[0, 2:]) ax10 = fig.add_subplot(gs[1, 1:3]) example_plot(ax10, fontsize=14) -plot_children(fig, fig._layoutgrid) +plot_children(fig) +plt.show() diff --git a/tutorials/intermediate/gridspec.py b/tutorials/intermediate/gridspec.py deleted file mode 100644 index d1d469ac02b7..000000000000 --- a/tutorials/intermediate/gridspec.py +++ /dev/null @@ -1,268 +0,0 @@ -""" -============================================================= -Customizing Figure Layouts Using GridSpec and Other Functions -============================================================= - -How to create grid-shaped combinations of axes. - - :func:`~matplotlib.pyplot.subplots` - Perhaps the primary function used to create figures and axes. - It's also similar to :func:`.matplotlib.pyplot.subplot`, - but creates and places all axes on the figure at once. See also - `matplotlib.figure.Figure.subplots`. - - :class:`~matplotlib.gridspec.GridSpec` - Specifies the geometry of the grid that a subplot will be - placed. The number of rows and number of columns of the grid - need to be set. Optionally, the subplot layout parameters - (e.g., left, right, etc.) can be tuned. - - :class:`~matplotlib.gridspec.SubplotSpec` - Specifies the location of the subplot in the given *GridSpec*. - - :func:`~matplotlib.pyplot.subplot2grid` - A helper function that is similar to - :func:`~matplotlib.pyplot.subplot`, - but uses 0-based indexing and let subplot to occupy multiple cells. - This function is not covered in this tutorial. - -""" - -import matplotlib.pyplot as plt -import matplotlib.gridspec as gridspec - -############################################################################ -# Basic Quickstart Guide -# ====================== -# -# These first two examples show how to create a basic 2-by-2 grid using -# both :func:`~matplotlib.pyplot.subplots` and :mod:`~matplotlib.gridspec`. -# -# Using :func:`~matplotlib.pyplot.subplots` is quite simple. -# It returns a :class:`~matplotlib.figure.Figure` instance and an array of -# :class:`~matplotlib.axes.Axes` objects. - -fig1, f1_axes = plt.subplots(ncols=2, nrows=2, constrained_layout=True) - -############################################################################ -# For a simple use case such as this, :mod:`~matplotlib.gridspec` is -# perhaps overly verbose. -# You have to create the figure and :class:`~matplotlib.gridspec.GridSpec` -# instance separately, then pass elements of gridspec instance to the -# :func:`~matplotlib.figure.Figure.add_subplot` method to create the axes -# objects. -# The elements of the gridspec are accessed in generally the same manner as -# numpy arrays. - -fig2 = plt.figure(constrained_layout=True) -spec2 = gridspec.GridSpec(ncols=2, nrows=2, figure=fig2) -f2_ax1 = fig2.add_subplot(spec2[0, 0]) -f2_ax2 = fig2.add_subplot(spec2[0, 1]) -f2_ax3 = fig2.add_subplot(spec2[1, 0]) -f2_ax4 = fig2.add_subplot(spec2[1, 1]) - -############################################################################# -# The power of gridspec comes in being able to create subplots that span -# rows and columns. Note the `NumPy slice syntax -# `_ -# for selecting the part of the gridspec each subplot will occupy. -# -# Note that we have also used the convenience method `.Figure.add_gridspec` -# instead of `.gridspec.GridSpec`, potentially saving the user an import, -# and keeping the namespace cleaner. - -fig3 = plt.figure(constrained_layout=True) -gs = fig3.add_gridspec(3, 3) -f3_ax1 = fig3.add_subplot(gs[0, :]) -f3_ax1.set_title('gs[0, :]') -f3_ax2 = fig3.add_subplot(gs[1, :-1]) -f3_ax2.set_title('gs[1, :-1]') -f3_ax3 = fig3.add_subplot(gs[1:, -1]) -f3_ax3.set_title('gs[1:, -1]') -f3_ax4 = fig3.add_subplot(gs[-1, 0]) -f3_ax4.set_title('gs[-1, 0]') -f3_ax5 = fig3.add_subplot(gs[-1, -2]) -f3_ax5.set_title('gs[-1, -2]') - -############################################################################# -# :mod:`~matplotlib.gridspec` is also indispensable for creating subplots -# of different widths via a couple of methods. -# -# The method shown here is similar to the one above and initializes a -# uniform grid specification, -# and then uses numpy indexing and slices to allocate multiple -# "cells" for a given subplot. - -fig4 = plt.figure(constrained_layout=True) -spec4 = fig4.add_gridspec(ncols=2, nrows=2) -anno_opts = dict(xy=(0.5, 0.5), xycoords='axes fraction', - va='center', ha='center') - -f4_ax1 = fig4.add_subplot(spec4[0, 0]) -f4_ax1.annotate('GridSpec[0, 0]', **anno_opts) -fig4.add_subplot(spec4[0, 1]).annotate('GridSpec[0, 1:]', **anno_opts) -fig4.add_subplot(spec4[1, 0]).annotate('GridSpec[1:, 0]', **anno_opts) -fig4.add_subplot(spec4[1, 1]).annotate('GridSpec[1:, 1:]', **anno_opts) - -############################################################################ -# Another option is to use the ``width_ratios`` and ``height_ratios`` -# parameters. These keyword arguments are lists of numbers. -# Note that absolute values are meaningless, only their relative ratios -# matter. That means that ``width_ratios=[2, 4, 8]`` is equivalent to -# ``width_ratios=[1, 2, 4]`` within equally wide figures. -# For the sake of demonstration, we'll blindly create the axes within -# ``for`` loops since we won't need them later. - -fig5 = plt.figure(constrained_layout=True) -widths = [2, 3, 1.5] -heights = [1, 3, 2] -spec5 = fig5.add_gridspec(ncols=3, nrows=3, width_ratios=widths, - height_ratios=heights) -for row in range(3): - for col in range(3): - ax = fig5.add_subplot(spec5[row, col]) - label = 'Width: {}\nHeight: {}'.format(widths[col], heights[row]) - ax.annotate(label, (0.1, 0.5), xycoords='axes fraction', va='center') - -############################################################################ -# Learning to use ``width_ratios`` and ``height_ratios`` is particularly -# useful since the top-level function :func:`~matplotlib.pyplot.subplots` -# accepts them within the ``gridspec_kw`` parameter. -# For that matter, any parameter accepted by -# :class:`~matplotlib.gridspec.GridSpec` can be passed to -# :func:`~matplotlib.pyplot.subplots` via the ``gridspec_kw`` parameter. -# This example recreates the previous figure without directly using a -# gridspec instance. - -gs_kw = dict(width_ratios=widths, height_ratios=heights) -fig6, f6_axes = plt.subplots(ncols=3, nrows=3, constrained_layout=True, - gridspec_kw=gs_kw) -for r, row in enumerate(f6_axes): - for c, ax in enumerate(row): - label = 'Width: {}\nHeight: {}'.format(widths[c], heights[r]) - ax.annotate(label, (0.1, 0.5), xycoords='axes fraction', va='center') - -############################################################################ -# The ``subplots`` and ``get_gridspec`` methods can be combined since it is -# sometimes more convenient to make most of the subplots using ``subplots`` -# and then remove some and combine them. Here we create a layout with -# the bottom two axes in the last column combined. - -fig7, f7_axs = plt.subplots(ncols=3, nrows=3) -gs = f7_axs[1, 2].get_gridspec() -# remove the underlying axes -for ax in f7_axs[1:, -1]: - ax.remove() -axbig = fig7.add_subplot(gs[1:, -1]) -axbig.annotate('Big Axes \nGridSpec[1:, -1]', (0.1, 0.5), - xycoords='axes fraction', va='center') - -fig7.tight_layout() - -############################################################################### -# Fine Adjustments to a Gridspec Layout -# ===================================== -# -# When a GridSpec is explicitly used, you can adjust the layout -# parameters of subplots that are created from the GridSpec. Note this -# option is not compatible with ``constrained_layout`` or -# `.Figure.tight_layout` which both adjust subplot sizes to fill the -# figure. - -fig8 = plt.figure(constrained_layout=False) -gs1 = fig8.add_gridspec(nrows=3, ncols=3, left=0.05, right=0.48, wspace=0.05) -f8_ax1 = fig8.add_subplot(gs1[:-1, :]) -f8_ax2 = fig8.add_subplot(gs1[-1, :-1]) -f8_ax3 = fig8.add_subplot(gs1[-1, -1]) - -############################################################################### -# This is similar to :func:`~matplotlib.pyplot.subplots_adjust`, but it only -# affects the subplots that are created from the given GridSpec. -# -# For example, compare the left and right sides of this figure: - -fig9 = plt.figure(constrained_layout=False) -gs1 = fig9.add_gridspec(nrows=3, ncols=3, left=0.05, right=0.48, - wspace=0.05) -f9_ax1 = fig9.add_subplot(gs1[:-1, :]) -f9_ax2 = fig9.add_subplot(gs1[-1, :-1]) -f9_ax3 = fig9.add_subplot(gs1[-1, -1]) - -gs2 = fig9.add_gridspec(nrows=3, ncols=3, left=0.55, right=0.98, - hspace=0.05) -f9_ax4 = fig9.add_subplot(gs2[:, :-1]) -f9_ax5 = fig9.add_subplot(gs2[:-1, -1]) -f9_ax6 = fig9.add_subplot(gs2[-1, -1]) - -############################################################################### -# GridSpec using SubplotSpec -# ========================== -# -# You can create GridSpec from the :class:`~matplotlib.gridspec.SubplotSpec`, -# in which case its layout parameters are set to that of the location of -# the given SubplotSpec. -# -# Note this is also available from the more verbose -# `.gridspec.GridSpecFromSubplotSpec`. - -fig10 = plt.figure(constrained_layout=True) -gs0 = fig10.add_gridspec(1, 2) - -gs00 = gs0[0].subgridspec(2, 3) -gs01 = gs0[1].subgridspec(3, 2) - -for a in range(2): - for b in range(3): - fig10.add_subplot(gs00[a, b]) - fig10.add_subplot(gs01[b, a]) - -############################################################################### -# A Complex Nested GridSpec using SubplotSpec -# =========================================== -# -# Here's a more sophisticated example of nested GridSpec where we put -# a box around each cell of the outer 4x4 grid, by hiding appropriate -# spines in each of the inner 3x3 grids. - -import numpy as np - - -def squiggle_xy(a, b, c, d, i=np.arange(0.0, 2*np.pi, 0.05)): - return np.sin(i*a)*np.cos(i*b), np.sin(i*c)*np.cos(i*d) - - -fig11 = plt.figure(figsize=(8, 8), constrained_layout=False) -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) - axs = inner_grid.subplots() # Create all subplots for the inner grid. - for (c, d), ax in np.ndenumerate(axs): - ax.plot(*squiggle_xy(a + 1, b + 1, c + 1, d + 1)) - ax.set(xticks=[], yticks=[]) - -# show only the outside spines -for ax in fig11.get_axes(): - ss = ax.get_subplotspec() - ax.spines.top.set_visible(ss.is_first_row()) - ax.spines.bottom.set_visible(ss.is_last_row()) - ax.spines.left.set_visible(ss.is_first_col()) - ax.spines.right.set_visible(ss.is_last_col()) - -plt.show() - -############################################################################# -# -# .. admonition:: References -# -# The use of the following functions, methods, classes and modules is shown -# in this example: -# -# - `matplotlib.pyplot.subplots` -# - `matplotlib.figure.Figure.add_gridspec` -# - `matplotlib.figure.Figure.add_subplot` -# - `matplotlib.gridspec.GridSpec` -# - `matplotlib.gridspec.SubplotSpec.subgridspec` -# - `matplotlib.gridspec.GridSpecFromSubplotSpec` diff --git a/tutorials/intermediate/imshow_extent.py b/tutorials/intermediate/imshow_extent.py index e1e87eb253f8..8065a06714b3 100644 --- a/tutorials/intermediate/imshow_extent.py +++ b/tutorials/intermediate/imshow_extent.py @@ -2,18 +2,18 @@ *origin* and *extent* in `~.Axes.imshow` ======================================== -:meth:`~.Axes.imshow` allows you to render an image (either a 2D array -which will be color-mapped (based on *norm* and *cmap*) or a 3D RGB(A) -array which will be used as-is) to a rectangular region in data space. -The orientation of the image in the final rendering is controlled by -the *origin* and *extent* kwargs (and attributes on the resulting -`~.AxesImage` instance) and the data limits of the axes. - -The *extent* kwarg controls the bounding box in data coordinates that -the image will fill specified as ``(left, right, bottom, top)`` in -**data coordinates**, the *origin* kwarg controls how the image fills -that bounding box, and the orientation in the final rendered image is -also affected by the axes limits. +:meth:`~.Axes.imshow` allows you to render an image (either a 2D array which +will be color-mapped (based on *norm* and *cmap*) or a 3D RGB(A) array which +will be used as-is) to a rectangular region in data space. The orientation of +the image in the final rendering is controlled by the *origin* and *extent* +keyword arguments (and attributes on the resulting `.AxesImage` instance) and +the data limits of the axes. + +The *extent* keyword arguments controls the bounding box in data coordinates +that the image will fill specified as ``(left, right, bottom, top)`` in **data +coordinates**, the *origin* keyword argument controls how the image fills that +bounding box, and the orientation in the final rendered image is also affected +by the axes limits. .. hint:: Most of the code below is used for adding labels and informative text to the plots. The described effects of *origin* and *extent* can be diff --git a/tutorials/intermediate/legend_guide.py b/tutorials/intermediate/legend_guide.py index c62a59d9c440..0fbe4b2d3526 100644 --- a/tutorials/intermediate/legend_guide.py +++ b/tutorials/intermediate/legend_guide.py @@ -47,22 +47,25 @@ not all artists can be added to a legend, at which point a "proxy" will have to be created (see :ref:`proxy_legend_handles` for further details). -Those artists with an empty string as label or with a label starting with -"_" will be ignored. +.. note:: + Artists with an empty string as label or with a label starting with an + underscore, "_", will be ignored. For full control of what is being added to the legend, it is common to pass the appropriate handles directly to :func:`legend`:: - line_up, = plt.plot([1, 2, 3], label='Line 2') - line_down, = plt.plot([3, 2, 1], label='Line 1') - plt.legend(handles=[line_up, line_down]) + fig, ax = plt.subplots() + line_up, = ax.plot([1, 2, 3], label='Line 2') + line_down, = ax.plot([3, 2, 1], label='Line 1') + ax.legend(handles=[line_up, line_down]) In some cases, it is not possible to set the label of the handle, so it is possible to pass through the list of labels to :func:`legend`:: - line_up, = plt.plot([1, 2, 3], label='Line 2') - line_down, = plt.plot([3, 2, 1], label='Line 1') - plt.legend([line_up, line_down], ['Line Up', 'Line Down']) + fig, ax = plt.subplots() + line_up, = ax.plot([1, 2, 3], label='Line 2') + line_down, = ax.plot([3, 2, 1], label='Line 1') + ax.legend([line_up, line_down], ['Line Up', 'Line Down']) .. _proxy_legend_handles: @@ -81,8 +84,9 @@ import matplotlib.patches as mpatches import matplotlib.pyplot as plt +fig, ax = plt.subplots() red_patch = mpatches.Patch(color='red', label='The red data') -plt.legend(handles=[red_patch]) +ax.legend(handles=[red_patch]) plt.show() @@ -92,9 +96,10 @@ import matplotlib.lines as mlines +fig, ax = plt.subplots() blue_line = mlines.Line2D([], [], color='blue', marker='*', markersize=15, label='Blue stars') -plt.legend(handles=[blue_line]) +ax.legend(handles=[blue_line]) plt.show() @@ -110,27 +115,74 @@ # figure's top right-hand corner instead of the axes' corner, simply specify # the corner's location and the coordinate system of that location:: # -# plt.legend(bbox_to_anchor=(1, 1), -# bbox_transform=plt.gcf().transFigure) +# ax.legend(bbox_to_anchor=(1, 1), +# bbox_transform=fig.transFigure) # # More examples of custom legend placement: -plt.subplot(211) -plt.plot([1, 2, 3], label="test1") -plt.plot([3, 2, 1], label="test2") - +fig, ax_dict = plt.subplot_mosaic([['top', 'top'], ['bottom', 'BLANK']], + empty_sentinel="BLANK") +ax_dict['top'].plot([1, 2, 3], label="test1") +ax_dict['top'].plot([3, 2, 1], label="test2") # Place a legend above this subplot, expanding itself to # fully use the given bounding box. -plt.legend(bbox_to_anchor=(0., 1.02, 1., .102), loc='lower left', - ncol=2, mode="expand", borderaxespad=0.) +ax_dict['top'].legend(bbox_to_anchor=(0., 1.02, 1., .102), loc='lower left', + ncols=2, mode="expand", borderaxespad=0.) -plt.subplot(223) -plt.plot([1, 2, 3], label="test1") -plt.plot([3, 2, 1], label="test2") +ax_dict['bottom'].plot([1, 2, 3], label="test1") +ax_dict['bottom'].plot([3, 2, 1], label="test2") # Place a legend to the right of this smaller subplot. -plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', borderaxespad=0.) +ax_dict['bottom'].legend(bbox_to_anchor=(1.05, 1), + loc='upper left', borderaxespad=0.) + +############################################################################## +# Figure legends +# -------------- +# +# Sometimes it makes more sense to place a legend relative to the (sub)figure +# rather than individual Axes. By using ``constrained_layout`` and +# specifying "outside" at the beginning of the *loc* keyword argument, +# the legend is drawn outside the Axes on the (sub)figure. + +fig, axs = plt.subplot_mosaic([['left', 'right']], layout='constrained') + +axs['left'].plot([1, 2, 3], label="test1") +axs['left'].plot([3, 2, 1], label="test2") + +axs['right'].plot([1, 2, 3], 'C2', label="test3") +axs['right'].plot([3, 2, 1], 'C3', label="test4") +# Place a legend to the right of this smaller subplot. +fig.legend(loc='outside upper right') + +############################################################################## +# This accepts a slightly different grammar than the normal *loc* keyword, +# where "outside right upper" is different from "outside upper right". +# +ucl = ['upper', 'center', 'lower'] +lcr = ['left', 'center', 'right'] +fig, ax = plt.subplots(figsize=(6, 4), layout='constrained', facecolor='0.7') + +ax.plot([1, 2], [1, 2], label='TEST') +# Place a legend to the right of this smaller subplot. +for loc in [ + 'outside upper left', + 'outside upper center', + 'outside upper right', + 'outside lower left', + 'outside lower center', + 'outside lower right']: + fig.legend(loc=loc, title=loc) + +fig, ax = plt.subplots(figsize=(6, 4), layout='constrained', facecolor='0.7') +ax.plot([1, 2], [1, 2], label='test') + +for loc in [ + 'outside left upper', + 'outside right upper', + 'outside left lower', + 'outside right lower']: + fig.legend(loc=loc, title=loc) -plt.show() ############################################################################### # Multiple legends on the same Axes @@ -144,17 +196,18 @@ # handles on the Axes. To keep old legend instances, we must add them # manually to the Axes: -line1, = plt.plot([1, 2, 3], label="Line 1", linestyle='--') -line2, = plt.plot([3, 2, 1], label="Line 2", linewidth=4) +fig, ax = plt.subplots() +line1, = ax.plot([1, 2, 3], label="Line 1", linestyle='--') +line2, = ax.plot([3, 2, 1], label="Line 2", linewidth=4) # Create a legend for the first line. -first_legend = plt.legend(handles=[line1], loc='upper right') +first_legend = ax.legend(handles=[line1], loc='upper right') -# Add the legend manually to the current Axes. -plt.gca().add_artist(first_legend) +# Add the legend manually to the Axes. +ax.add_artist(first_legend) # Create another legend for the second line. -plt.legend(handles=[line2], loc='lower right') +ax.legend(handles=[line2], loc='lower right') plt.show() @@ -188,15 +241,16 @@ from matplotlib.legend_handler import HandlerLine2D -line1, = plt.plot([3, 2, 1], marker='o', label='Line 1') -line2, = plt.plot([1, 2, 3], marker='o', label='Line 2') +fig, ax = plt.subplots() +line1, = ax.plot([3, 2, 1], marker='o', label='Line 1') +line2, = ax.plot([1, 2, 3], marker='o', label='Line 2') -plt.legend(handler_map={line1: HandlerLine2D(numpoints=4)}) +ax.legend(handler_map={line1: HandlerLine2D(numpoints=4)}) ############################################################################### # As you can see, "Line 1" now has 4 marker points, where "Line 2" has 2 (the # default). Try the above code, only change the map's key from ``line1`` to -# ``type(line1)``. Notice how now both `~.Line2D` instances get 4 markers. +# ``type(line1)``. Notice how now both `.Line2D` instances get 4 markers. # # Along with handlers for complex plot types such as errorbars, stem plots # and histograms, the default ``handler_map`` has a special ``tuple`` handler @@ -208,11 +262,12 @@ z = randn(10) -red_dot, = plt.plot(z, "ro", markersize=15) +fig, ax = plt.subplots() +red_dot, = ax.plot(z, "ro", markersize=15) # Put a white cross over some of the data. -white_cross, = plt.plot(z[:5], "w+", markeredgewidth=3, markersize=15) +white_cross, = ax.plot(z[:5], "w+", markeredgewidth=3, markersize=15) -plt.legend([red_dot, (red_dot, white_cross)], ["Attr A", "Attr A+B"]) +ax.legend([red_dot, (red_dot, white_cross)], ["Attr A", "Attr A+B"]) ############################################################################### # The `.legend_handler.HandlerTuple` class can also be used to @@ -220,11 +275,12 @@ from matplotlib.legend_handler import HandlerLine2D, HandlerTuple -p1, = plt.plot([1, 2.5, 3], 'r-d') -p2, = plt.plot([3, 2, 1], 'k-o') +fig, ax = plt.subplots() +p1, = ax.plot([1, 2.5, 3], 'r-d') +p2, = ax.plot([3, 2, 1], 'k-o') -l = plt.legend([(p1, p2)], ['Two keys'], numpoints=1, - handler_map={tuple: HandlerTuple(ndivide=None)}) +l = ax.legend([(p1, p2)], ['Two keys'], numpoints=1, + handler_map={tuple: HandlerTuple(ndivide=None)}) ############################################################################### # Implementing a custom legend handler @@ -253,9 +309,10 @@ def legend_artist(self, legend, orig_handle, fontsize, handlebox): handlebox.add_artist(patch) return patch +fig, ax = plt.subplots() -plt.legend([AnyObject()], ['My first handler'], - handler_map={AnyObject: AnyObjectHandler()}) +ax.legend([AnyObject()], ['My first handler'], + handler_map={AnyObject: AnyObjectHandler()}) ############################################################################### # Alternatively, had we wanted to globally accept ``AnyObject`` instances @@ -286,7 +343,9 @@ def create_artists(self, legend, orig_handle, c = mpatches.Circle((0.5, 0.5), 0.25, facecolor="green", edgecolor="red", linewidth=3) -plt.gca().add_patch(c) -plt.legend([c], ["An ellipse, not a rectangle"], - handler_map={mpatches.Circle: HandlerEllipse()}) +fig, ax = plt.subplots() + +ax.add_patch(c) +ax.legend([c], ["An ellipse, not a rectangle"], + handler_map={mpatches.Circle: HandlerEllipse()}) diff --git a/tutorials/intermediate/tight_layout_guide.py b/tutorials/intermediate/tight_layout_guide.py index bafd50e4ec28..27b7d0777126 100644 --- a/tutorials/intermediate/tight_layout_guide.py +++ b/tutorials/intermediate/tight_layout_guide.py @@ -46,9 +46,9 @@ def example_plot(ax, fontsize=12): ############################################################################### # To prevent this, the location of axes needs to be adjusted. For -# subplots, this can be done by adjusting the subplot params -# (:ref:`howto-subplots-adjust`). Matplotlib v1.1 introduced -# `.Figure.tight_layout` that does this automatically for you. +# subplots, this can be done manually by adjusting the subplot parameters +# using `.Figure.subplots_adjust`. `.Figure.tight_layout` does this +# automatically. fig, ax = plt.subplots() example_plot(ax, fontsize=24) @@ -117,7 +117,7 @@ def example_plot(ax, fontsize=12): ############################################################################### # It works with subplots created with # :func:`~matplotlib.pyplot.subplot2grid`. In general, subplots created -# from the gridspec (:doc:`/tutorials/intermediate/gridspec`) will work. +# from the gridspec (:doc:`/tutorials/intermediate/arranging_axes`) will work. plt.close('all') fig = plt.figure() @@ -161,7 +161,7 @@ def example_plot(ax, fontsize=12): # are rare cases where it is not. # # * ``pad=0`` can clip some texts by a few pixels. This may be a bug or -# a limitation of the current algorithm and it is not clear why it +# a limitation of the current algorithm, and it is not clear why it # happens. Meanwhile, use of pad larger than 0.3 is recommended. # # Use with GridSpec @@ -213,7 +213,7 @@ def example_plot(ax, fontsize=12): # ======================= # # Pre Matplotlib 2.2, legends and annotations were excluded from the bounding -# box calculations that decide the layout. Subsequently these artists were +# box calculations that decide the layout. Subsequently, these artists were # added to the calculation, but sometimes it is undesirable to include them. # For instance in this case it might be good to have the axes shrink a bit # to make room for the legend: @@ -276,7 +276,7 @@ def example_plot(ax, fontsize=12): ############################################################################### # Another option is to use the AxesGrid1 toolkit to -# explicitly create an axes for the colorbar. +# explicitly create an Axes for the colorbar. from mpl_toolkits.axes_grid1 import make_axes_locatable diff --git a/tutorials/introductory/animation_tutorial.py b/tutorials/introductory/animation_tutorial.py new file mode 100644 index 000000000000..a736580a36e4 --- /dev/null +++ b/tutorials/introductory/animation_tutorial.py @@ -0,0 +1,242 @@ +""" +=========================== +Animations using Matplotlib +=========================== + +Based on its plotting functionality, Matplotlib also provides an interface to +generate animations using the `~matplotlib.animation` module. An +animation is a sequence of frames where each frame corresponds to a plot on a +`~matplotlib.figure.Figure`. This tutorial covers a general guideline on +how to create such animations and the different options available. +""" + +import matplotlib.pyplot as plt +import matplotlib.animation as animation +import numpy as np + +############################################################################### +# Animation Classes +# ================= +# +# The animation process in Matplotlib can be thought of in 2 different ways: +# +# - `~matplotlib.animation.FuncAnimation`: Generate data for first +# frame and then modify this data for each frame to create an animated plot. +# +# - `~matplotlib.animation.ArtistAnimation`: Generate a list (iterable) +# of artists that will draw in each frame in the animation. +# +# `~matplotlib.animation.FuncAnimation` is more efficient in terms of +# speed and memory as it draws an artist once and then modifies it. On the +# other hand `~matplotlib.animation.ArtistAnimation` is flexible as it +# allows any iterable of artists to be animated in a sequence. +# +# ``FuncAnimation`` +# ----------------- +# +# The `~matplotlib.animation.FuncAnimation` class allows us to create an +# animation by passing a function that iteratively modifies the data of a plot. +# This is achieved by using the *setter* methods on various +# `~matplotlib.artist.Artist` (examples: `~matplotlib.lines.Line2D`, +# `~matplotlib.collections.PathCollection`, etc.). A usual +# `~matplotlib.animation.FuncAnimation` object takes a +# `~matplotlib.figure.Figure` that we want to animate and a function +# *func* that modifies the data plotted on the figure. It uses the *frames* +# parameter to determine the length of the animation. The *interval* parameter +# is used to determine time in milliseconds between drawing of two frames. +# Animating using `.FuncAnimation` would usually follow the following +# structure: +# +# - Plot the initial figure, including all the required artists. Save all the +# artists in variables so that they can be updated later on during the +# animation. +# - Create an animation function that updates the data in each artist to +# generate the new frame at each function call. +# - Create a `.FuncAnimation` object with the `.Figure` and the animation +# function, along with the keyword arguments that determine the animation +# properties. +# - Use `.animation.Animation.save` or `.pyplot.show` to save or show the +# animation. +# +# The update function uses the ``set_*`` function for different artists to +# modify the data. The following table shows a few plotting methods, the artist +# types they return and some methods that can be used to update them. +# +# ======================================== ============================= =========================== +# Plotting method Artist Set method +# ======================================== ============================= =========================== +# `.Axes.plot` `.lines.Line2D` `~.lines.Line2D.set_data` +# `.Axes.scatter` `.collections.PathCollection` `~.collections.\ +# PathCollection.set_offsets` +# `.Axes.imshow` `.image.AxesImage` ``AxesImage.set_data`` +# `.Axes.annotate` `.text.Annotation` `~.text.Annotation.\ +# update_positions` +# `.Axes.barh` `.patches.Rectangle` `~.Rectangle.set_angle`, +# `~.Rectangle.set_bounds`, +# `~.Rectangle.set_height`, +# `~.Rectangle.set_width`, +# `~.Rectangle.set_x`, +# `~.Rectangle.set_y`, +# `~.Rectangle.set_xy` +# `.Axes.fill` `.patches.Polygon` `~.Polygon.set_xy` +# `.Axes.add_patch`\(`.patches.Ellipse`\) `.patches.Ellipse` `~.Ellipse.set_angle`, +# `~.Ellipse.set_center`, +# `~.Ellipse.set_height`, +# `~.Ellipse.set_width` +# ======================================== ============================= =========================== +# +# Covering the set methods for all types of artists is beyond the scope of this +# tutorial but can be found in their respective documentations. An example of +# such update methods in use for `.Axes.scatter` and `.Axes.plot` is as follows. + +fig, ax = plt.subplots() +t = np.linspace(0, 3, 40) +g = -9.81 +v0 = 12 +z = g * t**2 / 2 + v0 * t + +v02 = 5 +z2 = g * t**2 / 2 + v02 * t + +scat = ax.scatter(t[0], z[0], c="b", s=5, label=f'v0 = {v0} m/s') +line2 = ax.plot(t[0], z2[0], label=f'v0 = {v02} m/s')[0] +ax.set(xlim=[0, 3], ylim=[-4, 10], xlabel='Time [s]', ylabel='Z [m]') +ax.legend() + + +def update(frame): + # for each frame, update the data stored on each artist. + x = t[:frame] + y = z[:frame] + # update the scatter plot: + data = np.stack([x, y]).T + scat.set_offsets(data) + # update the line plot: + line2.set_xdata(t[:frame]) + line2.set_ydata(z2[:frame]) + return (scat, line2) + + +ani = animation.FuncAnimation(fig=fig, func=update, frames=40, interval=30) +plt.show() + + +############################################################################### +# ``ArtistAnimation`` +# ------------------- +# +# `~matplotlib.animation.ArtistAnimation` can be used +# to generate animations if there is data stored on various different artists. +# This list of artists is then converted frame by frame into an animation. For +# example, when we use `.Axes.barh` to plot a bar-chart, it creates a number of +# artists for each of the bar and error bars. To update the plot, one would +# need to update each of the bars from the container individually and redraw +# them. Instead, `.animation.ArtistAnimation` can be used to plot each frame +# individually and then stitched together to form an animation. A barchart race +# is a simple example for this. + + +fig, ax = plt.subplots() +rng = np.random.default_rng(19680801) +data = np.array([20, 20, 20, 20]) +x = np.array([1, 2, 3, 4]) + +artists = [] +colors = ['tab:blue', 'tab:red', 'tab:green', 'tab:purple'] +for i in range(20): + data += rng.integers(low=0, high=10, size=data.shape) + container = ax.barh(x, data, color=colors) + artists.append(container) + + +ani = animation.ArtistAnimation(fig=fig, artists=artists, interval=400) +plt.show() + +############################################################################### +# Animation Writers +# ================= +# +# Animation objects can be saved to disk using various multimedia writers +# (ex: Pillow, *ffpmeg*, *imagemagick*). Not all video formats are supported +# by all writers. There are 4 major types of writers: +# +# - `~matplotlib.animation.PillowWriter` - Uses the Pillow library to +# create the animation. +# +# - `~matplotlib.animation.HTMLWriter` - Used to create JavaScript-based +# animations. +# +# - Pipe-based writers - `~matplotlib.animation.FFMpegWriter` and +# `~matplotlib.animation.ImageMagickWriter` are pipe based writers. +# These writers pipe each frame to the utility (*ffmpeg* / *imagemagick*) +# which then stitches all of them together to create the animation. +# +# - File-based writers - `~matplotlib.animation.FFMpegFileWriter` and +# `~matplotlib.animation.ImageMagickFileWriter` are examples of +# file-based writers. These writers are slower than their pipe-based +# alternatives but are more useful for debugging as they save each frame in +# a file before stitching them together into an animation. +# +# Saving Animations +# ----------------- +# +# .. list-table:: +# :header-rows: 1 +# +# * - Writer +# - Supported Formats +# * - `~matplotlib.animation.PillowWriter` +# - .gif, .apng, .webp +# * - `~matplotlib.animation.HTMLWriter` +# - .htm, .html, .png +# * - | `~matplotlib.animation.FFMpegWriter` +# | `~matplotlib.animation.FFMpegFileWriter` +# - All formats supported by |ffmpeg|_: ``ffmpeg -formats`` +# * - | `~matplotlib.animation.ImageMagickWriter` +# | `~matplotlib.animation.ImageMagickFileWriter` +# - All formats supported by |imagemagick|_: ``magick -list format`` +# +# .. _ffmpeg: https://www.ffmpeg.org/general.html#Supported-File-Formats_002c-Codecs-or-Features +# .. |ffmpeg| replace:: *ffmpeg* +# +# .. _imagemagick: https://imagemagick.org/script/formats.php#supported +# .. |imagemagick| replace:: *imagemagick* +# +# To save animations using any of the writers, we can use the +# `.animation.Animation.save` method. It takes the *filename* that we want to +# save the animation as and the *writer*, which is either a string or a writer +# object. It also takes an *fps* argument. This argument is different than the +# *interval* argument that `~.animation.FuncAnimation` or +# `~.animation.ArtistAnimation` uses. *fps* determines the frame rate that the +# **saved** animation uses, whereas *interval* determines the frame rate that +# the **displayed** animation uses. +# +# Below are a few examples that show how to save an animation with different +# writers. +# +# +# Pillow writers:: +# +# ani.save(filename="/tmp/pillow_example.gif", writer="pillow") +# ani.save(filename="/tmp/pillow_example.apng", writer="pillow") +# +# HTML writers:: +# +# ani.save(filename="/tmp/html_example.html", writer="html") +# ani.save(filename="/tmp/html_example.htm", writer="html") +# ani.save(filename="/tmp/html_example.png", writer="html") +# +# FFMpegWriter:: +# +# ani.save(filename="/tmp/ffmpeg_example.mkv", writer="ffmpeg") +# ani.save(filename="/tmp/ffmpeg_example.mp4", writer="ffmpeg") +# ani.save(filename="/tmp/ffmpeg_example.mjpeg", writer="ffmpeg") +# +# Imagemagick writers:: +# +# ani.save(filename="/tmp/imagemagick_example.gif", writer="imagemagick") +# ani.save(filename="/tmp/imagemagick_example.webp", writer="imagemagick") +# ani.save(filename="apng:/tmp/imagemagick_example.apng", +# writer="imagemagick", extra_args=["-quality", "100"]) +# +# (the ``extra_args`` for *apng* are needed to reduce filesize by ~10x) diff --git a/tutorials/introductory/customizing.py b/tutorials/introductory/customizing.py index 993692bc8dd0..10fc21d2187b 100644 --- a/tutorials/introductory/customizing.py +++ b/tutorials/introductory/customizing.py @@ -1,32 +1,113 @@ """ .. redirect-from:: /users/customizing +===================================================== Customizing Matplotlib with style sheets and rcParams ===================================================== Tips for customizing the properties and default styles of Matplotlib. -Using style sheets ------------------- +There are three ways to customize Matplotlib: + +1. :ref:`Setting rcParams at runtime`. +2. :ref:`Using style sheets`. +3. :ref:`Changing your matplotlibrc file`. + +Setting rcParams at runtime takes precedence over style sheets, style +sheets take precedence over :file:`matplotlibrc` files. -The :mod:`.style` package adds support for easy-to-switch plotting -"styles" with the same parameters as a :ref:`matplotlib rc -` file (which is read at startup to -configure Matplotlib). +.. _customizing-with-dynamic-rc-settings: -There are a number of pre-defined styles :doc:`provided by Matplotlib -`. For -example, there's a pre-defined style called "ggplot", which emulates the -aesthetics of ggplot_ (a popular plotting package for R_). To use this style, -just add: +Runtime rc settings +=================== + +You can dynamically change the default rc (runtime configuration) +settings in a python script or interactively from the python shell. All +rc settings are stored in a dictionary-like variable called +:data:`matplotlib.rcParams`, which is global to the matplotlib package. +See `matplotlib.rcParams` for a full list of configurable rcParams. +rcParams can be modified directly, for example: """ import numpy as np import matplotlib.pyplot as plt import matplotlib as mpl from cycler import cycler -plt.style.use('ggplot') +mpl.rcParams['lines.linewidth'] = 2 +mpl.rcParams['lines.linestyle'] = '--' data = np.random.randn(50) +plt.plot(data) + +############################################################################### +# 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 + +############################################################################### +# Matplotlib also provides a couple of convenience functions for modifying rc +# settings. `matplotlib.rc` can be used to modify multiple +# settings in a single group at once, using keyword arguments: + +mpl.rc('lines', linewidth=4, linestyle='-.') +plt.plot(data) + +############################################################################### +# Temporary rc settings +# --------------------- +# +# The :data:`matplotlib.rcParams` object can also be changed temporarily using +# the `matplotlib.rc_context` context manager: + +with mpl.rc_context({'lines.linewidth': 2, 'lines.linestyle': ':'}): + plt.plot(data) + +############################################################################### +# `matplotlib.rc_context` can also be used as a decorator to modify the +# defaults within a function: + + +@mpl.rc_context({'lines.linewidth': 3, 'lines.linestyle': '-'}) +def plotting_function(): + plt.plot(data) + +plotting_function() + +############################################################################### +# `matplotlib.rcdefaults` will restore the standard Matplotlib +# default settings. +# +# There is some degree of validation when setting the values of rcParams, see +# :mod:`matplotlib.rcsetup` for details. + +############################################################################### +# .. _customizing-with-style-sheets: +# +# Using style sheets +# ================== +# +# Another way to change the visual appearance of plots is to set the +# rcParams in a so-called style sheet and import that style sheet with +# `matplotlib.style.use`. In this way you can switch easily between +# different styles by simply changing the imported style sheet. A style +# sheets looks the same as a :ref:`matplotlibrc` +# file, but in a style sheet you can only set rcParams that are related +# to the actual style of a plot. Other rcParams, like *backend*, will be +# ignored. :file:`matplotlibrc` files support all rcParams. The +# rationale behind this is to make style sheets portable between +# different machines without having to worry about dependencies which +# might or might not be installed on another machine. For a full list of +# rcParams see `matplotlib.rcParams`. For a list of rcParams that are +# ignored in style sheets see `matplotlib.style.use`. +# +# There are a number of pre-defined styles :doc:`provided by Matplotlib +# `. For +# example, there's a pre-defined style called "ggplot", which emulates the +# aesthetics of ggplot_ (a popular plotting package for R_). To use this +# style, add: + +plt.style.use('ggplot') ############################################################################### # To list all available styles, use: @@ -56,6 +137,17 @@ # >>> import matplotlib.pyplot as plt # >>> plt.style.use('./images/presentation.mplstyle') # +# +# Distributing styles +# ------------------- +# +# You can include style sheets into standard importable Python packages (which +# can be e.g. distributed on PyPI). If your package is importable as +# ``import mypackage``, with a ``mypackage/__init__.py`` module, and you add +# a ``mypackage/presentation.mplstyle`` style sheet, then it can be used as +# ``plt.style.use("mypackage.presentation")``. Subpackages (e.g. +# ``dotted.package.name``) are also supported. +# # Alternatively, you can make your style known to Matplotlib by placing # your ``.mplstyle`` file into ``mpl_configdir/stylelib``. You # can then load your custom style sheet with a call to @@ -104,59 +196,18 @@ plt.show() ############################################################################### -# .. _matplotlib-rcparams: -# -# Matplotlib rcParams -# =================== -# -# .. _customizing-with-dynamic-rc-settings: -# -# Dynamic rc settings -# ------------------- -# -# You can also dynamically change the default rc settings in a python script or -# interactively from the python shell. All of the rc settings are stored in a -# dictionary-like variable called :data:`matplotlib.rcParams`, which is global to -# the matplotlib package. rcParams can be modified directly, for example: - -mpl.rcParams['lines.linewidth'] = 2 -mpl.rcParams['lines.linestyle'] = '--' -plt.plot(data) - -############################################################################### -# 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 - -############################################################################### -# Matplotlib also provides a couple of convenience functions for modifying rc -# settings. `matplotlib.rc` can be used to modify multiple -# settings in a single group at once, using keyword arguments: - -mpl.rc('lines', linewidth=4, linestyle='-.') -plt.plot(data) - -############################################################################### -# `matplotlib.rcdefaults` will restore the standard Matplotlib -# default settings. -# -# There is some degree of validation when setting the values of rcParams, see -# :mod:`matplotlib.rcsetup` for details. -# # .. _customizing-with-matplotlibrc-files: # # The :file:`matplotlibrc` file -# ----------------------------- +# ============================= # # Matplotlib uses :file:`matplotlibrc` configuration files to customize all # kinds of properties, which we call 'rc settings' or 'rc parameters'. You can # control the defaults of almost every property in Matplotlib: figure size and # DPI, line width, color and style, axes, axis and grid properties, text and -# font properties and so on. When a URL or path is not specified with a call to -# ``style.use('/.mplstyle')``, Matplotlib looks for -# :file:`matplotlibrc` in four locations, in the following order: +# font properties and so on. The :file:`matplotlibrc` is read at startup to +# configure Matplotlib. Matplotlib looks for :file:`matplotlibrc` in four +# locations, in the following order: # # 1. :file:`matplotlibrc` in the current working directory, usually used for # specific customizations that you do not want to apply elsewhere. @@ -177,14 +228,18 @@ # # 4. :file:`{INSTALL}/matplotlib/mpl-data/matplotlibrc`, where # :file:`{INSTALL}` is something like -# :file:`/usr/lib/python3.7/site-packages` on Linux, and maybe -# :file:`C:\\Python37\\Lib\\site-packages` on Windows. Every time you +# :file:`/usr/lib/python3.9/site-packages` on Linux, and maybe +# :file:`C:\\Python39\\Lib\\site-packages` on Windows. Every time you # install matplotlib, this file will be overwritten, so if you want # your customizations to be saved, please move this file to your # user-specific matplotlib directory. # -# Once a :file:`matplotlibrc` file has been found, it will *not* search any of -# the other paths. +# Once a :file:`matplotlibrc` file has been found, it will *not* search +# any of the other paths. When a +# :ref:`style sheet` is given with +# ``style.use('/.mplstyle')``, settings specified in +# the style sheet take precedence over settings in the +# :file:`matplotlibrc` file. # # To display where the currently active :file:`matplotlibrc` file was # loaded from, one can do the following:: @@ -193,14 +248,15 @@ # >>> matplotlib.matplotlib_fname() # '/home/foo/.config/matplotlib/matplotlibrc' # -# See below for a sample :ref:`matplotlibrc file`. +# See below for a sample :ref:`matplotlibrc file` +# and see `matplotlib.rcParams` for a full list of configurable rcParams. # # .. _matplotlibrc-sample: # -# A sample matplotlibrc file -# ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# The default :file:`matplotlibrc` file +# ------------------------------------- # -# .. literalinclude:: ../../../matplotlibrc.template +# .. literalinclude:: ../../../lib/matplotlib/mpl-data/matplotlibrc # # # .. _ggplot: https://ggplot2.tidyverse.org/ diff --git a/tutorials/introductory/images.py b/tutorials/introductory/images.py index 858c3ccd262f..7673037c4a9c 100644 --- a/tutorials/introductory/images.py +++ b/tutorials/introductory/images.py @@ -33,23 +33,24 @@ notebook. This has important implications for interactivity. For inline plotting, commands in cells below the cell that outputs a plot will not affect the plot. For example, changing the colormap is not possible from cells below the cell that creates a plot. -However, for other backends, such as Qt5, that open a separate window, +However, for other backends, such as Qt, that open a separate window, cells below those that create the plot will change the plot - it is a live object in memory. -This tutorial will use Matplotlib's imperative-style plotting -interface, pyplot. This interface maintains global state, and is very -useful for quickly and easily experimenting with various plot -settings. The alternative is the object-oriented interface, which is also -very powerful, and generally more suitable for large application -development. If you'd like to learn about the object-oriented -interface, a great place to start is our :doc:`Usage guide -`. For now, let's get on -with the imperative-style approach: +This tutorial will use Matplotlib's implicit plotting interface, pyplot. This +interface maintains global state, and is very useful for quickly and easily +experimenting with various plot settings. The alternative is the explicit, +which is more suitable for large application development. For an explanation +of the tradeoffs between the implicit and explicit interfaces see +:ref:`api_interfaces` and the :doc:`Quick start guide +` to start using the explicit interface. +For now, let's get on with the implicit approach: + """ import matplotlib.pyplot as plt -import matplotlib.image as mpimg +import numpy as np +from PIL import Image ############################################################################### # .. _importing_data: @@ -69,29 +70,20 @@ # on where you get your data, the other kinds of image that you'll most # likely encounter are RGBA images, which allow for transparency, or # single-channel grayscale (luminosity) images. Download `stinkbug.png -# `_ +# `_ # to your computer for the rest of this tutorial. # -# And here we go... +# We use Pillow to open an image (with `PIL.Image.open`), and immediately +# convert the `PIL.Image.Image` object into an 8-bit (``dtype=uint8``) numpy +# array. -img = mpimg.imread('../../doc/_static/stinkbug.png') -print(img) +img = np.asarray(Image.open('../../doc/_static/stinkbug.png')) +print(repr(img)) ############################################################################### -# Note the dtype there - float32. Matplotlib has rescaled the 8 bit -# data from each channel to floating point data between 0.0 and 1.0. As -# a side note, the only datatype that Pillow can work with is uint8. -# Matplotlib plotting can handle float32 and uint8, but image -# reading/writing for any format other than PNG is limited to uint8 -# data. Why 8 bits? Most displays can only render 8 bits per channel -# worth of color gradation. Why can they only render 8 bits/channel? -# Because that's about all the human eye can see. More here (from a -# photography standpoint): `Luminous Landscape bit depth tutorial -# `_. -# # Each inner list represents a pixel. Here, with an RGB image, there # are 3 values. Since it's a black and white image, R, G, and B are all -# similar. An RGBA (where A is alpha, or transparency), has 4 values +# similar. An RGBA (where A is alpha, or transparency) has 4 values # per inner list, and a simple luminance image just has one value (and # is thus only a 2-D array, not a 3-D array). For RGB and RGBA images, # Matplotlib supports float32 and uint8 data types. For grayscale, @@ -127,13 +119,11 @@ # Pseudocolor is only relevant to single-channel, grayscale, luminosity # images. We currently have an RGB image. Since R, G, and B are all # similar (see for yourself above or in your data), we can just pick one -# channel of our data: +# channel of our data using array slicing (you can read more in the +# `Numpy tutorial `_): lum_img = img[:, :, 0] - -# This is array slicing. You can read more in the `Numpy tutorial -# `_. - plt.imshow(lum_img) ############################################################################### @@ -188,7 +178,7 @@ # interesting regions is the histogram. To create a histogram of our # image data, we use the :func:`~matplotlib.pyplot.hist` function. -plt.hist(lum_img.ravel(), bins=256, range=(0.0, 1.0), fc='k', ec='k') +plt.hist(lum_img.ravel(), bins=range(256), fc='k', ec='k') ############################################################################### # Most often, the "interesting" part of the image is around the peak, @@ -196,29 +186,23 @@ # below the peak. In our histogram, it looks like there's not much # useful information in the high end (not many white things in the # image). Let's adjust the upper limit, so that we effectively "zoom in -# on" part of the histogram. We do this by passing the clim argument to -# imshow. You could also do this by calling the -# :meth:`~matplotlib.cm.ScalarMappable.set_clim` method of the image plot -# object, but make sure that you do so in the same cell as your plot -# command when working with the Jupyter Notebook - it will not change -# plots from earlier cells. +# on" part of the histogram. We do this by setting *clim*, the colormap +# limits. # -# You can specify the clim in the call to ``plot``. +# This can be done by passing a *clim* keyword argument in the call to +# ``imshow``. -imgplot = plt.imshow(lum_img, clim=(0.0, 0.7)) +plt.imshow(lum_img, clim=(0, 175)) ############################################################################### -# You can also specify the clim using the returned object -fig = plt.figure() -ax = fig.add_subplot(1, 2, 1) -imgplot = plt.imshow(lum_img) -ax.set_title('Before') -plt.colorbar(ticks=[0.1, 0.3, 0.5, 0.7], orientation='horizontal') -ax = fig.add_subplot(1, 2, 2) +# This can also be done by calling the +# :meth:`~matplotlib.cm.ScalarMappable.set_clim` method of the returned image +# plot object, but make sure that you do so in the same cell as your plot +# command when working with the Jupyter Notebook - it will not change +# plots from earlier cells. + imgplot = plt.imshow(lum_img) -imgplot.set_clim(0.0, 0.7) -ax.set_title('After') -plt.colorbar(ticks=[0.1, 0.3, 0.5, 0.7], orientation='horizontal') +imgplot.set_clim(0, 175) ############################################################################### # .. _Interpolation: @@ -242,19 +226,17 @@ # We'll use the Pillow library that we used to load the image also to resize # the image. -from PIL import Image - img = Image.open('../../doc/_static/stinkbug.png') -img.thumbnail((64, 64), Image.ANTIALIAS) # resizes image in-place +img.thumbnail((64, 64)) # resizes image in-place imgplot = plt.imshow(img) ############################################################################### -# Here we have the default interpolation, bilinear, since we did not +# Here we use the default interpolation ("nearest"), since we did not # give :func:`~matplotlib.pyplot.imshow` any interpolation argument. # -# Let's try some others. Here's "nearest", which does no interpolation. +# Let's try some others. Here's "bilinear": -imgplot = plt.imshow(img, interpolation="nearest") +imgplot = plt.imshow(img, interpolation="bilinear") ############################################################################### # and bicubic: diff --git a/tutorials/introductory/lifecycle.py b/tutorials/introductory/lifecycle.py index 00732d4acaed..3d3a1036ff2b 100644 --- a/tutorials/introductory/lifecycle.py +++ b/tutorials/introductory/lifecycle.py @@ -13,41 +13,45 @@ .. note:: This tutorial is based on - `this excellent blog post `_ + `this excellent blog post + `_ by Chris Moffitt. It was transformed into this tutorial by Chris Holdgraf. -A note on the Object-Oriented API vs. Pyplot -============================================ +A note on the explicit vs. implicit interfaces +============================================== -Matplotlib has two interfaces. The first is an object-oriented (OO) -interface. In this case, we utilize an instance of :class:`axes.Axes` -in order to render visualizations on an instance of :class:`figure.Figure`. +Matplotlib has two interfaces. For an explanation of the trade-offs between the +explicit and implicit interfaces see :ref:`api_interfaces`. -The second is based on MATLAB and uses a state-based interface. This is -encapsulated in the :mod:`.pyplot` module. See the :doc:`pyplot tutorials -` for a more in-depth look at the pyplot -interface. +In the explicit object-oriented (OO) interface we directly utilize instances of +:class:`axes.Axes` to build up the visualization in an instance of +:class:`figure.Figure`. In the implicit interface, inspired by and modeled on +MATLAB, we use a global state-based interface which is encapsulated in the +:mod:`.pyplot` module to plot to the "current Axes". See the :doc:`pyplot +tutorials ` for a more in-depth look at the +pyplot interface. Most of the terms are straightforward but the main thing to remember is that: -* The Figure is the final image that may contain 1 or more Axes. -* The Axes represent an individual plot (don't confuse this with the word - "axis", which refers to the x/y axis of a plot). +* The `.Figure` is the final image, and may contain one or more `~.axes.Axes`. +* The `~.axes.Axes` represents an individual plot (not to be confused with + `~.axis.Axis`, which refers to the x-, y-, or z-axis of a plot). We call methods that do the plotting directly from the Axes, which gives us much more flexibility and power in customizing our plot. .. note:: - In general, try to use the object-oriented interface over the pyplot - interface. + In general, use the explicit interface over the implicit pyplot interface + for plotting. Our data ======== We'll use the data from the post from which this tutorial was derived. It contains sales information for a number of companies. + """ # sphinx_gallery_thumbnail_number = 10 @@ -169,14 +173,14 @@ ############################################################################### # We can also adjust the size of this plot using the :func:`pyplot.subplots` -# function. We can do this with the ``figsize`` kwarg. +# function. We can do this with the *figsize* keyword argument. # # .. note:: # -# While indexing in NumPy follows the form (row, column), the figsize -# kwarg follows the form (width, height). This follows conventions in -# visualization, which unfortunately are different from those of linear -# algebra. +# While indexing in NumPy follows the form (row, column), the *figsize* +# keyword argument follows the form (width, height). This follows +# conventions in visualization, which unfortunately are different from those +# of linear algebra. fig, ax = plt.subplots(figsize=(8, 4)) ax.barh(group_names, group_data) @@ -198,7 +202,7 @@ def currency(x, pos): - """The two args are the value and tick position""" + """The two arguments are the value and tick position""" if x >= 1e6: s = '${:1.1f}M'.format(x*1e-6) else: diff --git a/tutorials/introductory/pyplot.py b/tutorials/introductory/pyplot.py index 1a8167c0616c..163c74b5b31c 100644 --- a/tutorials/introductory/pyplot.py +++ b/tutorials/introductory/pyplot.py @@ -3,19 +3,21 @@ Pyplot tutorial =============== -An introduction to the pyplot interface. +An introduction to the pyplot interface. Please also see +:doc:`/tutorials/introductory/quick_start` for an overview of how Matplotlib +works and :ref:`api_interfaces` for an explanation of the trade-offs between the +supported user APIs. + """ ############################################################################### -# Intro to pyplot -# =============== +# Introduction to pyplot +# ====================== # -# :mod:`matplotlib.pyplot` is a collection of functions -# that make matplotlib work like MATLAB. -# Each ``pyplot`` function makes -# some change to a figure: e.g., creates a figure, creates a plotting area -# in a figure, plots some lines in a plotting area, decorates the plot -# with labels, etc. +# :mod:`matplotlib.pyplot` is a collection of functions that make matplotlib +# work like MATLAB. Each ``pyplot`` function makes some change to a figure: +# e.g., creates a figure, creates a plotting area in a figure, plots some lines +# in a plotting area, decorates the plot with labels, etc. # # In :mod:`matplotlib.pyplot` various states are preserved # across function calls, so that it keeps track of things like @@ -27,10 +29,11 @@ # # .. note:: # -# the pyplot API is generally less-flexible than the object-oriented API. -# Most of the function calls you see here can also be called as methods -# from an ``Axes`` object. We recommend browsing the tutorials and -# examples to see how this works. +# The implicit pyplot API is generally less verbose but also not as flexible as the +# explicit API. Most of the function calls you see here can also be called +# as methods from an ``Axes`` object. We recommend browsing the tutorials +# and examples to see how this works. See :ref:`api_interfaces` for an +# explanation of the trade-off of the supported user APIs. # # Generating visualizations with pyplot is very quick: @@ -45,7 +48,7 @@ # `~.pyplot.plot`, matplotlib assumes it is a # sequence of y values, and automatically generates the x values for # you. Since python ranges start with 0, the default x vector has the -# same length as y but starts with 0. Hence the x data are +# same length as y but starts with 0; therefore, the x data are # ``[0, 1, 2, 3]``. # # `~.pyplot.plot` is a versatile function, and will take an arbitrary number of @@ -77,7 +80,7 @@ # # If matplotlib were limited to working with lists, it would be fairly # useless for numeric processing. Generally, you will use `numpy -# `_ arrays. In fact, all sequences are +# `_ arrays. In fact, all sequences are # converted to numpy arrays internally. The example below illustrates # plotting several lines with different format styles in one function call # using arrays. @@ -101,7 +104,7 @@ # access particular variables with strings. For example, with # `numpy.recarray` or `pandas.DataFrame`. # -# Matplotlib allows you provide such an object with +# Matplotlib allows you to provide such an object with # the ``data`` keyword argument. If provided, then you may generate plots with # the strings corresponding to these variables. @@ -150,7 +153,7 @@ # antialiased, etc; see `matplotlib.lines.Line2D`. There are # several ways to set line properties # -# * Use keyword args:: +# * Use keyword arguments:: # # plt.plot(x, y, linewidth=2.0) # @@ -171,7 +174,7 @@ # MATLAB-style string/value pairs:: # # lines = plt.plot(x1, y1, x2, y2) -# # use keyword args +# # use keyword arguments # plt.setp(lines, color='r', linewidth=2.0) # # or MATLAB style string value pairs # plt.setp(lines, 'color', 'r', 'linewidth', 2.0) @@ -259,7 +262,7 @@ def f(t): ############################################################################### # The `~.pyplot.figure` call here is optional because a figure will be created -# if none exists, just as an axes will be created (equivalent to an explicit +# if none exists, just as an Axes will be created (equivalent to an explicit # ``subplot()`` call) if none exists. # The `~.pyplot.subplot` call specifies ``numrows, # numcols, plot_number`` where ``plot_number`` ranges from 1 to @@ -268,15 +271,14 @@ def f(t): # to ``subplot(2, 1, 1)``. # # You can create an arbitrary number of subplots -# and axes. If you want to place an axes manually, i.e., not on a +# and axes. If you want to place an Axes manually, i.e., not on a # rectangular grid, use `~.pyplot.axes`, # which allows you to specify the location as ``axes([left, bottom, # width, height])`` where all values are in fractional (0 to 1) # coordinates. See :doc:`/gallery/subplots_axes_and_figures/axes_demo` for an example of -# placing axes manually and :doc:`/gallery/subplots_axes_and_figures/subplot_demo` for an +# placing axes manually and :doc:`/gallery/subplots_axes_and_figures/subplot` for an # example with lots of subplots. # -# # You can create multiple figures by using multiple # `~.pyplot.figure` calls with an increasing figure # number. Of course, each figure can contain as many axes and subplots @@ -293,15 +295,17 @@ def f(t): # plt.figure(2) # a second figure # plt.plot([4, 5, 6]) # creates a subplot() by default # -# plt.figure(1) # figure 1 current; subplot(212) still current -# plt.subplot(211) # make subplot(211) in figure1 current +# plt.figure(1) # first figure current; +# # subplot(212) still current +# plt.subplot(211) # make subplot(211) in the first figure +# # current # plt.title('Easy as 1, 2, 3') # subplot 211 title # # You can clear the current figure with `~.pyplot.clf` # and the current axes with `~.pyplot.cla`. If you find # it annoying that states (specifically the current image, figure and axes) # are being maintained for you behind the scenes, don't despair: this is just a thin -# stateful wrapper around an object oriented API, which you can use +# stateful wrapper around an object-oriented API, which you can use # instead (see :doc:`/tutorials/intermediate/artists`) # # If you are making lots of figures, you need to be aware of one @@ -327,7 +331,7 @@ def f(t): x = mu + sigma * np.random.randn(10000) # the histogram of the data -n, bins, patches = plt.hist(x, 50, density=1, facecolor='g', alpha=0.75) +n, bins, patches = plt.hist(x, 50, density=True, facecolor='g', alpha=0.75) plt.xlabel('Smarts') @@ -351,7 +355,7 @@ def f(t): # Using mathematical expressions in text # -------------------------------------- # -# matplotlib accepts TeX equation expressions in any text expression. +# Matplotlib accepts TeX equation expressions in any text expression. # For example to write the expression :math:`\sigma_i=15` in the title, # you can write a TeX expression surrounded by dollar signs:: # @@ -361,9 +365,9 @@ def f(t): # that the string is a *raw* string and not to treat backslashes as # python escapes. matplotlib has a built-in TeX expression parser and # layout engine, and ships its own math fonts -- for details see -# :doc:`/tutorials/text/mathtext`. Thus you can use mathematical text across platforms -# without requiring a TeX installation. For those who have LaTeX and -# dvipng installed, you can also use LaTeX to format your text and +# :doc:`/tutorials/text/mathtext`. Thus, you can use mathematical text across +# platforms without requiring a TeX installation. For those who have LaTeX +# and dvipng installed, you can also use LaTeX to format your text and # incorporate the output directly into your display figures or saved # postscript -- see :doc:`/tutorials/text/usetex`. # @@ -411,7 +415,7 @@ def f(t): # # plt.xscale('log') # -# An example of four plots with the same data and different scales for the y axis +# An example of four plots with the same data and different scales for the y-axis # is shown below. # Fixing random state for reproducibility @@ -461,5 +465,5 @@ def f(t): plt.show() ############################################################################### -# It is also possible to add your own scale, see :ref:`adding-new-scales` for +# It is also possible to add your own scale, see `matplotlib.scale` for # details. diff --git a/tutorials/introductory/quick_start.py b/tutorials/introductory/quick_start.py new file mode 100644 index 000000000000..818f44847239 --- /dev/null +++ b/tutorials/introductory/quick_start.py @@ -0,0 +1,587 @@ +""" +***************** +Quick start guide +***************** + +This tutorial covers some basic usage patterns and best practices to +help you get started with Matplotlib. + +.. redirect-from:: /tutorials/introductory/usage + +""" + +# sphinx_gallery_thumbnail_number = 3 +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np + +############################################################################## +# +# A simple example +# ================ +# +# Matplotlib graphs your data on `.Figure`\s (e.g., windows, Jupyter +# widgets, etc.), each of which can contain one or more `~.axes.Axes`, an +# area where points can be specified in terms of x-y coordinates (or theta-r +# in a polar plot, x-y-z in a 3D plot, etc.). The simplest way of +# creating a Figure with an Axes is using `.pyplot.subplots`. We can then use +# `.Axes.plot` to draw some data on the Axes: + +fig, ax = plt.subplots() # Create a figure containing a single axes. +ax.plot([1, 2, 3, 4], [1, 4, 2, 3]) # Plot some data on the axes. + +# %% +# +# Note that to get this Figure to display, you may have to call ``plt.show()``, +# depending on your backend. For more details of Figures and backends, see +# :ref:`figure_explanation`. +# +# .. _figure_parts: +# +# Parts of a Figure +# ================= +# +# Here are the components of a Matplotlib Figure. +# +# .. image:: ../../_static/anatomy.png +# +# :class:`~matplotlib.figure.Figure` +# ---------------------------------- +# +# The **whole** figure. The Figure keeps +# track of all the child :class:`~matplotlib.axes.Axes`, a group of +# 'special' Artists (titles, figure legends, colorbars, etc), and +# even nested subfigures. +# +# The easiest way to create a new Figure is with pyplot:: +# +# fig = plt.figure() # an empty figure with no Axes +# fig, ax = plt.subplots() # a figure with a single Axes +# fig, axs = plt.subplots(2, 2) # a figure with a 2x2 grid of Axes +# # a figure with one axes on the left, and two on the right: +# fig, axs = plt.subplot_mosaic([['left', 'right-top'], +# ['left', 'right_bottom]]) +# +# It is often convenient to create the Axes together with the Figure, but you +# can also manually add Axes later on. Note that many +# :doc:`Matplotlib backends ` support zooming and +# panning on figure windows. +# +# For more on Figures, see :ref:`figure_explanation`. +# +# :class:`~matplotlib.axes.Axes` +# ------------------------------ +# +# An Axes is an Artist attached to a Figure that contains a region for +# plotting data, and usually includes two (or three in the case of 3D) +# :class:`~matplotlib.axis.Axis` objects (be aware of the difference +# between **Axes** and **Axis**) that provide ticks and tick labels to +# provide scales for the data in the Axes. Each :class:`~.axes.Axes` also +# has a title +# (set via :meth:`~matplotlib.axes.Axes.set_title`), an x-label (set via +# :meth:`~matplotlib.axes.Axes.set_xlabel`), and a y-label set via +# :meth:`~matplotlib.axes.Axes.set_ylabel`). +# +# The :class:`~.axes.Axes` class and its member functions are the primary +# entry point to working with the OOP interface, and have most of the +# plotting methods defined on them (e.g. ``ax.plot()``, shown above, uses +# the `~.Axes.plot` method) +# +# :class:`~matplotlib.axis.Axis` +# ------------------------------ +# +# These objects set the scale and limits and generate ticks (the marks +# on the Axis) and ticklabels (strings labeling the ticks). The location +# of the ticks is determined by a `~matplotlib.ticker.Locator` object and the +# ticklabel strings are formatted by a `~matplotlib.ticker.Formatter`. The +# combination of the correct `.Locator` and `.Formatter` gives very fine +# control over the tick locations and labels. +# +# :class:`~matplotlib.artist.Artist` +# ---------------------------------- +# +# Basically, everything visible on the Figure is an Artist (even +# `.Figure`, `Axes <.axes.Axes>`, and `~.axis.Axis` objects). This includes +# `.Text` objects, `.Line2D` objects, :mod:`.collections` objects, `.Patch` +# objects, etc. When the Figure is rendered, all of the +# Artists are drawn to the **canvas**. Most Artists are tied to an Axes; such +# an Artist cannot be shared by multiple Axes, or moved from one to another. +# +# .. _input_types: +# +# Types of inputs to plotting functions +# ===================================== +# +# Plotting functions expect `numpy.array` or `numpy.ma.masked_array` as +# input, or objects that can be passed to `numpy.asarray`. +# Classes that are similar to arrays ('array-like') such as `pandas` +# data objects and `numpy.matrix` may not work as intended. Common convention +# is to convert these to `numpy.array` objects prior to plotting. +# For example, to convert a `numpy.matrix` :: +# +# b = np.matrix([[1, 2], [3, 4]]) +# b_asarray = np.asarray(b) +# +# Most methods will also parse an addressable object like a *dict*, a +# `numpy.recarray`, or a `pandas.DataFrame`. Matplotlib allows you to +# provide the ``data`` keyword argument and generate plots passing the +# strings corresponding to the *x* and *y* variables. +np.random.seed(19680801) # seed the random number generator. +data = {'a': np.arange(50), + 'c': np.random.randint(0, 50, 50), + 'd': np.random.randn(50)} +data['b'] = data['a'] + 10 * np.random.randn(50) +data['d'] = np.abs(data['d']) * 100 + +fig, ax = plt.subplots(figsize=(5, 2.7), layout='constrained') +ax.scatter('a', 'b', c='c', s='d', data=data) +ax.set_xlabel('entry a') +ax.set_ylabel('entry b') + +############################################################################## +# .. _coding_styles: +# +# Coding styles +# ============= +# +# The explicit and the implicit interfaces +# ---------------------------------------- +# +# As noted above, there are essentially two ways to use Matplotlib: +# +# - Explicitly create Figures and Axes, and call methods on them (the +# "object-oriented (OO) style"). +# - Rely on pyplot to implicitly create and manage the Figures and Axes, and +# use pyplot functions for plotting. +# +# See :ref:`api_interfaces` for an explanation of the tradeoffs between the +# implicit and explicit interfaces. +# +# So one can use the OO-style + +x = np.linspace(0, 2, 100) # Sample data. + +# Note that even in the OO-style, we use `.pyplot.figure` to create the Figure. +fig, ax = plt.subplots(figsize=(5, 2.7), layout='constrained') +ax.plot(x, x, label='linear') # Plot some data on the axes. +ax.plot(x, x**2, label='quadratic') # Plot more data on the axes... +ax.plot(x, x**3, label='cubic') # ... and some more. +ax.set_xlabel('x label') # Add an x-label to the axes. +ax.set_ylabel('y label') # Add a y-label to the axes. +ax.set_title("Simple Plot") # Add a title to the axes. +ax.legend() # Add a legend. + +############################################################################### +# or the pyplot-style: + +x = np.linspace(0, 2, 100) # Sample data. + +plt.figure(figsize=(5, 2.7), layout='constrained') +plt.plot(x, x, label='linear') # Plot some data on the (implicit) axes. +plt.plot(x, x**2, label='quadratic') # etc. +plt.plot(x, x**3, label='cubic') +plt.xlabel('x label') +plt.ylabel('y label') +plt.title("Simple Plot") +plt.legend() + +############################################################################### +# (In addition, there is a third approach, for the case when embedding +# Matplotlib in a GUI application, which completely drops pyplot, even for +# figure creation. See the corresponding section in the gallery for more info: +# :ref:`user_interfaces`.) +# +# Matplotlib's documentation and examples use both the OO and the pyplot +# styles. In general, we suggest using the OO style, particularly for +# complicated plots, and functions and scripts that are intended to be reused +# as part of a larger project. However, the pyplot style can be very convenient +# for quick interactive work. +# +# .. note:: +# +# You may find older examples that use the ``pylab`` interface, +# via ``from pylab import *``. This approach is strongly deprecated. +# +# Making a helper functions +# ------------------------- +# +# If you need to make the same plots over and over again with different data +# sets, or want to easily wrap Matplotlib methods, use the recommended +# signature function below. + + +def my_plotter(ax, data1, data2, param_dict): + """ + A helper function to make a graph. + """ + out = ax.plot(data1, data2, **param_dict) + return out + +############################################################################### +# which you would then use twice to populate two subplots: + +data1, data2, data3, data4 = np.random.randn(4, 100) # make 4 random data sets +fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(5, 2.7)) +my_plotter(ax1, data1, data2, {'marker': 'x'}) +my_plotter(ax2, data3, data4, {'marker': 'o'}) + +############################################################################### +# Note that if you want to install these as a python package, or any other +# customizations you could use one of the many templates on the web; +# Matplotlib has one at `mpl-cookiecutter +# `_ +# +# +# Styling Artists +# =============== +# +# Most plotting methods have styling options for the Artists, accessible either +# when a plotting method is called, or from a "setter" on the Artist. In the +# plot below we manually set the *color*, *linewidth*, and *linestyle* of the +# Artists created by `~.Axes.plot`, and we set the linestyle of the second line +# after the fact with `~.Line2D.set_linestyle`. + +fig, ax = plt.subplots(figsize=(5, 2.7)) +x = np.arange(len(data1)) +ax.plot(x, np.cumsum(data1), color='blue', linewidth=3, linestyle='--') +l, = ax.plot(x, np.cumsum(data2), color='orange', linewidth=2) +l.set_linestyle(':') + +############################################################################### +# Colors +# ------ +# +# Matplotlib has a very flexible array of colors that are accepted for most +# Artists; see the :doc:`colors tutorial ` for a +# list of specifications. Some Artists will take multiple colors. i.e. for +# a `~.Axes.scatter` plot, the edge of the markers can be different colors +# from the interior: + +fig, ax = plt.subplots(figsize=(5, 2.7)) +ax.scatter(data1, data2, s=50, facecolor='C0', edgecolor='k') + +############################################################################### +# Linewidths, linestyles, and markersizes +# --------------------------------------- +# +# Line widths are typically in typographic points (1 pt = 1/72 inch) and +# available for Artists that have stroked lines. Similarly, stroked lines +# can have a linestyle. See the :doc:`linestyles example +# `. +# +# Marker size depends on the method being used. `~.Axes.plot` specifies +# markersize in points, and is generally the "diameter" or width of the +# marker. `~.Axes.scatter` specifies markersize as approximately +# proportional to the visual area of the marker. There is an array of +# markerstyles available as string codes (see :mod:`~.matplotlib.markers`), or +# users can define their own `~.MarkerStyle` (see +# :doc:`/gallery/lines_bars_and_markers/marker_reference`): + +fig, ax = plt.subplots(figsize=(5, 2.7)) +ax.plot(data1, 'o', label='data1') +ax.plot(data2, 'd', label='data2') +ax.plot(data3, 'v', label='data3') +ax.plot(data4, 's', label='data4') +ax.legend() + +############################################################################### +# +# Labelling plots +# =============== +# +# Axes labels and text +# -------------------- +# +# `~.Axes.set_xlabel`, `~.Axes.set_ylabel`, and `~.Axes.set_title` are used to +# add text in the indicated locations (see :doc:`/tutorials/text/text_intro` +# for more discussion). Text can also be directly added to plots using +# `~.Axes.text`: + +mu, sigma = 115, 15 +x = mu + sigma * np.random.randn(10000) +fig, ax = plt.subplots(figsize=(5, 2.7), layout='constrained') +# the histogram of the data +n, bins, patches = ax.hist(x, 50, density=True, facecolor='C0', alpha=0.75) + +ax.set_xlabel('Length [cm]') +ax.set_ylabel('Probability') +ax.set_title('Aardvark lengths\n (not really)') +ax.text(75, .025, r'$\mu=115,\ \sigma=15$') +ax.axis([55, 175, 0, 0.03]) +ax.grid(True) + +############################################################################### +# All of the `~.Axes.text` functions return a `matplotlib.text.Text` +# instance. Just as with lines above, you can customize the properties by +# passing keyword arguments into the text functions:: +# +# t = ax.set_xlabel('my data', fontsize=14, color='red') +# +# These properties are covered in more detail in +# :doc:`/tutorials/text/text_props`. +# +# Using mathematical expressions in text +# -------------------------------------- +# +# Matplotlib accepts TeX equation expressions in any text expression. +# For example to write the expression :math:`\sigma_i=15` in the title, +# you can write a TeX expression surrounded by dollar signs:: +# +# ax.set_title(r'$\sigma_i=15$') +# +# where the ``r`` preceding the title string signifies that the string is a +# *raw* string and not to treat backslashes as python escapes. +# Matplotlib has a built-in TeX expression parser and +# layout engine, and ships its own math fonts – for details see +# :doc:`/tutorials/text/mathtext`. You can also use LaTeX directly to format +# your text and incorporate the output directly into your display figures or +# saved postscript – see :doc:`/tutorials/text/usetex`. +# +# Annotations +# ----------- +# +# We can also annotate points on a plot, often by connecting an arrow pointing +# to *xy*, to a piece of text at *xytext*: + +fig, ax = plt.subplots(figsize=(5, 2.7)) + +t = np.arange(0.0, 5.0, 0.01) +s = np.cos(2 * np.pi * t) +line, = ax.plot(t, s, lw=2) + +ax.annotate('local max', xy=(2, 1), xytext=(3, 1.5), + arrowprops=dict(facecolor='black', shrink=0.05)) + +ax.set_ylim(-2, 2) + +############################################################################### +# In this basic example, both *xy* and *xytext* are in data coordinates. +# There are a variety of other coordinate systems one can choose -- see +# :ref:`annotations-tutorial` and :ref:`plotting-guide-annotation` for +# details. More examples also can be found in +# :doc:`/gallery/text_labels_and_annotations/annotation_demo`. +# +# Legends +# ------- +# +# Often we want to identify lines or markers with a `.Axes.legend`: + +fig, ax = plt.subplots(figsize=(5, 2.7)) +ax.plot(np.arange(len(data1)), data1, label='data1') +ax.plot(np.arange(len(data2)), data2, label='data2') +ax.plot(np.arange(len(data3)), data3, 'd', label='data3') +ax.legend() + +############################################################################## +# Legends in Matplotlib are quite flexible in layout, placement, and what +# Artists they can represent. They are discussed in detail in +# :doc:`/tutorials/intermediate/legend_guide`. +# +# Axis scales and ticks +# ===================== +# +# Each Axes has two (or three) `~.axis.Axis` objects representing the x- and +# y-axis. These control the *scale* of the Axis, the tick *locators* and the +# tick *formatters*. Additional Axes can be attached to display further Axis +# objects. +# +# Scales +# ------ +# +# In addition to the linear scale, Matplotlib supplies non-linear scales, +# such as a log-scale. Since log-scales are used so much there are also +# direct methods like `~.Axes.loglog`, `~.Axes.semilogx`, and +# `~.Axes.semilogy`. There are a number of scales (see +# :doc:`/gallery/scales/scales` for other examples). Here we set the scale +# manually: + +fig, axs = plt.subplots(1, 2, figsize=(5, 2.7), layout='constrained') +xdata = np.arange(len(data1)) # make an ordinal for this +data = 10**data1 +axs[0].plot(xdata, data) + +axs[1].set_yscale('log') +axs[1].plot(xdata, data) + +############################################################################## +# The scale sets the mapping from data values to spacing along the Axis. This +# happens in both directions, and gets combined into a *transform*, which +# is the way that Matplotlib maps from data coordinates to Axes, Figure, or +# screen coordinates. See :doc:`/tutorials/advanced/transforms_tutorial`. +# +# Tick locators and formatters +# ---------------------------- +# +# Each Axis has a tick *locator* and *formatter* that choose where along the +# Axis objects to put tick marks. A simple interface to this is +# `~.Axes.set_xticks`: + +fig, axs = plt.subplots(2, 1, layout='constrained') +axs[0].plot(xdata, data1) +axs[0].set_title('Automatic ticks') + +axs[1].plot(xdata, data1) +axs[1].set_xticks(np.arange(0, 100, 30), ['zero', '30', 'sixty', '90']) +axs[1].set_yticks([-1.5, 0, 1.5]) # note that we don't need to specify labels +axs[1].set_title('Manual ticks') + +############################################################################## +# Different scales can have different locators and formatters; for instance +# the log-scale above uses `~.LogLocator` and `~.LogFormatter`. See +# :doc:`/gallery/ticks/tick-locators` and +# :doc:`/gallery/ticks/tick-formatters` for other formatters and +# locators and information for writing your own. +# +# Plotting dates and strings +# -------------------------- +# +# Matplotlib can handle plotting arrays of dates and arrays of strings, as +# well as floating point numbers. These get special locators and formatters +# as appropriate. For dates: + +fig, ax = plt.subplots(figsize=(5, 2.7), layout='constrained') +dates = np.arange(np.datetime64('2021-11-15'), np.datetime64('2021-12-25'), + np.timedelta64(1, 'h')) +data = np.cumsum(np.random.randn(len(dates))) +ax.plot(dates, data) +cdf = mpl.dates.ConciseDateFormatter(ax.xaxis.get_major_locator()) +ax.xaxis.set_major_formatter(cdf) + +############################################################################## +# For more information see the date examples +# (e.g. :doc:`/gallery/text_labels_and_annotations/date`) +# +# For strings, we get categorical plotting (see: +# :doc:`/gallery/lines_bars_and_markers/categorical_variables`). + +fig, ax = plt.subplots(figsize=(5, 2.7), layout='constrained') +categories = ['turnips', 'rutabaga', 'cucumber', 'pumpkins'] + +ax.bar(categories, np.random.rand(len(categories))) + +############################################################################## +# One caveat about categorical plotting is that some methods of parsing +# text files return a list of strings, even if the strings all represent +# numbers or dates. If you pass 1000 strings, Matplotlib will think you +# meant 1000 categories and will add 1000 ticks to your plot! +# +# +# Additional Axis objects +# ------------------------ +# +# Plotting data of different magnitude in one chart may require +# an additional y-axis. Such an Axis can be created by using +# `~.Axes.twinx` to add a new Axes with an invisible x-axis and a y-axis +# positioned at the right (analogously for `~.Axes.twiny`). See +# :doc:`/gallery/subplots_axes_and_figures/two_scales` for another example. +# +# Similarly, you can add a `~.Axes.secondary_xaxis` or +# `~.Axes.secondary_yaxis` having a different scale than the main Axis to +# represent the data in different scales or units. See +# :doc:`/gallery/subplots_axes_and_figures/secondary_axis` for further +# examples. + +fig, (ax1, ax3) = plt.subplots(1, 2, figsize=(7, 2.7), layout='constrained') +l1, = ax1.plot(t, s) +ax2 = ax1.twinx() +l2, = ax2.plot(t, range(len(t)), 'C1') +ax2.legend([l1, l2], ['Sine (left)', 'Straight (right)']) + +ax3.plot(t, s) +ax3.set_xlabel('Angle [rad]') +ax4 = ax3.secondary_xaxis('top', functions=(np.rad2deg, np.deg2rad)) +ax4.set_xlabel('Angle [°]') + +############################################################################## +# Color mapped data +# ================= +# +# Often we want to have a third dimension in a plot represented by a colors in +# a colormap. Matplotlib has a number of plot types that do this: + +X, Y = np.meshgrid(np.linspace(-3, 3, 128), np.linspace(-3, 3, 128)) +Z = (1 - X/2 + X**5 + Y**3) * np.exp(-X**2 - Y**2) + +fig, axs = plt.subplots(2, 2, layout='constrained') +pc = axs[0, 0].pcolormesh(X, Y, Z, vmin=-1, vmax=1, cmap='RdBu_r') +fig.colorbar(pc, ax=axs[0, 0]) +axs[0, 0].set_title('pcolormesh()') + +co = axs[0, 1].contourf(X, Y, Z, levels=np.linspace(-1.25, 1.25, 11)) +fig.colorbar(co, ax=axs[0, 1]) +axs[0, 1].set_title('contourf()') + +pc = axs[1, 0].imshow(Z**2 * 100, cmap='plasma', + norm=mpl.colors.LogNorm(vmin=0.01, vmax=100)) +fig.colorbar(pc, ax=axs[1, 0], extend='both') +axs[1, 0].set_title('imshow() with LogNorm()') + +pc = axs[1, 1].scatter(data1, data2, c=data3, cmap='RdBu_r') +fig.colorbar(pc, ax=axs[1, 1], extend='both') +axs[1, 1].set_title('scatter()') + +############################################################################## +# Colormaps +# --------- +# +# These are all examples of Artists that derive from `~.ScalarMappable` +# objects. They all can set a linear mapping between *vmin* and *vmax* into +# the colormap specified by *cmap*. Matplotlib has many colormaps to choose +# from (:doc:`/tutorials/colors/colormaps`) you can make your +# own (:doc:`/tutorials/colors/colormap-manipulation`) or download as +# `third-party packages +# `_. +# +# Normalizations +# -------------- +# +# Sometimes we want a non-linear mapping of the data to the colormap, as +# in the ``LogNorm`` example above. We do this by supplying the +# ScalarMappable with the *norm* argument instead of *vmin* and *vmax*. +# More normalizations are shown at :doc:`/tutorials/colors/colormapnorms`. +# +# Colorbars +# --------- +# +# Adding a `~.Figure.colorbar` gives a key to relate the color back to the +# underlying data. Colorbars are figure-level Artists, and are attached to +# a ScalarMappable (where they get their information about the norm and +# colormap) and usually steal space from a parent Axes. Placement of +# colorbars can be complex: see +# :doc:`/gallery/subplots_axes_and_figures/colorbar_placement` for +# details. You can also change the appearance of colorbars with the +# *extend* keyword to add arrows to the ends, and *shrink* and *aspect* to +# control the size. Finally, the colorbar will have default locators +# and formatters appropriate to the norm. These can be changed as for +# other Axis objects. +# +# +# Working with multiple Figures and Axes +# ====================================== +# +# You can open multiple Figures with multiple calls to +# ``fig = plt.figure()`` or ``fig2, ax = plt.subplots()``. By keeping the +# object references you can add Artists to either Figure. +# +# Multiple Axes can be added a number of ways, but the most basic is +# ``plt.subplots()`` as used above. One can achieve more complex layouts, +# with Axes objects spanning columns or rows, using `~.pyplot.subplot_mosaic`. + +fig, axd = plt.subplot_mosaic([['upleft', 'right'], + ['lowleft', 'right']], layout='constrained') +axd['upleft'].set_title('upleft') +axd['lowleft'].set_title('lowleft') +axd['right'].set_title('right') + +############################################################################### +# Matplotlib has quite sophisticated tools for arranging Axes: See +# :doc:`/tutorials/intermediate/arranging_axes` and +# :doc:`/gallery/subplots_axes_and_figures/mosaic`. +# +# +# More reading +# ============ +# +# For more plot types see :doc:`Plot types ` and the +# :doc:`API reference `, in particular the +# :doc:`Axes API `. diff --git a/tutorials/introductory/sample_plots.py b/tutorials/introductory/sample_plots.py deleted file mode 100644 index 67091ad1b414..000000000000 --- a/tutorials/introductory/sample_plots.py +++ /dev/null @@ -1,437 +0,0 @@ -""" -========================== -Sample plots in Matplotlib -========================== - -Here you'll find a host of example plots with the code that -generated them. - -.. _matplotlibscreenshots: - -Line Plot -========= - -Here's how to create a line plot with text labels using -:func:`~matplotlib.pyplot.plot`. - -.. figure:: ../../gallery/lines_bars_and_markers/images/sphx_glr_simple_plot_001.png - :target: ../../gallery/lines_bars_and_markers/simple_plot.html - :align: center - :scale: 50 - - Simple Plot - -.. _screenshots_subplot_demo: - -Multiple subplots in one figure -=============================== - -Multiple axes (i.e. subplots) are created with the -:func:`~matplotlib.pyplot.subplot` function: - -.. figure:: ../../gallery/subplots_axes_and_figures/images/sphx_glr_subplot_001.png - :target: ../../gallery/subplots_axes_and_figures/subplot.html - :align: center - :scale: 50 - - Subplot - -.. _screenshots_images_demo: - -Images -====== - -Matplotlib can display images (assuming equally spaced -horizontal dimensions) using the :func:`~matplotlib.pyplot.imshow` function. - -.. figure:: ../../gallery/images_contours_and_fields/images/sphx_glr_image_demo_003.png - :target: ../../gallery/images_contours_and_fields/image_demo.html - :align: center - :scale: 50 - - Example of using :func:`~matplotlib.pyplot.imshow` to display a CT scan - -.. _screenshots_pcolormesh_demo: - - -Contouring and pseudocolor -========================== - -The :func:`~matplotlib.pyplot.pcolormesh` function can make a colored -representation of a two-dimensional array, even if the horizontal dimensions -are unevenly spaced. The -:func:`~matplotlib.pyplot.contour` function is another way to represent -the same data: - -.. figure:: ../../gallery/images_contours_and_fields/images/sphx_glr_pcolormesh_levels_001.png - :target: ../../gallery/images_contours_and_fields/pcolormesh_levels.html - :align: center - :scale: 50 - - Example comparing :func:`~matplotlib.pyplot.pcolormesh` and :func:`~matplotlib.pyplot.contour` for plotting two-dimensional data - -.. _screenshots_histogram_demo: - -Histograms -========== - -The :func:`~matplotlib.pyplot.hist` function automatically generates -histograms and returns the bin counts or probabilities: - -.. figure:: ../../gallery/statistics/images/sphx_glr_histogram_features_001.png - :target: ../../gallery/statistics/histogram_features.html - :align: center - :scale: 50 - - Histogram Features - - -.. _screenshots_path_demo: - -Paths -===== - -You can add arbitrary paths in Matplotlib using the -:mod:`matplotlib.path` module: - -.. figure:: ../../gallery/shapes_and_collections/images/sphx_glr_path_patch_001.png - :target: ../../gallery/shapes_and_collections/path_patch.html - :align: center - :scale: 50 - - Path Patch - -.. _screenshots_mplot3d_surface: - -Three-dimensional plotting -========================== - -The mplot3d toolkit (see :ref:`toolkit_mplot3d-tutorial` and -:ref:`mplot3d-examples-index`) has support for simple 3D graphs -including surface, wireframe, scatter, and bar charts. - -.. figure:: ../../gallery/mplot3d/images/sphx_glr_surface3d_001.png - :target: ../../gallery/mplot3d/surface3d.html - :align: center - :scale: 50 - - Surface3d - -Thanks to John Porter, Jonathon Taylor, Reinier Heeres, and Ben Root for -the `.mplot3d` toolkit. This toolkit is included with all standard Matplotlib -installs. - -.. _screenshots_ellipse_demo: - - -Streamplot -========== - -The :meth:`~matplotlib.pyplot.streamplot` function plots the streamlines of -a vector field. In addition to simply plotting the streamlines, it allows you -to map the colors and/or line widths of streamlines to a separate parameter, -such as the speed or local intensity of the vector field. - -.. figure:: ../../gallery/images_contours_and_fields/images/sphx_glr_plot_streamplot_001.png - :target: ../../gallery/images_contours_and_fields/plot_streamplot.html - :align: center - :scale: 50 - - Streamplot with various plotting options. - -This feature complements the :meth:`~matplotlib.pyplot.quiver` function for -plotting vector fields. Thanks to Tom Flannaghan and Tony Yu for adding the -streamplot function. - - -Ellipses -======== - -In support of the `Phoenix `_ -mission to Mars (which used Matplotlib to display ground tracking of -spacecraft), Michael Droettboom built on work by Charlie Moad to provide -an extremely accurate 8-spline approximation to elliptical arcs (see -:class:`~matplotlib.patches.Arc`), which are insensitive to zoom level. - -.. figure:: ../../gallery/shapes_and_collections/images/sphx_glr_ellipse_demo_001.png - :target: ../../gallery/shapes_and_collections/ellipse_demo.html - :align: center - :scale: 50 - - Ellipse Demo - -.. _screenshots_barchart_demo: - -Bar charts -========== - -Use the :func:`~matplotlib.pyplot.bar` function to make bar charts, which -includes customizations such as error bars: - -.. figure:: ../../gallery/statistics/images/sphx_glr_barchart_demo_001.png - :target: ../../gallery/statistics/barchart_demo.html - :align: center - :scale: 50 - - Barchart Demo - -You can also create stacked bars -(`bar_stacked.py <../../gallery/lines_bars_and_markers/bar_stacked.html>`_), -or horizontal bar charts -(`barh.py <../../gallery/lines_bars_and_markers/barh.html>`_). - -.. _screenshots_pie_demo: - - -Pie charts -========== - -The :func:`~matplotlib.pyplot.pie` function allows you to create pie -charts. Optional features include auto-labeling the percentage of area, -exploding one or more wedges from the center of the pie, and a shadow effect. -Take a close look at the attached code, which generates this figure in just -a few lines of code. - -.. figure:: ../../gallery/pie_and_polar_charts/images/sphx_glr_pie_features_001.png - :target: ../../gallery/pie_and_polar_charts/pie_features.html - :align: center - :scale: 50 - - Pie Features - -.. _screenshots_table_demo: - -Tables -====== - -The :func:`~matplotlib.pyplot.table` function adds a text table -to an axes. - -.. figure:: ../../gallery/misc/images/sphx_glr_table_demo_001.png - :target: ../../gallery/misc/table_demo.html - :align: center - :scale: 50 - - Table Demo - - -.. _screenshots_scatter_demo: - - -Scatter plots -============= - -The :func:`~matplotlib.pyplot.scatter` function makes a scatter plot -with (optional) size and color arguments. This example plots changes -in Google's stock price, with marker sizes reflecting the -trading volume and colors varying with time. Here, the -alpha attribute is used to make semitransparent circle markers. - -.. figure:: ../../gallery/lines_bars_and_markers/images/sphx_glr_scatter_demo2_001.png - :target: ../../gallery/lines_bars_and_markers/scatter_demo2.html - :align: center - :scale: 50 - - Scatter Demo2 - - -.. _screenshots_slider_demo: - -GUI widgets -=========== - -Matplotlib has basic GUI widgets that are independent of the graphical -user interface you are using, allowing you to write cross GUI figures -and widgets. See :mod:`matplotlib.widgets` and the -`widget examples <../../gallery/index.html#widgets>`_. - -.. figure:: ../../gallery/widgets/images/sphx_glr_slider_demo_001.png - :target: ../../gallery/widgets/slider_demo.html - :align: center - :scale: 50 - - Slider and radio-button GUI. - - -.. _screenshots_fill_demo: - -Filled curves -============= - -The :func:`~matplotlib.pyplot.fill` function lets you -plot filled curves and polygons: - -.. figure:: ../../gallery/lines_bars_and_markers/images/sphx_glr_fill_001.png - :target: ../../gallery/lines_bars_and_markers/fill.html - :align: center - :scale: 50 - - Fill - -Thanks to Andrew Straw for adding this function. - -.. _screenshots_date_demo: - -Date handling -============= - -You can plot timeseries data with major and minor ticks and custom -tick formatters for both. - -.. figure:: ../../gallery/text_labels_and_annotations/images/sphx_glr_date_001.png - :target: ../../gallery/text_labels_and_annotations/date.html - :align: center - :scale: 50 - - Date - -See :mod:`matplotlib.ticker` and :mod:`matplotlib.dates` for details and usage. - - -.. _screenshots_log_demo: - -Log plots -========= - -The :func:`~matplotlib.pyplot.semilogx`, -:func:`~matplotlib.pyplot.semilogy` and -:func:`~matplotlib.pyplot.loglog` functions simplify the creation of -logarithmic plots. - -.. figure:: ../../gallery/scales/images/sphx_glr_log_demo_001.png - :target: ../../gallery/scales/log_demo.html - :align: center - :scale: 50 - - Log Demo - -Thanks to Andrew Straw, Darren Dale and Gregory Lielens for contributions -log-scaling infrastructure. - -.. _screenshots_polar_demo: - -Polar plots -=========== - -The :func:`~matplotlib.pyplot.polar` function generates polar plots. - -.. figure:: ../../gallery/pie_and_polar_charts/images/sphx_glr_polar_demo_001.png - :target: ../../gallery/pie_and_polar_charts/polar_demo.html - :align: center - :scale: 50 - - Polar Demo - -.. _screenshots_legend_demo: - - -Legends -======= - -The :func:`~matplotlib.pyplot.legend` function automatically -generates figure legends, with MATLAB-compatible legend-placement -functions. - -.. figure:: ../../gallery/text_labels_and_annotations/images/sphx_glr_legend_001.png - :target: ../../gallery/text_labels_and_annotations/legend.html - :align: center - :scale: 50 - - Legend - -Thanks to Charles Twardy for input on the legend function. - -.. _screenshots_mathtext_examples_demo: - -TeX-notation for text objects -============================= - -Below is a sampling of the many TeX expressions now supported by Matplotlib's -internal mathtext engine. The mathtext module provides TeX style mathematical -expressions using `FreeType `_ -and the DejaVu, BaKoMa computer modern, or `STIX `_ -fonts. See the :mod:`matplotlib.mathtext` module for additional details. - -.. figure:: ../../gallery/text_labels_and_annotations/images/sphx_glr_mathtext_examples_001.png - :target: ../../gallery/text_labels_and_annotations/mathtext_examples.html - :align: center - :scale: 50 - - Mathtext Examples - -Matplotlib's mathtext infrastructure is an independent implementation and -does not require TeX or any external packages installed on your computer. See -the tutorial at :doc:`/tutorials/text/mathtext`. - - -.. _screenshots_tex_demo: - -Native TeX rendering -==================== - -Although Matplotlib's internal math rendering engine is quite -powerful, sometimes you need TeX. Matplotlib supports external TeX -rendering of strings with the *usetex* option. - -.. figure:: ../../gallery/text_labels_and_annotations/images/sphx_glr_tex_demo_001.png - :target: ../../gallery/text_labels_and_annotations/tex_demo.html - :align: center - :scale: 50 - - Tex Demo - -.. _screenshots_eeg_demo: - -EEG GUI -======= - -You can embed Matplotlib into Qt, GTK, Tk, or wxWidgets applications. -Here is a screenshot of an EEG viewer called `pbrain -`__. - -.. image:: ../../_static/eeg_small.png - -The lower axes uses :func:`~matplotlib.pyplot.specgram` -to plot the spectrogram of one of the EEG channels. - -For examples of how to embed Matplotlib in different toolkits, see: - - * :doc:`/gallery/user_interfaces/embedding_in_gtk3_sgskip` - * :doc:`/gallery/user_interfaces/embedding_in_wx2_sgskip` - * :doc:`/gallery/user_interfaces/mpl_with_glade3_sgskip` - * :doc:`/gallery/user_interfaces/embedding_in_qt_sgskip` - * :doc:`/gallery/user_interfaces/embedding_in_tk_sgskip` - -XKCD-style sketch plots -======================= - -Just for fun, Matplotlib supports plotting in the style of `xkcd -`_. - -.. figure:: ../../gallery/showcase/images/sphx_glr_xkcd_001.png - :target: ../../gallery/showcase/xkcd.html - :align: center - :scale: 50 - - xkcd - -Subplot example -=============== - -Many plot types can be combined in one figure to create -powerful and flexible representations of data. -""" - -import matplotlib.pyplot as plt -import numpy as np - -np.random.seed(19680801) -data = np.random.randn(2, 100) - -fig, axs = plt.subplots(2, 2, figsize=(5, 5)) -axs[0, 0].hist(data[0]) -axs[1, 0].scatter(data[0], data[1]) -axs[0, 1].plot(data[0], data[1]) -axs[1, 1].hist2d(data[0], data[1]) - -plt.show() diff --git a/tutorials/introductory/usage.py b/tutorials/introductory/usage.py deleted file mode 100644 index 5f234e366ce6..000000000000 --- a/tutorials/introductory/usage.py +++ /dev/null @@ -1,791 +0,0 @@ -""" -*********** -Usage Guide -*********** - -This tutorial covers some basic usage patterns and best-practices to -help you get started with Matplotlib. -""" - -# sphinx_gallery_thumbnail_number = 3 -import matplotlib.pyplot as plt -import numpy as np - -############################################################################## -# -# A simple example -# ================ -# -# Matplotlib graphs your data on `~.figure.Figure`\s (i.e., windows, Jupyter -# widgets, etc.), each of which can contain one or more `~.axes.Axes` (i.e., an -# area where points can be specified in terms of x-y coordinates, or theta-r -# in a polar plot, or x-y-z in a 3D plot, etc.). The simplest way of -# creating a figure with an axes is using `.pyplot.subplots`. We can then use -# `.Axes.plot` to draw some data on the axes: - -fig, ax = plt.subplots() # Create a figure containing a single axes. -ax.plot([1, 2, 3, 4], [1, 4, 2, 3]) # Plot some data on the axes. - -############################################################################### -# Many other plotting libraries or languages do not require you to explicitly -# create an axes. For example, in MATLAB, one can just do -# -# .. code-block:: matlab -# -# plot([1, 2, 3, 4], [1, 4, 2, 3]) % MATLAB plot. -# -# and get the desired graph. -# -# In fact, you can do the same in Matplotlib: for each `~.axes.Axes` graphing -# method, there is a corresponding function in the :mod:`matplotlib.pyplot` -# module that performs that plot on the "current" axes, creating that axes (and -# its parent figure) if they don't exist yet. So, the previous example can be -# written more shortly as - -plt.plot([1, 2, 3, 4], [1, 4, 2, 3]) # Matplotlib plot. - -############################################################################### -# .. _figure_parts: -# -# Parts of a Figure -# ================= -# -# Now, let's have a deeper look at the components of a Matplotlib figure. -# -# .. image:: ../../_static/anatomy.png -# -# :class:`~matplotlib.figure.Figure` -# ---------------------------------- -# -# The **whole** figure. The figure keeps -# track of all the child :class:`~matplotlib.axes.Axes`, a smattering of -# 'special' artists (titles, figure legends, etc), and the **canvas**. -# (Don't worry too much about the canvas, it is crucial as it is the -# object that actually does the drawing to get you your plot, but as the -# user it is more-or-less invisible to you). A figure can contain any -# number of :class:`~matplotlib.axes.Axes`, but will typically have -# at least one. -# -# The easiest way to create a new figure is with pyplot:: -# -# fig = plt.figure() # an empty figure with no Axes -# fig, ax = plt.subplots() # a figure with a single Axes -# fig, axs = plt.subplots(2, 2) # a figure with a 2x2 grid of Axes -# -# It's convenient to create the axes together with the figure, but you can -# also add axes later on, allowing for more complex axes layouts. -# -# :class:`~matplotlib.axes.Axes` -# ------------------------------ -# -# This is what you think of as 'a plot', it is the region of the image -# with the data space. A given figure -# can contain many Axes, but a given :class:`~matplotlib.axes.Axes` -# object can only be in one :class:`~matplotlib.figure.Figure`. The -# Axes contains two (or three in the case of 3D) -# :class:`~matplotlib.axis.Axis` objects (be aware of the difference -# between **Axes** and **Axis**) which take care of the data limits (the -# data limits can also be controlled via the :meth:`.axes.Axes.set_xlim` and -# :meth:`.axes.Axes.set_ylim` methods). Each :class:`~.axes.Axes` has a title -# (set via :meth:`~matplotlib.axes.Axes.set_title`), an x-label (set via -# :meth:`~matplotlib.axes.Axes.set_xlabel`), and a y-label set via -# :meth:`~matplotlib.axes.Axes.set_ylabel`). -# -# The :class:`~.axes.Axes` class and its member functions are the primary entry -# point to working with the OO interface. -# -# :class:`~matplotlib.axis.Axis` -# ------------------------------ -# -# These are the number-line-like objects. They take -# care of setting the graph limits and generating the ticks (the marks -# on the axis) and ticklabels (strings labeling the ticks). The location of -# the ticks is determined by a `~matplotlib.ticker.Locator` object and the -# ticklabel strings are formatted by a `~matplotlib.ticker.Formatter`. The -# combination of the correct `.Locator` and `.Formatter` gives very fine -# control over the tick locations and labels. -# -# :class:`~matplotlib.artist.Artist` -# ---------------------------------- -# -# Basically, everything you can see on the figure is an artist (even the -# `.Figure`, `Axes <.axes.Axes>`, and `~.axis.Axis` objects). This includes -# `.Text` objects, `.Line2D` objects, :mod:`.collections` objects, `.Patch` -# objects ... (you get the idea). When the figure is rendered, all of the -# artists are drawn to the **canvas**. Most Artists are tied to an Axes; such -# an Artist cannot be shared by multiple Axes, or moved from one to another. -# -# .. _input_types: -# -# Types of inputs to plotting functions -# ===================================== -# -# All of plotting functions expect `numpy.array` or `numpy.ma.masked_array` as -# input. Classes that are 'array-like' such as `pandas` data objects -# and `numpy.matrix` may or may not work as intended. It is best to -# convert these to `numpy.array` objects prior to plotting. -# -# For example, to convert a `pandas.DataFrame` :: -# -# a = pandas.DataFrame(np.random.rand(4, 5), columns = list('abcde')) -# a_asarray = a.values -# -# and to convert a `numpy.matrix` :: -# -# b = np.matrix([[1, 2], [3, 4]]) -# b_asarray = np.asarray(b) -# -# .. _coding_styles: -# -# The object-oriented interface and the pyplot interface -# ====================================================== -# -# As noted above, there are essentially two ways to use Matplotlib: -# -# - Explicitly create figures and axes, and call methods on them (the -# "object-oriented (OO) style"). -# - Rely on pyplot to automatically create and manage the figures and axes, and -# use pyplot functions for plotting. -# -# So one can do (OO-style) - -x = np.linspace(0, 2, 100) - -# Note that even in the OO-style, we use `.pyplot.figure` to create the figure. -fig, ax = plt.subplots() # Create a figure and an axes. -ax.plot(x, x, label='linear') # Plot some data on the axes. -ax.plot(x, x**2, label='quadratic') # Plot more data on the axes... -ax.plot(x, x**3, label='cubic') # ... and some more. -ax.set_xlabel('x label') # Add an x-label to the axes. -ax.set_ylabel('y label') # Add a y-label to the axes. -ax.set_title("Simple Plot") # Add a title to the axes. -ax.legend() # Add a legend. - -############################################################################### -# or (pyplot-style) - -x = np.linspace(0, 2, 100) - -plt.plot(x, x, label='linear') # Plot some data on the (implicit) axes. -plt.plot(x, x**2, label='quadratic') # etc. -plt.plot(x, x**3, label='cubic') -plt.xlabel('x label') -plt.ylabel('y label') -plt.title("Simple Plot") -plt.legend() - -############################################################################### -# In addition, there is a third approach, for the case when embedding -# Matplotlib in a GUI application, which completely drops pyplot, even for -# figure creation. We won't discuss it here; see the corresponding section in -# the gallery for more info (:ref:`user_interfaces`). -# -# Matplotlib's documentation and examples use both the OO and the pyplot -# approaches (which are equally powerful), and you should feel free to use -# either (however, it is preferable pick one of them and stick to it, instead -# of mixing them). In general, we suggest to restrict pyplot to interactive -# plotting (e.g., in a Jupyter notebook), and to prefer the OO-style for -# non-interactive plotting (in functions and scripts that are intended to be -# reused as part of a larger project). -# -# .. note:: -# -# In older examples, you may find examples that instead used the so-called -# ``pylab`` interface, via ``from pylab import *``. This star-import -# imports everything both from pyplot and from :mod:`numpy`, so that one -# could do :: -# -# x = linspace(0, 2, 100) -# plot(x, x, label='linear') -# ... -# -# for an even more MATLAB-like style. This approach is strongly discouraged -# nowadays and deprecated. It is only mentioned here because you may still -# encounter it in the wild. -# -# Typically one finds oneself making the same plots over and over -# again, but with different data sets, which leads to needing to write -# specialized functions to do the plotting. The recommended function -# signature is something like: - - -def my_plotter(ax, data1, data2, param_dict): - """ - A helper function to make a graph - - Parameters - ---------- - ax : Axes - The axes to draw to - - data1 : array - The x data - - data2 : array - The y data - - param_dict : dict - Dictionary of kwargs to pass to ax.plot - - Returns - ------- - out : list - list of artists added - """ - out = ax.plot(data1, data2, **param_dict) - return out - -############################################################################### -# which you would then use as: - -data1, data2, data3, data4 = np.random.randn(4, 100) -fig, ax = plt.subplots(1, 1) -my_plotter(ax, data1, data2, {'marker': 'x'}) - -############################################################################### -# or if you wanted to have 2 sub-plots: - -fig, (ax1, ax2) = plt.subplots(1, 2) -my_plotter(ax1, data1, data2, {'marker': 'x'}) -my_plotter(ax2, data3, data4, {'marker': 'o'}) - -############################################################################### -# For these simple examples this style seems like overkill, however -# once the graphs get slightly more complex it pays off. -# -# -# .. _backends: -# -# Backends -# ======== -# -# .. _what-is-a-backend: -# -# What is a backend? -# ------------------ -# -# A lot of documentation on the website and in the mailing lists refers -# to the "backend" and many new users are confused by this term. -# Matplotlib targets many different use cases and output formats. Some -# people use Matplotlib interactively from the python shell and have -# plotting windows pop up when they type commands. Some people run -# `Jupyter `_ notebooks and draw inline plots for -# quick data analysis. Others embed Matplotlib into graphical user -# interfaces like PyQt or PyGObject to build rich applications. Some -# people use Matplotlib in batch scripts to generate postscript images -# from numerical simulations, and still others run web application -# servers to dynamically serve up graphs. -# -# To support all of these use cases, Matplotlib can target different -# outputs, and each of these capabilities is called a backend; the -# "frontend" is the user facing code, i.e., the plotting code, whereas the -# "backend" does all the hard work behind-the-scenes to make the figure. -# There are two types of backends: user interface backends (for use in -# PyQt/PySide, PyGObject, Tkinter, wxPython, or macOS/Cocoa); also referred to -# as "interactive backends") and hardcopy backends to make image files -# (PNG, SVG, PDF, PS; also referred to as "non-interactive backends"). -# -# Selecting a backend -# ------------------- -# -# There are three ways to configure your backend: -# -# 1. The :rc:`backend` parameter in your :file:`matplotlibrc` file -# 2. The :envvar:`MPLBACKEND` environment variable -# 3. The function :func:`matplotlib.use` -# -# A more detailed description is given below. -# -# If multiple of these are configurations are present, the last one from the -# list takes precedence; e.g. calling :func:`matplotlib.use()` will override -# the setting in your :file:`matplotlibrc`. -# -# If no backend is explicitly set, Matplotlib automatically detects a usable -# backend based on what is available on your system and on whether a GUI event -# loop is already running. On Linux, if the environment variable -# :envvar:`DISPLAY` is unset, the "event loop" is identified as "headless", -# which causes a fallback to a noninteractive backend (agg); in all other -# cases, an interactive backend is preferred (usually, at least tkagg will be -# available). -# -# Here is a detailed description of the configuration methods: -# -# #. Setting :rc:`backend` in your :file:`matplotlibrc` file:: -# -# backend : qt5agg # use pyqt5 with antigrain (agg) rendering -# -# See also :doc:`/tutorials/introductory/customizing`. -# -# #. Setting the :envvar:`MPLBACKEND` environment variable: -# -# You can set the environment variable either for your current shell or for -# a single script. -# -# On Unix:: -# -# > export MPLBACKEND=qt5agg -# > python simple_plot.py -# -# > MPLBACKEND=qt5agg python simple_plot.py -# -# On Windows, only the former is possible:: -# -# > set MPLBACKEND=qt5agg -# > python simple_plot.py -# -# Setting this environment variable will override the ``backend`` parameter -# in *any* :file:`matplotlibrc`, even if there is a :file:`matplotlibrc` in -# your current working directory. Therefore, setting :envvar:`MPLBACKEND` -# globally, e.g. in your :file:`.bashrc` or :file:`.profile`, is discouraged -# as it might lead to counter-intuitive behavior. -# -# #. If your script depends on a specific backend you can use the function -# :func:`matplotlib.use`:: -# -# import matplotlib -# matplotlib.use('qt5agg') -# -# This should be done before any figure is created, otherwise Matplotlib may -# fail to switch the backend and raise an ImportError. -# -# Using `~matplotlib.use` will require changes in your code if users want to -# use a different backend. Therefore, you should avoid explicitly calling -# `~matplotlib.use` unless absolutely necessary. -# -# .. _the-builtin-backends: -# -# The builtin backends -# -------------------- -# -# By default, Matplotlib should automatically select a default backend which -# allows both interactive work and plotting from scripts, with output to the -# screen and/or to a file, so at least initially, you will not need to worry -# about the backend. The most common exception is if your Python distribution -# comes without :mod:`tkinter` and you have no other GUI toolkit installed. -# This happens on certain Linux distributions, where you need to install a -# Linux package named ``python-tk`` (or similar). -# -# If, however, you want to write graphical user interfaces, or a web -# application server -# (:doc:`/gallery/user_interfaces/web_application_server_sgskip`), or need a -# better understanding of what is going on, read on. To make things a little -# more customizable for graphical user interfaces, Matplotlib separates -# the concept of the renderer (the thing that actually does the drawing) -# from the canvas (the place where the drawing goes). The canonical -# renderer for user interfaces is ``Agg`` which uses the `Anti-Grain -# Geometry`_ C++ library to make a raster (pixel) image of the figure; it -# is used by the ``Qt5Agg``, ``Qt4Agg``, ``GTK3Agg``, ``wxAgg``, ``TkAgg``, and -# ``macosx`` backends. An alternative renderer is based on the Cairo library, -# used by ``Qt5Cairo``, ``Qt4Cairo``, etc. -# -# For the rendering engines, one can also distinguish between `vector -# `_ or `raster -# `_ renderers. Vector -# graphics languages issue drawing commands like "draw a line from this -# point to this point" and hence are scale free, and raster backends -# generate a pixel representation of the line whose accuracy depends on a -# DPI setting. -# -# Here is a summary of the Matplotlib renderers (there is an eponymous -# backend for each; these are *non-interactive backends*, capable of -# writing to a file): -# -# ======== ========= ======================================================= -# Renderer Filetypes Description -# ======== ========= ======================================================= -# AGG png raster_ graphics -- high quality images using the -# `Anti-Grain Geometry`_ engine -# PDF pdf vector_ graphics -- `Portable Document Format`_ -# PS ps, eps vector_ graphics -- Postscript_ output -# SVG svg vector_ graphics -- `Scalable Vector Graphics`_ -# PGF pgf, pdf vector_ graphics -- using the pgf_ package -# Cairo png, ps, raster_ or vector_ graphics -- using the Cairo_ library -# pdf, svg -# ======== ========= ======================================================= -# -# To save plots using the non-interactive backends, use the -# ``matplotlib.pyplot.savefig('filename')`` method. -# -# And here are the user interfaces and renderer combinations supported; -# these are *interactive backends*, capable of displaying to the screen -# and of using appropriate renderers from the table above to write to -# a file: -# -# ========= ================================================================ -# Backend Description -# ========= ================================================================ -# Qt5Agg Agg rendering in a Qt5_ canvas (requires PyQt5_). This -# backend can be activated in IPython with ``%matplotlib qt5``. -# ipympl Agg rendering embedded in a Jupyter widget. (requires ipympl). -# This backend can be enabled in a Jupyter notebook with -# ``%matplotlib ipympl``. -# GTK3Agg Agg rendering to a GTK_ 3.x canvas (requires PyGObject_, -# and pycairo_ or cairocffi_). This backend can be activated in -# IPython with ``%matplotlib gtk3``. -# macosx Agg rendering into a Cocoa canvas in OSX. This backend can be -# activated in IPython with ``%matplotlib osx``. -# TkAgg Agg rendering to a Tk_ canvas (requires TkInter_). This -# backend can be activated in IPython with ``%matplotlib tk``. -# nbAgg Embed an interactive figure in a Jupyter classic notebook. This -# backend can be enabled in Jupyter notebooks via -# ``%matplotlib notebook``. -# WebAgg On ``show()`` will start a tornado server with an interactive -# figure. -# GTK3Cairo Cairo rendering to a GTK_ 3.x canvas (requires PyGObject_, -# and pycairo_ or cairocffi_). -# Qt4Agg Agg rendering to a Qt4_ canvas (requires PyQt4_ or -# ``pyside``). This backend can be activated in IPython with -# ``%matplotlib qt4``. -# wxAgg Agg rendering to a wxWidgets_ canvas (requires wxPython_ 4). -# This backend can be activated in IPython with ``%matplotlib wx``. -# ========= ================================================================ -# -# .. note:: -# The names of builtin backends case-insensitive; e.g., 'Qt5Agg' and -# 'qt5agg' are equivalent. -# -# .. _`Anti-Grain Geometry`: http://antigrain.com/ -# .. _`Portable Document Format`: https://en.wikipedia.org/wiki/Portable_Document_Format -# .. _Postscript: https://en.wikipedia.org/wiki/PostScript -# .. _`Scalable Vector Graphics`: https://en.wikipedia.org/wiki/Scalable_Vector_Graphics -# .. _pgf: https://ctan.org/pkg/pgf -# .. _Cairo: https://www.cairographics.org -# .. _PyGObject: https://wiki.gnome.org/action/show/Projects/PyGObject -# .. _pycairo: https://www.cairographics.org/pycairo/ -# .. _cairocffi: https://pythonhosted.org/cairocffi/ -# .. _wxPython: https://www.wxpython.org/ -# .. _TkInter: https://docs.python.org/3/library/tk.html -# .. _PyQt4: https://riverbankcomputing.com/software/pyqt/intro -# .. _PyQt5: https://riverbankcomputing.com/software/pyqt/intro -# .. _Qt5: https://doc.qt.io/qt-5/index.html -# .. _GTK: https://www.gtk.org/ -# .. _Tk: https://www.tcl.tk/ -# .. _Qt4: https://doc.qt.io/archives/qt-4.8/index.html -# .. _wxWidgets: https://www.wxwidgets.org/ -# -# ipympl -# ^^^^^^ -# -# The Jupyter widget ecosystem is moving too fast to support directly in -# Matplotlib. To install ipympl -# -# .. code-block:: bash -# -# pip install ipympl -# jupyter nbextension enable --py --sys-prefix ipympl -# -# or -# -# .. code-block:: bash -# -# conda install ipympl -c conda-forge -# -# See `jupyter-matplotlib `__ -# for more details. -# -# .. _QT_API-usage: -# -# How do I select PyQt4 or PySide? -# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -# -# The :envvar:`QT_API` environment variable can be set to either ``pyqt`` or -# ``pyside`` to use ``PyQt4`` or ``PySide``, respectively. -# -# Since the default value for the bindings to be used is ``PyQt4``, Matplotlib -# first tries to import it. If the import fails, it tries to import ``PySide``. -# -# Using non-builtin backends -# -------------------------- -# More generally, any importable backend can be selected by using any of the -# methods above. If ``name.of.the.backend`` is the module containing the -# backend, use ``module://name.of.the.backend`` as the backend name, e.g. -# ``matplotlib.use('module://name.of.the.backend')``. -# -# -# .. _interactive-mode: -# -# What is interactive mode? -# ========================= -# -# Use of an interactive backend (see :ref:`what-is-a-backend`) -# permits--but does not by itself require or ensure--plotting -# to the screen. Whether and when plotting to the screen occurs, -# and whether a script or shell session continues after a plot -# is drawn on the screen, depends on the functions and methods -# that are called, and on a state variable that determines whether -# Matplotlib is in "interactive mode". The default Boolean value is set -# by the :file:`matplotlibrc` file, and may be customized like any other -# configuration parameter (see :doc:`/tutorials/introductory/customizing`). It -# may also be set via :func:`matplotlib.interactive`, and its -# value may be queried via :func:`matplotlib.is_interactive`. Turning -# interactive mode on and off in the middle of a stream of plotting -# commands, whether in a script or in a shell, is rarely needed -# and potentially confusing. In the following, we will assume all -# plotting is done with interactive mode either on or off. -# -# .. note:: -# Major changes related to interactivity, and in particular the -# role and behavior of :func:`~matplotlib.pyplot.show`, were made in the -# transition to Matplotlib version 1.0, and bugs were fixed in -# 1.0.1. Here we describe the version 1.0.1 behavior for the -# primary interactive backends, with the partial exception of -# *macosx*. -# -# Interactive mode may also be turned on via :func:`matplotlib.pyplot.ion`, -# and turned off via :func:`matplotlib.pyplot.ioff`. -# -# .. note:: -# Interactive mode works with suitable backends in ipython and in -# the ordinary python shell, but it does *not* work in the IDLE IDE. -# If the default backend does not support interactivity, an interactive -# backend can be explicitly activated using any of the methods discussed -# in `What is a backend?`_. -# -# -# Interactive example -# -------------------- -# -# From an ordinary python prompt, or after invoking ipython with no options, -# try this:: -# -# import matplotlib.pyplot as plt -# plt.ion() -# plt.plot([1.6, 2.7]) -# -# This will pop up a plot window. Your terminal prompt will remain active, so -# that you can type additional commands such as:: -# -# plt.title("interactive test") -# plt.xlabel("index") -# -# On most interactive backends, the figure window will also be updated if you -# change it via the object-oriented interface. E.g. get a reference to the -# `~matplotlib.axes.Axes` instance, and call a method of that instance:: -# -# ax = plt.gca() -# ax.plot([3.1, 2.2]) -# -# If you are using certain backends (like ``macosx``), or an older version -# of Matplotlib, you may not see the new line added to the plot immediately. -# In this case, you need to explicitly call :func:`~matplotlib.pyplot.draw` -# in order to update the plot:: -# -# plt.draw() -# -# -# Non-interactive example -# ----------------------- -# -# Start a fresh session as in the previous example, but now -# turn interactive mode off:: -# -# import matplotlib.pyplot as plt -# plt.ioff() -# plt.plot([1.6, 2.7]) -# -# Nothing happened--or at least nothing has shown up on the -# screen (unless you are using *macosx* backend, which is -# anomalous). To make the plot appear, you need to do this:: -# -# plt.show() -# -# Now you see the plot, but your terminal command line is -# unresponsive; `.pyplot.show()` *blocks* the input -# of additional commands until you manually kill the plot -# window. -# -# What good is this--being forced to use a blocking function? -# Suppose you need a script that plots the contents of a file -# to the screen. You want to look at that plot, and then end -# the script. Without some blocking command such as ``show()``, the -# script would flash up the plot and then end immediately, -# leaving nothing on the screen. -# -# In addition, non-interactive mode delays all drawing until -# ``show()`` is called; this is more efficient than redrawing -# the plot each time a line in the script adds a new feature. -# -# Prior to version 1.0, ``show()`` generally could not be called -# more than once in a single script (although sometimes one -# could get away with it); for version 1.0.1 and above, this -# restriction is lifted, so one can write a script like this:: -# -# import numpy as np -# import matplotlib.pyplot as plt -# -# plt.ioff() -# for i in range(3): -# plt.plot(np.random.rand(10)) -# plt.show() -# -# This makes three plots, one at a time. I.e., the second plot will show up -# once the first plot is closed. -# -# Summary -# ------- -# -# In interactive mode, pyplot functions automatically draw -# to the screen. -# -# When plotting interactively, if using -# object method calls in addition to pyplot functions, then -# call :func:`~matplotlib.pyplot.draw` whenever you want to -# refresh the plot. -# -# Use non-interactive mode in scripts in which you want to -# generate one or more figures and display them before ending -# or generating a new set of figures. In that case, use -# :func:`~matplotlib.pyplot.show` to display the figure(s) and -# to block execution until you have manually destroyed them. -# -# .. _performance: -# -# Performance -# =========== -# -# Whether exploring data in interactive mode or programmatically -# saving lots of plots, rendering performance can be a painful -# bottleneck in your pipeline. Matplotlib provides a couple -# ways to greatly reduce rendering time at the cost of a slight -# change (to a settable tolerance) in your plot's appearance. -# The methods available to reduce rendering time depend on the -# type of plot that is being created. -# -# Line segment simplification -# --------------------------- -# -# For plots that have line segments (e.g. typical line plots, outlines -# of polygons, etc.), rendering performance can be controlled by -# :rc:`path.simplify` and :rc:`path.simplify_threshold`, which -# can be defined e.g. in the :file:`matplotlibrc` file (see -# :doc:`/tutorials/introductory/customizing` for more information about -# the :file:`matplotlibrc` file). :rc:`path.simplify` is a boolean -# indicating whether or not line segments are simplified at all. -# :rc:`path.simplify_threshold` controls how much line segments are simplified; -# higher thresholds result in quicker rendering. -# -# The following script will first display the data without any -# simplification, and then display the same data with simplification. -# Try interacting with both of them:: -# -# import numpy as np -# import matplotlib.pyplot as plt -# import matplotlib as mpl -# -# # Setup, and create the data to plot -# y = np.random.rand(100000) -# y[50000:] *= 2 -# y[np.geomspace(10, 50000, 400).astype(int)] = -1 -# mpl.rcParams['path.simplify'] = True -# -# mpl.rcParams['path.simplify_threshold'] = 0.0 -# plt.plot(y) -# plt.show() -# -# mpl.rcParams['path.simplify_threshold'] = 1.0 -# plt.plot(y) -# plt.show() -# -# Matplotlib currently defaults to a conservative simplification -# threshold of ``1/9``. If you want to change your default settings -# to use a different value, you can change your :file:`matplotlibrc` -# file. Alternatively, you could create a new style for -# interactive plotting (with maximal simplification) and another -# style for publication quality plotting (with minimal -# simplification) and activate them as necessary. See -# :doc:`/tutorials/introductory/customizing` for -# instructions on how to perform these actions. -# -# The simplification works by iteratively merging line segments -# into a single vector until the next line segment's perpendicular -# distance to the vector (measured in display-coordinate space) -# is greater than the ``path.simplify_threshold`` parameter. -# -# .. note:: -# Changes related to how line segments are simplified were made -# in version 2.1. Rendering time will still be improved by these -# parameters prior to 2.1, but rendering time for some kinds of -# data will be vastly improved in versions 2.1 and greater. -# -# Marker simplification -# --------------------- -# -# Markers can also be simplified, albeit less robustly than -# line segments. Marker simplification is only available -# to :class:`~matplotlib.lines.Line2D` objects (through the -# ``markevery`` property). Wherever -# :class:`~matplotlib.lines.Line2D` construction parameters -# are passed through, such as -# :func:`matplotlib.pyplot.plot` and -# :meth:`matplotlib.axes.Axes.plot`, the ``markevery`` -# parameter can be used:: -# -# plt.plot(x, y, markevery=10) -# -# The ``markevery`` argument allows for naive subsampling, or an -# attempt at evenly spaced (along the *x* axis) sampling. See the -# :doc:`/gallery/lines_bars_and_markers/markevery_demo` -# for more information. -# -# Splitting lines into smaller chunks -# ----------------------------------- -# -# If you are using the Agg backend (see :ref:`what-is-a-backend`), -# then you can make use of :rc:`agg.path.chunksize` -# This allows you to specify a chunk size, and any lines with -# greater than that many vertices will be split into multiple -# lines, each of which has no more than ``agg.path.chunksize`` -# many vertices. (Unless ``agg.path.chunksize`` is zero, in -# which case there is no chunking.) For some kind of data, -# chunking the line up into reasonable sizes can greatly -# decrease rendering time. -# -# The following script will first display the data without any -# chunk size restriction, and then display the same data with -# a chunk size of 10,000. The difference can best be seen when -# the figures are large, try maximizing the GUI and then -# interacting with them:: -# -# import numpy as np -# import matplotlib.pyplot as plt -# import matplotlib as mpl -# mpl.rcParams['path.simplify_threshold'] = 1.0 -# -# # Setup, and create the data to plot -# y = np.random.rand(100000) -# y[50000:] *= 2 -# y[np.geomspace(10, 50000, 400).astype(int)] = -1 -# mpl.rcParams['path.simplify'] = True -# -# mpl.rcParams['agg.path.chunksize'] = 0 -# plt.plot(y) -# plt.show() -# -# mpl.rcParams['agg.path.chunksize'] = 10000 -# plt.plot(y) -# plt.show() -# -# Legends -# ------- -# -# The default legend behavior for axes attempts to find the location -# that covers the fewest data points (``loc='best'``). This can be a -# very expensive computation if there are lots of data points. In -# this case, you may want to provide a specific location. -# -# Using the *fast* style -# ---------------------- -# -# The *fast* style can be used to automatically set -# simplification and chunking parameters to reasonable -# settings to speed up plotting large amounts of data. -# It can be used simply by running:: -# -# import matplotlib.style as mplstyle -# mplstyle.use('fast') -# -# It is very lightweight, so it plays nicely with other -# styles, just make sure the fast style is applied last -# so that other styles do not overwrite the settings:: -# -# mplstyle.use(['dark_background', 'ggplot', 'fast']) diff --git a/tutorials/text/README.txt b/tutorials/text/README.txt index 7ad6e9b14b18..4eaaa4de9c23 100644 --- a/tutorials/text/README.txt +++ b/tutorials/text/README.txt @@ -6,5 +6,5 @@ Text matplotlib has extensive text support, including support for mathematical expressions, truetype support for raster and vector outputs, newline separated text with arbitrary -rotations, and unicode support. These tutorials cover +rotations, and Unicode support. These tutorials cover the basics of working with text in Matplotlib. diff --git a/tutorials/text/annotations.py b/tutorials/text/annotations.py index 878835b2f7b7..bee2e244238d 100644 --- a/tutorials/text/annotations.py +++ b/tutorials/text/annotations.py @@ -2,574 +2,671 @@ Annotations =========== -Annotating text with Matplotlib. +Annotations are graphical elements, often pieces of text, that explain, add +context to, or otherwise highlight some portion of the visualized data. +`~.Axes.annotate` supports a number of coordinate systems for flexibly +positioning data and annotations relative to each other and a variety of +options of for styling the text. Axes.annotate also provides an optional arrow +from the text to the data and this arrow can be styled in various ways. +`~.Axes.text` can also be used for simple text annotation, but does not +provide as much flexibility in positioning and styling as `~.Axes.annotate`. .. contents:: Table of Contents :depth: 3 - -.. _annotations-tutorial: - -Basic annotation ----------------- - -The uses of the basic :func:`~matplotlib.pyplot.text` will place text -at an arbitrary position on the Axes. A common use case of text is to -annotate some feature of the plot, and the -:func:`~matplotlib.axes.Axes.annotate` method provides helper functionality -to make annotations easy. In an annotation, there are two points to -consider: the location being annotated represented by the argument -*xy* and the location of the text *xytext*. Both of these -arguments are ``(x, y)`` tuples. - -.. figure:: ../../gallery/pyplots/images/sphx_glr_annotation_basic_001.png - :target: ../../gallery/pyplots/annotation_basic.html - :align: center - :scale: 50 - - Annotation Basic - -In this example, both the *xy* (arrow tip) and *xytext* locations -(text location) are in data coordinates. There are a variety of other -coordinate systems one can choose -- you can specify the coordinate -system of *xy* and *xytext* with one of the following strings for -*xycoords* and *textcoords* (default is 'data') - -================== ======================================================== -argument coordinate system -================== ======================================================== -'figure points' points from the lower left corner of the figure -'figure pixels' pixels from the lower left corner of the figure -'figure fraction' (0, 0) is lower left of figure and (1, 1) is upper right -'axes points' points from lower left corner of axes -'axes pixels' pixels from lower left corner of axes -'axes fraction' (0, 0) is lower left of axes and (1, 1) is upper right -'data' use the axes data coordinate system -================== ======================================================== - -For example to place the text coordinates in fractional axes -coordinates, one could do:: - - ax.annotate('local max', xy=(3, 1), xycoords='data', - xytext=(0.8, 0.95), textcoords='axes fraction', - arrowprops=dict(facecolor='black', shrink=0.05), - horizontalalignment='right', verticalalignment='top', - ) - -For physical coordinate systems (points or pixels) the origin is the -bottom-left of the figure or axes. - -Optionally, you can enable drawing of an arrow from the text to the annotated -point by giving a dictionary of arrow properties in the optional keyword -argument *arrowprops*. - - -==================== ===================================================== -*arrowprops* key description -==================== ===================================================== -width the width of the arrow in points -frac the fraction of the arrow length occupied by the head -headwidth the width of the base of the arrow head in points -shrink move the tip and base some percent away from - the annotated point and text - -\*\*kwargs any key for :class:`matplotlib.patches.Polygon`, - e.g., ``facecolor`` -==================== ===================================================== - - -In the example below, the *xy* point is in native coordinates -(*xycoords* defaults to 'data'). For a polar axes, this is in -(theta, radius) space. The text in this example is placed in the -fractional figure coordinate system. :class:`matplotlib.text.Text` -keyword arguments like *horizontalalignment*, *verticalalignment* and -*fontsize* are passed from `~matplotlib.axes.Axes.annotate` to the -``Text`` instance. - -.. figure:: ../../gallery/pyplots/images/sphx_glr_annotation_polar_001.png - :target: ../../gallery/pyplots/annotation_polar.html - :align: center - :scale: 50 - - Annotation Polar - -For more on all the wild and wonderful things you can do with -annotations, including fancy arrows, see :ref:`plotting-guide-annotation` -and :doc:`/gallery/text_labels_and_annotations/annotation_demo`. - - -Do not proceed unless you have already read :ref:`annotations-tutorial`, -:func:`~matplotlib.pyplot.text` and :func:`~matplotlib.pyplot.annotate`! - - -.. _plotting-guide-annotation: - -Advanced Annotations --------------------- - -Annotating with Text with Box -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Let's start with a simple example. - -.. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_text_arrow_001.png - :target: ../../gallery/userdemo/annotate_text_arrow.html - :align: center - :scale: 50 - - Annotate Text Arrow - -`~.Axes.text` takes a *bbox* keyword argument, which draws a box around the -text:: - - t = ax.text( - 0, 0, "Direction", ha="center", va="center", rotation=45, size=15, - bbox=dict(boxstyle="rarrow,pad=0.3", fc="cyan", ec="b", lw=2)) - -The patch object associated with the text can be accessed by:: - - bb = t.get_bbox_patch() - -The return value is a `.FancyBboxPatch`; patch properties -(facecolor, edgewidth, etc.) can be accessed and modified as usual. -`.FancyBboxPatch.set_boxstyle` sets the box shape:: - - bb.set_boxstyle("rarrow", pad=0.6) - -The arguments are the name of the box style with its attributes as -keyword arguments. Currently, following box styles are implemented. - - ========== ============== ========================== - Class Name Attrs - ========== ============== ========================== - Circle ``circle`` pad=0.3 - DArrow ``darrow`` pad=0.3 - LArrow ``larrow`` pad=0.3 - RArrow ``rarrow`` pad=0.3 - Round ``round`` pad=0.3,rounding_size=None - Round4 ``round4`` pad=0.3,rounding_size=None - Roundtooth ``roundtooth`` pad=0.3,tooth_size=None - Sawtooth ``sawtooth`` pad=0.3,tooth_size=None - Square ``square`` pad=0.3 - ========== ============== ========================== - -.. figure:: ../../gallery/shapes_and_collections/images/sphx_glr_fancybox_demo_001.png - :target: ../../gallery/shapes_and_collections/fancybox_demo.html - :align: center - :scale: 50 - - Fancybox Demo - -Note that the attribute arguments can be specified within the style -name with separating comma (this form can be used as "boxstyle" value -of bbox argument when initializing the text instance) :: - - bb.set_boxstyle("rarrow,pad=0.6") - -Annotating with Arrow -~~~~~~~~~~~~~~~~~~~~~ - -`~.Axes.annotate` draws an arrow connecting two points in an axes:: - - ax.annotate("Annotation", - xy=(x1, y1), xycoords='data', - xytext=(x2, y2), textcoords='offset points', - ) - -This annotates a point at *xy* in the given coordinate (*xycoords*) -with the text at *xytext* given in *textcoords*. Often, the -annotated point is specified in the *data* coordinate and the annotating -text in *offset points*. -See `~.Axes.annotate` for available coordinate systems. - -An arrow connecting *xy* to *xytext* can be optionally drawn by -specifying the *arrowprops* argument. To draw only an arrow, use -empty string as the first argument. :: - - ax.annotate("", - xy=(0.2, 0.2), xycoords='data', - xytext=(0.8, 0.8), textcoords='data', - arrowprops=dict(arrowstyle="->", - connectionstyle="arc3"), - ) - -.. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_simple01_001.png - :target: ../../gallery/userdemo/annotate_simple01.html - :align: center - :scale: 50 - - Annotate Simple01 - -The arrow is drawn as follows: - -1. A path connecting the two points is created, as specified by the - *connectionstyle* parameter. -2. The path is clipped to avoid patches *patchA* and *patchB*, if these are - set. -3. The path is further shrunk by *shrinkA* and *shrinkB* (in pixels). -4. The path is transmuted to an arrow patch, as specified by the *arrowstyle* - parameter. - -.. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_explain_001.png - :target: ../../gallery/userdemo/annotate_explain.html - :align: center - :scale: 50 - - Annotate Explain - - -The creation of the connecting path between two points is controlled by -``connectionstyle`` key and the following styles are available. - - ========== ============================================= - Name Attrs - ========== ============================================= - ``angle`` angleA=90,angleB=0,rad=0.0 - ``angle3`` angleA=90,angleB=0 - ``arc`` angleA=0,angleB=0,armA=None,armB=None,rad=0.0 - ``arc3`` rad=0.0 - ``bar`` armA=0.0,armB=0.0,fraction=0.3,angle=None - ========== ============================================= - -Note that "3" in ``angle3`` and ``arc3`` is meant to indicate that the -resulting path is a quadratic spline segment (three control -points). As will be discussed below, some arrow style options can only -be used when the connecting path is a quadratic spline. - -The behavior of each connection style is (limitedly) demonstrated in the -example below. (Warning: The behavior of the ``bar`` style is currently not -well defined, it may be changed in the future). - -.. figure:: ../../gallery/userdemo/images/sphx_glr_connectionstyle_demo_001.png - :target: ../../gallery/userdemo/connectionstyle_demo.html - :align: center - :scale: 50 - - Connectionstyle Demo - - -The connecting path (after clipping and shrinking) is then mutated to -an arrow patch, according to the given ``arrowstyle``. - - ========== ============================================= - Name Attrs - ========== ============================================= - ``-`` None - ``->`` head_length=0.4,head_width=0.2 - ``-[`` widthB=1.0,lengthB=0.2,angleB=None - ``|-|`` widthA=1.0,widthB=1.0 - ``-|>`` head_length=0.4,head_width=0.2 - ``<-`` head_length=0.4,head_width=0.2 - ``<->`` head_length=0.4,head_width=0.2 - ``<|-`` head_length=0.4,head_width=0.2 - ``<|-|>`` head_length=0.4,head_width=0.2 - ``fancy`` head_length=0.4,head_width=0.4,tail_width=0.4 - ``simple`` head_length=0.5,head_width=0.5,tail_width=0.2 - ``wedge`` tail_width=0.3,shrink_factor=0.5 - ========== ============================================= - -.. figure:: ../../gallery/text_labels_and_annotations/images/sphx_glr_fancyarrow_demo_001.png - :target: ../../gallery/text_labels_and_annotations/fancyarrow_demo.html - :align: center - :scale: 50 - - Fancyarrow Demo - -Some arrowstyles only work with connection styles that generate a -quadratic-spline segment. They are ``fancy``, ``simple``, and ``wedge``. -For these arrow styles, you must use the "angle3" or "arc3" connection -style. - -If the annotation string is given, the patchA is set to the bbox patch -of the text by default. - -.. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_simple02_001.png - :target: ../../gallery/userdemo/annotate_simple02.html - :align: center - :scale: 50 - - Annotate Simple02 - -As with `~.Axes.text`, a box around the text can be drawn using the *bbox* -argument. - -.. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_simple03_001.png - :target: ../../gallery/userdemo/annotate_simple03.html - :align: center - :scale: 50 - - Annotate Simple03 - -By default, the starting point is set to the center of the text -extent. This can be adjusted with ``relpos`` key value. The values -are normalized to the extent of the text. For example, (0, 0) means -lower-left corner and (1, 1) means top-right. - -.. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_simple04_001.png - :target: ../../gallery/userdemo/annotate_simple04.html - :align: center - :scale: 50 - - Annotate Simple04 - - -Placing Artist at the anchored location of the Axes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -There are classes of artists that can be placed at an anchored -location in the Axes. A common example is the legend. This type -of artist can be created by using the `.OffsetBox` class. A few -predefined classes are available in :mod:`matplotlib.offsetbox` and in -:mod:`mpl_toolkits.axes_grid1.anchored_artists`. :: - - from matplotlib.offsetbox import AnchoredText - at = AnchoredText("Figure 1a", - prop=dict(size=15), frameon=True, - loc='upper left', - ) - at.patch.set_boxstyle("round,pad=0.,rounding_size=0.2") - ax.add_artist(at) - - -.. figure:: ../../gallery/userdemo/images/sphx_glr_anchored_box01_001.png - :target: ../../gallery/userdemo/anchored_box01.html - :align: center - :scale: 50 - - Anchored Box01 - - -The *loc* keyword has same meaning as in the legend command. - -A simple application is when the size of the artist (or collection of -artists) is known in pixel size during the time of creation. For -example, If you want to draw a circle with fixed size of 20 pixel x 20 -pixel (radius = 10 pixel), you can utilize -``AnchoredDrawingArea``. The instance is created with a size of the -drawing area (in pixels), and arbitrary artists can added to the -drawing area. Note that the extents of the artists that are added to -the drawing area are not related to the placement of the drawing -area itself. Only the initial size matters. :: - - from mpl_toolkits.axes_grid1.anchored_artists import AnchoredDrawingArea - - ada = AnchoredDrawingArea(20, 20, 0, 0, - loc='upper right', pad=0., frameon=False) - p1 = Circle((10, 10), 10) - ada.drawing_area.add_artist(p1) - p2 = Circle((30, 10), 5, fc="r") - ada.drawing_area.add_artist(p2) - -The artists that are added to the drawing area should not have a -transform set (it will be overridden) and the dimensions of those -artists are interpreted as a pixel coordinate, i.e., the radius of the -circles in above example are 10 pixels and 5 pixels, respectively. - -.. figure:: ../../gallery/userdemo/images/sphx_glr_anchored_box02_001.png - :target: ../../gallery/userdemo/anchored_box02.html - :align: center - :scale: 50 - - Anchored Box02 - -Sometimes, you want your artists to scale with the data coordinate (or -coordinates other than canvas pixels). You can use -``AnchoredAuxTransformBox`` class. This is similar to -``AnchoredDrawingArea`` except that the extent of the artist is -determined during the drawing time respecting the specified transform. :: - - from mpl_toolkits.axes_grid1.anchored_artists import AnchoredAuxTransformBox - - box = AnchoredAuxTransformBox(ax.transData, loc='upper left') - el = Ellipse((0, 0), width=0.1, height=0.4, angle=30) # in data coordinates! - box.drawing_area.add_artist(el) - -The ellipse in the above example will have width and height -corresponding to 0.1 and 0.4 in data coordinates and will be -automatically scaled when the view limits of the axes change. - -.. figure:: ../../gallery/userdemo/images/sphx_glr_anchored_box03_001.png - :target: ../../gallery/userdemo/anchored_box03.html - :align: center - :scale: 50 - - Anchored Box03 - -As in the legend, the bbox_to_anchor argument can be set. Using the -HPacker and VPacker, you can have an arrangement(?) of artist as in the -legend (as a matter of fact, this is how the legend is created). - -.. figure:: ../../gallery/userdemo/images/sphx_glr_anchored_box04_001.png - :target: ../../gallery/userdemo/anchored_box04.html - :align: center - :scale: 50 - - Anchored Box04 - -Note that unlike the legend, the ``bbox_transform`` is set -to IdentityTransform by default. - -Using Complex Coordinates with Annotations -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -The Annotation in matplotlib supports several types of coordinates as -described in :ref:`annotations-tutorial`. For an advanced user who wants -more control, it supports a few other options. - -1. A `.Transform` instance. For example, :: - - ax.annotate("Test", xy=(0.5, 0.5), xycoords=ax.transAxes) - - is identical to :: - - ax.annotate("Test", xy=(0.5, 0.5), xycoords="axes fraction") - - This allows annotating a point in another axes:: - - fig, (ax1, ax2) = plt.subplots(1, 2) - ax2.annotate("Test", xy=(0.5, 0.5), xycoords=ax1.transData, - xytext=(0.5, 0.5), textcoords=ax2.transData, +""" +############################################################################### +# .. _annotations-tutorial: +# +# Basic annotation +# ---------------- +# +# In an annotation, there are two points to consider: the location of the data +# being annotated *xy* and the location of the annotation text *xytext*. Both +# of these arguments are ``(x, y)`` tuples: + +import numpy as np +import matplotlib.pyplot as plt + +fig, ax = plt.subplots(figsize=(3, 3)) + +t = np.arange(0.0, 5.0, 0.01) +s = np.cos(2*np.pi*t) +line, = ax.plot(t, s, lw=2) + +ax.annotate('local max', xy=(2, 1), xytext=(3, 1.5), + arrowprops=dict(facecolor='black', shrink=0.05)) +ax.set_ylim(-2, 2) + +############################################################################### +# In this example, both the *xy* (arrow tip) and *xytext* locations +# (text location) are in data coordinates. There are a variety of other +# coordinate systems one can choose -- you can specify the coordinate +# system of *xy* and *xytext* with one of the following strings for +# *xycoords* and *textcoords* (default is 'data') +# +# ================== ======================================================== +# argument coordinate system +# ================== ======================================================== +# 'figure points' points from the lower left corner of the figure +# 'figure pixels' pixels from the lower left corner of the figure +# 'figure fraction' (0, 0) is lower left of figure and (1, 1) is upper right +# 'axes points' points from lower left corner of axes +# 'axes pixels' pixels from lower left corner of axes +# 'axes fraction' (0, 0) is lower left of axes and (1, 1) is upper right +# 'data' use the axes data coordinate system +# ================== ======================================================== +# +# The following strings are also valid arguments for *textcoords* +# +# ================== ======================================================== +# argument coordinate system +# ================== ======================================================== +# 'offset points' offset (in points) from the xy value +# 'offset pixels' offset (in pixels) from the xy value +# ================== ======================================================== +# +# For physical coordinate systems (points or pixels) the origin is the +# bottom-left of the figure or axes. Points are +# `typographic points `_ +# meaning that they are a physical unit measuring 1/72 of an inch. Points and +# pixels are discussed in further detail in :ref:`transforms-fig-scale-dpi`. +# +# .. _annotation-data: +# +# Annotating data +# ~~~~~~~~~~~~~~~ +# +# This example places the text coordinates in fractional axes coordinates: + +fig, ax = plt.subplots(figsize=(3, 3)) + +t = np.arange(0.0, 5.0, 0.01) +s = np.cos(2*np.pi*t) +line, = ax.plot(t, s, lw=2) + +ax.annotate('local max', xy=(2, 1), xycoords='data', + xytext=(0.01, .99), textcoords='axes fraction', + va='top', ha='left', + arrowprops=dict(facecolor='black', shrink=0.05)) +ax.set_ylim(-2, 2) + +################################################################### +# +# .. _annotation-with-arrow: +# +# Annotating with arrows +# ~~~~~~~~~~~~~~~~~~~~~~ +# +# You can enable drawing of an arrow from the text to the annotated point +# by giving a dictionary of arrow properties in the optional keyword +# argument *arrowprops*. +# +# ==================== ===================================================== +# *arrowprops* key description +# ==================== ===================================================== +# width the width of the arrow in points +# frac the fraction of the arrow length occupied by the head +# headwidth the width of the base of the arrow head in points +# shrink move the tip and base some percent away from +# the annotated point and text +# +# \*\*kwargs any key for :class:`matplotlib.patches.Polygon`, +# e.g., ``facecolor`` +# ==================== ===================================================== +# +# In the example below, the *xy* point is in the data coordinate system +# since *xycoords* defaults to 'data'. For a polar axes, this is in +# (theta, radius) space. The text in this example is placed in the +# fractional figure coordinate system. :class:`matplotlib.text.Text` +# keyword arguments like *horizontalalignment*, *verticalalignment* and +# *fontsize* are passed from `~matplotlib.axes.Axes.annotate` to the +# ``Text`` instance. + +fig = plt.figure() +ax = fig.add_subplot(projection='polar') +r = np.arange(0, 1, 0.001) +theta = 2 * 2*np.pi * r +line, = ax.plot(theta, r, color='#ee8d18', lw=3) + +ind = 800 +thisr, thistheta = r[ind], theta[ind] +ax.plot([thistheta], [thisr], 'o') +ax.annotate('a polar annotation', + xy=(thistheta, thisr), # theta, radius + xytext=(0.05, 0.05), # fraction, fraction + textcoords='figure fraction', + arrowprops=dict(facecolor='black', shrink=0.05), + horizontalalignment='left', + verticalalignment='bottom') + +############################################################################### +# For more on plotting with arrows, see :ref:`annotation_with_custom_arrow` +# +# .. _annotations-offset-text: +# +# Placing text annotations relative to data +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# Annotations can be positioned at a relative offset to the *xy* input to +# annotation by setting the *textcoords* keyword argument to ``'offset points'`` +# or ``'offset pixels'``. + +fig, ax = plt.subplots(figsize=(3, 3)) +x = [1, 3, 5, 7, 9] +y = [2, 4, 6, 8, 10] +annotations = ["A", "B", "C", "D", "E"] +ax.scatter(x, y, s=20) + +for xi, yi, text in zip(x, y, annotations): + ax.annotate(text, + xy=(xi, yi), xycoords='data', + xytext=(1.5, 1.5), textcoords='offset points') + +############################################################################### +# The annotations are offset 1.5 points (1.5*1/72 inches) from the *xy* values. +# +# .. _plotting-guide-annotation: +# +# Advanced annotation +# ------------------- +# +# We recommend reading :ref:`annotations-tutorial`, :func:`~matplotlib.pyplot.text` +# and :func:`~matplotlib.pyplot.annotate` before reading this section. +# +# Annotating with boxed text +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# `~.Axes.text` takes a *bbox* keyword argument, which draws a box around the +# text: + +fig, ax = plt.subplots(figsize=(5, 5)) +t = ax.text(0.5, 0.5, "Direction", + ha="center", va="center", rotation=45, size=15, + bbox=dict(boxstyle="rarrow,pad=0.3", + fc="lightblue", ec="steelblue", lw=2)) + +############################################################################### +# The arguments are the name of the box style with its attributes as +# keyword arguments. Currently, following box styles are implemented. +# +# ========== ============== ========================== +# Class Name Attrs +# ========== ============== ========================== +# Circle ``circle`` pad=0.3 +# DArrow ``darrow`` pad=0.3 +# Ellipse ``ellipse`` pad=0.3 +# LArrow ``larrow`` pad=0.3 +# RArrow ``rarrow`` pad=0.3 +# Round ``round`` pad=0.3,rounding_size=None +# Round4 ``round4`` pad=0.3,rounding_size=None +# Roundtooth ``roundtooth`` pad=0.3,tooth_size=None +# Sawtooth ``sawtooth`` pad=0.3,tooth_size=None +# Square ``square`` pad=0.3 +# ========== ============== ========================== +# +# .. figure:: ../../gallery/shapes_and_collections/images/sphx_glr_fancybox_demo_001.png +# :target: ../../gallery/shapes_and_collections/fancybox_demo.html +# :align: center +# +# The patch object (box) associated with the text can be accessed using:: +# +# bb = t.get_bbox_patch() +# +# The return value is a `.FancyBboxPatch`; patch properties +# (facecolor, edgewidth, etc.) can be accessed and modified as usual. +# `.FancyBboxPatch.set_boxstyle` sets the box shape:: +# +# bb.set_boxstyle("rarrow", pad=0.6) +# +# The attribute arguments can also be specified within the style +# name with separating comma:: +# +# bb.set_boxstyle("rarrow, pad=0.6") +# +# +# Defining custom box styles +# ~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# You can use a custom box style. The value for the ``boxstyle`` can be a +# callable object in the following forms: + +from matplotlib.path import Path + + +def custom_box_style(x0, y0, width, height, mutation_size): + """ + Given the location and size of the box, return the path of the box around + it. Rotation is automatically taken care of. + + Parameters + ---------- + x0, y0, width, height : float + Box location and size. + mutation_size : float + Mutation reference scale, typically the text font size. + """ + # padding + mypad = 0.3 + pad = mutation_size * mypad + # width and height with padding added. + width = width + 2 * pad + height = height + 2 * pad + # boundary of the padded box + x0, y0 = x0 - pad, y0 - pad + x1, y1 = x0 + width, y0 + height + # return the new path + return Path([(x0, y0), (x1, y0), (x1, y1), (x0, y1), + (x0-pad, (y0+y1)/2), (x0, y0), (x0, y0)], + closed=True) + +fig, ax = plt.subplots(figsize=(3, 3)) +ax.text(0.5, 0.5, "Test", size=30, va="center", ha="center", rotation=30, + bbox=dict(boxstyle=custom_box_style, alpha=0.2)) + +############################################################################### +# See also :doc:`/gallery/userdemo/custom_boxstyle01`. Similarly, you can define a +# custom `.ConnectionStyle` and a custom `.ArrowStyle`. View the source code at +# `.patches` to learn how each class is defined. +# +# .. _annotation_with_custom_arrow: +# +# Customizing annotation arrows +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# An arrow connecting *xy* to *xytext* can be optionally drawn by +# specifying the *arrowprops* argument. To draw only an arrow, use +# empty string as the first argument: + +fig, ax = plt.subplots(figsize=(3, 3)) +ax.annotate("", + xy=(0.2, 0.2), xycoords='data', + xytext=(0.8, 0.8), textcoords='data', + arrowprops=dict(arrowstyle="->", connectionstyle="arc3")) + +############################################################################### +# The arrow is drawn as follows: +# +# 1. A path connecting the two points is created, as specified by the +# *connectionstyle* parameter. +# 2. The path is clipped to avoid patches *patchA* and *patchB*, if these are +# set. +# 3. The path is further shrunk by *shrinkA* and *shrinkB* (in pixels). +# 4. The path is transmuted to an arrow patch, as specified by the *arrowstyle* +# parameter. +# +# .. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_explain_001.png +# :target: ../../gallery/userdemo/annotate_explain.html +# :align: center +# +# The creation of the connecting path between two points is controlled by +# ``connectionstyle`` key and the following styles are available. +# +# ========== ============================================= +# Name Attrs +# ========== ============================================= +# ``angle`` angleA=90,angleB=0,rad=0.0 +# ``angle3`` angleA=90,angleB=0 +# ``arc`` angleA=0,angleB=0,armA=None,armB=None,rad=0.0 +# ``arc3`` rad=0.0 +# ``bar`` armA=0.0,armB=0.0,fraction=0.3,angle=None +# ========== ============================================= +# +# Note that "3" in ``angle3`` and ``arc3`` is meant to indicate that the +# resulting path is a quadratic spline segment (three control +# points). As will be discussed below, some arrow style options can only +# be used when the connecting path is a quadratic spline. +# +# The behavior of each connection style is (limitedly) demonstrated in the +# example below. (Warning: The behavior of the ``bar`` style is currently not +# well-defined and may be changed in the future). +# +# .. figure:: ../../gallery/userdemo/images/sphx_glr_connectionstyle_demo_001.png +# :target: ../../gallery/userdemo/connectionstyle_demo.html +# :align: center +# +# The connecting path (after clipping and shrinking) is then mutated to +# an arrow patch, according to the given ``arrowstyle``. +# +# ========== ============================================= +# Name Attrs +# ========== ============================================= +# ``-`` None +# ``->`` head_length=0.4,head_width=0.2 +# ``-[`` widthB=1.0,lengthB=0.2,angleB=None +# ``|-|`` widthA=1.0,widthB=1.0 +# ``-|>`` head_length=0.4,head_width=0.2 +# ``<-`` head_length=0.4,head_width=0.2 +# ``<->`` head_length=0.4,head_width=0.2 +# ``<|-`` head_length=0.4,head_width=0.2 +# ``<|-|>`` head_length=0.4,head_width=0.2 +# ``fancy`` head_length=0.4,head_width=0.4,tail_width=0.4 +# ``simple`` head_length=0.5,head_width=0.5,tail_width=0.2 +# ``wedge`` tail_width=0.3,shrink_factor=0.5 +# ========== ============================================= +# +# .. figure:: ../../gallery/text_labels_and_annotations/images/sphx_glr_fancyarrow_demo_001.png +# :target: ../../gallery/text_labels_and_annotations/fancyarrow_demo.html +# :align: center +# +# Some arrowstyles only work with connection styles that generate a +# quadratic-spline segment. They are ``fancy``, ``simple``, and ``wedge``. +# For these arrow styles, you must use the "angle3" or "arc3" connection +# style. +# +# If the annotation string is given, the patch is set to the bbox patch +# of the text by default. + +fig, ax = plt.subplots(figsize=(3, 3)) + +ax.annotate("Test", + xy=(0.2, 0.2), xycoords='data', + xytext=(0.8, 0.8), textcoords='data', + size=20, va="center", ha="center", + arrowprops=dict(arrowstyle="simple", + connectionstyle="arc3,rad=-0.2")) + +############################################################################## +# As with `~.Axes.text`, a box around the text can be drawn using the *bbox* +# argument. + +fig, ax = plt.subplots(figsize=(3, 3)) + +ann = ax.annotate("Test", + xy=(0.2, 0.2), xycoords='data', + xytext=(0.8, 0.8), textcoords='data', + size=20, va="center", ha="center", + bbox=dict(boxstyle="round4", fc="w"), + arrowprops=dict(arrowstyle="-|>", + connectionstyle="arc3,rad=-0.2", + fc="w")) + +############################################################################## +# By default, the starting point is set to the center of the text +# extent. This can be adjusted with ``relpos`` key value. The values +# are normalized to the extent of the text. For example, (0, 0) means +# lower-left corner and (1, 1) means top-right. + +fig, ax = plt.subplots(figsize=(3, 3)) + +ann = ax.annotate("Test", + xy=(0.2, 0.2), xycoords='data', + xytext=(0.8, 0.8), textcoords='data', + size=20, va="center", ha="center", + bbox=dict(boxstyle="round4", fc="w"), + arrowprops=dict(arrowstyle="-|>", + connectionstyle="arc3,rad=0.2", + relpos=(0., 0.), + fc="w")) + +ann = ax.annotate("Test", + xy=(0.2, 0.2), xycoords='data', + xytext=(0.8, 0.8), textcoords='data', + size=20, va="center", ha="center", + bbox=dict(boxstyle="round4", fc="w"), + arrowprops=dict(arrowstyle="-|>", + connectionstyle="arc3,rad=-0.2", + relpos=(1., 0.), + fc="w")) + +############################################################################## +# Placing Artist at anchored Axes locations +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# There are classes of artists that can be placed at an anchored +# location in the Axes. A common example is the legend. This type +# of artist can be created by using the `.OffsetBox` class. A few +# predefined classes are available in :mod:`matplotlib.offsetbox` and in +# :mod:`mpl_toolkits.axes_grid1.anchored_artists`. + +from matplotlib.offsetbox import AnchoredText + +fig, ax = plt.subplots(figsize=(3, 3)) +at = AnchoredText("Figure 1a", + prop=dict(size=15), frameon=True, loc='upper left') +at.patch.set_boxstyle("round,pad=0.,rounding_size=0.2") +ax.add_artist(at) + +############################################################################### +# The *loc* keyword has same meaning as in the legend command. +# +# A simple application is when the size of the artist (or collection of +# artists) is known in pixel size during the time of creation. For +# example, If you want to draw a circle with fixed size of 20 pixel x 20 +# pixel (radius = 10 pixel), you can utilize +# `~mpl_toolkits.axes_grid1.anchored_artists.AnchoredDrawingArea`. The instance +# is created with a size of the drawing area (in pixels), and arbitrary artists +# can be added to the drawing area. Note that the extents of the artists that are +# added to the drawing area are not related to the placement of the drawing +# area itself. Only the initial size matters. +# +# The artists that are added to the drawing area should not have a +# transform set (it will be overridden) and the dimensions of those +# artists are interpreted as a pixel coordinate, i.e., the radius of the +# circles in above example are 10 pixels and 5 pixels, respectively. + +from matplotlib.patches import Circle +from mpl_toolkits.axes_grid1.anchored_artists import AnchoredDrawingArea + +fig, ax = plt.subplots(figsize=(3, 3)) +ada = AnchoredDrawingArea(40, 20, 0, 0, + loc='upper right', pad=0., frameon=False) +p1 = Circle((10, 10), 10) +ada.drawing_area.add_artist(p1) +p2 = Circle((30, 10), 5, fc="r") +ada.drawing_area.add_artist(p2) +ax.add_artist(ada) + +############################################################################### +# Sometimes, you want your artists to scale with the data coordinate (or +# coordinates other than canvas pixels). You can use +# `~mpl_toolkits.axes_grid1.anchored_artists.AnchoredAuxTransformBox` class. +# This is similar to +# `~mpl_toolkits.axes_grid1.anchored_artists.AnchoredDrawingArea` except that +# the extent of the artist is determined during the drawing time respecting the +# specified transform. +# +# The ellipse in the example below will have width and height +# corresponding to 0.1 and 0.4 in data coordinates and will be +# automatically scaled when the view limits of the axes change. + +from matplotlib.patches import Ellipse +from mpl_toolkits.axes_grid1.anchored_artists import AnchoredAuxTransformBox + +fig, ax = plt.subplots(figsize=(3, 3)) +box = AnchoredAuxTransformBox(ax.transData, loc='upper left') +el = Ellipse((0, 0), width=0.1, height=0.4, angle=30) # in data coordinates! +box.drawing_area.add_artist(el) +ax.add_artist(box) + +############################################################################### +# Another method of anchoring an artist relative to a parent axes or anchor +# point is via the *bbox_to_anchor* argument of `.AnchoredOffsetbox`. This +# artist can then be automatically positioned relative to another artist using +# `.HPacker` and `.VPacker`: + +from matplotlib.offsetbox import (AnchoredOffsetbox, DrawingArea, HPacker, + TextArea) + +fig, ax = plt.subplots(figsize=(3, 3)) + +box1 = TextArea(" Test: ", textprops=dict(color="k")) +box2 = DrawingArea(60, 20, 0, 0) + +el1 = Ellipse((10, 10), width=16, height=5, angle=30, fc="r") +el2 = Ellipse((30, 10), width=16, height=5, angle=170, fc="g") +el3 = Ellipse((50, 10), width=16, height=5, angle=230, fc="b") +box2.add_artist(el1) +box2.add_artist(el2) +box2.add_artist(el3) + +box = HPacker(children=[box1, box2], + align="center", + pad=0, sep=5) + +anchored_box = AnchoredOffsetbox(loc='lower left', + child=box, pad=0., + frameon=True, + bbox_to_anchor=(0., 1.02), + bbox_transform=ax.transAxes, + borderpad=0.,) + +ax.add_artist(anchored_box) +fig.subplots_adjust(top=0.8) + +############################################################################### +# Note that, unlike in `.Legend`, the ``bbox_transform`` is set to +# `.IdentityTransform` by default +# +# .. _annotating_coordinate_systems: +# +# Coordinate systems for annotations +# ---------------------------------- +# +# Matplotlib Annotations support several types of coordinate systems. The +# examples in :ref:`annotations-tutorial` used the ``data`` coordinate system; +# Some others more advanced options are: +# +# 1. A `.Transform` instance. For more information on transforms, see the +# :doc:`../advanced/transforms_tutorial` For example, the +# ``Axes.transAxes`` transform positions the annotation relative to the Axes +# coordinates and using it is therefore identical to setting the +# coordinate system to "axes fraction": + +fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(6, 3)) +ax1.annotate("Test", xy=(0.5, 0.5), xycoords=ax1.transAxes) +ax2.annotate("Test", xy=(0.5, 0.5), xycoords="axes fraction") + +############################################################################### +# Another commonly used `.Transform` instance is ``Axes.transData``. This +# transform is the coordinate system of the data plotted in the axes. In this +# example, it is used to draw an arrow from a point in *ax1* to text in *ax2*, +# where the point and text are positioned relative to the coordinates of *ax1* +# and *ax2* respectively: + +fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(6, 3)) + +ax1.annotate("Test1", xy=(0.5, 0.5), xycoords="axes fraction") +ax2.annotate("Test2", + xy=(0.5, 0.5), xycoords=ax1.transData, + xytext=(0.5, 0.5), textcoords=ax2.transData, + arrowprops=dict(arrowstyle="->")) + +############################################################################# +# 2. An `.Artist` instance. The *xy* value (or *xytext*) is interpreted as a +# fractional coordinate of the bounding box (bbox) of the artist: + +fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(3, 3)) +an1 = ax.annotate("Test 1", + xy=(0.5, 0.5), xycoords="data", + va="center", ha="center", + bbox=dict(boxstyle="round", fc="w")) + +an2 = ax.annotate("Test 2", + xy=(1, 0.5), xycoords=an1, # (1, 0.5) of an1's bbox + xytext=(30, 0), textcoords="offset points", + va="center", ha="left", + bbox=dict(boxstyle="round", fc="w"), arrowprops=dict(arrowstyle="->")) -2. An `.Artist` instance. The *xy* value (or *xytext*) is interpreted as a - fractional coordinate of the bbox (return value of *get_window_extent*) of - the artist:: - - an1 = ax.annotate("Test 1", xy=(0.5, 0.5), xycoords="data", - va="center", ha="center", - bbox=dict(boxstyle="round", fc="w")) - an2 = ax.annotate("Test 2", xy=(1, 0.5), xycoords=an1, # (1, 0.5) of the an1's bbox - xytext=(30, 0), textcoords="offset points", - va="center", ha="left", - bbox=dict(boxstyle="round", fc="w"), - arrowprops=dict(arrowstyle="->")) - - .. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_simple_coord01_001.png - :target: ../../gallery/userdemo/annotate_simple_coord01.html - :align: center - :scale: 50 - - Annotation with Simple Coordinates - - Note that you must ensure that the extent of the coordinate artist (*an1* in - above example) is determined before *an2* gets drawn. Usually, this means - that *an2* needs to be drawn after *an1*. - -3. A callable object that takes the renderer instance as single argument, and - returns either a `.Transform` or a `.BboxBase`. The return value is then - handled as in (1), for transforms, or in (2), for bboxes. For example, :: - - an2 = ax.annotate("Test 2", xy=(1, 0.5), xycoords=an1, - xytext=(30, 0), textcoords="offset points") - - is identical to:: - - an2 = ax.annotate("Test 2", xy=(1, 0.5), xycoords=an1.get_window_extent, - xytext=(30, 0), textcoords="offset points") - -4. A pair of coordinate specifications -- the first for the x-coordinate, and - the second is for the y-coordinate; e.g. :: - - annotate("Test", xy=(0.5, 1), xycoords=("data", "axes fraction")) - - Here, 0.5 is in data coordinates, and 1 is in normalized axes coordinates. - Each of the coordinate specifications can also be an artist or a transform. - For example, - - .. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_simple_coord02_001.png - :target: ../../gallery/userdemo/annotate_simple_coord02.html - :align: center - :scale: 50 - - Annotation with Simple Coordinates 2 - -5. Sometimes, you want your annotation with some "offset points", not from the - annotated point but from some other point. `.text.OffsetFrom` is a helper - for such cases. - - .. figure:: ../../gallery/userdemo/images/sphx_glr_annotate_simple_coord03_001.png - :target: ../../gallery/userdemo/annotate_simple_coord03.html - :align: center - :scale: 50 - - Annotation with Simple Coordinates 3 - - You may take a look at this example - :doc:`/gallery/text_labels_and_annotations/annotation_demo`. - -Using ConnectionPatch -~~~~~~~~~~~~~~~~~~~~~ - -ConnectionPatch is like an annotation without text. While `~.Axes.annotate` -is sufficient in most situations, ConnectionPatch is useful when you want to -connect points in different axes. :: - - from matplotlib.patches import ConnectionPatch - xy = (0.2, 0.2) - con = ConnectionPatch(xyA=xy, coordsA=ax1.transData, - xyB=xy, coordsB=ax2.transData) - fig.add_artist(con) - -The above code connects point *xy* in the data coordinates of ``ax1`` to -point *xy* in the data coordinates of ``ax2``. Here is a simple example. - -.. figure:: ../../gallery/userdemo/images/sphx_glr_connect_simple01_001.png - :target: ../../gallery/userdemo/connect_simple01.html - :align: center - :scale: 50 - - Connect Simple01 - -Here, we added the ConnectionPatch to the *figure* (with `~.Figure.add_artist`) -rather than to either axes: this ensures that it is drawn on top of both axes, -and is also necessary if using :doc:`constrained_layout -` for positioning the axes. - -Advanced Topics ---------------- - -Zoom effect between Axes -~~~~~~~~~~~~~~~~~~~~~~~~ - -``mpl_toolkits.axes_grid1.inset_locator`` defines some patch classes useful for -interconnecting two axes. Understanding the code requires some knowledge of -Matplotlib's transform system. +############################################################################### +# Note that you must ensure that the extent of the coordinate artist (*an1* in +# this example) is determined before *an2* gets drawn. Usually, this means +# that *an2* needs to be drawn after *an1*. The base class for all bounding +# boxes is `.BboxBase` +# +# 3. A callable object that takes the renderer instance as single argument, and +# returns either a `.Transform` or a `.BboxBase`. For example, the return +# value of `.Artist.get_window_extent` is a bbox, so this method is identical +# to (2) passing in the artist: + +fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(3, 3)) +an1 = ax.annotate("Test 1", + xy=(0.5, 0.5), xycoords="data", + va="center", ha="center", + bbox=dict(boxstyle="round", fc="w")) + +an2 = ax.annotate("Test 2", + xy=(1, 0.5), xycoords=an1.get_window_extent, + xytext=(30, 0), textcoords="offset points", + va="center", ha="left", + bbox=dict(boxstyle="round", fc="w"), + arrowprops=dict(arrowstyle="->")) -.. figure:: ../../gallery/subplots_axes_and_figures/images/sphx_glr_axes_zoom_effect_001.png - :target: ../../gallery/subplots_axes_and_figures/axes_zoom_effect.html - :align: center - :scale: 50 +############################################################################### +# `.Artist.get_window_extent` is the bounding box of the Axes object and is +# therefore identical to setting the coordinate system to axes fraction: - Axes Zoom Effect +fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(6, 3)) -Define Custom BoxStyle -~~~~~~~~~~~~~~~~~~~~~~ +an1 = ax1.annotate("Test1", xy=(0.5, 0.5), xycoords="axes fraction") +an2 = ax2.annotate("Test 2", xy=(0.5, 0.5), xycoords=ax2.get_window_extent) -You can use a custom box style. The value for the ``boxstyle`` can be a -callable object in the following forms.:: +############################################################################### +# 4. A blended pair of coordinate specifications -- the first for the +# x-coordinate, and the second is for the y-coordinate. For example, x=0.5 is +# in data coordinates, and y=1 is in normalized axes coordinates: - def __call__(self, x0, y0, width, height, mutation_size, - aspect_ratio=1.): - ''' - Given the location and size of the box, return the path of - the box around it. +fig, ax = plt.subplots(figsize=(3, 3)) +ax.annotate("Test", xy=(0.5, 1), xycoords=("data", "axes fraction")) +ax.axvline(x=.5, color='lightgray') +ax.set(xlim=(0, 2), ylim=(1, 2)) - - *x0*, *y0*, *width*, *height* : location and size of the box - - *mutation_size* : a reference scale for the mutation. - - *aspect_ratio* : aspect-ratio for the mutation. - ''' - path = ... - return path +############################################################################### +# 5. Sometimes, you want your annotation with some "offset points", not from the +# annotated point but from some other point or artist. `.text.OffsetFrom` is +# a helper for such cases. -Here is a complete example. +from matplotlib.text import OffsetFrom -.. figure:: ../../gallery/userdemo/images/sphx_glr_custom_boxstyle01_001.png - :target: ../../gallery/userdemo/custom_boxstyle01.html - :align: center - :scale: 50 +fig, ax = plt.subplots(figsize=(3, 3)) +an1 = ax.annotate("Test 1", xy=(0.5, 0.5), xycoords="data", + va="center", ha="center", + bbox=dict(boxstyle="round", fc="w")) - Custom Boxstyle01 +offset_from = OffsetFrom(an1, (0.5, 0)) +an2 = ax.annotate("Test 2", xy=(0.1, 0.1), xycoords="data", + xytext=(0, -10), textcoords=offset_from, + # xytext is offset points from "xy=(0.5, 0), xycoords=an1" + va="top", ha="center", + bbox=dict(boxstyle="round", fc="w"), + arrowprops=dict(arrowstyle="->")) -Similarly, you can define a custom ConnectionStyle and a custom ArrowStyle. -See the source code of ``lib/matplotlib/patches.py`` and check -how each style class is defined. -""" +############################################################################### +# Using ConnectionPatch +# ~~~~~~~~~~~~~~~~~~~~~ +# +# `.ConnectionPatch` is like an annotation without text. While `~.Axes.annotate` +# is sufficient in most situations, `.ConnectionPatch` is useful when you want +# to connect points in different axes. For example, here we connect the point +# *xy* in the data coordinates of ``ax1`` to point *xy* in the data coordinates +# of ``ax2``: + +from matplotlib.patches import ConnectionPatch + +fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(6, 3)) +xy = (0.3, 0.2) +con = ConnectionPatch(xyA=xy, coordsA=ax1.transData, + xyB=xy, coordsB=ax2.transData) + +fig.add_artist(con) + +############################################################################### +# Here, we added the `.ConnectionPatch` to the *figure* +# (with `~.Figure.add_artist`) rather than to either axes. This ensures that +# the ConnectionPatch artist is drawn on top of both axes, and is also necessary +# when using :doc:`constrained_layout +# ` for positioning the axes. +# +# Zoom effect between Axes +# ~~~~~~~~~~~~~~~~~~~~~~~~ +# +# `mpl_toolkits.axes_grid1.inset_locator` defines some patch classes useful for +# interconnecting two axes. +# +# .. figure:: ../../gallery/subplots_axes_and_figures/images/sphx_glr_axes_zoom_effect_001.png +# :target: ../../gallery/subplots_axes_and_figures/axes_zoom_effect.html +# :align: center +# +# The code for this figure is at +# :doc:`/gallery/subplots_axes_and_figures/axes_zoom_effect` and +# familiarity with :doc:`../advanced/transforms_tutorial` +# is recommended. diff --git a/tutorials/text/mathtext.py b/tutorials/text/mathtext.py index 3663d27f5f8b..2be0d67ca3b0 100644 --- a/tutorials/text/mathtext.py +++ b/tutorials/text/mathtext.py @@ -1,25 +1,24 @@ r""" Writing mathematical expressions ================================ -An introduction to writing mathematical expressions in Matplotlib. -You can use a subset TeX markup in any matplotlib text string by placing it +You can use a subset of TeX markup in any Matplotlib text string by placing it inside a pair of dollar signs ($). Note that you do not need to have TeX installed, since Matplotlib ships its own TeX expression parser, layout engine, and fonts. The layout engine is a fairly direct adaptation of the layout algorithms in Donald Knuth's -TeX, so the quality is quite good (matplotlib also provides a ``usetex`` -option for those who do want to call out to TeX to generate their text (see +TeX, so the quality is quite good (Matplotlib also provides a ``usetex`` +option for those who do want to call out to TeX to generate their text; see :doc:`/tutorials/text/usetex`). Any text element can use math text. You should use raw strings (precede the quotes with an ``'r'``), and surround the math text with dollar signs ($), as in TeX. Regular text and mathtext can be interleaved within the same string. Mathtext can use DejaVu Sans (default), DejaVu Serif, the Computer Modern fonts -(from (La)TeX), `STIX `_ fonts (with are designed +(from (La)TeX), `STIX `_ fonts (which are designed to blend well with Times), or a Unicode font that you provide. The mathtext -font can be selected with the customization variable ``mathtext.fontset`` (see +font can be selected via :rc:`mathtext.fontset` (see :doc:`/tutorials/introductory/customizing`) Here is a simple example:: @@ -112,7 +111,7 @@ r'$(\frac{5 - \frac{1}{x}}{4})$' -.. math :: +.. math:: (\frac{5 - \frac{1}{x}}{4}) @@ -121,7 +120,7 @@ r'$\left(\frac{5 - \frac{1}{x}}{4}\right)$' -.. math :: +.. math:: \left(\frac{5 - \frac{1}{x}}{4}\right) @@ -131,7 +130,7 @@ r'$\sqrt{2}$' -.. math :: +.. math:: \sqrt{2} @@ -141,7 +140,7 @@ r'$\sqrt[3]{x}$' -.. math :: +.. math:: \sqrt[3]{x} @@ -309,8 +308,10 @@ ``\acute a`` or ``\'a`` :mathmpl:`\acute a` ``\bar a`` :mathmpl:`\bar a` ``\breve a`` :mathmpl:`\breve a` - ``\ddot a`` or ``\''a`` :mathmpl:`\ddot a` ``\dot a`` or ``\.a`` :mathmpl:`\dot a` + ``\ddot a`` or ``\''a`` :mathmpl:`\ddot a` + ``\dddot a`` :mathmpl:`\dddot a` + ``\ddddot a`` :mathmpl:`\ddddot a` ``\grave a`` or ``\`a`` :mathmpl:`\grave a` ``\hat a`` or ``\^a`` :mathmpl:`\hat a` ``\tilde a`` or ``\~a`` :mathmpl:`\tilde a` @@ -353,10 +354,7 @@ ------- Here is an example illustrating many of these features in context. -.. figure:: ../../gallery/pyplots/images/sphx_glr_pyplot_mathtext_001.png - :target: ../../gallery/pyplots/pyplot_mathtext.html +.. figure:: ../../gallery/text_labels_and_annotations/images/sphx_glr_mathtext_demo_001.png + :target: ../../gallery/text_labels_and_annotations/mathtext_demo.html :align: center - :scale: 50 - - Pyplot Mathtext """ diff --git a/tutorials/text/pgf.py b/tutorials/text/pgf.py index 43e7a9628973..5cbf4cfa8b98 100644 --- a/tutorials/text/pgf.py +++ b/tutorials/text/pgf.py @@ -1,9 +1,7 @@ r""" -********************************* -Typesetting With XeLaTeX/LuaLaTeX -********************************* - -How to typeset text with the ``pgf`` backend in Matplotlib. +************************************************************ +Text rendering with XeLaTeX/LuaLaTeX via the ``pgf`` backend +************************************************************ Using the ``pgf`` backend, Matplotlib can export figures as pgf drawing commands that can be processed with pdflatex, xelatex or lualatex. XeLaTeX and @@ -53,7 +51,13 @@ Generally, these characters must be escaped correctly. For convenience, some characters (_, ^, %) are automatically escaped outside of math - environments. + environments. Other characters are not escaped as they are commonly needed + in actual TeX expressions. However, one can configure TeX to treat them as + "normal" characters (known as "catcode 12" to TeX) via a custom preamble, + such as:: + + plt.rcParams["pgf.preamble"] = ( + r"\AtBeginDocument{\catcode`\&=12\catcode`\#=12}") .. _pgf-rcfonts: @@ -150,11 +154,6 @@ executables. See :ref:`environment-variables` and :ref:`setting-windows-environment-variables` for details. -* A limitation on Windows causes the backend to keep file handles that have - been opened by your application open. As a result, it may not be possible - to delete the corresponding files until the application closes (see - `#1324 `_). - * Sometimes the font rendering in figures that are saved to png images is very bad. This happens when the pdftocairo tool is not available and ghostscript is used for the pdf to png conversion. @@ -175,7 +174,7 @@ alternatively make the fonts available to your OS. See this `tex.stackexchange.com question`__ for more details. - __ http://tex.stackexchange.com/questions/43642 + __ https://tex.stackexchange.com/q/43642/ * If the font configuration used by Matplotlib differs from the font setting in yout LaTeX document, the alignment of text elements in imported figures @@ -187,11 +186,17 @@ big scatter graphs. In an extreme case this can cause TeX to run out of memory: "TeX capacity exceeded, sorry" You can configure latex to increase the amount of memory available to generate the ``.pdf`` image as discussed on - `tex.stackexchange.com `_. + `tex.stackexchange.com `_. Another way would be to "rasterize" parts of the graph causing problems using either the ``rasterized=True`` keyword, or ``.set_rasterized(True)`` as per :doc:`this example `. +* Various math fonts are compiled and rendered only if corresponding font + packages are loaded. Specifically, when using ``\mathbf{}`` on Greek letters, + the default computer modern font may not contain them, in which case the + letter is not rendered. In such scenarios, the ``lmodern`` package should be + loaded. + * If you still need help, please see :ref:`reporting-problems` .. _LaTeX: http://www.tug.org diff --git a/tutorials/text/text_intro.py b/tutorials/text/text_intro.py index 10432971e151..42539302a730 100644 --- a/tutorials/text/text_intro.py +++ b/tutorials/text/text_intro.py @@ -8,7 +8,7 @@ Matplotlib has extensive text support, including support for mathematical expressions, truetype support for raster and vector outputs, newline separated text with arbitrary -rotations, and unicode support. +rotations, and Unicode support. Because it embeds fonts directly in output documents, e.g., for postscript or PDF, what you see on the screen is what you get in the hardcopy. @@ -31,11 +31,11 @@ Basic text commands =================== -The following commands are used to create text in the pyplot -interface and the object-oriented API: +The following commands are used to create text in the implicit and explicit +interfaces (see :ref:`api_interfaces` for an explanation of the tradeoffs): =================== =================== ====================================== -`.pyplot` API OO API description +implicit API explicit API description =================== =================== ====================================== `~.pyplot.text` `~.Axes.text` Add text at an arbitrary location of the `~matplotlib.axes.Axes`. @@ -63,6 +63,7 @@ configured with a variety of font and other properties. The example below shows all of these commands in action, and more detail is provided in the sections that follow. + """ import matplotlib @@ -87,7 +88,7 @@ ax.text(2, 6, r'an equation: $E=mc^2$', fontsize=15) -ax.text(3, 2, 'unicode: Institut für Festkörperphysik') +ax.text(3, 2, 'Unicode: Institut für Festkörperphysik') ax.text(0.95, 0.01, 'colored text in axes coords', verticalalignment='bottom', horizontalalignment='right', @@ -117,7 +118,7 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1) -ax.set_xlabel('time [s]') +ax.set_xlabel('Time [s]') ax.set_ylabel('Damped oscillation [V]') plt.show() @@ -130,7 +131,7 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1*10000) -ax.set_xlabel('time [s]') +ax.set_xlabel('Time [s]') ax.set_ylabel('Damped oscillation [V]') plt.show() @@ -143,7 +144,7 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1*10000) -ax.set_xlabel('time [s]') +ax.set_xlabel('Time [s]') ax.set_ylabel('Damped oscillation [V]', labelpad=18) plt.show() @@ -153,20 +154,20 @@ # *position*, via which we can manually specify the label positions. Here we # put the xlabel to the far left of the axis. Note, that the y-coordinate of # this position has no effect - to adjust the y-position we need to use the -# *labelpad* kwarg. +# *labelpad* keyword argument. fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1) -ax.set_xlabel('time [s]', position=(0., 1e6), horizontalalignment='left') +ax.set_xlabel('Time [s]', position=(0., 1e6), horizontalalignment='left') ax.set_ylabel('Damped oscillation [V]') plt.show() ############################################################################## # All the labelling in this tutorial can be changed by manipulating the -# `matplotlib.font_manager.FontProperties` method, or by named kwargs to -# `~matplotlib.axes.Axes.set_xlabel` +# `matplotlib.font_manager.FontProperties` method, or by named keyword +# arguments to `~matplotlib.axes.Axes.set_xlabel` from matplotlib.font_manager import FontProperties @@ -178,7 +179,7 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.15, left=0.2) ax.plot(x1, y1) -ax.set_xlabel('time [s]', fontsize='large', fontweight='bold') +ax.set_xlabel('Time [s]', fontsize='large', fontweight='bold') ax.set_ylabel('Damped oscillation [V]', fontproperties=font) plt.show() @@ -190,7 +191,7 @@ fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(bottom=0.2, left=0.2) ax.plot(x1, np.cumsum(y1**2)) -ax.set_xlabel('time [s] \n This was a long experiment') +ax.set_xlabel('Time [s] \n This was a long experiment') ax.set_ylabel(r'$\int\ Y^2\ dt\ \ [V^2 s]$') plt.show() @@ -211,8 +212,8 @@ plt.show() ############################################################################## -# Vertical spacing for titles is controlled via :rc:`axes.titlepad`, which -# defaults to 5 points. Setting to a different value moves the title. +# Vertical spacing for titles is controlled via :rc:`axes.titlepad`. +# Setting to a different value moves the title. fig, ax = plt.subplots(figsize=(5, 3)) fig.subplots_adjust(top=0.8) @@ -249,7 +250,7 @@ # Simple ticks # ~~~~~~~~~~~~ # -# It often is convenient to simply define the +# It is often convenient to simply define the # tick values, and sometimes the tick labels, overriding the default # locators and formatters. This is discouraged because it breaks interactive # navigation of the plot. It also can reset the axis limits: note that @@ -282,7 +283,7 @@ # Tick Locators and Formatters # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # -# Instead of making a list of all the tickalbels, we could have +# Instead of making a list of all the ticklabels, we could have # used `matplotlib.ticker.StrMethodFormatter` (new-style ``str.format()`` # format string) or `matplotlib.ticker.FormatStrFormatter` (old-style '%' # format string) and passed it to the ``ax.xaxis``. A @@ -322,7 +323,7 @@ # ``nbins=auto`` uses an algorithm to determine how many ticks will # be acceptable based on how long the axis is. The fontsize of the # ticklabel is taken into account, but the length of the tick string -# is not (because its not yet known.) In the bottom row, the +# is not (because it's not yet known.) In the bottom row, the # ticklabels are quite large, so we set ``nbins=4`` to make the # labels fit in the right-hand plot. diff --git a/tutorials/text/text_props.py b/tutorials/text/text_props.py index 95dd8b54fc46..a2acbd0bd27f 100644 --- a/tutorials/text/text_props.py +++ b/tutorials/text/text_props.py @@ -149,9 +149,9 @@ # +---------------------+----------------------------------------------------+ # | rcParam | usage | # +=====================+====================================================+ -# | ``'font.family'`` | List of either names of font or ``{'cursive', | -# | | 'fantasy', 'monospace', 'sans', 'sans serif', | -# | | 'sans-serif', 'serif'}``. | +# | ``'font.family'`` | List of font families (installed on user's machine)| +# | | and/or ``{'cursive', 'fantasy', 'monospace', | +# | | 'sans', 'sans serif', 'sans-serif', 'serif'}``. | # | | | # +---------------------+----------------------------------------------------+ # | ``'font.style'`` | The default style, ex ``'normal'``, | @@ -174,13 +174,23 @@ # | | this size. | # +---------------------+----------------------------------------------------+ # -# The mapping between the family aliases (``{'cursive', 'fantasy', -# 'monospace', 'sans', 'sans serif', 'sans-serif', 'serif'}``) and actual font names +# Matplotlib can use font families installed on the user's computer, i.e. +# Helvetica, Times, etc. Font families can also be specified with +# generic-family aliases like (``{'cursive', 'fantasy', 'monospace', +# 'sans', 'sans serif', 'sans-serif', 'serif'}``). +# +# .. note:: +# To access the full list of available fonts: :: +# +# matplotlib.font_manager.get_font_names() +# +# The mapping between the generic family aliases and actual font families +# (mentioned at :doc:`default rcParams `) # is controlled by the following rcParams: # # # +------------------------------------------+--------------------------------+ -# | family alias | rcParam with mappings | +# | CSS-based generic-family alias | rcParam with mappings | # +==========================================+================================+ # | ``'serif'`` | ``'font.serif'`` | # +------------------------------------------+--------------------------------+ @@ -194,7 +204,18 @@ # +------------------------------------------+--------------------------------+ # # -# which are lists of font names. +# If any of generic family names appear in ``'font.family'``, we replace that entry +# by all the entries in the corresponding rcParam mapping. +# For example: :: +# +# matplotlib.rcParams['font.family'] = ['Family1', 'serif', 'Family2'] +# matplotlib.rcParams['font.serif'] = ['SerifFamily1', 'SerifFamily2'] +# +# # This is effectively translated to: +# matplotlib.rcParams['font.family'] = ['Family1', 'SerifFamily1', 'SerifFamily2', 'Family2'] +# +# +# .. _font-nonlatin: # # Text with non-latin glyphs # ========================== @@ -204,17 +225,29 @@ # Korean, or Japanese. # # To set the default font to be one that supports the code points you -# need, prepend the font name to ``'font.family'`` or the desired alias -# lists :: +# need, prepend the font name to ``'font.family'`` (recommended), or to the +# desired alias lists. :: +# +# # first method +# matplotlib.rcParams['font.family'] = ['Source Han Sans TW', 'sans-serif'] +# +# # second method +# matplotlib.rcParams['font.family'] = ['sans-serif'] +# matplotlib.rcParams['sans-serif'] = ['Source Han Sans TW', ...] +# +# The generic family alias lists contain fonts that are either shipped +# alongside Matplotlib (so they have 100% chance of being found), or fonts +# which have a very high probability of being present in most systems. # -# matplotlib.rcParams['font.sans-serif'] = ['Source Han Sans TW', 'sans-serif'] +# A good practice when setting custom font families is to append +# a generic-family to the font-family list as a last resort. # -# or set it in your :file:`.matplotlibrc` file:: +# You can also set it in your :file:`.matplotlibrc` file:: # -# font.sans-serif: Source Han Sans TW, Arial, sans-serif +# font.family: Source Han Sans TW, Arial, sans-serif # -# To control the font used on per-artist basis use the ``'name'``, -# ``'fontname'`` or ``'fontproperties'`` kwargs documented :doc:`above +# To control the font used on per-artist basis use the *name*, *fontname* or +# *fontproperties* keyword arguments documented :doc:`above # `. # # diff --git a/tutorials/text/usetex.py b/tutorials/text/usetex.py index 3dc4e377a766..2b55864b18f6 100644 --- a/tutorials/text/usetex.py +++ b/tutorials/text/usetex.py @@ -1,6 +1,6 @@ r""" ************************* -Text rendering With LaTeX +Text rendering with LaTeX ************************* Matplotlib can use LaTeX to render text. This is activated by setting @@ -11,51 +11,69 @@ etc.) can be used. The results can be striking, especially when you take care to use the same fonts in your figures as in the main document. -Matplotlib's LaTeX support requires a working LaTeX_ installation. For the -\*Agg backends, dvipng_ is additionally required; for the PS backend, psfrag_, -dvips_ and Ghostscript_ are additionally required. The executables for these -external dependencies must all be located on your :envvar:`PATH`. - -There are a couple of options to mention, which can be changed using -:doc:`rc settings `. Here is an example -matplotlibrc file:: - - font.family : serif - font.serif : Times, Palatino, New Century Schoolbook, Bookman, Computer Modern Roman - font.sans-serif : Helvetica, Avant Garde, Computer Modern Sans Serif - font.cursive : Zapf Chancery - font.monospace : Courier, Computer Modern Typewriter - - text.usetex : true - -The first valid font in each family is the one that will be loaded. If the -fonts are not specified, the Computer Modern fonts are used by default. All of -the other fonts are Adobe fonts. Times and Palatino each have their own -accompanying math fonts, while the other Adobe serif fonts make use of the -Computer Modern math fonts. See the PSNFSS_ documentation for more details. - -To use LaTeX and select Helvetica as the default font, without editing -matplotlibrc use:: - - import matplotlib.pyplot as plt - plt.rcParams.update({ - "text.usetex": True, - "font.family": "sans-serif", - "font.sans-serif": ["Helvetica"]}) - # for Palatino and other serif fonts use: - plt.rcParams.update({ - "text.usetex": True, - "font.family": "serif", - "font.serif": ["Palatino"], - }) +Matplotlib's LaTeX support requires a working LaTeX_ installation. For +the \*Agg backends, dvipng_ is additionally required; for the PS backend, +PSfrag_, dvips_ and Ghostscript_ are additionally required. For the PDF +and SVG backends, if LuaTeX is present, it will be used to speed up some +post-processing steps, but note that it is not used to parse the TeX string +itself (only LaTeX is supported). The executables for these external +dependencies must all be located on your :envvar:`PATH`. + +Only a small number of font families (defined by the PSNFSS_ scheme) are +supported. They are listed here, with the corresponding LaTeX font selection +commands and LaTeX packages, which are automatically used. + +=========================== ================================================= +generic family fonts +=========================== ================================================= +serif (``\rmfamily``) Computer Modern Roman, Palatino (``mathpazo``), + Times (``mathptmx``), Bookman (``bookman``), + New Century Schoolbook (``newcent``), + Charter (``charter``) + +sans-serif (``\sffamily``) Computer Modern Serif, Helvetica (``helvet``), + Avant Garde (``avant``) + +cursive (``\rmfamily``) Zapf Chancery (``chancery``) + +monospace (``\ttfamily``) Computer Modern Typewriter, Courier (``courier``) +=========================== ================================================= + +The default font family (which does not require loading any LaTeX package) is +Computer Modern. All other families are Adobe fonts. Times and Palatino each +have their own accompanying math fonts, while the other Adobe serif fonts make +use of the Computer Modern math fonts. + +To enable LaTeX and select a font, use e.g.:: + + plt.rcParams.update({ + "text.usetex": True, + "font.family": "Helvetica" + }) + +or equivalently, set your :doc:`matplotlibrc +` to:: + + text.usetex : true + font.family : Helvetica + +It is also possible to instead set ``font.family`` to one of the generic family +names and then configure the corresponding generic family; e.g.:: + + plt.rcParams.update({ + "text.usetex": True, + "font.family": "sans-serif", + "font.sans-serif": "Helvetica", + }) + +(this was the required approach until Matplotlib 3.5). Here is the standard example, -:file:`/gallery/text_labels_and_annotations/tex_demo`: +:doc:`/gallery/text_labels_and_annotations/tex_demo`: .. figure:: ../../gallery/text_labels_and_annotations/images/sphx_glr_tex_demo_001.png :target: ../../gallery/text_labels_and_annotations/tex_demo.html :align: center - :scale: 50 Note that display math mode (``$$ e=mc^2 $$``) is not supported, but adding the command ``\displaystyle``, as in the above demo, will produce the same results. @@ -63,13 +81,23 @@ Non-ASCII characters (e.g. the degree sign in the y-label above) are supported to the extent that they are supported by inputenc_. +.. note:: + For consistency with the non-usetex case, Matplotlib special-cases newlines, + so that single-newlines yield linebreaks (rather than being interpreted as + whitespace in standard LaTeX). + + Matplotlib uses the underscore_ package so that underscores (``_``) are + printed "as-is" in text mode (rather than causing an error as in standard + LaTeX). Underscores still introduce subscripts in math mode. + .. note:: Certain characters require special escaping in TeX, such as:: - # $ % & ~ _ ^ \ { } \( \) \[ \] + # $ % & ~ ^ \ { } \( \) \[ \] Therefore, these characters will behave differently depending on - :rc:`text.usetex`. + :rc:`text.usetex`. As noted above, underscores (``_``) do not require + escaping outside of math mode. PostScript options ================== @@ -102,13 +130,13 @@ * On Ubuntu and Gentoo, the base texlive install does not ship with the type1cm package. You may need to install some of the extra - packages to get all the goodies that come bundled with other latex + packages to get all the goodies that come bundled with other LaTeX distributions. -* Some progress has been made so matplotlib uses the dvi files - directly for text layout. This allows latex to be used for text +* Some progress has been made so Matplotlib uses the dvi files + directly for text layout. This allows LaTeX to be used for text layout with the pdf and svg backends, as well as the \*Agg and PS - backends. In the future, a latex installation may be the only + backends. In the future, a LaTeX installation may be the only external dependency. .. _usetex-troubleshooting: @@ -131,7 +159,7 @@ problems. Please disable this option before reporting problems to the mailing list. -* If you still need help, please see :ref:`reporting-problems` +* If you still need help, please see :ref:`reporting-problems`. .. _dvipng: http://www.nongnu.org/dvipng/ .. _dvips: https://tug.org/texinfohtml/dvips.html @@ -140,6 +168,7 @@ .. _LaTeX: http://www.tug.org .. _Poppler: https://poppler.freedesktop.org/ .. _PSNFSS: http://www.ctan.org/tex-archive/macros/latex/required/psnfss/psnfss2e.pdf -.. _psfrag: https://ctan.org/pkg/psfrag +.. _PSfrag: https://ctan.org/pkg/psfrag +.. _underscore: https://ctan.org/pkg/underscore .. _Xpdf: http://www.xpdfreader.com/ """ diff --git a/tutorials/toolkits/axes_grid.py b/tutorials/toolkits/axes_grid.py index e55555a4aab3..0206b7ed816b 100644 --- a/tutorials/toolkits/axes_grid.py +++ b/tutorials/toolkits/axes_grid.py @@ -1,38 +1,24 @@ r""" -============================== -Overview of axes_grid1 toolkit -============================== - -Controlling the layout of plots with the -:mod:`mpl_toolkits.axes_grid1` toolkit. - .. _axes_grid1_users-guide-index: +====================== +The axes_grid1 toolkit +====================== -What is axes_grid1 toolkit? -=========================== +:mod:`.axes_grid1` provides the following features: -:mod:`mpl_toolkits.axes_grid1` is a collection of helper classes to ease -displaying (multiple) images with matplotlib. In matplotlib, the axes location -(and size) is specified in the normalized figure coordinates, which -may not be ideal for displaying images that needs to have a given -aspect ratio. For example, it helps if you have a colorbar whose -height always matches that of the image. `ImageGrid`_, `RGB Axes`_ and -`AxesDivider`_ are helper classes that deal with adjusting the -location of (multiple) Axes. They provides a framework to adjust the -position of multiple axes at the drawing time. `ParasiteAxes`_ -provides twinx(or twiny)-like features so that you can plot different -data (e.g., different y-scale) in a same Axes. `AnchoredArtists`_ -includes custom artists which are placed at some anchored position, -like the legend. +- Helper classes (ImageGrid_, RGBAxes_, AxesDivider_) to ease the layout of + axes displaying images with a fixed aspect ratio while satisfying additional + constraints (matching the heights of a colorbar and an image, or fixing the + padding between images); +- ParasiteAxes_ (twinx/twiny-like features so that you can plot different data + (e.g., different y-scale) in a same Axes); +- AnchoredArtists_ (custom artists which are placed at an anchored position, + similarly to legends). .. figure:: ../../gallery/axes_grid1/images/sphx_glr_demo_axes_grid_001.png :target: ../../gallery/axes_grid1/demo_axes_grid.html :align: center - :scale: 50 - - Demo Axes Grid - axes_grid1 ========== @@ -40,87 +26,62 @@ ImageGrid --------- -A grid of Axes. - -In Matplotlib, the axes location (and size) is specified in normalized -figure coordinates. This may not be ideal for images that needs to be -displayed with a given aspect ratio; for example, it is difficult to -display multiple images of a same size with some fixed padding between -them. `~.axes_grid1.axes_grid.ImageGrid` can be used in such a case; see its -docs for a detailed list of the parameters it accepts. +In Matplotlib, axes location and size are usually specified in normalized +figure coordinates (0 = bottom left, 1 = top right), which makes +it difficult to achieve a fixed (absolute) padding between images. +`~.axes_grid1.axes_grid.ImageGrid` can be used to achieve such a padding; see +its docs for detailed API information. .. figure:: ../../gallery/axes_grid1/images/sphx_glr_simple_axesgrid_001.png :target: ../../gallery/axes_grid1/simple_axesgrid.html :align: center - :scale: 50 - - Simple Axesgrid * The position of each axes is determined at the drawing time (see - `AxesDivider`_), so that the size of the entire grid fits in the + AxesDivider_), so that the size of the entire grid fits in the given rectangle (like the aspect of axes). Note that in this example, - the paddings between axes are fixed even if you changes the figure + the paddings between axes are fixed even if you change the figure size. -* axes in the same column has a same axes width (in figure - coordinate), and similarly, axes in the same row has a same - height. The widths (height) of the axes in the same row (column) are - scaled according to their view limits (xlim or ylim). +* Axes in the same column share their x-axis, and axes in the same row share + their y-axis (in the sense of `~.Axes.sharex`, `~.Axes.sharey`). + Additionally, Axes in the same column all have the same width, and axes in + the same row all have the same height. These widths and heights are scaled + in proportion to the axes' view limits (xlim or ylim). .. figure:: ../../gallery/axes_grid1/images/sphx_glr_simple_axesgrid2_001.png :target: ../../gallery/axes_grid1/simple_axesgrid2.html :align: center - :scale: 50 - - Simple Axes Grid - -* xaxis are shared among axes in a same column. Similarly, yaxis are - shared among axes in a same row. Therefore, changing axis properties - (view limits, tick location, etc. either by plot commands or using - your mouse in interactive backends) of one axes will affect all - other shared axes. The examples below show what you can do with ImageGrid. .. figure:: ../../gallery/axes_grid1/images/sphx_glr_demo_axes_grid_001.png :target: ../../gallery/axes_grid1/demo_axes_grid.html :align: center - :scale: 50 - - Demo Axes Grid - AxesDivider Class ----------------- -Behind the scene, the ImageGrid class and the RGBAxes class utilize the -`~.axes_grid1.axes_divider.AxesDivider` class, whose role is to calculate the -location of the axes at drawing time. Direct use of the -AxesDivider class will not be necessary for most users. The -axes_divider module provides a helper function -`~.axes_grid1.axes_divider.make_axes_locatable`, which can be useful. -It takes a existing axes instance and create a divider for it. :: +Behind the scenes, ImageGrid (and RGBAxes, described below) rely on +`~.axes_grid1.axes_divider.AxesDivider`, whose role is to calculate the +location of the axes at drawing time. + +Users typically do not need to directly instantiate dividers +by calling `~.axes_grid1.axes_divider.AxesDivider`; instead, +`~.axes_grid1.axes_divider.make_axes_locatable` can be used to create a divider +for an Axes:: ax = subplot(1, 1, 1) divider = make_axes_locatable(ax) -*make_axes_locatable* returns an instance of the -`~.axes_grid1.axes_divider.AxesDivider` class. It provides an -`~.AxesDivider.append_axes` method that -creates a new axes on the given side of ("top", "right", "bottom" and -"left") of the original axes. - +`.AxesDivider.append_axes` can then be used to create a new axes on a given +side ("left", "right", "top", "bottom") of the original axes. -colorbar whose height (or width) in sync with the master axes -------------------------------------------------------------- +colorbar whose height (or width) is in sync with the main axes +-------------------------------------------------------------- .. figure:: ../../gallery/axes_grid1/images/sphx_glr_simple_colorbar_001.png :target: ../../gallery/axes_grid1/simple_colorbar.html :align: center - :scale: 50 - - Simple Colorbar - scatter_hist.py with AxesDivider ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -148,156 +109,124 @@ .. figure:: ../../gallery/axes_grid1/images/sphx_glr_scatter_hist_locatable_axes_001.png :target: ../../gallery/axes_grid1/scatter_hist_locatable_axes.html :align: center - :scale: 50 - - Scatter Hist The :doc:`/gallery/axes_grid1/scatter_hist_locatable_axes` using the -AxesDivider has some advantage over the +AxesDivider has some advantages over the original :doc:`/gallery/lines_bars_and_markers/scatter_hist` in Matplotlib. For example, you can set the aspect ratio of the scatter plot, even with the x-axis or y-axis is shared accordingly. - ParasiteAxes ------------ -The ParasiteAxes is an axes whose location is identical to its host +The ParasiteAxes is an Axes whose location is identical to its host axes. The location is adjusted in the drawing time, thus it works even if the host change its location (e.g., images). In most cases, you first create a host axes, which provides a few -method that can be used to create parasite axes. They are *twinx*, -*twiny* (which are similar to twinx and twiny in the matplotlib) and -*twin*. *twin* takes an arbitrary transformation that maps between the -data coordinates of the host axes and the parasite axes. *draw* +methods that can be used to create parasite axes. They are ``twinx``, +``twiny`` (which are similar to ``twinx`` and ``twiny`` in the matplotlib) and +``twin``. ``twin`` takes an arbitrary transformation that maps between the +data coordinates of the host axes and the parasite axes. The ``draw`` method of the parasite axes are never called. Instead, host axes -collects artists in parasite axes and draw them as if they belong to +collects artists in parasite axes and draws them as if they belong to the host axes, i.e., artists in parasite axes are merged to those of the host axes and then drawn according to their zorder. The host and parasite axes modifies some of the axes behavior. For example, color cycle for plot lines are shared between host and parasites. Also, the legend command in host, creates a legend that includes lines in the -parasite axes. To create a host axes, you may use *host_subplot* or -*host_axes* command. +parasite axes. To create a host axes, you may use ``host_subplot`` or +``host_axes`` command. - -Example 1. twinx +Example 1: twinx ~~~~~~~~~~~~~~~~ .. figure:: ../../gallery/axes_grid1/images/sphx_glr_parasite_simple_001.png :target: ../../gallery/axes_grid1/parasite_simple.html :align: center - :scale: 50 - - Parasite Simple - -Example 2. twin +Example 2: twin ~~~~~~~~~~~~~~~ -*twin* without a transform argument assumes that the parasite axes has the +``twin`` without a transform argument assumes that the parasite axes has the same data transform as the host. This can be useful when you want the top(or right)-axis to have different tick-locations, tick-labels, or tick-formatter for bottom(or left)-axis. :: ax2 = ax.twin() # now, ax2 is responsible for "top" axis and "right" axis - ax2.set_xticks([0., .5*np.pi, np.pi, 1.5*np.pi, 2*np.pi]) - ax2.set_xticklabels(["0", r"$\frac{1}{2}\pi$", - r"$\pi$", r"$\frac{3}{2}\pi$", r"$2\pi$"]) - + ax2.set_xticks([0., .5*np.pi, np.pi, 1.5*np.pi, 2*np.pi], + labels=["0", r"$\frac{1}{2}\pi$", + r"$\pi$", r"$\frac{3}{2}\pi$", r"$2\pi$"]) .. figure:: ../../gallery/axes_grid1/images/sphx_glr_simple_axisline4_001.png :target: ../../gallery/axes_grid1/simple_axisline4.html :align: center - :scale: 50 - - Simple Axisline4 A more sophisticated example using twin. Note that if you change the x-limit in the host axes, the x-limit of the parasite axes will change accordingly. - .. figure:: ../../gallery/axes_grid1/images/sphx_glr_parasite_simple2_001.png :target: ../../gallery/axes_grid1/parasite_simple2.html :align: center - :scale: 50 - - Parasite Simple2 - AnchoredArtists --------------- -It's a collection of artists whose location is anchored to the (axes) -bbox, like the legend. It is derived from *OffsetBox* in Matplotlib, and -artist need to be drawn in the canvas coordinate. But, there is a -limited support for an arbitrary transform. For example, the ellipse -in the example below will have width and height in the data -coordinate. +:mod:`.axes_grid1.anchored_artists` is a collection of artists whose location +is anchored to the (axes) bbox, similarly to legends. These artists derive +from `.offsetbox.OffsetBox`, and the artist need to be drawn in canvas +coordinates. There is limited support for arbitrary transforms. For example, +the ellipse in the example below will have width and height in data coordinates. .. figure:: ../../gallery/axes_grid1/images/sphx_glr_simple_anchored_artists_001.png :target: ../../gallery/axes_grid1/simple_anchored_artists.html :align: center - :scale: 50 - - Simple Anchored Artists - InsetLocator ------------ -:mod:`mpl_toolkits.axes_grid1.inset_locator` provides helper classes -and functions to place your (inset) axes at the anchored position of -the parent axes, similarly to AnchoredArtist. +.. seealso:: + `.Axes.inset_axes` and `.Axes.indicate_inset_zoom` in the main library. -Using :func:`mpl_toolkits.axes_grid1.inset_locator.inset_axes`, you -can have inset axes whose size is either fixed, or a fixed proportion -of the parent axes:: +:mod:`.axes_grid1.inset_locator` provides helper classes and functions to +place inset axes at an anchored position of the parent axes, similarly to +AnchoredArtist. + +`.inset_locator.inset_axes` creates an inset axes whose size is either fixed, +or a fixed proportion of the parent axes:: inset_axes = inset_axes(parent_axes, - width="30%", # width = 30% of parent_bbox - height=1., # height : 1 inch + width="30%", # width = 30% of parent_bbox + height=1., # height = 1 inch loc='lower left') creates an inset axes whose width is 30% of the parent axes and whose height is fixed at 1 inch. -You may creates your inset whose size is determined so that the data -scale of the inset axes to be that of the parent axes multiplied by -some factor. For example, :: +`.inset_locator.zoomed_inset_axes` creates an inset axes whose data scale is +that of the parent axes multiplied by some factor, e.g. :: inset_axes = zoomed_inset_axes(ax, - 0.5, # zoom = 0.5 + 0.5, # zoom = 0.5 loc='upper right') -creates an inset axes whose data scale is half of the parent axes. -Here is complete examples. +creates an inset axes whose data scale is half of the parent axes. This can be +useful to mark the zoomed area on the parent axes: .. figure:: ../../gallery/axes_grid1/images/sphx_glr_inset_locator_demo_001.png :target: ../../gallery/axes_grid1/inset_locator_demo.html :align: center - :scale: 50 - - Inset Locator Demo -For example, :func:`.zoomed_inset_axes` can be used when you want the -inset represents the zoom-up of the small portion in the parent axes. -And :mod:`~mpl_toolkits.axes_grid1.inset_locator` provides a helper -function :func:`.mark_inset` to mark the location of the area -represented by the inset axes. +`.inset_locator.mark_inset` allows marking the location of the area represented +by the inset axes: .. figure:: ../../gallery/axes_grid1/images/sphx_glr_inset_locator_demo2_001.png :target: ../../gallery/axes_grid1/inset_locator_demo2.html :align: center - :scale: 50 - Inset Locator Demo2 - - -RGB Axes -~~~~~~~~ +RGBAxes +------- RGBAxes is a helper class to conveniently show RGB composite images. Like ImageGrid, the location of axes are adjusted so that the @@ -311,12 +240,9 @@ r, g, b = get_rgb() # r, g, b are 2D images. ax.imshow_rgb(r, g, b) - .. figure:: ../../gallery/axes_grid1/images/sphx_glr_demo_axes_rgb_001.png :target: ../../gallery/axes_grid1/demo_axes_rgb.html :align: center - :scale: 50 - AxesDivider =========== @@ -342,13 +268,13 @@ .. code-block:: none - +--------+--------+--------+--------+ - | (2, 0) | (2, 1) | (2, 2) | (2, 3) | - +--------+--------+--------+--------+ - | (1, 0) | (1, 1) | (1, 2) | (1, 3) | - +--------+--------+--------+--------+ - | (0, 0) | (0, 1) | (0, 2) | (0, 3) | - +--------+--------+--------+--------+ + ┌────────┬────────┬────────┬────────┠+ │ (2, 0) │ (2, 1) │ (2, 2) │ (2, 3) │ + ├────────┼────────┼────────┼────────┤ + │ (1, 0) │ (1, 1) │ (1, 2) │ (1, 3) │ + ├────────┼────────┼────────┼────────┤ + │ (0, 0) │ (0, 1) │ (0, 2) │ (0, 3) │ + └────────┴────────┴────────┴────────┘ such that the bottom row has a fixed height of 2 (inches) and the top two rows have a height ratio of 2 (middle) to 3 (top). (For example, if the grid has @@ -392,10 +318,9 @@ See the example, -.. figure:: ../../gallery/axes_grid1/images/sphx_glr_simple_axes_divider1_002.png +.. figure:: ../../gallery/axes_grid1/images/sphx_glr_simple_axes_divider1_001.png :target: ../../gallery/axes_grid1/simple_axes_divider1.html :align: center - :scale: 50 You can also adjust the size of each axes according to its x or y data limits (AxesX and AxesY). @@ -403,5 +328,4 @@ .. figure:: ../../gallery/axes_grid1/images/sphx_glr_simple_axes_divider3_001.png :target: ../../gallery/axes_grid1/simple_axes_divider3.html :align: center - :scale: 50 """ diff --git a/tutorials/toolkits/axisartist.py b/tutorials/toolkits/axisartist.py index 6a9e6edc9f48..d05087e9f8b2 100644 --- a/tutorials/toolkits/axisartist.py +++ b/tutorials/toolkits/axisartist.py @@ -1,9 +1,7 @@ r""" -============================== -Overview of axisartist toolkit -============================== - -The axisartist toolkit tutorial. +====================== +The axisartist toolkit +====================== .. warning:: *axisartist* uses a custom Axes class @@ -20,9 +18,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_demo_floating_axis_001.png :target: ../../gallery/axisartist/demo_floating_axis.html :align: center - :scale: 50 - - Demo Floating Axis Since it uses special artists, some Matplotlib commands that work on Axes.xaxis and Axes.yaxis may not work. @@ -48,7 +43,7 @@ callable method and it behaves as an original Axes.axis method in Matplotlib). -To create an axes, :: +To create an Axes, :: import mpl_toolkits.axisartist as AA fig = plt.figure() @@ -68,9 +63,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_simple_axisline3_001.png :target: ../../gallery/axisartist/simple_axisline3.html :align: center - :scale: 50 - - Simple Axisline3 It is also possible to add a horizontal axis. For example, you may have an horizontal axis at y=0 (in data coordinate). :: @@ -80,9 +72,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_simple_axisartist1_001.png :target: ../../gallery/axisartist/simple_axisartist1.html :align: center - :scale: 50 - - Simple Axisartist1 Or a fixed axis with some offset :: @@ -93,7 +82,7 @@ ---------------------------- Most commands in the axes_grid1 toolkit can take an axes_class keyword -argument, and the commands create an axes of the given class. For example, +argument, and the commands create an Axes of the given class. For example, to create a host subplot with axisartist.Axes, :: import mpl_toolkits.axisartist as AA @@ -106,9 +95,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_demo_parasite_axes2_001.png :target: ../../gallery/axisartist/demo_parasite_axes2.html :align: center - :scale: 50 - - Demo Parasite Axes2 Curvilinear Grid ---------------- @@ -119,9 +105,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_demo_curvelinear_grid_001.png :target: ../../gallery/axisartist/demo_curvelinear_grid.html :align: center - :scale: 50 - - Demo CurveLinear Grid Floating Axes ------------- @@ -132,9 +115,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_demo_floating_axes_001.png :target: ../../gallery/axisartist/demo_floating_axes.html :align: center - :scale: 50 - - Demo Floating Axes axisartist namespace ==================== @@ -167,9 +147,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_demo_floating_axis_001.png :target: ../../gallery/axisartist/demo_floating_axis.html :align: center - :scale: 50 - - Demo Floating Axis *mpl_toolkits.axisartist.Axes* class defines a *axis* attribute, which is a dictionary of AxisArtist instances. By default, the dictionary @@ -288,14 +265,14 @@ 1. Changing tick locations and label. - Same as the original Matplotlib's axes:: + Same as the original Matplotlib's axes:: - ax.set_xticks([1, 2, 3]) + ax.set_xticks([1, 2, 3]) 2. Changing axis properties like color, etc. - Change the properties of appropriate artists. For example, to change - the color of the ticklabels:: + Change the properties of appropriate artists. For example, to change + the color of the ticklabels:: ax.axis["left"].major_ticklabels.set_color("r") @@ -331,9 +308,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_simple_axis_direction01_001.png :target: ../../gallery/axisartist/simple_axis_direction01.html :align: center - :scale: 50 - - Simple Axis Direction01 The parameter for set_axis_direction is one of ["left", "right", "bottom", "top"]. @@ -359,24 +333,23 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_axis_direction_001.png :target: ../../gallery/axisartist/axis_direction.html :align: center - :scale: 50 On the other hand, there is a concept of "axis_direction". This is a default setting of above properties for each, "bottom", "left", "top", and "right" axis. - ========== =========== ========= ========== ========= ========== - ? ? left bottom right top - ---------- ----------- --------- ---------- --------- ---------- - axislabel direction '-' '+' '+' '-' - axislabel rotation 180 0 0 180 - axislabel va center top center bottom - axislabel ha right center right center - ticklabel direction '-' '+' '+' '-' - ticklabels rotation 90 0 -90 180 - ticklabel ha right center right center - ticklabel va center baseline center baseline - ========== =========== ========= ========== ========= ========== +========== =========== ========= ========== ========= ========== + ? ? left bottom right top +---------- ----------- --------- ---------- --------- ---------- +axislabel direction '-' '+' '+' '-' +axislabel rotation 180 0 0 180 +axislabel va center top center bottom +axislabel ha right center right center +ticklabel direction '-' '+' '+' '-' +ticklabels rotation 90 0 -90 180 +ticklabel ha right center right center +ticklabel va center baseline center baseline +========== =========== ========= ========== ========= ========== And, 'set_axis_direction("top")' means to adjust the text rotation etc, for settings suitable for "top" axis. The concept of axis @@ -385,9 +358,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_demo_axis_direction_001.png :target: ../../gallery/axisartist/demo_axis_direction.html :align: center - :scale: 50 - - Demo Axis Direction The axis_direction can be adjusted in the AxisArtist level, or in the level of its child artists, i.e., ticks, ticklabels, and axis-label. :: @@ -419,9 +389,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_simple_axis_direction03_001.png :target: ../../gallery/axisartist/simple_axis_direction03.html :align: center - :scale: 50 - - Simple Axis Direction03 So, in summary, @@ -457,9 +424,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_demo_ticklabel_alignment_001.png :target: ../../gallery/axisartist/demo_ticklabel_alignment.html :align: center - :scale: 50 - - Demo Ticklabel Alignment Adjusting pad ------------- @@ -475,9 +439,6 @@ .. figure:: ../../gallery/axisartist/images/sphx_glr_simple_axis_pad_001.png :target: ../../gallery/axisartist/simple_axis_pad.html :align: center - :scale: 50 - - Simple Axis Pad GridHelper ========== @@ -527,10 +488,10 @@ def inv_tr(x, y): # has a cycle of 360 degree. # The second coordinate (latitude, but radius in polar) has a minimum of 0 extreme_finder = angle_helper.ExtremeFinderCycle(20, 20, - lon_cycle = 360, - lat_cycle = None, - lon_minmax = None, - lat_minmax = (0, np.inf), + lon_cycle=360, + lat_cycle=None, + lon_minmax=None, + lat_minmax=(0, np.inf), ) # Find a grid values appropriate for the coordinate (degree, @@ -556,17 +517,13 @@ def inv_tr(x, y): ax1 = SubplotHost(fig, 1, 2, 2, grid_helper=grid_helper) # A parasite axes with given transform - ax2 = ParasiteAxesAuxTrans(ax1, tr, "equal") + ax2 = ax1.get_aux_axes(tr, "equal") # note that ax2.transData == tr + ax1.transData # Anything you draw in ax2 will match the ticks and grids of ax1. - ax1.parasites.append(ax2) .. figure:: ../../gallery/axisartist/images/sphx_glr_demo_curvelinear_grid_001.png :target: ../../gallery/axisartist/demo_curvelinear_grid.html :align: center - :scale: 50 - - Demo CurveLinear Grid FloatingAxis ============ diff --git a/tutorials/toolkits/mplot3d.py b/tutorials/toolkits/mplot3d.py index d5ab4f2b10b6..f3ff920e036f 100644 --- a/tutorials/toolkits/mplot3d.py +++ b/tutorials/toolkits/mplot3d.py @@ -1,19 +1,16 @@ """ =================== -The mplot3d Toolkit +The mplot3d toolkit =================== Generating 3D plots using the mplot3d toolkit. -.. currentmodule:: mpl_toolkits.mplot3d +This tutorial showcases various 3D plots. Click on the figures to see each full +gallery example with the code that generates the figures. .. contents:: - :backlinks: none + :backlinks: none -.. _toolkit_mplot3d-tutorial: - -Getting started ---------------- 3D Axes (of class `.Axes3D`) are created by passing the ``projection="3d"`` keyword argument to `.Figure.add_subplot`:: @@ -21,9 +18,11 @@ fig = plt.figure() ax = fig.add_subplot(projection='3d') -.. versionchanged:: 1.0.0 - Prior to Matplotlib 1.0.0, `.Axes3D` needed to be directly instantiated with - ``from mpl_toolkits.mplot3d import Axes3D; ax = Axes3D(fig)``. +Multiple 3D subplots can be added on the same figure, as for 2D subplots. + +.. figure:: ../../gallery/mplot3d/images/sphx_glr_subplot3d_001.png + :target: ../../gallery/mplot3d/subplot3d.html + :align: center .. versionchanged:: 3.2.0 Prior to Matplotlib 3.2.0, it was necessary to explicitly import the @@ -36,104 +35,72 @@ .. _plot3d: Line plots -==================== -.. automethod:: Axes3D.plot +========== +See `.Axes3D.plot` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_lines3d_001.png :target: ../../gallery/mplot3d/lines3d.html :align: center - :scale: 50 - - Lines3d .. _scatter3d: Scatter plots ============= -.. automethod:: Axes3D.scatter +See `.Axes3D.scatter` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_scatter3d_001.png :target: ../../gallery/mplot3d/scatter3d.html :align: center - :scale: 50 - - Scatter3d .. _wireframe: Wireframe plots =============== -.. automethod:: Axes3D.plot_wireframe +See `.Axes3D.plot_wireframe` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_wire3d_001.png :target: ../../gallery/mplot3d/wire3d.html :align: center - :scale: 50 - - Wire3d .. _surface: Surface plots ============= -.. automethod:: Axes3D.plot_surface +See `.Axes3D.plot_surface` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_surface3d_001.png :target: ../../gallery/mplot3d/surface3d.html :align: center - :scale: 50 - - Surface3d - - Surface3d 2 - - Surface3d 3 .. _trisurface: Tri-Surface plots ================= -.. automethod:: Axes3D.plot_trisurf +See `.Axes3D.plot_trisurf` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_trisurf3d_001.png :target: ../../gallery/mplot3d/trisurf3d.html :align: center - :scale: 50 - - Trisurf3d - .. _contour3d: Contour plots ============= -.. automethod:: Axes3D.contour +See `.Axes3D.contour` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_contour3d_001.png :target: ../../gallery/mplot3d/contour3d.html :align: center - :scale: 50 - - Contour3d - - Contour3d 2 - - Contour3d 3 .. _contourf3d: Filled contour plots ==================== -.. automethod:: Axes3D.contourf +See `.Axes3D.contourf` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_contourf3d_001.png :target: ../../gallery/mplot3d/contourf3d.html :align: center - :scale: 50 - - Contourf3d - - Contourf3d 2 .. versionadded:: 1.1.0 The feature demoed in the second contourf3d example was enabled as a @@ -142,84 +109,48 @@ .. _polygon3d: Polygon plots -==================== -.. automethod:: Axes3D.add_collection3d +============= +See `.Axes3D.add_collection3d` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_polys3d_001.png :target: ../../gallery/mplot3d/polys3d.html :align: center - :scale: 50 - - Polys3d .. _bar3d: Bar plots -==================== -.. automethod:: Axes3D.bar +========= +See `.Axes3D.bar` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_bars3d_001.png :target: ../../gallery/mplot3d/bars3d.html :align: center - :scale: 50 - - Bars3d .. _quiver3d: Quiver -==================== -.. automethod:: Axes3D.quiver +====== +See `.Axes3D.quiver` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_quiver3d_001.png :target: ../../gallery/mplot3d/quiver3d.html :align: center - :scale: 50 - - Quiver3d .. _2dcollections3d: 2D plots in 3D -==================== +============== .. figure:: ../../gallery/mplot3d/images/sphx_glr_2dcollections3d_001.png :target: ../../gallery/mplot3d/2dcollections3d.html :align: center - :scale: 50 - - 2dcollections3d .. _text3d: Text -==================== -.. automethod:: Axes3D.text +==== +See `.Axes3D.text` for API documentation. .. figure:: ../../gallery/mplot3d/images/sphx_glr_text3d_001.png :target: ../../gallery/mplot3d/text3d.html :align: center - :scale: 50 - - Text3d - -.. _3dsubplots: - -Subplotting -==================== -Having multiple 3D plots in a single figure is the same -as it is for 2D plots. Also, you can have both 2D and 3D plots -in the same figure. - -.. versionadded:: 1.0.0 - Subplotting 3D plots was added in v1.0.0. Earlier version can not - do this. - -.. figure:: ../../gallery/mplot3d/images/sphx_glr_subplot3d_001.png - :target: ../../gallery/mplot3d/subplot3d.html - :align: center - :scale: 50 - - Subplot3d - - Mixed Subplots """ diff --git a/versioneer.py b/versioneer.py deleted file mode 100644 index 304372b5bf7e..000000000000 --- a/versioneer.py +++ /dev/null @@ -1,1704 +0,0 @@ - -# Version: 0.15 +matplotlib modifications to avoid the need for setup.cfg to exist. - -""" -The Versioneer -============== - -* like a rocketeer, but for versions! -* https://github.com/warner/python-versioneer -* Brian Warner -* License: Public Domain -* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, and pypy -* [![Latest Version] -(https://pypip.in/version/versioneer/badge.svg?style=flat) -](https://pypi.python.org/pypi/versioneer/) -* [![Build Status] -(https://travis-ci.org/warner/python-versioneer.png?branch=master) -](https://travis-ci.org/warner/python-versioneer) - -This is a tool for managing a recorded version number in distutils-based -python projects. The goal is to remove the tedious and error-prone "update -the embedded version string" step from your release process. Making a new -release should be as easy as recording a new tag in your version-control -system, and maybe making new tarballs. - - -## Quick Install - -* `pip install versioneer` to somewhere to your $PATH -* add a `[versioneer]` section to your setup.cfg (see below) -* run `versioneer install` in your source tree, commit the results - -## Version Identifiers - -Source trees come from a variety of places: - -* a version-control system checkout (mostly used by developers) -* a nightly tarball, produced by build automation -* a snapshot tarball, produced by a web-based VCS browser, like github's - "tarball from tag" feature -* a release tarball, produced by "setup.py sdist", distributed through PyPI - -Within each source tree, the version identifier (either a string or a number, -this tool is format-agnostic) can come from a variety of places: - -* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows - about recent "tags" and an absolute revision-id -* the name of the directory into which the tarball was unpacked -* an expanded VCS keyword ($Id$, etc) -* a `_version.py` created by some earlier build step - -For released software, the version identifier is closely related to a VCS -tag. Some projects use tag names that include more than just the version -string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool -needs to strip the tag prefix to extract the version identifier. For -unreleased software (between tags), the version identifier should provide -enough information to help developers recreate the same tree, while also -giving them an idea of roughly how old the tree is (after version 1.2, before -version 1.3). Many VCS systems can report a description that captures this, -for example `git describe --tags --dirty --always` reports things like -"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the -0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes. - -The version identifier is used for multiple purposes: - -* to allow the module to self-identify its version: `myproject.__version__` -* to choose a name and prefix for a 'setup.py sdist' tarball - -## Theory of Operation - -Versioneer works by adding a special `_version.py` file into your source -tree, where your `__init__.py` can import it. This `_version.py` knows how to -dynamically ask the VCS tool for version information at import time. - -`_version.py` also contains `$Revision$` markers, and the installation -process marks `_version.py` to have this marker rewritten with a tag name -during the `git archive` command. As a result, generated tarballs will -contain enough information to get the proper version. - -To allow `setup.py` to compute a version too, a `versioneer.py` is added to -the top level of your source tree, next to `setup.py` and the `setup.cfg` -that configures it. This overrides several distutils/setuptools commands to -compute the version when invoked, and changes `setup.py build` and `setup.py -sdist` to replace `_version.py` with a small static file that contains just -the generated version data. - -## Installation - -First, decide on values for the following configuration variables: - -* `VCS`: the version control system you use. Currently accepts "git". - -* `style`: the style of version string to be produced. See "Styles" below for - details. Defaults to "pep440", which looks like - `TAG[+DISTANCE.gSHORTHASH[.dirty]]`. - -* `versionfile_source`: - - A project-relative pathname into which the generated version strings should - be written. This is usually a `_version.py` next to your project's main - `__init__.py` file, so it can be imported at runtime. If your project uses - `src/myproject/__init__.py`, this should be `src/myproject/_version.py`. - This file should be checked in to your VCS as usual: the copy created below - by `setup.py setup_versioneer` will include code that parses expanded VCS - keywords in generated tarballs. The 'build' and 'sdist' commands will - replace it with a copy that has just the calculated version string. - - This must be set even if your project does not have any modules (and will - therefore never import `_version.py`), since "setup.py sdist" -based trees - still need somewhere to record the pre-calculated version strings. Anywhere - in the source tree should do. If there is a `__init__.py` next to your - `_version.py`, the `setup.py setup_versioneer` command (described below) - will append some `__version__`-setting assignments, if they aren't already - present. - -* `versionfile_build`: - - Like `versionfile_source`, but relative to the build directory instead of - the source directory. These will differ when your setup.py uses - 'package_dir='. If you have `package_dir={'myproject': 'src/myproject'}`, - then you will probably have `versionfile_build='myproject/_version.py'` and - `versionfile_source='src/myproject/_version.py'`. - - If this is set to None, then `setup.py build` will not attempt to rewrite - any `_version.py` in the built tree. If your project does not have any - libraries (e.g. if it only builds a script), then you should use - `versionfile_build = None` and override `distutils.command.build_scripts` - to explicitly insert a copy of `versioneer.get_version()` into your - generated script. - -* `tag_prefix`: - - a string, like 'PROJECTNAME-', which appears at the start of all VCS tags. - If your tags look like 'myproject-1.2.0', then you should use - tag_prefix='myproject-'. If you use unprefixed tags like '1.2.0', this - should be an empty string. - -* `parentdir_prefix`: - - a optional string, frequently the same as tag_prefix, which appears at the - start of all unpacked tarball filenames. If your tarball unpacks into - 'myproject-1.2.0', this should be 'myproject-'. To disable this feature, - just omit the field from your `setup.cfg`. - -This tool provides one script, named `versioneer`. That script has one mode, -"install", which writes a copy of `versioneer.py` into the current directory -and runs `versioneer.py setup` to finish the installation. - -To versioneer-enable your project: - -* 1: Modify your `setup.cfg`, adding a section named `[versioneer]` and - populating it with the configuration values you decided earlier (note that - the option names are not case-sensitive): - - ```` - [versioneer] - VCS = git - style = pep440 - versionfile_source = src/myproject/_version.py - versionfile_build = myproject/_version.py - tag_prefix = "" - parentdir_prefix = myproject- - ```` - -* 2: Run `versioneer install`. This will do the following: - - * copy `versioneer.py` into the top of your source tree - * create `_version.py` in the right place (`versionfile_source`) - * modify your `__init__.py` (if one exists next to `_version.py`) to define - `__version__` (by calling a function from `_version.py`) - * modify your `MANIFEST.in` to include both `versioneer.py` and the - generated `_version.py` in sdist tarballs - - `versioneer install` will complain about any problems it finds with your - `setup.py` or `setup.cfg`. Run it multiple times until you have fixed all - the problems. - -* 3: add a `import versioneer` to your setup.py, and add the following - arguments to the setup() call: - - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), - -* 4: commit these changes to your VCS. To make sure you won't forget, - `versioneer install` will mark everything it touched for addition using - `git add`. Don't forget to add `setup.py` and `setup.cfg` too. - -## Post-Installation Usage - -Once established, all uses of your tree from a VCS checkout should get the -current version string. All generated tarballs should include an embedded -version string (so users who unpack them will not need a VCS tool installed). - -If you distribute your project through PyPI, then the release process should -boil down to two steps: - -* 1: git tag 1.0 -* 2: python setup.py register sdist upload - -If you distribute it through github (i.e. users use github to generate -tarballs with `git archive`), the process is: - -* 1: git tag 1.0 -* 2: git push; git push --tags - -Versioneer will report "0+untagged.NUMCOMMITS.gHASH" until your tree has at -least one tag in its history. - -## Version-String Flavors - -Code which uses Versioneer can learn about its version string at runtime by -importing `_version` from your main `__init__.py` file and running the -`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can -import the top-level `versioneer.py` and run `get_versions()`. - -Both functions return a dictionary with different flavors of version -information: - -* `['version']`: A condensed version string, rendered using the selected - style. This is the most commonly used value for the project's version - string. The default "pep440" style yields strings like `0.11`, - `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section - below for alternative styles. - -* `['full-revisionid']`: detailed revision identifier. For Git, this is the - full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". - -* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that - this is only accurate if run in a VCS checkout, otherwise it is likely to - be False or None - -* `['error']`: if the version string could not be computed, this will be set - to a string describing the problem, otherwise it will be None. It may be - useful to throw an exception in setup.py if this is set, to avoid e.g. - creating tarballs with a version string of "unknown". - -Some variants are more useful than others. Including `full-revisionid` in a -bug report should allow developers to reconstruct the exact code being tested -(or indicate the presence of local changes that should be shared with the -developers). `version` is suitable for display in an "about" box or a CLI -`--version` output: it can be easily compared against release notes and lists -of bugs fixed in various releases. - -The installer adds the following text to your `__init__.py` to place a basic -version in `YOURPROJECT.__version__`: - - from ._version import get_versions - __version__ = get_versions()['version'] - del get_versions - -## Styles - -The setup.cfg `style=` configuration controls how the VCS information is -rendered into a version string. - -The default style, "pep440", produces a PEP440-compliant string, equal to the -un-prefixed tag name for actual releases, and containing an additional "local -version" section with more detail for in-between builds. For Git, this is -TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags ---dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the -tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and -that this commit is two revisions ("+2") beyond the "0.11" tag. For released -software (exactly equal to a known tag), the identifier will only contain the -stripped tag, e.g. "0.11". - -Other styles are available. See details.md in the Versioneer source tree for -descriptions. - -## Debugging - -Versioneer tries to avoid fatal errors: if something goes wrong, it will tend -to return a version of "0+unknown". To investigate the problem, run `setup.py -version`, which will run the version-lookup code in a verbose mode, and will -display the full contents of `get_versions()` (including the `error` string, -which may help identify what went wrong). - -## Updating Versioneer - -To upgrade your project to a new release of Versioneer, do the following: - -* install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes -* re-run `versioneer install` in your source tree, to replace - `SRC/_version.py` -* commit any changed files - -### Upgrading to 0.15 - -Starting with this version, Versioneer is configured with a `[versioneer]` -section in your `setup.cfg` file. Earlier versions required the `setup.py` to -set attributes on the `versioneer` module immediately after import. The new -version will refuse to run (raising an exception during import) until you -have provided the necessary `setup.cfg` section. - -In addition, the Versioneer package provides an executable named -`versioneer`, and the installation process is driven by running `versioneer -install`. In 0.14 and earlier, the executable was named -`versioneer-installer` and was run without an argument. - -### Upgrading to 0.14 - -0.14 changes the format of the version string. 0.13 and earlier used -hyphen-separated strings like "0.11-2-g1076c97-dirty". 0.14 and beyond use a -plus-separated "local version" section strings, with dot-separated -components, like "0.11+2.g1076c97". PEP440-strict tools did not like the old -format, but should be ok with the new one. - -### Upgrading from 0.11 to 0.12 - -Nothing special. - -### Upgrading from 0.10 to 0.11 - -You must add a `versioneer.VCS = "git"` to your `setup.py` before re-running -`setup.py setup_versioneer`. This will enable the use of additional -version-control systems (SVN, etc) in the future. - -## Future Directions - -This tool is designed to make it easily extended to other version-control -systems: all VCS-specific components are in separate directories like -src/git/ . The top-level `versioneer.py` script is assembled from these -components by running make-versioneer.py . In the future, make-versioneer.py -will take a VCS name as an argument, and will construct a version of -`versioneer.py` that is specific to the given VCS. It might also take the -configuration arguments that are currently provided manually during -installation by editing setup.py . Alternatively, it might go the other -direction and include code from all supported VCS systems, reducing the -number of intermediate scripts. - - -## License - -To make Versioneer easier to embed, all its code is hereby released into the -public domain. The `_version.py` that it creates is also in the public -domain. - -""" - -from __future__ import print_function -try: - import configparser -except ImportError: - import ConfigParser as configparser -import errno -import json -import os -import re -import subprocess -import sys - - -class VersioneerConfig: - pass - - -def get_root(): - # we require that all commands are run from the project root, i.e. the - # directory that contains setup.py, setup.cfg, and versioneer.py . - root = os.path.realpath(os.path.abspath(os.getcwd())) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - # allow 'python path/to/setup.py COMMAND' - root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - err = ("Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND').") - raise VersioneerBadRootError(err) - try: - # Certain runtime workflows (setup.py install/develop in a setuptools - # tree) execute all dependencies in a single python process, so - # "versioneer" may be imported multiple times, and python's shared - # module-import table will cache the first one. So we can't use - # os.path.dirname(__file__), as that will find whichever - # versioneer.py was first imported, even in later projects. - me = os.path.realpath(os.path.abspath(__file__)) - if os.path.splitext(me)[0] != os.path.splitext(versioneer_py)[0]: - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py)) - except NameError: - pass - return root - - -def get_config_from_root(root): - # This might raise EnvironmentError (if setup.cfg is missing), or - # configparser.NoSectionError (if it lacks a [versioneer] section), or - # configparser.NoOptionError (if it lacks "VCS="). See the docstring at - # the top of versioneer.py for instructions on writing your setup.cfg . - setup_cfg = os.path.join(root, "setup.cfg") - - - parser = configparser.SafeConfigParser() - if os.path.exists(setup_cfg): - with open(setup_cfg, "r") as f: - parser.readfp(f) - - def get(parser, name): - if parser.has_option("versioneer", name): - return parser.get("versioneer", name) - return None - cfg = VersioneerConfig() - - VCS = get(parser, "VCS") or 'git' - cfg.VCS = VCS - cfg.style = get(parser, "style") or "pep440" - version_file = os.path.join('lib', 'matplotlib', '_version.py') - cfg.versionfile_source = get(parser, "versionfile_source") or version_file - cfg.versionfile_build = get(parser, "versionfile_build") or os.path.join('matplotlib', '_version.py') - cfg.tag_prefix = get(parser, "tag_prefix") or 'v' - cfg.parentdir_prefix = get(parser, "parentdir_prefix") or 'matplotlib-' - cfg.verbose = get(parser, "verbose") - return cfg - - -class NotThisMethod(Exception): - pass - -# these dictionaries contain VCS-specific tools -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - def decorate(f): - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - return None - return stdout -LONG_VERSION_PY['git'] = r''' -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.15 (https://github.com/warner/python-versioneer) - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" - git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" - keywords = {"refnames": git_refnames, "full": git_full} - return keywords - - -class VersioneerConfig: - pass - - -def get_config(): - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "%(STYLE)s" - cfg.tag_prefix = "%(TAG_PREFIX)s" - cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" - cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - pass - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - def decorate(f): - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %%s" %% dispcmd) - print(e) - return None - else: - if verbose: - print("unable to find command, tried %%s" %% (commands,)) - return None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %%s (error)" %% dispcmd) - return None - return stdout - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - # Source tarballs conventionally unpack into a directory that includes - # both the project name and a version string. - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%%s', but '%%s' doesn't start with " - "prefix '%%s'" %% (root, dirname, parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None} - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - if not keywords: - raise NotThisMethod("no keywords at all, weird") - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %%d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%%s', no digits" %% ",".join(refs-tags)) - if verbose: - print("likely tags: %%s" %% ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %%s" %% r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None - } - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags"} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - # this runs 'git' from the root of the source tree. This only gets called - # if the git-archive 'subst' keywords were *not* expanded, and - # _version.py hasn't already been rewritten with a short version string, - # meaning we're inside a checked out source tree. - - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %%s" %% root) - raise NotThisMethod("no .git directory") - - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - # if there is a tag, this yields TAG-NUM-gHEX[-dirty] - # if there are no tags, this yields HEX[-dirty] (no NUM) - describe_out = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long"], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%%s'" - %% describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%%s' doesn't start with prefix '%%s'" - print(fmt %% (full_tag, tag_prefix)) - pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" - %% (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - return pieces - - -def plus_or_dot(pieces): - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - # now build up version string, with post-release "local version - # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - # exceptions: - # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - # TAG[.post.devDISTANCE] . No -dirty - - # exceptions: - # 1: no tags. 0.post.devDISTANCE - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%%d" %% pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%%d" %% pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that - # .dev0 sorts backwards (a dirty tree will appear "older" than the - # corresponding clean one), but you shouldn't be releasing software with - # -dirty anyways. - - # exceptions: - # 1: no tags. 0.postDISTANCE[.dev0] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%%s" %% pieces["short"] - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%%s" %% pieces["short"] - return rendered - - -def render_pep440_old(pieces): - # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - - # exceptions: - # 1: no tags. 0.postDISTANCE[.dev0] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty - # --always' - - # exceptions: - # 1: no tags. HEX[-dirty] (note: no 'g' prefix) - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty - # --always -long'. The distance/hash is unconditional. - - # exceptions: - # 1: no tags. HEX[-dirty] (note: no 'g' prefix) - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"]} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%%s'" %% style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None} - - -def get_versions(): - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree"} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version"} -''' - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - if not keywords: - raise NotThisMethod("no keywords at all, weird") - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs-tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None - } - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags"} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - # this runs 'git' from the root of the source tree. This only gets called - # if the git-archive 'subst' keywords were *not* expanded, and - # _version.py hasn't already been rewritten with a short version string, - # meaning we're inside a checked out source tree. - - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %s" % root) - raise NotThisMethod("no .git directory") - - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - # if there is a tag, this yields TAG-NUM-gHEX[-dirty] - # if there are no tags, this yields HEX[-dirty] (no NUM) - describe_out = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long"], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparsable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - return pieces - - -def do_vcs_install(manifest_in, versionfile_source, ipy): - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - files = [manifest_in, versionfile_source] - if ipy: - files.append(ipy) - try: - me = __file__ - if me.endswith(".pyc") or me.endswith(".pyo"): - me = os.path.splitext(me)[0] + ".py" - versioneer_file = os.path.relpath(me) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) - present = False - try: - f = open(".gitattributes", "r") - for line in f.readlines(): - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - f.close() - except EnvironmentError: - pass - if not present: - f = open(".gitattributes", "a+") - f.write("%s export-subst\n" % versionfile_source) - f.close() - files.append(".gitattributes") - run_command(GITS, ["add", "--"] + files) - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - # Source tarballs conventionally unpack into a directory that includes - # both the project name and a version string. - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with " - "prefix '%s'" % (root, dirname, parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None} - -SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.15) from -# revision-control system data, or from the parent directory name of an -# unpacked source archive. Distribution tarballs contain a pre-generated copy -# of this file. - -import json -import sys - -version_json = ''' -%s -''' # END VERSION_JSON - - -def get_versions(): - return json.loads(version_json) -""" - - -def versions_from_file(filename): - try: - with open(filename) as f: - contents = f.read() - except EnvironmentError: - raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - raise NotThisMethod("no version_json in _version.py") - return json.loads(mo.group(1)) - - -def write_to_version_file(filename, versions): - os.unlink(filename) - contents = json.dumps(versions, sort_keys=True, - indent=1, separators=(",", ": ")) - with open(filename, "w") as f: - f.write(SHORT_VERSION_PY % contents) - - print("set %s to '%s'" % (filename, versions["version"])) - - -def plus_or_dot(pieces): - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - # now build up version string, with post-release "local version - # identifier". Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - # get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - # exceptions: - # 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - # TAG[.post.devDISTANCE] . No -dirty - - # exceptions: - # 1: no tags. 0.post.devDISTANCE - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - # TAG[.postDISTANCE[.dev0]+gHEX] . The ".dev0" means dirty. Note that - # .dev0 sorts backwards (a dirty tree will appear "older" than the - # corresponding clean one), but you shouldn't be releasing software with - # -dirty anyways. - - # exceptions: - # 1: no tags. 0.postDISTANCE[.dev0] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - # TAG[.postDISTANCE[.dev0]] . The ".dev0" means dirty. - - # exceptions: - # 1: no tags. 0.postDISTANCE[.dev0] - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - # TAG[-DISTANCE-gHEX][-dirty], like 'git describe --tags --dirty - # --always' - - # exceptions: - # 1: no tags. HEX[-dirty] (note: no 'g' prefix) - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - # TAG-DISTANCE-gHEX[-dirty], like 'git describe --tags --dirty - # --always -long'. The distance/hash is unconditional. - - # exceptions: - # 1: no tags. HEX[-dirty] (note: no 'g' prefix) - - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"]} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None} - - -class VersioneerBadRootError(Exception): - pass - - -def get_versions(verbose=False): - # returns dict with two keys: 'version' and 'full' - - if "versioneer" in sys.modules: - # see the discussion in cmdclass.py:get_cmdclass() - del sys.modules["versioneer"] - - root = get_root() - cfg = get_config_from_root(root) - - assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" - handlers = HANDLERS.get(cfg.VCS) - assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or cfg.verbose - assert cfg.versionfile_source is not None, \ - "please set versioneer.versionfile_source" - assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" - - versionfile_abs = os.path.join(root, cfg.versionfile_source) - - # extract version from first of: _version.py, VCS command (e.g. 'git - # describe'), parentdir. This is meant to work for developers using a - # source checkout, for users of a tarball created by 'setup.py sdist', - # and for users of a tarball/zipball created by 'git archive' or github's - # download-from-tag feature or the equivalent in other VCSes. - - get_keywords_f = handlers.get("get_keywords") - from_keywords_f = handlers.get("keywords") - if get_keywords_f and from_keywords_f: - try: - keywords = get_keywords_f(versionfile_abs) - ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) - if verbose: - print("got version from expanded keyword %s" % ver) - return ver - except NotThisMethod: - pass - - try: - ver = versions_from_file(versionfile_abs) - if verbose: - print("got version from file %s %s" % (versionfile_abs, ver)) - return ver - except NotThisMethod: - pass - - from_vcs_f = handlers.get("pieces_from_vcs") - if from_vcs_f: - try: - pieces = from_vcs_f(cfg.tag_prefix, root, verbose) - ver = render(pieces, cfg.style) - if verbose: - print("got version from VCS %s" % ver) - return ver - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - if verbose: - print("got version from parentdir %s" % ver) - return ver - except NotThisMethod: - pass - - if verbose: - print("unable to compute version") - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version"} - - -def get_version(): - return get_versions()["version"] - - -def get_cmdclass(): - if "versioneer" in sys.modules: - del sys.modules["versioneer"] - # this fixes the "python setup.py develop" case (also 'install' and - # 'easy_install .'), in which subdependencies of the main project are - # built (using setup.py bdist_egg) in the same python process. Assume - # a main project A and a dependency B, which use different versions - # of Versioneer. A's setup.py imports A's Versioneer, leaving it in - # sys.modules by the time B's setup.py is executed, causing B to run - # with the wrong versioneer. Setuptools wraps the sub-dep builds in a - # sandbox that restores sys.modules to it's pre-build state, so the - # parent is protected against the child's "import versioneer". By - # removing ourselves from sys.modules here, before the child build - # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/warner/python-versioneer/issues/52 - - cmds = {} - - # we add "version" to both distutils and setuptools - from distutils.core import Command - - class cmd_version(Command): - description = "report generated version string" - user_options = [] - boolean_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - vers = get_versions(verbose=True) - print("Version: %s" % vers["version"]) - print(" full-revisionid: %s" % vers.get("full-revisionid")) - print(" dirty: %s" % vers.get("dirty")) - if vers["error"]: - print(" error: %s" % vers["error"]) - cmds["version"] = cmd_version - - # we override "build_py" in both distutils and setuptools - # - # most invocation pathways end up running build_py: - # distutils/build -> build_py - # distutils/install -> distutils/build ->.. - # setuptools/bdist_wheel -> distutils/install ->.. - # setuptools/bdist_egg -> distutils/install_lib -> build_py - # setuptools/install -> bdist_egg ->.. - # setuptools/develop -> ? - - from distutils.command.build_py import build_py as _build_py - - class cmd_build_py(_build_py): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - _build_py.run(self) - # now locate _version.py in the new build/ directory and replace - # it with an updated value - if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - cmds["build_py"] = cmd_build_py - - if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe - - class cmd_build_exe(_build_exe): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _build_exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["build_exe"] = cmd_build_exe - del cmds["build_py"] - - # we override different "sdist" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.sdist import sdist as _sdist - else: - from distutils.command.sdist import sdist as _sdist - - class cmd_sdist(_sdist): - def run(self): - versions = get_versions() - self._versioneer_generated_versions = versions - # unless we update this, the command will keep using the old - # version - self.distribution.metadata.version = versions["version"] - return _sdist.run(self) - - def make_release_tree(self, base_dir, files): - root = get_root() - cfg = get_config_from_root(root) - _sdist.make_release_tree(self, base_dir, files) - # now locate _version.py in the new base_dir directory - # (remembering that it may be a hardlink) and replace it with an - # updated value - target_versionfile = os.path.join(base_dir, cfg.versionfile_source) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, - self._versioneer_generated_versions) - cmds["sdist"] = cmd_sdist - - return cmds - - -CONFIG_ERROR = """ -setup.cfg is missing the necessary Versioneer configuration. You need -a section like: - - [versioneer] - VCS = git - style = pep440 - versionfile_source = src/myproject/_version.py - versionfile_build = myproject/_version.py - tag_prefix = "" - parentdir_prefix = myproject- - -You will also need to edit your setup.py to use the results: - - import versioneer - setup(version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), ...) - -Please read the docstring in ./versioneer.py for configuration instructions, -edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. -""" - -SAMPLE_CONFIG = """ -# See the docstring in versioneer.py for instructions. Note that you must -# re-run 'versioneer.py setup' after changing this section, and commit the -# resulting files. - -[versioneer] -#VCS = git -#style = pep440 -#versionfile_source = -#versionfile_build = -#tag_prefix = -#parentdir_prefix = - -""" - -INIT_PY_SNIPPET = """ -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions -""" - - -def do_setup(): - root = get_root() - try: - cfg = get_config_from_root(root) - except (EnvironmentError, configparser.NoSectionError, - configparser.NoOptionError) as e: - if isinstance(e, (EnvironmentError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) - with open(os.path.join(root, "setup.cfg"), "a") as f: - f.write(SAMPLE_CONFIG) - print(CONFIG_ERROR, file=sys.stderr) - return 1 - - print(" creating %s" % cfg.versionfile_source) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), - "__init__.py") - if os.path.exists(ipy): - try: - with open(ipy, "r") as f: - old = f.read() - except EnvironmentError: - old = "" - if INIT_PY_SNIPPET not in old: - print(" appending to %s" % ipy) - with open(ipy, "a") as f: - f.write(INIT_PY_SNIPPET) - else: - print(" %s unmodified" % ipy) - else: - print(" %s doesn't exist, ok" % ipy) - ipy = None - - # Make sure both the top-level "versioneer.py" and versionfile_source - # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so - # they'll be copied into source distributions. Pip won't be able to - # install the package without this. - manifest_in = os.path.join(root, "MANIFEST.in") - simple_includes = set() - try: - with open(manifest_in, "r") as f: - for line in f: - if line.startswith("include "): - for include in line.split()[1:]: - simple_includes.add(include) - except EnvironmentError: - pass - # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so - # it might give some false negatives. Appending redundant 'include' - # lines is safe, though. - if "versioneer.py" not in simple_includes: - print(" appending 'versioneer.py' to MANIFEST.in") - with open(manifest_in, "a") as f: - f.write("include versioneer.py\n") - else: - print(" 'versioneer.py' already in MANIFEST.in") - if cfg.versionfile_source not in simple_includes: - print(" appending versionfile_source ('%s') to MANIFEST.in" % - cfg.versionfile_source) - with open(manifest_in, "a") as f: - f.write("include %s\n" % cfg.versionfile_source) - else: - print(" versionfile_source already in MANIFEST.in") - - # Make VCS-specific changes. For git, this means creating/changing - # .gitattributes to mark _version.py for export-time keyword - # substitution. - do_vcs_install(manifest_in, cfg.versionfile_source, ipy) - return 0 - - -def scan_setup_py(): - found = set() - setters = False - errors = 0 - with open("setup.py", "r") as f: - for line in f.readlines(): - if "import versioneer" in line: - found.add("import") - if "versioneer.get_cmdclass()" in line: - found.add("cmdclass") - if "versioneer.get_version()" in line: - found.add("get_version") - if "versioneer.VCS" in line: - setters = True - if "versioneer.versionfile_source" in line: - setters = True - if len(found) != 3: - print("") - print("Your setup.py appears to be missing some important items") - print("(but I might be wrong). Please make sure it has something") - print("roughly like the following:") - print("") - print(" import versioneer") - print(" setup( version=versioneer.get_version(),") - print(" cmdclass=versioneer.get_cmdclass(), ...)") - print("") - errors += 1 - if setters: - print("You should remove lines like 'versioneer.VCS = ' and") - print("'versioneer.versionfile_source = ' . This configuration") - print("now lives in setup.cfg, and should be removed from setup.py") - print("") - errors += 1 - return errors - -if __name__ == "__main__": - cmd = sys.argv[1] - if cmd == "setup": - errors = do_setup() - errors += scan_setup_py() - if errors: - sys.exit(1)